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,