Improve script error dialogs #304
- Include the script's directory path #304. - Exclude Windows-specific instructions on non-Windows OS. - Standardize language across dialogs for consistency. Other supporting changes: - Add script diagnostics data collection from main process. - Document script file storage and execution tamper protection in SECURITY.md. - Remove redundant comment in `NodeReadbackFileWriter`. - Centralize error display for uniformity and simplicity. - Simpify `WindowVariablesValidator` to omit checks when not on the renderer process. - Improve and centralize Electron environment detection. - Use more emphatic language (don't worry) in error messages.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ContextIsolatedElectronDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ContextIsolatedElectronDetector';
|
||||
import { ElectronProcessType } from '@/infrastructure/RuntimeEnvironment/Electron/ElectronEnvironmentDetector';
|
||||
|
||||
describe('ContextIsolatedElectronDetector', () => {
|
||||
describe('isRunningInsideElectron', () => {
|
||||
describe('detects Electron environment correctly', () => {
|
||||
it('returns true on Electron main process', () => {
|
||||
// arrange
|
||||
const expectedValue = true;
|
||||
const process = createProcessStub({ isElectron: true });
|
||||
const userAgent = undefined;
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.isRunningInsideElectron();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedValue);
|
||||
});
|
||||
it('returns true on Electron preloader process', () => {
|
||||
// arrange
|
||||
const expectedValue = true;
|
||||
const process = createProcessStub({ isElectron: true });
|
||||
const userAgent = getElectronUserAgent();
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.isRunningInsideElectron();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedValue);
|
||||
});
|
||||
it('returns true on Electron renderer process', () => {
|
||||
// arrange
|
||||
const expectedValue = true;
|
||||
const process = undefined;
|
||||
const userAgent = getElectronUserAgent();
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.isRunningInsideElectron();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedValue);
|
||||
});
|
||||
it('returns false on non-Electron environment', () => {
|
||||
// arrange
|
||||
const expectedValue = false;
|
||||
const process = undefined;
|
||||
const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'; // non-Electron
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.isRunningInsideElectron();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedValue);
|
||||
});
|
||||
});
|
||||
describe('determineElectronProcessType', () => {
|
||||
it('gets Electron process type as main', () => {
|
||||
// arrange
|
||||
const expectedProcessType: ElectronProcessType = 'main';
|
||||
const process = createProcessStub({ isElectron: true });
|
||||
const userAgent = undefined;
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.determineElectronProcessType();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedProcessType);
|
||||
});
|
||||
it('gets Electron process type as preloader', () => {
|
||||
// arrange
|
||||
const expectedProcessType: ElectronProcessType = 'preloader';
|
||||
const process = createProcessStub({ isElectron: true });
|
||||
const userAgent = getElectronUserAgent();
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.determineElectronProcessType();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedProcessType);
|
||||
});
|
||||
it('gets Electron process type as renderer', () => {
|
||||
// arrange
|
||||
const expectedProcessType: ElectronProcessType = 'renderer';
|
||||
const process = undefined;
|
||||
const userAgent = getElectronUserAgent();
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.determineElectronProcessType();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedProcessType);
|
||||
});
|
||||
it('throws non-Electron environment', () => {
|
||||
// arrange
|
||||
const expectedError = 'Unable to determine the Electron process type. Neither Node.js nor browser-based Electron contexts were detected.';
|
||||
const process = undefined;
|
||||
const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'; // non-Electron
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const act = () => detector.determineElectronProcessType();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class ContextIsolatedElectronDetectorBuilder {
|
||||
private process: NodeJS.Process | undefined;
|
||||
|
||||
private userAgent: string | undefined;
|
||||
|
||||
public withProcess(process: NodeJS.Process | undefined): this {
|
||||
this.process = process;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withUserAgent(userAgent: string | undefined): this {
|
||||
this.userAgent = userAgent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(): ContextIsolatedElectronDetector {
|
||||
return new ContextIsolatedElectronDetector(
|
||||
() => this.process,
|
||||
() => this.userAgent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getElectronUserAgent() {
|
||||
return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.54 Electron/27.0.0 Safari/537.36';
|
||||
}
|
||||
|
||||
function createProcessStub(options?: {
|
||||
readonly isElectron: boolean;
|
||||
}): NodeJS.Process {
|
||||
if (options?.isElectron === true) {
|
||||
return {
|
||||
versions: {
|
||||
electron: '28.1.3',
|
||||
} as NodeJS.ProcessVersions,
|
||||
} as NodeJS.Process;
|
||||
}
|
||||
return {} as NodeJS.Process;
|
||||
}
|
||||
@@ -1,58 +1,38 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
BrowserRuntimeEnvironmentFactory, GlobalPropertiesAccessor, NodeRuntimeEnvironmentFactory,
|
||||
BrowserRuntimeEnvironmentFactory, NodeRuntimeEnvironmentFactory,
|
||||
determineAndCreateRuntimeEnvironment,
|
||||
} from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub';
|
||||
import { ElectronEnvironmentDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ElectronEnvironmentDetector';
|
||||
import { ElectronEnvironmentDetectorStub } from '@tests/unit/shared/Stubs/ElectronEnvironmentDetectorStub';
|
||||
|
||||
describe('RuntimeEnvironmentFactory', () => {
|
||||
describe('determineAndCreateRuntimeEnvironment', () => {
|
||||
describe('Node environment creation', () => {
|
||||
it('selects Node environment if Electron main process detected', () => {
|
||||
it('creates Node environment in Electron main process', () => {
|
||||
// arrange
|
||||
const processStub = createProcessStub({
|
||||
versions: {
|
||||
electron: '28.1.3',
|
||||
} as NodeJS.ProcessVersions,
|
||||
});
|
||||
const expectedEnvironment = new RuntimeEnvironmentStub();
|
||||
const mainProcessDetector = new ElectronEnvironmentDetectorStub()
|
||||
.withElectronEnvironment('main');
|
||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||
.withGlobalProcess(processStub)
|
||||
.withElectronEnvironmentDetector(mainProcessDetector)
|
||||
.withNodeEnvironmentFactory(() => expectedEnvironment);
|
||||
// act
|
||||
const actualEnvironment = context.buildEnvironment();
|
||||
// assert
|
||||
expect(actualEnvironment).to.equal(expectedEnvironment);
|
||||
});
|
||||
it('passes correct process to Node environment factory', () => {
|
||||
// arrange
|
||||
const expectedProcess = createProcessStub({
|
||||
versions: {
|
||||
electron: '28.1.3',
|
||||
} as NodeJS.ProcessVersions,
|
||||
});
|
||||
let actualProcess: GlobalProcess;
|
||||
const nodeEnvironmentFactoryMock: NodeRuntimeEnvironmentFactory = (providedProcess) => {
|
||||
actualProcess = providedProcess;
|
||||
return new RuntimeEnvironmentStub();
|
||||
};
|
||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||
.withGlobalProcess(expectedProcess)
|
||||
.withNodeEnvironmentFactory(nodeEnvironmentFactoryMock);
|
||||
// act
|
||||
context.buildEnvironment();
|
||||
// assert
|
||||
expect(actualProcess).to.equal(expectedProcess);
|
||||
});
|
||||
});
|
||||
describe('browser environment creation', () => {
|
||||
it('selects browser environment if Electron main process not detected', () => {
|
||||
it('creates browser environment in Electron renderer process', () => {
|
||||
// arrange
|
||||
const expectedEnvironment = new RuntimeEnvironmentStub();
|
||||
const undefinedProcess: GlobalProcess = undefined;
|
||||
const rendererProcessDetector = new ElectronEnvironmentDetectorStub()
|
||||
.withElectronEnvironment('renderer');
|
||||
const windowStub = createWindowStub();
|
||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||
.withGlobalProcess(undefinedProcess)
|
||||
.withElectronEnvironmentDetector(rendererProcessDetector)
|
||||
.withGlobalWindow(windowStub)
|
||||
.withBrowserEnvironmentFactory(() => expectedEnvironment);
|
||||
// act
|
||||
@@ -60,21 +40,37 @@ describe('RuntimeEnvironmentFactory', () => {
|
||||
// assert
|
||||
expect(actualEnvironment).to.equal(expectedEnvironment);
|
||||
});
|
||||
it('passes correct window to browser environment factory', () => {
|
||||
it('creates browser environment in Electron preloader process', () => {
|
||||
// arrange
|
||||
const expectedEnvironment = new RuntimeEnvironmentStub();
|
||||
const preloaderProcessDetector = new ElectronEnvironmentDetectorStub()
|
||||
.withElectronEnvironment('preloader');
|
||||
const windowStub = createWindowStub();
|
||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||
.withElectronEnvironmentDetector(preloaderProcessDetector)
|
||||
.withGlobalWindow(windowStub)
|
||||
.withBrowserEnvironmentFactory(() => expectedEnvironment);
|
||||
// act
|
||||
const actualEnvironment = context.buildEnvironment();
|
||||
// assert
|
||||
expect(actualEnvironment).to.equal(expectedEnvironment);
|
||||
});
|
||||
it('provides correct window to browser environment factory', () => {
|
||||
// arrange
|
||||
const expectedWindow = createWindowStub({
|
||||
isRunningAsDesktopApplication: undefined,
|
||||
});
|
||||
let actualWindow: GlobalWindow;
|
||||
let actualWindow: Window | undefined;
|
||||
const browserEnvironmentFactoryMock
|
||||
: BrowserRuntimeEnvironmentFactory = (providedWindow) => {
|
||||
actualWindow = providedWindow;
|
||||
return new RuntimeEnvironmentStub();
|
||||
};
|
||||
const undefinedProcess: GlobalProcess = undefined;
|
||||
const nonElectronDetector = new ElectronEnvironmentDetectorStub()
|
||||
.withNonElectronEnvironment();
|
||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||
.withGlobalWindow(expectedWindow)
|
||||
.withGlobalProcess(undefinedProcess)
|
||||
.withElectronEnvironmentDetector(nonElectronDetector)
|
||||
.withBrowserEnvironmentFactory(browserEnvironmentFactoryMock);
|
||||
// act
|
||||
context.buildEnvironment();
|
||||
@@ -82,14 +78,15 @@ describe('RuntimeEnvironmentFactory', () => {
|
||||
expect(actualWindow).to.equal(expectedWindow);
|
||||
});
|
||||
});
|
||||
it('throws error when both window and process are undefined', () => {
|
||||
it('throws error without global window in non-Electron environment', () => {
|
||||
// arrange
|
||||
const undefinedWindow: GlobalWindow = undefined;
|
||||
const undefinedProcess: GlobalProcess = undefined;
|
||||
const expectedError = 'Unsupported runtime environment: The current context is neither a recognized browser nor a desktop environment.';
|
||||
const nullWindow = null;
|
||||
const nonElectronDetector = new ElectronEnvironmentDetectorStub()
|
||||
.withNonElectronEnvironment();
|
||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||
.withGlobalProcess(undefinedProcess)
|
||||
.withGlobalWindow(undefinedWindow);
|
||||
.withElectronEnvironmentDetector(nonElectronDetector)
|
||||
.withGlobalWindow(nullWindow);
|
||||
// act
|
||||
const act = () => context.buildEnvironment();
|
||||
// assert
|
||||
@@ -104,16 +101,11 @@ function createWindowStub(partialWindowProperties?: Partial<Window>): Window {
|
||||
} as Window;
|
||||
}
|
||||
|
||||
function createProcessStub(partialProcessProperties?: Partial<NodeJS.Process>): NodeJS.Process {
|
||||
return {
|
||||
...partialProcessProperties,
|
||||
} as NodeJS.Process;
|
||||
}
|
||||
|
||||
export class RuntimeEnvironmentFactoryTestSetup {
|
||||
private globalWindow: GlobalWindow = createWindowStub();
|
||||
private globalWindow: Window | undefined | null = createWindowStub();
|
||||
|
||||
private globalProcess: GlobalProcess = createProcessStub();
|
||||
private electronEnvironmentDetector
|
||||
: ElectronEnvironmentDetector = new ElectronEnvironmentDetectorStub();
|
||||
|
||||
private browserEnvironmentFactory
|
||||
: BrowserRuntimeEnvironmentFactory = () => new RuntimeEnvironmentStub();
|
||||
@@ -121,13 +113,13 @@ export class RuntimeEnvironmentFactoryTestSetup {
|
||||
private nodeEnvironmentFactory
|
||||
: NodeRuntimeEnvironmentFactory = () => new RuntimeEnvironmentStub();
|
||||
|
||||
public withGlobalWindow(globalWindow: GlobalWindow): this {
|
||||
public withGlobalWindow(globalWindow: Window | undefined | null): this {
|
||||
this.globalWindow = globalWindow;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withGlobalProcess(globalProcess: GlobalProcess): this {
|
||||
this.globalProcess = globalProcess;
|
||||
public withElectronEnvironmentDetector(detector: ElectronEnvironmentDetector): this {
|
||||
this.electronEnvironmentDetector = detector;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -147,16 +139,10 @@ export class RuntimeEnvironmentFactoryTestSetup {
|
||||
|
||||
public buildEnvironment(): ReturnType<typeof determineAndCreateRuntimeEnvironment> {
|
||||
return determineAndCreateRuntimeEnvironment(
|
||||
{
|
||||
window: this.globalWindow,
|
||||
process: this.globalProcess,
|
||||
},
|
||||
this.globalWindow,
|
||||
this.electronEnvironmentDetector,
|
||||
this.browserEnvironmentFactory,
|
||||
this.nodeEnvironmentFactory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type GlobalWindow = GlobalPropertiesAccessor['window'];
|
||||
|
||||
type GlobalProcess = GlobalPropertiesAccessor['process'];
|
||||
|
||||
@@ -2,222 +2,238 @@ import { describe, it, expect } from 'vitest';
|
||||
import { validateWindowVariables } from '@/infrastructure/WindowVariables/WindowVariablesValidator';
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { getAbsentObjectTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { getAbsentObjectTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { WindowVariablesStub } from '@tests/unit/shared/Stubs/WindowVariablesStub';
|
||||
import { CodeRunnerStub } from '@tests/unit/shared/Stubs/CodeRunnerStub';
|
||||
import { PropertyKeys } from '@/TypeHelpers';
|
||||
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
||||
import { DialogStub } from '@tests/unit/shared/Stubs/DialogStub';
|
||||
import { ScriptDiagnosticsCollectorStub } from '@tests/unit/shared/Stubs/ScriptDiagnosticsCollectorStub';
|
||||
import { ElectronEnvironmentDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ElectronEnvironmentDetector';
|
||||
import { ElectronEnvironmentDetectorStub } from '@tests/unit/shared/Stubs/ElectronEnvironmentDetectorStub';
|
||||
|
||||
describe('WindowVariablesValidator', () => {
|
||||
describe('validateWindowVariables', () => {
|
||||
describe('validates window type', () => {
|
||||
itEachInvalidObjectValue((invalidObjectValue) => {
|
||||
// arrange
|
||||
const expectedError = 'window is not an object';
|
||||
const window: Partial<WindowVariables> = invalidObjectValue as never;
|
||||
// act
|
||||
const act = () => validateWindowVariables(window);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('property validations', () => {
|
||||
it('throws an error with a description of all invalid properties', () => {
|
||||
// arrange
|
||||
const invalidOs = 'invalid' as unknown as OperatingSystem;
|
||||
const invalidIsRunningAsDesktopApplication = 'not a boolean' as never;
|
||||
const expectedError = getExpectedError(
|
||||
{
|
||||
name: 'os',
|
||||
object: invalidOs,
|
||||
},
|
||||
{
|
||||
name: 'isRunningAsDesktopApplication',
|
||||
object: invalidIsRunningAsDesktopApplication,
|
||||
},
|
||||
);
|
||||
const input = new WindowVariablesStub()
|
||||
.withOs(invalidOs)
|
||||
.withIsRunningAsDesktopApplication(invalidIsRunningAsDesktopApplication);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
|
||||
describe('`os` property', () => {
|
||||
it('throws an error when os is not a number', () => {
|
||||
// arrange
|
||||
const invalidOs = 'Linux' as unknown as OperatingSystem;
|
||||
const expectedError = getExpectedError(
|
||||
{
|
||||
name: 'os',
|
||||
object: invalidOs,
|
||||
},
|
||||
);
|
||||
const input = new WindowVariablesStub()
|
||||
.withOs(invalidOs);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
|
||||
it('throws an error for an invalid numeric os value', () => {
|
||||
// arrange
|
||||
const invalidOs = Number.MAX_SAFE_INTEGER;
|
||||
const expectedError = getExpectedError(
|
||||
{
|
||||
name: 'os',
|
||||
object: invalidOs,
|
||||
},
|
||||
);
|
||||
const input = new WindowVariablesStub()
|
||||
.withOs(invalidOs);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
|
||||
it('does not throw for a missing os value', () => {
|
||||
// arrange
|
||||
const input = new WindowVariablesStub()
|
||||
.withIsRunningAsDesktopApplication(true)
|
||||
.withOs(undefined);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
|
||||
describe('`isRunningAsDesktopApplication` property', () => {
|
||||
it('does not throw when true with valid services', () => {
|
||||
// arrange
|
||||
const windowVariables = new WindowVariablesStub();
|
||||
const windowVariableConfigurators: Record< // Ensure types match for compile-time checking
|
||||
PropertyKeys<Required<WindowVariables>>,
|
||||
(stub: WindowVariablesStub) => WindowVariablesStub> = {
|
||||
isRunningAsDesktopApplication: (s) => s.withIsRunningAsDesktopApplication(true),
|
||||
codeRunner: (s) => s.withCodeRunner(new CodeRunnerStub()),
|
||||
os: (s) => s.withOs(OperatingSystem.Windows),
|
||||
log: (s) => s.withLog(new LoggerStub()),
|
||||
dialog: (s) => s.withDialog(new DialogStub()),
|
||||
};
|
||||
Object
|
||||
.values(windowVariableConfigurators)
|
||||
.forEach((configure) => configure(windowVariables));
|
||||
// act
|
||||
const act = () => validateWindowVariables(windowVariables);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
|
||||
describe('does not throw when false without services', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const absentCodeRunner = absentValue;
|
||||
const input = new WindowVariablesStub()
|
||||
.withIsRunningAsDesktopApplication(undefined)
|
||||
.withCodeRunner(absentCodeRunner);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
}, { excludeNull: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('`codeRunner` property', () => {
|
||||
expectObjectOnDesktop('codeRunner');
|
||||
});
|
||||
|
||||
describe('`log` property', () => {
|
||||
expectObjectOnDesktop('log');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not throw for a valid object', () => {
|
||||
const input = new WindowVariablesStub();
|
||||
it('throws an error with a description of all invalid properties', () => {
|
||||
// arrange
|
||||
const invalidOs = 'invalid' as unknown as OperatingSystem;
|
||||
const invalidIsRunningAsDesktopApplication = 'not a boolean' as never;
|
||||
const expectedError = getExpectedError(
|
||||
{
|
||||
name: 'os',
|
||||
value: invalidOs,
|
||||
},
|
||||
{
|
||||
name: 'isRunningAsDesktopApplication',
|
||||
value: invalidIsRunningAsDesktopApplication,
|
||||
},
|
||||
);
|
||||
const input = new WindowVariablesStub()
|
||||
.withOs(invalidOs)
|
||||
.withIsRunningAsDesktopApplication(invalidIsRunningAsDesktopApplication);
|
||||
const context = new ValidateWindowVariablesTestSetup()
|
||||
.withWindowVariables(input);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
const act = () => context.validateWindowVariables();
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
describe('when not in Electron renderer process', () => {
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly environment: ElectronEnvironmentDetector;
|
||||
}> = [
|
||||
{
|
||||
description: 'skips in non-Electron environments',
|
||||
environment: new ElectronEnvironmentDetectorStub()
|
||||
.withNonElectronEnvironment(),
|
||||
},
|
||||
{
|
||||
description: 'skips in Electron main process',
|
||||
environment: new ElectronEnvironmentDetectorStub()
|
||||
.withElectronEnvironment('main'),
|
||||
},
|
||||
{
|
||||
description: 'skips in Electron preloader process',
|
||||
environment: new ElectronEnvironmentDetectorStub()
|
||||
.withElectronEnvironment('preloader'),
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({ description, environment }) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const invalidOs = 'invalid' as unknown as OperatingSystem;
|
||||
const input = new WindowVariablesStub()
|
||||
.withOs(invalidOs);
|
||||
const context = new ValidateWindowVariablesTestSetup()
|
||||
.withElectronDetector(environment)
|
||||
.withWindowVariables(input);
|
||||
// act
|
||||
const act = () => context.validateWindowVariables();
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('does not throw when a property is valid', () => {
|
||||
const testScenarios: Record<PropertyKeys<Required<WindowVariables>>, ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly validValue: unknown;
|
||||
}>> = {
|
||||
isRunningAsDesktopApplication: [{
|
||||
description: 'accepts boolean true',
|
||||
validValue: true,
|
||||
}],
|
||||
os: [
|
||||
{
|
||||
description: 'accepts undefined',
|
||||
validValue: undefined,
|
||||
},
|
||||
{
|
||||
description: 'accepts valid enum value',
|
||||
validValue: OperatingSystem.WindowsPhone,
|
||||
},
|
||||
],
|
||||
codeRunner: [{
|
||||
description: 'accepts an object',
|
||||
validValue: new CodeRunnerStub(),
|
||||
}],
|
||||
log: [{
|
||||
description: 'accepts an object',
|
||||
validValue: new LoggerStub(),
|
||||
}],
|
||||
dialog: [{
|
||||
description: 'accepts an object',
|
||||
validValue: new DialogStub(),
|
||||
}],
|
||||
scriptDiagnosticsCollector: [{
|
||||
description: 'accepts an object',
|
||||
validValue: new ScriptDiagnosticsCollectorStub(),
|
||||
}],
|
||||
};
|
||||
Object.entries(testScenarios).forEach(([propertyKey, validValueScenarios]) => {
|
||||
describe(propertyKey, () => {
|
||||
validValueScenarios.forEach(({ description, validValue }) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const input = new WindowVariablesStub();
|
||||
input[propertyKey] = validValue;
|
||||
const context = new ValidateWindowVariablesTestSetup()
|
||||
.withWindowVariables(input);
|
||||
// act
|
||||
const act = () => context.validateWindowVariables();
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('throws an error when a property is invalid', () => {
|
||||
interface InvalidValueTestCase {
|
||||
readonly description: string;
|
||||
readonly invalidValue: unknown;
|
||||
}
|
||||
const testScenarios: Record<
|
||||
PropertyKeys<Required<WindowVariables>>,
|
||||
ReadonlyArray<InvalidValueTestCase>> = {
|
||||
isRunningAsDesktopApplication: [
|
||||
{
|
||||
description: 'rejects false',
|
||||
invalidValue: false,
|
||||
},
|
||||
{
|
||||
description: 'rejects undefined',
|
||||
invalidValue: undefined,
|
||||
},
|
||||
],
|
||||
os: [
|
||||
{
|
||||
description: 'rejects non-numeric',
|
||||
invalidValue: 'Linux',
|
||||
},
|
||||
{
|
||||
description: 'rejects out-of-range',
|
||||
invalidValue: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
],
|
||||
codeRunner: getInvalidObjectValueTestCases(),
|
||||
log: getInvalidObjectValueTestCases(),
|
||||
dialog: getInvalidObjectValueTestCases(),
|
||||
scriptDiagnosticsCollector: getInvalidObjectValueTestCases(),
|
||||
};
|
||||
Object.entries(testScenarios).forEach(([propertyKey, validValueScenarios]) => {
|
||||
describe(propertyKey, () => {
|
||||
validValueScenarios.forEach(({ description, invalidValue }) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const expectedErrorMessage = getExpectedError({
|
||||
name: propertyKey as keyof WindowVariables,
|
||||
value: invalidValue,
|
||||
});
|
||||
const input = new WindowVariablesStub();
|
||||
input[propertyKey] = invalidValue;
|
||||
const context = new ValidateWindowVariablesTestSetup()
|
||||
.withWindowVariables(input);
|
||||
// act
|
||||
const act = () => context.validateWindowVariables();
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
function getInvalidObjectValueTestCases(): InvalidValueTestCase[] {
|
||||
return [
|
||||
{
|
||||
description: 'rejects string',
|
||||
invalidValue: 'invalid object',
|
||||
},
|
||||
{
|
||||
description: 'rejects array of objects',
|
||||
invalidValue: [{}, {}],
|
||||
},
|
||||
...getAbsentObjectTestCases().map((testCase) => ({
|
||||
description: `rejects absent: ${testCase.valueName}`,
|
||||
invalidValue: testCase.absentValue,
|
||||
})),
|
||||
];
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expectObjectOnDesktop<T>(key: keyof WindowVariables) {
|
||||
describe('validates object type on desktop', () => {
|
||||
itEachInvalidObjectValue((invalidObjectValue) => {
|
||||
// arrange
|
||||
const isOnDesktop = true;
|
||||
const invalidObject = invalidObjectValue as T;
|
||||
const expectedError = getExpectedError({
|
||||
name: key,
|
||||
object: invalidObject,
|
||||
});
|
||||
const input: WindowVariables = {
|
||||
...new WindowVariablesStub(),
|
||||
isRunningAsDesktopApplication: isOnDesktop,
|
||||
[key]: invalidObject,
|
||||
};
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('does not validate object type when not on desktop', () => {
|
||||
itEachInvalidObjectValue((invalidObjectValue) => {
|
||||
// arrange
|
||||
const invalidObject = invalidObjectValue as T;
|
||||
const input: WindowVariables = {
|
||||
...new WindowVariablesStub(),
|
||||
isRunningAsDesktopApplication: undefined,
|
||||
[key]: invalidObject,
|
||||
};
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
}
|
||||
class ValidateWindowVariablesTestSetup {
|
||||
private electronDetector: ElectronEnvironmentDetector = new ElectronEnvironmentDetectorStub()
|
||||
.withElectronEnvironment('renderer');
|
||||
|
||||
function itEachInvalidObjectValue<T>(runner: (invalidObjectValue: T) => void) {
|
||||
const testCases: Array<{
|
||||
readonly name: string;
|
||||
readonly value: T;
|
||||
}> = [
|
||||
{
|
||||
name: 'given string',
|
||||
value: 'invalid object' as unknown as T,
|
||||
},
|
||||
{
|
||||
name: 'given array of objects',
|
||||
value: [{}, {}] as unknown as T,
|
||||
},
|
||||
...getAbsentObjectTestCases().map((testCase) => ({
|
||||
name: `given absent: ${testCase.valueName}`,
|
||||
value: testCase.absentValue as unknown as T,
|
||||
})),
|
||||
];
|
||||
testCases.forEach((testCase) => {
|
||||
it(testCase.name, () => {
|
||||
runner(testCase.value);
|
||||
});
|
||||
});
|
||||
private windowVariables: WindowVariables = new WindowVariablesStub();
|
||||
|
||||
public withWindowVariables(windowVariables: WindowVariables): this {
|
||||
this.windowVariables = windowVariables;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withElectronDetector(electronDetector: ElectronEnvironmentDetector): this {
|
||||
this.electronDetector = electronDetector;
|
||||
return this;
|
||||
}
|
||||
|
||||
public validateWindowVariables() {
|
||||
return validateWindowVariables(
|
||||
this.windowVariables,
|
||||
this.electronDetector,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getExpectedError(...unexpectedObjects: Array<{
|
||||
readonly name: keyof WindowVariables;
|
||||
readonly object: unknown;
|
||||
readonly value: unknown;
|
||||
}>) {
|
||||
const errors = unexpectedObjects
|
||||
.map(({ name, object }) => `Unexpected ${name} (${typeof object})`);
|
||||
.map(({ name, value: object }) => `Unexpected ${name} (${typeof object})`);
|
||||
return errors.join('\n');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user