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.
127 lines
3.7 KiB
TypeScript
127 lines
3.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { FileSaverDialog, SaveAsFunction, WindowOpenFunction } from '@/infrastructure/Dialog/Browser/FileSaverDialog';
|
|
import { FileType } from '@/presentation/common/Dialog';
|
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
|
|
|
describe('FileSaverDialog', () => {
|
|
describe('saveFile', () => {
|
|
describe('saving file with correct mime type', () => {
|
|
const testCases: ReadonlyArray<{
|
|
readonly fileType: FileType;
|
|
readonly expectedMimeType: string;
|
|
}> = [
|
|
{ fileType: FileType.BatchFile, expectedMimeType: 'application/bat' },
|
|
{ fileType: FileType.ShellScript, expectedMimeType: 'text/x-shellscript' },
|
|
];
|
|
testCases.forEach(({ fileType, expectedMimeType }) => {
|
|
it(`correct mimeType for ${FileType[fileType]}`, () => {
|
|
// arrange
|
|
let actualMimeType: string | undefined;
|
|
const saveAsSpy: SaveAsFunction = (blob) => {
|
|
actualMimeType = blob.type;
|
|
};
|
|
|
|
// act
|
|
new SaveFileTestSetup()
|
|
.withFileType(fileType)
|
|
.withSaveAs(saveAsSpy)
|
|
.saveFile();
|
|
|
|
// assert
|
|
expect(actualMimeType).to.equal(expectedMimeType);
|
|
});
|
|
});
|
|
});
|
|
it('blob contains correct file contents', async () => {
|
|
// arrange
|
|
const expectedFileContents = 'expected file contents';
|
|
let actualBlob: Blob | undefined;
|
|
const saveAsSpy: SaveAsFunction = (blob) => {
|
|
actualBlob = blob;
|
|
};
|
|
|
|
// act
|
|
new SaveFileTestSetup()
|
|
.withSaveAs(saveAsSpy)
|
|
.withFileContents(expectedFileContents)
|
|
.saveFile();
|
|
|
|
// assert
|
|
expectExists(actualBlob);
|
|
const actualFileContents = await actualBlob.text();
|
|
expect(actualFileContents).to.equal(expectedFileContents);
|
|
});
|
|
it('opens new window on save failure', () => {
|
|
// arrange
|
|
const fileContents = 'test file contents';
|
|
const failingSaveAs: SaveAsFunction = () => {
|
|
throw new Error('injected fail');
|
|
};
|
|
let calledArgs: Parameters<WindowOpenFunction> | undefined;
|
|
const windowOpenSpy: WindowOpenFunction = (...args) => {
|
|
calledArgs = args;
|
|
};
|
|
|
|
// act
|
|
new SaveFileTestSetup()
|
|
.withSaveAs(failingSaveAs)
|
|
.withFileType(FileType.BatchFile)
|
|
.withFileContents(fileContents)
|
|
.withWindowOpen(windowOpenSpy)
|
|
.saveFile();
|
|
|
|
// assert
|
|
expectExists(calledArgs);
|
|
const [url, target, features] = calledArgs;
|
|
const mimeType = 'application/bat';
|
|
expect(url).to.equal(`data:${mimeType},${encodeURIComponent(fileContents)}`);
|
|
expect(target).to.equal('_blank');
|
|
expect(features).to.equal('');
|
|
});
|
|
});
|
|
});
|
|
|
|
class SaveFileTestSetup {
|
|
private saveAs: SaveAsFunction = () => {};
|
|
|
|
private windowOpen: WindowOpenFunction = () => {};
|
|
|
|
private fileContents: string = `${SaveFileTestSetup.name} file contents`;
|
|
|
|
private filename: string = `${SaveFileTestSetup.name} filename`;
|
|
|
|
private fileType: FileType = FileType.BatchFile;
|
|
|
|
public withSaveAs(saveAs: SaveAsFunction): this {
|
|
this.saveAs = saveAs;
|
|
return this;
|
|
}
|
|
|
|
public withFileContents(fileContents: string): this {
|
|
this.fileContents = fileContents;
|
|
return this;
|
|
}
|
|
|
|
public withFileType(fileType: FileType): this {
|
|
this.fileType = fileType;
|
|
return this;
|
|
}
|
|
|
|
public withWindowOpen(windowOpen: WindowOpenFunction): this {
|
|
this.windowOpen = windowOpen;
|
|
return this;
|
|
}
|
|
|
|
public saveFile() {
|
|
const dialog = new FileSaverDialog(
|
|
this.saveAs,
|
|
this.windowOpen,
|
|
);
|
|
return dialog.saveFile(
|
|
this.fileContents,
|
|
this.filename,
|
|
this.fileType,
|
|
);
|
|
}
|
|
}
|