Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/renderer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
<div className="container">
<Header appState={this.state} />
Expand All @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/remote-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<false> {
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}`,
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
61 changes: 47 additions & 14 deletions src/renderer/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const fallback = this.findUsableVersion();

public async setVersion(
input: string,
attemptsLeft: number = 3,
): Promise<void> {
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;
Expand All @@ -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,
Expand Down
38 changes: 36 additions & 2 deletions tests/renderer/state-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand All @@ -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,
Expand Down