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.
164 lines
5.5 KiB
TypeScript
164 lines
5.5 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { Logger } from '@/application/Common/Log/Logger';
|
|
import { decorateWithLogging } from '@/infrastructure/Dialog/LoggingDialogDecorator';
|
|
import { Dialog, FileType, SaveFileOutcome } from '@/presentation/common/Dialog';
|
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
|
import { DialogStub } from '@tests/unit/shared/Stubs/DialogStub';
|
|
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
|
|
|
describe('LoggingDialogDecorator', () => {
|
|
describe('decorateWithLogging', () => {
|
|
describe('saveFile', () => {
|
|
it('delegates call to dialog', async () => {
|
|
// arrange
|
|
const expectedArguments = createTestSaveFileArguments();
|
|
const dialog = new DialogStub();
|
|
const context = new LoggingDialogDecoratorTestSetup()
|
|
.withDialog(dialog);
|
|
|
|
// act
|
|
const decorator = context.decorateWithLogging();
|
|
await decorator.saveFile(...expectedArguments);
|
|
|
|
// assert
|
|
expect(dialog.callHistory).to.have.lengthOf(1);
|
|
const call = dialog.callHistory.find((c) => c.methodName === 'saveFile');
|
|
expectExists(call);
|
|
const actualArguments = call.args;
|
|
expect(expectedArguments).to.deep.equal(actualArguments);
|
|
});
|
|
it('returns dialog\'s response', async () => {
|
|
// arrange
|
|
const expectedResult: SaveFileOutcome = { success: true };
|
|
const dialog = new DialogStub();
|
|
dialog.saveFile = () => Promise.resolve(expectedResult);
|
|
const context = new LoggingDialogDecoratorTestSetup()
|
|
.withDialog(dialog);
|
|
|
|
// act
|
|
const decorator = context.decorateWithLogging();
|
|
const actualResult = await decorator.saveFile(...createTestSaveFileArguments());
|
|
|
|
// assert
|
|
expect(expectedResult).to.equal(actualResult);
|
|
});
|
|
it('logs information on invocation', async () => {
|
|
// arrange
|
|
const expectedLogMessagePart = 'Opening save file dialog';
|
|
const loggerStub = new LoggerStub();
|
|
const context = new LoggingDialogDecoratorTestSetup()
|
|
.withLogger(loggerStub);
|
|
|
|
// act
|
|
const decorator = context.decorateWithLogging();
|
|
await decorator.saveFile(...createTestSaveFileArguments());
|
|
|
|
// assert
|
|
loggerStub.assertLogsContainMessagePart('info', expectedLogMessagePart);
|
|
});
|
|
it('logs information on success', async () => {
|
|
// arrange
|
|
const expectedLogMessagePart = 'completed successfully';
|
|
const loggerStub = new LoggerStub();
|
|
const context = new LoggingDialogDecoratorTestSetup()
|
|
.withLogger(loggerStub);
|
|
|
|
// act
|
|
const decorator = context.decorateWithLogging();
|
|
await decorator.saveFile(...createTestSaveFileArguments());
|
|
|
|
// assert
|
|
loggerStub.assertLogsContainMessagePart('info', expectedLogMessagePart);
|
|
});
|
|
it('logs error on save failure', async () => {
|
|
// arrange
|
|
const expectedLogMessagePart = 'Error encountered';
|
|
const loggerStub = new LoggerStub();
|
|
const dialog = new DialogStub();
|
|
dialog.saveFile = () => Promise.resolve({ success: false, error: { message: 'error', type: 'DialogDisplayError' } });
|
|
const context = new LoggingDialogDecoratorTestSetup()
|
|
.withLogger(loggerStub);
|
|
|
|
// act
|
|
const decorator = context.decorateWithLogging();
|
|
await decorator.saveFile(...createTestSaveFileArguments());
|
|
|
|
// assert
|
|
loggerStub.assertLogsContainMessagePart('error', expectedLogMessagePart);
|
|
});
|
|
});
|
|
describe('showError', () => {
|
|
it('delegates call to the dialog', () => {
|
|
// arrange
|
|
const expectedArguments = createTestShowErrorArguments();
|
|
const dialog = new DialogStub();
|
|
const context = new LoggingDialogDecoratorTestSetup()
|
|
.withDialog(dialog);
|
|
|
|
// act
|
|
const decorator = context.decorateWithLogging();
|
|
decorator.showError(...expectedArguments);
|
|
|
|
// assert
|
|
expect(dialog.callHistory).to.have.lengthOf(1);
|
|
const call = dialog.callHistory.find((c) => c.methodName === 'showError');
|
|
expectExists(call);
|
|
const actualArguments = call.args;
|
|
expect(expectedArguments).to.deep.equal(actualArguments);
|
|
});
|
|
it('logs error message', () => {
|
|
// arrange
|
|
const expectedLogMessagePart = 'Showing error dialog';
|
|
const loggerStub = new LoggerStub();
|
|
const context = new LoggingDialogDecoratorTestSetup()
|
|
.withLogger(loggerStub);
|
|
|
|
// act
|
|
const decorator = context.decorateWithLogging();
|
|
decorator.showError(...createTestShowErrorArguments());
|
|
|
|
// assert
|
|
loggerStub.assertLogsContainMessagePart('error', expectedLogMessagePart);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
class LoggingDialogDecoratorTestSetup {
|
|
private dialog: Dialog = new DialogStub();
|
|
|
|
private logger: Logger = new LoggerStub();
|
|
|
|
public withDialog(dialog: Dialog): this {
|
|
this.dialog = dialog;
|
|
return this;
|
|
}
|
|
|
|
public withLogger(logger: Logger): this {
|
|
this.logger = logger;
|
|
return this;
|
|
}
|
|
|
|
public decorateWithLogging() {
|
|
return decorateWithLogging(
|
|
this.dialog,
|
|
this.logger,
|
|
);
|
|
}
|
|
}
|
|
|
|
function createTestSaveFileArguments(): Parameters<Dialog['saveFile']> {
|
|
return [
|
|
'test-file-contents',
|
|
'test-default-filename',
|
|
FileType.BatchFile,
|
|
];
|
|
}
|
|
|
|
function createTestShowErrorArguments(): Parameters<Dialog['showError']> {
|
|
return [
|
|
'test-error-title',
|
|
'test-error-message',
|
|
];
|
|
}
|