Repository: jupyterlab/ui-profiler (language: python)

Issue:
Error 'file or directory does not exist: 'ui-profiler-results' while benchmarking Execution time
<!-- Welcome! Thank you for contributing. These HTML comments will not render in the issue.

Before creating a new issue:
* Search for relevant issues
* Follow the issue reporting guidelines:
https://jupyterlab.readthedocs.io/en/latest/getting_started/issue.html
-->

## Description

While benchmarking Execution time, the tests run successfully but the results are not stored as expected. I get the error '- file or directory does not exist: 'ui-profiler-results' - and the results directory is not created at all.

## Reproduce

<!--Describe step-by-step instructions to reproduce the behavior-->

1. Select benchmark - Execution Time
2. Select scenario - Open Menu
3. Click start
4. The benchmark test runs through without any issues; the box plot is displayed as expected
5. However, there are no .profile.json result files on the left
6. Terminal has the following error - file or directory does not exist: 'ui-profiler-results' 

<!--Describe how you diagnosed the issue. See the guidelines at
 https://jupyterlab.readthedocs.io/en/latest/getting_started/issue.html -->

## Expected behavior

The folder ui-profiler-results is created as needed

## Context

<!--Complete the following for context, and add any other relevant context-->

- Browser and version: Chrome 110
- JupyterLab version: 3.6.1
- ui-profiler-version: 0.1.6





Discussion / hints:
Here is a screenshot of the error in terminal with more details - 

![image](https://user-images.githubusercontent.com/19615498/223432936-825fdd29-d5e6-4a68-8d90-b5ddf422abe2.png)

The results directory should be created by this call:

https://github.com/jupyterlab/ui-profiler/blob/b351f8f0edd5da9281b398e660efd4bbe3f8c5e1/src/ui.tsx#L615-L617

however if user deletes the directory after it will be created they will see an error as above. If you did not delete the directory in the meantime that would be an interesting bug and it would be helpful to see the server long on the initial `save` that was supposed to create the directory.
Interestingly, I had the issue occur in both scenarios - the files were not created in the existing `ui-profiler-results` folder. So, I deleted the existing folder to see if a new folder would be created and still had the issue.

The screenshot I shared previously was taken after deleting the folder. 

Fix patch:
diff --git a/src/ui.tsx b/src/ui.tsx
index 18405dd..688a691 100644
--- a/src/ui.tsx
+++ b/src/ui.tsx
@@ -610,12 +610,26 @@ export class UIProfiler extends ReactWidget implements IUIProfiler {
     this.progress = new Signal(this);
     this.handleResult = this.handleResult.bind(this);
     this.loadResult = this.loadResult.bind(this);
+    this.upload = this.upload.bind(this);
     this.manager = new ServiceManager();
     this.resultAdded = new Signal(this);
-    this.manager.contents.save(this.props.resultLocation, {
-      type: 'directory'
-    });
+    this.ensureResultsDirectory();
+  }
+
+  async ensureResultsDirectory() {
+    return this.manager.contents
+      .save(this.props.resultLocation, {
+        type: 'directory'
+      })
+      .catch(reason => {
+        showErrorMessage(
+          'filesystem error',
+          'ui-profiler could not create results directory; see console for details'
+        );
+        console.error('directory creation failure reason:', reason);
+      });
   }
+
   handleResult(result: IBenchmarkResult): void {
     this.result = result;
     this.update();
@@ -632,6 +646,11 @@ export class UIProfiler extends ReactWidget implements IUIProfiler {
     this.update();
   }
 
+  async upload(file: File): Promise<Contents.IModel> {
+    await this.ensureResultsDirectory();
+    return this.props.upload(file);
+  }
+
   render<T extends IProfilingOutcome | ITimingOutcome>(): JSX.Element {
     return (
       <div className="up-UIProfiler">
@@ -639,6 +658,7 @@ export class UIProfiler extends ReactWidget implements IUIProfiler {
           onResult={this.handleResult}
           progress={this.progress}
           {...this.props}
+          upload={this.upload}
         />
         <BenchmarkHistory
           resultAdded={this.resultAdded}
@@ -1164,7 +1184,7 @@ export class BenchmarkLauncher extends React.Component<
           }
           const result = await this.runBenchmark(scenario, benchmark, config);
           const filename = benchmarkFilename(result);
-          this.props.upload(
+          await this.props.upload(
             new File(
               JSON.stringify(result).split('\n'),
               PathExt.join(this.props.resultLocation, filename),


Test patch:
diff --git a/ui-tests/tests/profiler.spec.ts b/ui-tests/tests/profiler.spec.ts
index 072a9b3..b9cecd1 100644
--- a/ui-tests/tests/profiler.spec.ts
+++ b/ui-tests/tests/profiler.spec.ts
@@ -1,9 +1,30 @@
-import { expect, test } from '@jupyterlab/galata';
+import { expect, galata, test } from '@jupyterlab/galata';
 
 const PROFILER_CARD_SELECTOR =
   '.jp-LauncherCard[title="Open JupyterLab UI Profiler"]';
 
+const START_BUTTON_SELECTOR =
+  '.up-BenchmarkLauncher-launchbar-buttons .jp-mod-accept';
+
+const REPEATS_INPUT_SELECTOR = '#up-profiler-benchmark_repeats';
+
+const REFRESH_BUTTON_SELECTOR = '[data-command="filebrowser:refresh"]';
+
+const HOME_SELECTOR = '.jp-BreadCrumbs-home';
+
 test.describe('Profiler UI', () => {
+  test.beforeEach(async ({ baseURL, request }) => {
+    const contents = galata.newContentsHelper(baseURL, undefined, request);
+    // delete directory to ensure there are no stale results
+    await contents.deleteDirectory('ui-profiler-results');
+  });
+
+  test.afterAll(async ({ baseURL, request }) => {
+    const contents = galata.newContentsHelper(baseURL, undefined, request);
+    // clean up results
+    await contents.deleteDirectory('ui-profiler-results');
+  });
+
   test('adds launcher card', async ({ page }) => {
     const section = page.locator('.jp-Launcher-section', {
       has: page.locator(PROFILER_CARD_SELECTOR)
@@ -23,4 +44,36 @@ test.describe('Profiler UI', () => {
     });
     expect(await panels.screenshot()).toMatchSnapshot('ui-profiler.png');
   });
+
+  test('creates result even after removing results directory', async ({
+    page,
+    request,
+    baseURL
+  }) => {
+    const contents = galata.newContentsHelper(baseURL, undefined, request);
+    const handle = await page.waitForSelector(PROFILER_CARD_SELECTOR);
+    await handle.click();
+    const resultLocator = page.locator(
+      '.up-BenchmarkHistory-file >> text=execution-time_menuOpen'
+    );
+    const directoryLocator = page.locator(
+      '.jp-DirListing-item >> text="ui-profiler-results"'
+    );
+    await page.locator(HOME_SELECTOR).click();
+    await expect(directoryLocator).toHaveCount(1);
+    await expect(resultLocator).toHaveCount(0);
+    const startButton = await page.waitForSelector(START_BUTTON_SELECTOR);
+    // delete it second time after it was created
+    await contents.deleteDirectory('ui-profiler-results');
+    const refreshButton = page.locator(REFRESH_BUTTON_SELECTOR);
+    await refreshButton.click();
+    await expect(directoryLocator).toHaveCount(0);
+    const repeatsInput = await page.waitForSelector(REPEATS_INPUT_SELECTOR);
+    await repeatsInput.fill('5');
+    await startButton.click();
+    await page.waitForSelector('.up-mod-completed');
+    await expect(resultLocator).toHaveCount(1);
+    await refreshButton.click();
+    await expect(directoryLocator).toHaveCount(1);
+  });
 });
diff --git a/ui-tests/tests/results.spec.ts b/ui-tests/tests/results.spec.ts
index 75b8dfb..51d360e 100644
--- a/ui-tests/tests/results.spec.ts
+++ b/ui-tests/tests/results.spec.ts
@@ -20,6 +20,9 @@ test.describe.configure({ mode: 'parallel' });
 test.describe('Results', () => {
   test.beforeAll(async ({ baseURL, request, tmpPath }) => {
     const contents = galata.newContentsHelper(baseURL, undefined, request);
+    // re-create directory to ensure there are no stale results
+    await contents.deleteDirectory('ui-profiler-results');
+    await contents.createDirectory('ui-profiler-results');
     for (const fileName of fileNames) {
       await contents.uploadFile(
         path.resolve(__dirname, `./ui-profiler-results/${fileName}`),