diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index cbb28d2dda..32d0e5e795 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -134,10 +134,6 @@ export class App { import('./components/header.js'), ]); - // The AppState constructor started loading a fiddle. - // Wait for it here so the UI doesn't start life in `nonIdealState`. - await when(() => this.state.editorMosaic.files.size !== 0); - const app = (
@@ -148,6 +144,10 @@ export class App { const rendered = render(app, document.getElementById('app')); + // The AppState constructor started loading a fiddle. + // Wait for it here so the UI doesn't start life in `nonIdealState`. + await when(() => this.state.editorMosaic.files.size !== 0); + this.setupResizeListener(); this.setupOfflineListener(); this.setupThemeListeners(); diff --git a/src/renderer/remote-loader.ts b/src/renderer/remote-loader.ts index 3a309bca47..5ee38ec168 100644 --- a/src/renderer/remote-loader.ts +++ b/src/renderer/remote-loader.ts @@ -284,9 +284,9 @@ export class RemoteLoader { * Loading a fiddle from GitHub failed - this method handles this case * gracefully. */ - private handleLoadingFailed(error: Error): false { + private async handleLoadingFailed(error: Error): Promise { const failedLabel = `Loading the fiddle failed: ${error.message}`; - this.appState.showErrorDialog( + await this.appState.showErrorDialog( this.appState.isOnline ? failedLabel : `Your computer seems to be offline. ${failedLabel}`, diff --git a/src/renderer/runner.ts b/src/renderer/runner.ts index 684e181be9..7c45057a09 100644 --- a/src/renderer/runner.ts +++ b/src/renderer/runner.ts @@ -154,7 +154,7 @@ export class Runner { const { err, ver } = appState.isVersionUsable(version); if (!ver) { console.warn(`Running fiddle with version ('${version}') failed: ${err}`); - appState.showErrorDialog(err!); + await appState.showErrorDialog(err!); const fallback = appState.findUsableVersion(); if (fallback) await appState.setVersion(fallback.version); return RunResult.INVALID; @@ -167,7 +167,7 @@ export class Runner { const entryPoint = appState.editorMosaic.mainEntryPointFile(); if (entryPoint === MAIN_MJS) { - appState.showErrorDialog( + await appState.showErrorDialog( 'ESM main entry points are only supported starting in Electron 28', ); return RunResult.INVALID; @@ -255,7 +255,7 @@ export class Runner { message += `Node.js and npm, or https://classic.yarnpkg.com/lang/en/ `; message += `to install Yarn`; - this.appState.pushOutput(message, { isNotPre: true }); + pushOutput(message, { isNotPre: true }); return false; } diff --git a/src/renderer/state.ts b/src/renderer/state.ts index c421af435f..38910bf6d4 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -910,22 +910,32 @@ export class AppState { public findUsableVersion(): RunnableVersion | undefined { return this.versionsToShow.find((version) => { const { ver } = this.isVersionUsable(version.version); - return !!ver; + return ( + !!ver && + (ver.state === InstallState.installed || + ver.state === InstallState.downloaded) + ); }); } /** * Select a version of Electron (and download it if necessary). */ - public async setVersion(input: string): Promise { - const fallback = this.findUsableVersion(); - + public async setVersion( + input: string, + attemptsLeft: number = 3, + ): Promise { const { err, ver } = this.isVersionUsable(input); if (!ver) { console.error(`setVersion('${input}') failed: ${err}`); - this.showErrorDialog(err!); - if (fallback) await this.setVersion(fallback.version); - return; + await this.showErrorDialog(err!); + + const fallback = this.findUsableVersion(); + if (fallback) { + return this.setVersion(fallback.version, attemptsLeft); + } + + throw new Error(err); } const { version } = ver; @@ -935,14 +945,37 @@ export class AppState { try { await this.downloadVersion(ver); - } catch { - await this.removeVersion(ver); - console.error( - `setVersion('${input}') failed: Couldn't download ${version}`, + } catch (downloadErr) { + try { + await this.removeVersion(ver); + } catch (removeErr) { + console.warn('setVersion.removeVersion failed:', removeErr); + } + + if (attemptsLeft > 1) { + const backoffMs = 100 * (4 - attemptsLeft); + if (backoffMs > 0) { + await new Promise((r) => setTimeout(r, backoffMs)); + } + return this.setVersion(input, attemptsLeft - 1); + } + + const failedLabel = `Failed to download Electron version "${version}". Try again later.`; + const noInternetLabel = `Failed to download Electron version "${version}". Check your internet connection and try again.`; + + const userMessage = this.isOnline ? failedLabel : noInternetLabel; + await this.showErrorDialog(userMessage); + + const fallback = this.findUsableVersion(); + if (fallback) { + // If we found a usable fallback, try it once + return this.setVersion(fallback.version, 1); + } + + // Exhausted everything — propagate an error to caller + throw new Error( + `Failed to download Electron version "${version}": ${downloadErr}`, ); - this.showErrorDialog(`Failed to download Electron version ${version}`); - if (fallback) await this.setVersion(fallback.version); - return; } // If there's no current fiddle, diff --git a/tests/renderer/state-spec.ts b/tests/renderer/state-spec.ts index d4bff37084..d7d5508b31 100644 --- a/tests/renderer/state-spec.ts +++ b/tests/renderer/state-spec.ts @@ -404,6 +404,9 @@ describe('AppState', () => { describe('setVersion()', () => { it('uses the newest version iff the specified version does not exist', async () => { + // Mock so the "unknown version" error dialog resolves immediately; + // otherwise showGenericDialog waits on when() and the test times out. + appState.showErrorDialog = vi.fn().mockResolvedValue(undefined); await appState.setVersion('v999.99.99'); expect(appState.version).toBe(mockVersionsArray[0].version); }); @@ -416,16 +419,47 @@ describe('AppState', () => { }); it('falls back if downloading the new version fails', async () => { + // Reject 3 times so setVersion exhausts retries and shows the dialog; + // then resolve so the fallback setVersion(fallback.version) completes. appState.downloadVersion = vi .fn() - .mockRejectedValueOnce(new Error('FAILURE')); + .mockRejectedValueOnce(new Error('FAILURE')) + .mockRejectedValueOnce(new Error('FAILURE')) + .mockRejectedValueOnce(new Error('FAILURE')) + .mockResolvedValueOnce(undefined); appState.showGenericDialog = vi.fn().mockResolvedValueOnce({ confirm: true, + input: '', }); + // appState.isOnline = false; await appState.setVersion('v2.0.2'); expect(appState.showGenericDialog).toHaveBeenCalledWith({ - label: 'Failed to download Electron version 2.0.2', + label: `Failed to download Electron version "2.0.2". Try again later.`, + ok: 'Close', + type: GenericDialogType.warning, + wantsInput: false, + }); + }); + + it('falls back if downloading the new version fails and is not online', async () => { + // Reject 3 times so setVersion exhausts retries and shows the dialog; + // then resolve so the fallback setVersion(fallback.version) completes. + appState.downloadVersion = vi + .fn() + .mockRejectedValueOnce(new Error('FAILURE')) + .mockRejectedValueOnce(new Error('FAILURE')) + .mockRejectedValueOnce(new Error('FAILURE')) + .mockResolvedValueOnce(undefined); + appState.showGenericDialog = vi.fn().mockResolvedValueOnce({ + confirm: true, + input: '', + }); + appState.isOnline = false; + + await appState.setVersion('v2.0.2'); + expect(appState.showGenericDialog).toHaveBeenCalledWith({ + label: `Failed to download Electron version "2.0.2". Check your internet connection and try again.`, ok: 'Close', type: GenericDialogType.warning, wantsInput: false,