Files
privacy.sexy/tests/unit/infrastructure/RuntimeEnvironment/WindowVariablesValidator.spec.ts
undergroundwires e17744faf0 Bump to TypeScript 5.5 and enable noImplicitAny
This commit upgrades TypeScript from 5.4 to 5.5 and enables the
`noImplicitAny` option for stricter type checking. It refactors code to
comply with `noImplicitAny` and adapts to new TypeScript features and
limitations.

Key changes:

- Migrate from TypeScript 5.4 to 5.5
- Enable `noImplicitAny` for stricter type checking
- Refactor code to comply with new TypeScript features and limitations

Other supporting changes:

- Refactor progress bar handling for type safety
- Drop 'I' prefix from interfaces to align with new code convention
- Update TypeScript target from `ES2017` and `ES2018`.
  This allows named capturing groups. Otherwise, new TypeScript compiler
  does not compile the project and shows the following error:
  ```
  ...
  TimestampedFilenameGenerator.spec.ts:105:23 - error TS1503: Named capturing groups are only available when targeting 'ES2018' or later
  const pattern = /^(?<timestamp>\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})-(?<scriptName>[^.]+?)(?:\.(?<extension>[^.]+))?$/;// timestamp-scriptName.extension
  ...
  ```
- Refactor usage of `electron-progressbar` for type safety and
  less complexity.
2024-09-26 16:07:37 +02:00

245 lines
8.6 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { validateWindowVariables } from '@/infrastructure/WindowVariables/WindowVariablesValidator';
import type { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
import { OperatingSystem } from '@/domain/OperatingSystem';
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 type { 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 type { ElectronEnvironmentDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ElectronEnvironmentDetector';
import { ElectronEnvironmentDetectorStub } from '@tests/unit/shared/Stubs/ElectronEnvironmentDetectorStub';
describe('WindowVariablesValidator', () => {
describe('validateWindowVariables', () => {
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 = () => context.validateWindowVariables();
// assert
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', () => {
type WindowVariable = PropertyKeys<Required<WindowVariables>>;
const testScenarios: Record<WindowVariable, 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: WindowVariables = {
...new WindowVariablesStub(),
[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: WindowVariables = {
...new WindowVariablesStub(),
[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,
})),
];
}
});
});
});
class ValidateWindowVariablesTestSetup {
private electronDetector: ElectronEnvironmentDetector = new ElectronEnvironmentDetectorStub()
.withElectronEnvironment('renderer');
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 value: unknown;
}>) {
const errors = unexpectedObjects
.map(({ name, value: object }) => `Unexpected ${name} (${typeof object})`);
return errors.join('\n');
}