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,32 +1,104 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { FileType } from '@/presentation/common/Dialog';
|
||||
import { BrowserDialog } from '@/infrastructure/Dialog/Browser/BrowserDialog';
|
||||
import { FileType, SaveFileOutcome } from '@/presentation/common/Dialog';
|
||||
import { ElectronSaveFileDialog } from '@/infrastructure/Dialog/Electron/ElectronSaveFileDialog';
|
||||
import { ElectronDialog, ElectronDialogAccessor } from '@/infrastructure/Dialog/Electron/ElectronDialog';
|
||||
|
||||
describe('BrowserDialog', () => {
|
||||
describe('ElectronDialog', () => {
|
||||
describe('saveFile', () => {
|
||||
it('passes correct arguments', async () => {
|
||||
it('forwards arguments', async () => {
|
||||
// arrange
|
||||
const expectedFileContents = 'test content';
|
||||
const expectedFileName = 'test.sh';
|
||||
const expectedFileType = FileType.ShellScript;
|
||||
const expectedSaveFileArgs = createTestSaveFileArguments();
|
||||
let actualSaveFileArgs: Parameters<ElectronSaveFileDialog['saveFile']> | undefined;
|
||||
const fileSaverDialogSpy: ElectronSaveFileDialog = {
|
||||
saveFile: (...args) => {
|
||||
actualSaveFileArgs = args;
|
||||
return Promise.resolve();
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
const browserDialog = new BrowserDialog(fileSaverDialogSpy);
|
||||
const electronDialog = new ElectronDialogBuilder()
|
||||
.withSaveFileDialog(fileSaverDialogSpy)
|
||||
.build();
|
||||
|
||||
// act
|
||||
await browserDialog.saveFile(expectedFileContents, expectedFileName, expectedFileType);
|
||||
await electronDialog.saveFile(...expectedSaveFileArgs);
|
||||
|
||||
// assert
|
||||
expect(actualSaveFileArgs)
|
||||
.to
|
||||
.deep
|
||||
.equal([expectedFileContents, expectedFileName, expectedFileType]);
|
||||
expect(actualSaveFileArgs).to.deep.equal(expectedSaveFileArgs);
|
||||
});
|
||||
it('forwards outcome', async () => {
|
||||
// arrange
|
||||
const expectedResult: SaveFileOutcome = {
|
||||
success: true,
|
||||
};
|
||||
const fileSaverDialogMock: ElectronSaveFileDialog = {
|
||||
saveFile: () => Promise.resolve(expectedResult),
|
||||
};
|
||||
const browserDialog = new ElectronDialogBuilder()
|
||||
.withSaveFileDialog(fileSaverDialogMock)
|
||||
.build();
|
||||
|
||||
// act
|
||||
const actualResult = await browserDialog.saveFile(...createTestSaveFileArguments());
|
||||
|
||||
// assert
|
||||
expect(actualResult).to.equal(expectedResult);
|
||||
});
|
||||
});
|
||||
describe('showError', () => {
|
||||
it('forwards arguments', () => {
|
||||
// arrange
|
||||
const expectedShowErrorArguments: Parameters<ElectronDialog['showError']> = [
|
||||
'test title', 'test message',
|
||||
];
|
||||
let actualShowErrorArgs: Parameters<ElectronDialogAccessor['showErrorBox']> | undefined;
|
||||
const electronDialogAccessorSpy: ElectronDialogAccessor = {
|
||||
showErrorBox: (...args) => {
|
||||
actualShowErrorArgs = args;
|
||||
},
|
||||
};
|
||||
const electronDialog = new ElectronDialogBuilder()
|
||||
.withElectron(electronDialogAccessorSpy)
|
||||
.build();
|
||||
|
||||
// act
|
||||
electronDialog.showError(...expectedShowErrorArguments);
|
||||
|
||||
// assert
|
||||
expect(actualShowErrorArgs).to.deep.equal(expectedShowErrorArguments);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createTestSaveFileArguments(): Parameters<ElectronSaveFileDialog['saveFile']> {
|
||||
return [
|
||||
'test file content',
|
||||
'test filename',
|
||||
FileType.ShellScript,
|
||||
];
|
||||
}
|
||||
|
||||
class ElectronDialogBuilder {
|
||||
private electron: ElectronDialogAccessor = {
|
||||
showErrorBox: () => {},
|
||||
};
|
||||
|
||||
private saveFileDialog: ElectronSaveFileDialog = {
|
||||
saveFile: () => Promise.resolve({ success: true }),
|
||||
};
|
||||
|
||||
public withElectron(electron: ElectronDialogAccessor): this {
|
||||
this.electron = electron;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSaveFileDialog(saveFileDialog: ElectronSaveFileDialog): this {
|
||||
this.saveFileDialog = saveFileDialog;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build() {
|
||||
return new ElectronDialog(this.saveFileDialog, this.electron);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user