Files
privacy.sexy/tests/unit/infrastructure/CodeRunner/Creation/Directory/PersistentDirectoryProvider.spec.ts
undergroundwires e09db0f1bd 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.
2024-01-14 22:35:53 +01:00

212 lines
8.7 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { Logger } from '@/application/Common/Log/Logger';
import { ExecutionSubdirectory, PersistentDirectoryProvider } from '@/infrastructure/CodeRunner/Creation/Directory/PersistentDirectoryProvider';
import { SystemOperations } from '@/infrastructure/CodeRunner/System/SystemOperations';
import { LocationOpsStub } from '@tests/unit/shared/Stubs/LocationOpsStub';
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
import { OperatingSystemOpsStub } from '@tests/unit/shared/Stubs/OperatingSystemOpsStub';
import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub';
import { FileSystemOpsStub } from '@tests/unit/shared/Stubs/FileSystemOpsStub';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
import { CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
import { expectTrue } from '@tests/shared/Assertions/ExpectTrue';
describe('PersistentDirectoryProvider', () => {
describe('createDirectory', () => {
describe('path construction', () => {
it('bases path on user directory', async () => {
// arrange
const expectedBaseDirectory = 'base-directory';
const pathSegmentSeparator = '/STUB-SEGMENT-SEPARATOR/';
const locationOps = new LocationOpsStub()
.withDefaultSeparator(pathSegmentSeparator);
const context = new PersistentDirectoryProviderTestSetup()
.withSystem(new SystemOperationsStub()
.withOperatingSystem(new OperatingSystemOpsStub()
.withUserDirectoryResult(expectedBaseDirectory))
.withLocation(locationOps));
// act
const { success, directoryAbsolutePath } = await context.provideScriptDirectory();
// assert
expectTrue(success);
const actualBaseDirectory = directoryAbsolutePath.split(pathSegmentSeparator)[0];
expect(actualBaseDirectory).to.equal(expectedBaseDirectory);
const calls = locationOps.callHistory.filter((call) => call.methodName === 'combinePaths');
expect(calls.length).to.equal(1);
const [combinedBaseDirectory] = calls[0].args;
expect(combinedBaseDirectory).to.equal(expectedBaseDirectory);
});
it('includes execution subdirectory in path', async () => {
// arrange
const expectedSubdirectory = ExecutionSubdirectory;
const pathSegmentSeparator = '/STUB-SEGMENT-SEPARATOR/';
const locationOps = new LocationOpsStub().withDefaultSeparator(pathSegmentSeparator);
const context = new PersistentDirectoryProviderTestSetup()
.withSystem(new SystemOperationsStub()
.withLocation(locationOps));
// act
const { success, directoryAbsolutePath } = await context.provideScriptDirectory();
// assert
expectTrue(success);
const actualSubdirectory = directoryAbsolutePath
.split(pathSegmentSeparator)
.pop();
expect(actualSubdirectory).to.equal(expectedSubdirectory);
const calls = locationOps.callHistory.filter((call) => call.methodName === 'combinePaths');
expect(calls.length).to.equal(1);
const [,combinedSubdirectory] = calls[0].args;
expect(combinedSubdirectory).to.equal(expectedSubdirectory);
});
it('forms full path correctly', async () => {
// arrange
const pathSegmentSeparator = '/';
const baseDirectory = 'base-directory';
const expectedDirectory = [baseDirectory, ExecutionSubdirectory].join(pathSegmentSeparator);
const context = new PersistentDirectoryProviderTestSetup()
.withSystem(new SystemOperationsStub()
.withLocation(new LocationOpsStub().withDefaultSeparator(pathSegmentSeparator))
.withOperatingSystem(
new OperatingSystemOpsStub().withUserDirectoryResult(baseDirectory),
));
// act
const { success, directoryAbsolutePath } = await context.provideScriptDirectory();
// assert
expectTrue(success);
expect(directoryAbsolutePath).to.equal(expectedDirectory);
});
});
describe('directory creation', () => {
it('creates directory with recursion', async () => {
// arrange
const expectedIsRecursive = true;
const filesystem = new FileSystemOpsStub();
const context = new PersistentDirectoryProviderTestSetup()
.withSystem(new SystemOperationsStub().withFileSystem(filesystem));
// act
const { success, directoryAbsolutePath } = await context.provideScriptDirectory();
// assert
expectTrue(success);
const calls = filesystem.callHistory.filter((call) => call.methodName === 'createDirectory');
expect(calls.length).to.equal(1);
const [actualPath, actualIsRecursive] = calls[0].args;
expect(actualPath).to.equal(directoryAbsolutePath);
expect(actualIsRecursive).to.equal(expectedIsRecursive);
});
});
describe('error handling', () => {
const testScenarios: ReadonlyArray<{
readonly description: string;
readonly expectedErrorType: CodeRunErrorType;
readonly expectedErrorMessage: string;
buildFaultyContext(
setup: PersistentDirectoryProviderTestSetup,
errorMessage: string,
): PersistentDirectoryProviderTestSetup;
}> = [
{
description: 'path combination failure',
expectedErrorType: 'DirectoryCreationError',
expectedErrorMessage: 'Error when combining paths',
buildFaultyContext: (setup, errorMessage) => {
const locationStub = new LocationOpsStub();
locationStub.combinePaths = () => {
throw new Error(errorMessage);
};
return setup.withSystem(new SystemOperationsStub().withLocation(locationStub));
},
},
{
description: 'user data retrieval failure',
expectedErrorType: 'DirectoryCreationError',
expectedErrorMessage: 'Error when locating user data directory',
buildFaultyContext: (setup, errorMessage) => {
const operatingSystemStub = new OperatingSystemOpsStub();
operatingSystemStub.getUserDataDirectory = () => {
throw new Error(errorMessage);
};
return setup.withSystem(
new SystemOperationsStub().withOperatingSystem(operatingSystemStub),
);
},
},
{
description: 'directory creation failure',
expectedErrorType: 'DirectoryCreationError',
expectedErrorMessage: 'Error when creating directory',
buildFaultyContext: (setup, errorMessage) => {
const fileSystemStub = new FileSystemOpsStub();
fileSystemStub.createDirectory = () => {
throw new Error(errorMessage);
};
return setup.withSystem(new SystemOperationsStub().withFileSystem(fileSystemStub));
},
},
];
testScenarios.forEach(({
description, expectedErrorType, expectedErrorMessage, buildFaultyContext,
}) => {
it(`handles error - ${description}`, async () => {
// arrange
const context = buildFaultyContext(
new PersistentDirectoryProviderTestSetup(),
expectedErrorMessage,
);
// act
const { success, error } = await context.provideScriptDirectory();
// assert
expect(success).to.equal(false);
expectExists(error);
expect(error.message).to.include(expectedErrorMessage);
expect(error.type).to.equal(expectedErrorType);
});
it(`logs error: ${description}`, async () => {
// arrange
const loggerStub = new LoggerStub();
const context = buildFaultyContext(
new PersistentDirectoryProviderTestSetup()
.withLogger(loggerStub),
expectedErrorMessage,
);
// act
await context.provideScriptDirectory();
// assert
loggerStub.assertLogsContainMessagePart('error', expectedErrorMessage);
});
});
});
});
});
class PersistentDirectoryProviderTestSetup {
private system: SystemOperations = new SystemOperationsStub();
private logger: Logger = new LoggerStub();
public withSystem(system: SystemOperations): this {
this.system = system;
return this;
}
public withLogger(logger: Logger): this {
this.logger = logger;
return this;
}
public provideScriptDirectory(): ReturnType<PersistentDirectoryProvider['provideScriptDirectory']> {
const provider = new PersistentDirectoryProvider(this.system, this.logger);
return provider.provideScriptDirectory();
}
}