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,7 +1,9 @@
|
||||
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
||||
import { CodeRunOutcome, CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
||||
|
||||
export class CodeRunnerStub implements CodeRunner {
|
||||
public runCode(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
public runCode(): Promise<CodeRunOutcome> {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { Dialog, SaveFileOutcome } from '@/presentation/common/Dialog';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class DialogStub implements Dialog {
|
||||
public saveFile(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
export class DialogStub
|
||||
extends StubWithObservableMethodCalls<Dialog>
|
||||
implements Dialog {
|
||||
public saveFile(...args: Parameters<Dialog['saveFile']>): Promise<SaveFileOutcome> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'saveFile',
|
||||
args: [...args],
|
||||
});
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
|
||||
public showError(...args: Parameters<Dialog['showError']>): void {
|
||||
this.registerMethodCall({
|
||||
methodName: 'showError',
|
||||
args: [...args],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FilenameGenerator } from '@/infrastructure/CodeRunner/Creation/Filename/FilenameGenerator';
|
||||
import { ScriptFileNameParts } from '@/infrastructure/CodeRunner/Creation/ScriptFileCreator';
|
||||
import { ScriptFilenameParts } from '@/infrastructure/CodeRunner/Creation/ScriptFileCreator';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class FilenameGeneratorStub
|
||||
@@ -7,7 +7,7 @@ export class FilenameGeneratorStub
|
||||
implements FilenameGenerator {
|
||||
private filename = `[${FilenameGeneratorStub.name}]file-name-stub`;
|
||||
|
||||
public generateFilename(scriptFileNameParts: ScriptFileNameParts): string {
|
||||
public generateFilename(scriptFileNameParts: ScriptFilenameParts): string {
|
||||
this.registerMethodCall({
|
||||
methodName: 'generateFilename',
|
||||
args: [scriptFileNameParts],
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { FunctionKeys, isString } from '@/TypeHelpers';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class LoggerStub extends StubWithObservableMethodCalls<Logger> implements Logger {
|
||||
@@ -29,4 +31,27 @@ export class LoggerStub extends StubWithObservableMethodCalls<Logger> implements
|
||||
args: params,
|
||||
});
|
||||
}
|
||||
|
||||
public assertLogsContainMessagePart(
|
||||
methodName: FunctionKeys<Logger>,
|
||||
expectedLogMessagePart: string,
|
||||
) {
|
||||
const loggedMessages = this.getLoggedMessages(methodName);
|
||||
expect(
|
||||
loggedMessages.some((m) => m.includes(expectedLogMessagePart)),
|
||||
formatAssertionMessage([
|
||||
`Log function: ${methodName}`,
|
||||
`Expected log message part: ${expectedLogMessagePart}`,
|
||||
'Actual log messages:',
|
||||
loggedMessages.join('\n- '),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
private getLoggedMessages(methodName: FunctionKeys<Logger>): string[] {
|
||||
const calls = this.callHistory.filter((m) => m.methodName === methodName);
|
||||
const loggedItems = calls.flatMap((call) => call.args);
|
||||
const stringLogs = loggedItems.filter((message): message is string => isString(message));
|
||||
return stringLogs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ScriptDirectoryProvider } from '@/infrastructure/CodeRunner/Creation/Directory/ScriptDirectoryProvider';
|
||||
import { ScriptDirectoryOutcome, ScriptDirectoryProvider } from '@/infrastructure/CodeRunner/Creation/Directory/ScriptDirectoryProvider';
|
||||
|
||||
export class ScriptDirectoryProviderStub implements ScriptDirectoryProvider {
|
||||
private directoryPath = `[${ScriptDirectoryProviderStub.name}]scriptDirectory`;
|
||||
@@ -8,7 +8,10 @@ export class ScriptDirectoryProviderStub implements ScriptDirectoryProvider {
|
||||
return this;
|
||||
}
|
||||
|
||||
public provideScriptDirectory(): Promise<string> {
|
||||
return Promise.resolve(this.directoryPath);
|
||||
public provideScriptDirectory(): Promise<ScriptDirectoryOutcome> {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
directoryAbsolutePath: this.directoryPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ScriptFileCreator, ScriptFileNameParts } from '@/infrastructure/CodeRunner/Creation/ScriptFileCreator';
|
||||
import { ScriptFileCreationOutcome, ScriptFileCreator, ScriptFilenameParts } from '@/infrastructure/CodeRunner/Creation/ScriptFileCreator';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class ScriptFileCreatorStub
|
||||
@@ -13,12 +13,15 @@ export class ScriptFileCreatorStub
|
||||
|
||||
public createScriptFile(
|
||||
contents: string,
|
||||
scriptFileNameParts: ScriptFileNameParts,
|
||||
): Promise<string> {
|
||||
scriptFileNameParts: ScriptFilenameParts,
|
||||
): Promise<ScriptFileCreationOutcome> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'createScriptFile',
|
||||
args: [contents, scriptFileNameParts],
|
||||
});
|
||||
return Promise.resolve(this.createdFilePath);
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
scriptFileAbsolutePath: this.createdFilePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { ScriptFileExecutor } from '@/infrastructure/CodeRunner/Execution/ScriptFileExecutor';
|
||||
import { ScriptFileExecutionOutcome, ScriptFileExecutor } from '@/infrastructure/CodeRunner/Execution/ScriptFileExecutor';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class ScriptFileExecutorStub
|
||||
extends StubWithObservableMethodCalls<ScriptFileExecutor>
|
||||
implements ScriptFileExecutor {
|
||||
public executeScriptFile(filePath: string): Promise<void> {
|
||||
public executeScriptFile(filePath: string): Promise<ScriptFileExecutionOutcome> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'executeScriptFile',
|
||||
args: [filePath],
|
||||
});
|
||||
return Promise.resolve();
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user