Show save/execution error dialogs on desktop #264
This commit introduces system-native error dialogs on desktop application for code save or execution failures, addressing user confusion described in issue #264. This commit adds informative feedback when script execution or saving fails. Changes: - Implement support for system-native error dialogs. - Refactor `CodeRunner` and `Dialog` interfaces and their implementations to improve error handling and provide better type safety. - Introduce structured error handling, allowing UI to display detailed error messages. - Replace error throwing with an error object interface for controlled handling. This ensures that errors are propagated to the renderer process without being limited by Electron's error object serialization limitations as detailed in electron/electron#24427. - Add logging for dialog actions to aid in troubleshooting. - Rename `fileName` to `defaultFilename` in `saveFile` functions to clarify its purpose. - Centralize message assertion in `LoggerStub` for consistency. - Introduce `expectTrue` in tests for clearer boolean assertions. - Standardize `filename` usage across the codebase. - Enhance existing test names and organization for clarity. - Update related documentation.
This commit is contained in:
@@ -1,54 +1,94 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { determineDialogBasedOnEnvironment, WindowDialogCreationFunction, BrowserDialogCreationFunction } from '@/presentation/components/Shared/Hooks/Dialog/ClientDialogFactory';
|
||||
import {
|
||||
createEnvironmentSpecificLoggedDialog, WindowDialogCreationFunction,
|
||||
BrowserDialogCreationFunction, DialogLoggingDecorator,
|
||||
} from '@/presentation/components/Shared/Hooks/Dialog/ClientDialogFactory';
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub';
|
||||
import { DialogStub } from '@tests/unit/shared/Stubs/DialogStub';
|
||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
|
||||
describe('ClientDialogFactory', () => {
|
||||
describe('determineDialogBasedOnEnvironment', () => {
|
||||
describe('non-desktop environment', () => {
|
||||
it('returns browser dialog', () => {
|
||||
// arrange
|
||||
const expectedDialog = new DialogStub();
|
||||
const context = new DialogCreationTestSetup()
|
||||
.withEnvironment(new RuntimeEnvironmentStub().withIsRunningAsDesktopApplication(false))
|
||||
.withBrowserDialogFactory(() => expectedDialog);
|
||||
describe('createEnvironmentSpecificLoggedDialog', () => {
|
||||
describe('dialog selection based on environment', () => {
|
||||
describe('when in non-desktop environment', () => {
|
||||
it('provides a browser dialog', () => {
|
||||
// arrange
|
||||
const expectedDialog = new DialogStub();
|
||||
const context = new DialogCreationTestSetup()
|
||||
.withEnvironment(new RuntimeEnvironmentStub().withIsRunningAsDesktopApplication(false))
|
||||
.withBrowserDialogFactory(() => expectedDialog);
|
||||
|
||||
// act
|
||||
const actualDialog = context.createDialogForTest();
|
||||
// act
|
||||
const actualDialog = context.createDialogForTest();
|
||||
|
||||
// assert
|
||||
expect(expectedDialog).to.equal(actualDialog);
|
||||
// assert
|
||||
expect(expectedDialog).to.equal(actualDialog);
|
||||
});
|
||||
});
|
||||
describe('when in desktop environment', () => {
|
||||
it('provides a window-injected dialog', () => {
|
||||
// arrange
|
||||
const expectedDialog = new DialogStub();
|
||||
const context = new DialogCreationTestSetup()
|
||||
.withEnvironment(new RuntimeEnvironmentStub().withIsRunningAsDesktopApplication(true))
|
||||
.withWindowInjectedDialogFactory(() => expectedDialog);
|
||||
|
||||
// act
|
||||
const actualDialog = context.createDialogForTest();
|
||||
|
||||
// assert
|
||||
expect(expectedDialog).to.equal(actualDialog);
|
||||
});
|
||||
it('throws error if window-injected dialog is not available', () => {
|
||||
// arrange
|
||||
const expectedError = 'Failed to retrieve Dialog API from window object in desktop environment.';
|
||||
const context = new DialogCreationTestSetup()
|
||||
.withEnvironment(new RuntimeEnvironmentStub().withIsRunningAsDesktopApplication(true))
|
||||
.withWindowInjectedDialogFactory(() => undefined);
|
||||
|
||||
// act
|
||||
const act = () => context.createDialogForTest();
|
||||
|
||||
// assert
|
||||
const actualError = collectExceptionMessage(act);
|
||||
expect(actualError).to.include(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('desktop environment', () => {
|
||||
it('returns window-injected dialog', () => {
|
||||
describe('dialog decoration with logging', () => {
|
||||
it('returns a dialog decorated with logging', () => {
|
||||
// arrange
|
||||
const expectedDialog = new DialogStub();
|
||||
const expectedLoggingDialogStub = new DialogStub();
|
||||
const decoratorStub: DialogLoggingDecorator = () => expectedLoggingDialogStub;
|
||||
const context = new DialogCreationTestSetup()
|
||||
.withEnvironment(new RuntimeEnvironmentStub().withIsRunningAsDesktopApplication(true))
|
||||
.withWindowInjectedDialogFactory(() => expectedDialog);
|
||||
.withDialogLoggingDecorator(decoratorStub);
|
||||
|
||||
// act
|
||||
const actualDialog = context.createDialogForTest();
|
||||
|
||||
// assert
|
||||
expect(expectedDialog).to.equal(actualDialog);
|
||||
expect(expectedLoggingDialogStub).to.equal(actualDialog);
|
||||
});
|
||||
it('throws error when window-injected dialog is unavailable', () => {
|
||||
it('applies logging decorator to the provided dialog', () => {
|
||||
// arrange
|
||||
const expectedError = 'The Dialog API could not be retrieved from the window object.';
|
||||
const expectedDialog = new DialogStub();
|
||||
let actualDecoratedDialog: Dialog | undefined;
|
||||
const decoratorStub: DialogLoggingDecorator = (dialog) => {
|
||||
actualDecoratedDialog = dialog;
|
||||
return new DialogStub();
|
||||
};
|
||||
const context = new DialogCreationTestSetup()
|
||||
.withEnvironment(new RuntimeEnvironmentStub().withIsRunningAsDesktopApplication(true))
|
||||
.withWindowInjectedDialogFactory(() => undefined);
|
||||
.withEnvironment(new RuntimeEnvironmentStub().withIsRunningAsDesktopApplication(false))
|
||||
.withBrowserDialogFactory(() => expectedDialog)
|
||||
.withDialogLoggingDecorator(decoratorStub);
|
||||
|
||||
// act
|
||||
const act = () => context.createDialogForTest();
|
||||
context.createDialogForTest();
|
||||
|
||||
// assert
|
||||
const actualError = collectExceptionMessage(act);
|
||||
expect(actualError).to.include(expectedError);
|
||||
expect(expectedDialog).to.equal(actualDecoratedDialog);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -61,6 +101,8 @@ class DialogCreationTestSetup {
|
||||
|
||||
private windowInjectedDialogFactory: WindowDialogCreationFunction = () => new DialogStub();
|
||||
|
||||
private dialogLoggingDecorator: DialogLoggingDecorator = (dialog) => dialog;
|
||||
|
||||
public withEnvironment(environment: RuntimeEnvironment): this {
|
||||
this.environment = environment;
|
||||
return this;
|
||||
@@ -78,9 +120,17 @@ class DialogCreationTestSetup {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withDialogLoggingDecorator(
|
||||
dialogLoggingDecorator: DialogLoggingDecorator,
|
||||
): this {
|
||||
this.dialogLoggingDecorator = dialogLoggingDecorator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public createDialogForTest() {
|
||||
return determineDialogBasedOnEnvironment(
|
||||
return createEnvironmentSpecificLoggedDialog(
|
||||
this.environment,
|
||||
this.dialogLoggingDecorator,
|
||||
this.windowInjectedDialogFactory,
|
||||
this.browserDialogFactory,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user