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.
This commit is contained in:
undergroundwires
2024-09-26 16:07:37 +02:00
parent a05a600071
commit e17744faf0
77 changed files with 656 additions and 332 deletions

View File

@@ -25,9 +25,10 @@ describe('PlatformTimer', () => {
describe('setTimeout', () => {
it('calls the global setTimeout with the provided delay', () => {
// arrange
type Delay = Parameters<typeof setTimeout>[1];
const expectedDelay = 55;
let actualDelay: number | undefined;
global.setTimeout = ((_, delay) => {
let actualDelay: Delay | undefined;
global.setTimeout = ((_: never, delay: Delay) => {
actualDelay = delay;
}) as typeof global.setTimeout;
// act
@@ -37,9 +38,10 @@ describe('PlatformTimer', () => {
});
it('calls the global setTimeout with the provided callback', () => {
// arrange
type Callback = Parameters<typeof setTimeout>[0];
const expectedCallback = () => { /* NOOP */ };
let actualCallback: typeof expectedCallback | undefined;
global.setTimeout = ((callback) => {
let actualCallback: Callback | undefined;
global.setTimeout = ((callback: Callback) => {
actualCallback = callback;
}) as typeof global.setTimeout;
// act
@@ -52,8 +54,9 @@ describe('PlatformTimer', () => {
describe('clearTimeout', () => {
it('should clear timeout', () => {
// arrange
let actualTimer: ReturnType<typeof PlatformTimer.setTimeout> | undefined;
global.clearTimeout = ((timer) => {
type Timer = ReturnType<typeof PlatformTimer.setTimeout>;
let actualTimer: Timer | undefined;
global.clearTimeout = ((timer: Timer) => {
actualTimer = timer;
}) as typeof global.clearTimeout;
const expectedTimer = PlatformTimer.setTimeout(() => { /* NOOP */ }, 1);

View File

@@ -50,16 +50,33 @@ describe('ApplicationContext', () => {
// assert
expectEmptyState(sut.state);
});
it('throws when OS is unknown to application', () => {
it('rethrows when application cannot provide collection for supported OS', () => {
// arrange
const expectedError = 'expected error from application';
const applicationStub = new ApplicationStub();
const initialOs = OperatingSystem.Android;
const targetOs = OperatingSystem.ChromeOS;
const context = new ObservableApplicationContextFactory()
.withAppContainingCollections(initialOs, targetOs)
.withInitialOs(initialOs);
// act
const sut = context.construct();
const { app } = context;
app.getCollection = () => { throw new Error(expectedError); };
const act = () => sut.changeContext(targetOs);
// assert
expect(act).to.throw(expectedError);
});
it('throws when OS state is unknown to application', () => {
// arrange
const knownOs = OperatingSystem.Android;
const unknownOs = OperatingSystem.ChromeOS;
const expectedError = `Operating system "${OperatingSystem[unknownOs]}" state is unknown.`;
const sut = new ObservableApplicationContextFactory()
.withApp(applicationStub)
.withAppContainingCollections(knownOs)
.withInitialOs(knownOs)
.construct();
// act
applicationStub.getCollection = () => { throw new Error(expectedError); };
const act = () => sut.changeContext(OperatingSystem.Android);
const act = () => sut.changeContext(unknownOs);
// assert
expect(act).to.throw(expectedError);
});
@@ -181,14 +198,28 @@ describe('ApplicationContext', () => {
const actual = sut.state.os;
expect(actual).to.deep.equal(expected);
});
it('throws when OS is unknown to application', () => {
it('rethrows when application cannot provide collection for supported OS', () => {
// arrange
const expectedError = 'expected error from application';
const applicationStub = new ApplicationStub();
applicationStub.getCollection = () => { throw new Error(expectedError); };
const knownOperatingSystem = OperatingSystem.macOS;
const context = new ObservableApplicationContextFactory()
.withAppContainingCollections(knownOperatingSystem)
.withInitialOs(knownOperatingSystem);
const { app } = context;
app.getCollection = () => { throw new Error(expectedError); };
// act
const act = () => context.construct();
// assert
expect(act).to.throw(expectedError);
});
it('throws when OS is not supported', () => {
// arrange
const unknownInitialOperatingSystem = OperatingSystem.BlackBerry10;
const expectedError = `Operating system "${OperatingSystem[unknownInitialOperatingSystem]}" is not supported.`;
// act
const act = () => new ObservableApplicationContextFactory()
.withApp(applicationStub)
.withAppContainingCollections(OperatingSystem.Android /* unrelated */)
.withInitialOs(unknownInitialOperatingSystem)
.construct();
// assert
expect(act).to.throw(expectedError);
@@ -222,24 +253,24 @@ class ObservableApplicationContextFactory {
private initialOs = ObservableApplicationContextFactory.DefaultOs;
constructor() {
public constructor() {
this.withAppContainingCollections(ObservableApplicationContextFactory.DefaultOs);
}
public withAppContainingCollections(
...oses: OperatingSystem[]
): ObservableApplicationContextFactory {
): this {
const collectionValues = oses.map((os) => new CategoryCollectionStub().withOs(os));
const app = new ApplicationStub().withCollections(...collectionValues);
return this.withApp(app);
}
public withApp(app: IApplication): ObservableApplicationContextFactory {
public withApp(app: IApplication): this {
this.app = app;
return this;
}
public withInitialOs(initialOs: OperatingSystem) {
public withInitialOs(initialOs: OperatingSystem): this {
this.initialOs = initialOs;
return this;
}
@@ -250,6 +281,7 @@ class ObservableApplicationContextFactory {
return sut;
}
}
function getDuplicates<T>(list: readonly T[]): T[] {
return list.filter((item, index) => list.indexOf(item) !== index);
}

View File

@@ -74,7 +74,7 @@ describe('CategoryCollectionState', () => {
describe('selection', () => {
it('initializes with empty scripts', () => {
// arrange
const expectedScripts = [];
const expectedScripts: readonly SelectedScript[] = [];
let actualScripts: readonly SelectedScript[] | undefined;
const selectionFactoryMock: SelectionFactory = (_, scripts) => {
actualScripts = scripts;

View File

@@ -16,7 +16,7 @@ describe('ApplicationCode', () => {
describe('ctor', () => {
it('empty when selection is empty', () => {
// arrange
const selectedScripts = [];
const selectedScripts: readonly SelectedScript[] = [];
const selection = new ScriptSelectionStub()
.withSelectedScripts(selectedScripts);
const definition = new ScriptingDefinitionStub();

View File

@@ -7,6 +7,7 @@ import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefin
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
describe('UserScriptGenerator', () => {
describe('scriptingDefinition', () => {
@@ -143,7 +144,7 @@ describe('UserScriptGenerator', () => {
it('without script; returns empty', () => {
// arrange
const sut = new UserScriptGenerator();
const selectedScripts = [];
const selectedScripts: readonly SelectedScript[] = [];
const definition = new ScriptingDefinitionStub();
// act
const actual = sut.buildCode(selectedScripts, definition);

View File

@@ -279,7 +279,7 @@ describe('DebouncedScriptSelection', () => {
it('throws error when an empty script array is passed', () => {
// arrange
const expectedError = 'Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.';
const scripts = [];
const scripts: readonly Script[] = [];
const scriptSelection = new DebouncedScriptSelectionBuilder().build();
// act
const act = () => scriptSelection.selectOnly(scripts);

View File

@@ -135,7 +135,7 @@ describe('createTypeValidator', () => {
});
it('throws error for empty collection', () => {
// arrange
const emptyArrayValue = [];
const emptyArrayValue: unknown[] = [];
const valueName = 'empty collection value';
const expectedMessage = `'${valueName}' cannot be an empty array.`;
const { assertNonEmptyCollection } = createTypeValidator();
@@ -251,7 +251,7 @@ describe('createTypeValidator', () => {
});
function createObjectWithProperties(properties: readonly string[]): object {
const object = {};
const object: Record<string, unknown> = {};
properties.forEach((propertyName) => {
object[propertyName] = 'arbitrary value';
});

View File

@@ -383,7 +383,7 @@ function createExpressionFactorySpy() {
};
return {
createExpression,
getInitParameters: (expression) => createdExpressions.get(expression),
getInitParameters: (expression: IExpression) => createdExpressions.get(expression),
};
}

View File

@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
import { PipeFactory } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeFactory';
import { PipeStub } from '@tests/unit/shared/Stubs/PipeStub';
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import type { Pipe } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/Pipe';
describe('PipeFactory', () => {
describe('ctor', () => {
@@ -49,7 +50,7 @@ describe('PipeFactory', () => {
// arrange
const missingName = 'missingName';
const expectedError = `Unknown pipe: "${missingName}"`;
const pipes = [];
const pipes: readonly Pipe[] = [];
const sut = new PipeFactory(pipes);
// act
const act = () => sut.get(missingName);

View File

@@ -9,20 +9,20 @@ import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTes
describe('PipelineCompiler', () => {
describe('compile', () => {
describe('throws for invalid arguments', () => {
interface ITestCase {
interface ThrowingPipeScenario {
readonly name: string;
readonly act: (test: PipelineTestRunner) => PipelineTestRunner;
readonly expectedError: string;
}
const testCases: ITestCase[] = [
const testScenarios: ThrowingPipeScenario[] = [
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase) => ({
.map((testCase): ThrowingPipeScenario => ({
name: `"value" is ${testCase.valueName}`,
act: (test) => test.withValue(testCase.absentValue),
expectedError: 'missing value',
})),
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase) => ({
.map((testCase): ThrowingPipeScenario => ({
name: `"pipeline" is ${testCase.valueName}`,
act: (test) => test.withPipeline(testCase.absentValue),
expectedError: 'missing pipeline',
@@ -33,7 +33,7 @@ describe('PipelineCompiler', () => {
expectedError: 'pipeline does not start with pipe',
},
];
for (const testCase of testCases) {
for (const testCase of testScenarios) {
it(testCase.name, () => {
// act
const runner = new PipelineTestRunner();

View File

@@ -114,7 +114,7 @@ export class SyntaxParserTestsRunner {
}
}
interface ExpectResultTestScenario {
export interface ExpectResultTestScenario {
readonly name: string;
readonly code: string;
readonly args: (

View File

@@ -2,7 +2,7 @@ import { describe } from 'vitest';
import { ExpressionPosition } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition';
import { WithParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers/WithParser';
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import { SyntaxParserTestsRunner } from './SyntaxParserTestsRunner';
import { SyntaxParserTestsRunner, type ExpectResultTestScenario } from './SyntaxParserTestsRunner';
describe('WithParser', () => {
const sut = new WithParser();
@@ -120,7 +120,7 @@ describe('WithParser', () => {
describe('does not render scope', () => {
runner.expectResults(
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase) => ({
.map((testCase): ExpectResultTestScenario => ({
name: `does not render when value is "${testCase.valueName}"`,
code: '{{ with $parameter }}dark{{ end }} ',
args: (args) => args
@@ -138,7 +138,7 @@ describe('WithParser', () => {
describe('renders scope', () => {
runner.expectResults(
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase) => ({
.map((testCase): ExpectResultTestScenario => ({
name: `does not render when value is "${testCase.valueName}"`,
code: '{{ with $parameter }}dark{{ end }} ',
args: (args) => args

View File

@@ -165,11 +165,14 @@ function createScriptLanguageScenarios(): readonly ScriptLanguageScenario[] {
[ScriptingLanguage.batchfile]: 8191,
[ScriptingLanguage.shellscript]: 1048576,
};
return Object.entries(maxLengths).map(([language, length]): ScriptLanguageScenario => ({
description: `${ScriptingLanguage[language]} (max: ${length})`,
language: Number.parseInt(language, 10) as ScriptingLanguage,
maxLength: length,
}));
return Object.entries(maxLengths).map(([language, length]): ScriptLanguageScenario => {
const languageValue = Number.parseInt(language, 10) as ScriptingLanguage;
return {
description: `${ScriptingLanguage[languageValue]} (max: ${length})`,
language: languageValue,
maxLength: length,
};
});
}
class TestContext {

View File

@@ -37,9 +37,9 @@ describe('TimestampedFilenameGenerator', () => {
// act
const filename = generateFilenamePartsForTesting({ date });
// assert
expect(filename.timestamp).to.equal(expectedTimestamp, formatAssertionMessage[
`Generated filename: ${filename.generatedFilename}`
]);
expect(filename.timestamp).to.equal(expectedTimestamp, formatAssertionMessage([
`Generated filename: ${filename.generatedFilename}`,
]));
});
describe('extension', () => {
it('uses correct extension', () => {
@@ -48,9 +48,9 @@ describe('TimestampedFilenameGenerator', () => {
// act
const filename = generateFilenamePartsForTesting({ extension: expectedExtension });
// assert
expect(filename.extension).to.equal(expectedExtension, formatAssertionMessage[
`Generated filename: ${filename.generatedFilename}`
]);
expect(filename.extension).to.equal(expectedExtension, formatAssertionMessage([
`Generated filename: ${filename.generatedFilename}`,
]));
});
describe('handles absent extension', () => {
itEachAbsentStringValue((absentExtension) => {
@@ -59,9 +59,9 @@ describe('TimestampedFilenameGenerator', () => {
// act
const filename = generateFilenamePartsForTesting({ extension: absentExtension });
// assert
expect(filename.extension).to.equal(expectedExtension, formatAssertionMessage[
`Generated file name: ${filename.generatedFilename}`
]);
expect(filename.extension).to.equal(expectedExtension, formatAssertionMessage([
`Generated file name: ${filename.generatedFilename}`,
]));
}, { excludeNull: true });
});
it('errors on dot-starting extension', () => {

View File

@@ -17,7 +17,8 @@ describe('OsSpecificTerminalLaunchCommandFactory', () => {
[OperatingSystem.Linux]: LinuxVisibleTerminalCommand,
[OperatingSystem.macOS]: MacOsVisibleTerminalCommand,
};
AllSupportedOperatingSystems.forEach((operatingSystem) => {
AllSupportedOperatingSystems.forEach((operatingSystemValue) => {
const operatingSystem = operatingSystemValue as SupportedOperatingSystem;
it(`${OperatingSystem[operatingSystem]}`, () => {
// arrange
const expectedDefinitionType = testScenarios[operatingSystem];

View File

@@ -8,7 +8,7 @@ import { ViteEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/
describe('ViteEnvironmentVariables', () => {
describe('reads values from import.meta.env', () => {
let originalMetaEnv;
let originalMetaEnv: ImportMetaEnv;
beforeEach(() => {
originalMetaEnv = { ...import.meta.env };
});
@@ -16,14 +16,15 @@ describe('ViteEnvironmentVariables', () => {
Object.assign(import.meta.env, originalMetaEnv);
});
interface ITestCase<T> {
interface EnvironmentVariableTestScenario<T> {
readonly getActualValue: (sut: IEnvironmentVariables) => T;
readonly environmentVariable: typeof VITE_ENVIRONMENT_KEYS[
keyof typeof VITE_ENVIRONMENT_KEYS];
readonly expected: T;
}
const testCases: {
readonly [K in PropertyKeys<IEnvironmentVariables>]: ITestCase<string | boolean>;
readonly [K in PropertyKeys<IEnvironmentVariables>]:
EnvironmentVariableTestScenario<string | boolean>;
} = {
name: {
environmentVariable: VITE_ENVIRONMENT_KEYS.NAME,

View File

@@ -119,14 +119,15 @@ describe('ConditionBasedOsDetector', () => {
});
describe('user agent checks', () => {
const testScenarios: ReadonlyArray<{
interface UserAgentTestScenario {
readonly description: string;
readonly buildEnvironment: (environment: BrowserEnvironmentStub) => BrowserEnvironmentStub;
readonly buildCondition: (condition: BrowserConditionStub) => BrowserConditionStub;
readonly detects: boolean;
}> = [
}
const testScenarios: ReadonlyArray<UserAgentTestScenario> = [
...getAbsentStringTestCases({ excludeUndefined: true, excludeNull: true })
.map((testCase) => ({
.map((testCase): UserAgentTestScenario => ({
description: `does not detect when user agent is empty (${testCase.valueName})`,
buildEnvironment: (environment) => environment.withUserAgent(testCase.absentValue),
buildCondition: (condition) => condition,

View File

@@ -77,7 +77,8 @@ describe('WindowVariablesValidator', () => {
});
describe('does not throw when a property is valid', () => {
const testScenarios: Record<PropertyKeys<Required<WindowVariables>>, ReadonlyArray<{
type WindowVariable = PropertyKeys<Required<WindowVariables>>;
const testScenarios: Record<WindowVariable, ReadonlyArray<{
readonly description: string;
readonly validValue: unknown;
}>> = {
@@ -117,8 +118,10 @@ describe('WindowVariablesValidator', () => {
validValueScenarios.forEach(({ description, validValue }) => {
it(description, () => {
// arrange
const input = new WindowVariablesStub();
input[propertyKey] = validValue;
const input: WindowVariables = {
...new WindowVariablesStub(),
[propertyKey]: validValue,
};
const context = new ValidateWindowVariablesTestSetup()
.withWindowVariables(input);
// act
@@ -173,8 +176,10 @@ describe('WindowVariablesValidator', () => {
name: propertyKey as keyof WindowVariables,
value: invalidValue,
});
const input = new WindowVariablesStub();
input[propertyKey] = invalidValue;
const input: WindowVariables = {
...new WindowVariablesStub(),
[propertyKey]: invalidValue,
};
const context = new ValidateWindowVariablesTestSetup()
.withWindowVariables(input);
// act

View File

@@ -1,8 +1,8 @@
import { describe, it, expect } from 'vitest';
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
import type { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions';
import type { SanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/SanityCheckOptions';
import { SanityCheckOptionsStub } from '@tests/unit/shared/Stubs/SanityCheckOptionsStub';
import type { ISanityValidator } from '@/infrastructure/RuntimeSanity/Common/ISanityValidator';
import type { SanityValidator } from '@/infrastructure/RuntimeSanity/Common/SanityValidator';
import { SanityValidatorStub } from '@tests/unit/shared/Stubs/SanityValidatorStub';
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
@@ -11,7 +11,7 @@ describe('SanityChecks', () => {
describe('validateRuntimeSanity', () => {
describe('parameter validation', () => {
describe('throws when validators are empty', () => {
itEachAbsentCollectionValue<ISanityValidator>((absentCollection) => {
itEachAbsentCollectionValue<SanityValidator>((absentCollection) => {
// arrange
const expectedError = 'missing validators';
const validators = absentCollection;
@@ -138,9 +138,9 @@ describe('SanityChecks', () => {
});
class TestContext {
private options: ISanityCheckOptions = new SanityCheckOptionsStub();
private options: SanityCheckOptions = new SanityCheckOptionsStub();
private validators: ISanityValidator[] = [new SanityValidatorStub()];
private validators: SanityValidator[] = [new SanityValidatorStub()];
public withOptionsSetup(
setup: (stub: SanityCheckOptionsStub) => SanityCheckOptionsStub,
@@ -148,12 +148,12 @@ class TestContext {
return this.withOptions(setup(new SanityCheckOptionsStub()));
}
public withOptions(options: ISanityCheckOptions): this {
public withOptions(options: SanityCheckOptions): this {
this.options = options;
return this;
}
public withValidators(validators: ISanityValidator[]): this {
public withValidators(validators: SanityValidator[]): this {
this.validators = validators;
return this;
}

View File

@@ -1,23 +1,23 @@
import type { PropertyKeys } from '@/TypeHelpers';
import type { FactoryFunction, FactoryValidator } from '@/infrastructure/RuntimeSanity/Common/FactoryValidator';
import type { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions';
import type { SanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/SanityCheckOptions';
import { SanityCheckOptionsStub } from '@tests/unit/shared/Stubs/SanityCheckOptionsStub';
interface ITestOptions<T> {
createValidator: (factory?: FactoryFunction<T>) => FactoryValidator<T>;
enablingOptionProperty: PropertyKeys<ISanityCheckOptions>;
factoryFunctionStub: FactoryFunction<T>;
expectedValidatorName: string;
interface TestOptions<T> {
readonly createValidator: (factory?: FactoryFunction<T>) => FactoryValidator<T>;
readonly enablingOptionProperty: PropertyKeys<SanityCheckOptions>;
readonly factoryFunctionStub: FactoryFunction<T>;
readonly expectedValidatorName: string;
}
export function runFactoryValidatorTests<T>(
testOptions: ITestOptions<T>,
testOptions: TestOptions<T>,
) {
describe('shouldValidate', () => {
it('returns true when option is true', () => {
// arrange
const expectedValue = true;
const options: ISanityCheckOptions = {
const options: SanityCheckOptions = {
...new SanityCheckOptionsStub(),
[testOptions.enablingOptionProperty]: true,
};
@@ -31,7 +31,7 @@ export function runFactoryValidatorTests<T>(
it('returns false when option is false', () => {
// arrange
const expectedValue = false;
const options: ISanityCheckOptions = {
const options: SanityCheckOptions = {
...new SanityCheckOptionsStub(),
[testOptions.enablingOptionProperty]: false,
};

View File

@@ -7,12 +7,14 @@ import { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFact
import type { IApplicationContext } from '@/application/Context/IApplicationContext';
import { itIsTransientFactory } from '@tests/unit/shared/TestCases/TransientFactoryTests';
import { executeInComponentSetupContext } from '@tests/shared/Vue/ExecuteInComponentSetupContext';
import type { PropertyKeys } from '@/TypeHelpers';
type InjectionKeyType = PropertyKeys<typeof InjectionKeys>;
type DependencyInjectionTestFunction = (injectionKey: symbol) => void;
describe('DependencyProvider', () => {
describe('provideDependencies', () => {
const testCases: {
readonly [K in keyof typeof InjectionKeys]: (injectionKey: symbol) => void;
} = {
const testCases: Record<InjectionKeyType, DependencyInjectionTestFunction> = {
useCollectionState: createTransientTests(),
useApplication: createSingletonTests(),
useRuntimeEnvironment: createSingletonTests(),
@@ -27,7 +29,8 @@ describe('DependencyProvider', () => {
useAutoUnsubscribedEventListener: createTransientTests(),
};
Object.entries(testCases).forEach(([key, runTests]) => {
const registeredKey = InjectionKeys[key].key;
const injectionKey = key as InjectionKeyType;
const registeredKey = InjectionKeys[injectionKey].key;
describe(`Key: "${registeredKey.toString()}"`, () => {
runTests(registeredKey);
});
@@ -35,7 +38,7 @@ describe('DependencyProvider', () => {
});
});
function createTransientTests() {
function createTransientTests(): DependencyInjectionTestFunction {
return (injectionKey: symbol) => {
it('should register a function when transient dependency is resolved', () => {
// arrange
@@ -73,7 +76,7 @@ function createTransientTests() {
};
}
function createSingletonTests() {
function createSingletonTests(): DependencyInjectionTestFunction {
return (injectionKey: symbol) => {
it('should register an object when singleton dependency is resolved', () => {
// arrange

View File

@@ -1,20 +1,21 @@
import { describe, it, expect } from 'vitest';
import type { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions';
import { RuntimeSanityValidator } from '@/presentation/bootstrapping/Modules/RuntimeSanityValidator';
import type { SanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/SanityCheckOptions';
import { RuntimeSanityBootstrapper } from '@/presentation/bootstrapping/Modules/RuntimeSanityBootstrapper';
import { expectDoesNotThrowAsync, expectThrowsAsync } from '@tests/shared/Assertions/ExpectThrowsAsync';
import type { RuntimeSanityValidator } from '@/infrastructure/RuntimeSanity/SanityChecks';
describe('RuntimeSanityValidator', () => {
describe('RuntimeSanityBootstrapper', () => {
it('calls validator with correct options upon bootstrap', async () => {
// arrange
const expectedOptions: ISanityCheckOptions = {
const expectedOptions: SanityCheckOptions = {
validateEnvironmentVariables: true,
validateWindowVariables: true,
};
let actualOptions: ISanityCheckOptions | undefined;
const validatorMock = (options) => {
let actualOptions: SanityCheckOptions | undefined;
const validatorMock: RuntimeSanityValidator = (options) => {
actualOptions = options;
};
const sut = new RuntimeSanityValidator(validatorMock);
const sut = new RuntimeSanityBootstrapper(validatorMock);
// act
await sut.bootstrap();
// assert
@@ -26,7 +27,7 @@ describe('RuntimeSanityValidator', () => {
const validatorMock = () => {
throw new Error(expectedMessage);
};
const sut = new RuntimeSanityValidator(validatorMock);
const sut = new RuntimeSanityBootstrapper(validatorMock);
// act
const act = async () => { await sut.bootstrap(); };
// assert
@@ -35,7 +36,7 @@ describe('RuntimeSanityValidator', () => {
it('runs successfully if validator passes', async () => {
// arrange
const validatorMock = () => { /* NOOP */ };
const sut = new RuntimeSanityValidator(validatorMock);
const sut = new RuntimeSanityBootstrapper(validatorMock);
// act
const act = async () => { await sut.bootstrap(); };
// assert

View File

@@ -17,7 +17,8 @@ describe('PlatformInstructionSteps', () => {
[OperatingSystem.macOS]: MacOsInstructions,
[OperatingSystem.Linux]: LinuxInstructions,
};
AllSupportedOperatingSystems.forEach((operatingSystem) => {
AllSupportedOperatingSystems.forEach((operatingSystemKey) => {
const operatingSystem = operatingSystemKey as SupportedOperatingSystem;
it(`renders the correct component for ${OperatingSystem[operatingSystem]}`, () => {
// arrange
const expectedComponent = testScenarios[operatingSystem];
@@ -47,7 +48,9 @@ describe('PlatformInstructionSteps', () => {
// assert
const componentWrapper = wrapper.findComponent(wrappedComponent);
expect(componentWrapper.props('filename')).to.equal(expectedFilename);
const propertyValues = componentWrapper.props();
const propertyValue = 'filename' in propertyValues ? propertyValues.filename : undefined;
expect(propertyValue).to.equal(expectedFilename);
});
});
});

View File

@@ -8,7 +8,7 @@ describe('CompositeMarkdownRenderer', () => {
it('throws error without renderers', () => {
// arrange
const expectedError = 'missing renderers';
const renderers = [];
const renderers = new Array<MarkdownRenderer>();
const context = new MarkdownRendererTestBuilder()
.withMarkdownRenderers(renderers);
// act

View File

@@ -33,7 +33,7 @@ describe('TreeNodeHierarchy', () => {
it('returns `true` without children', () => {
// arrange
const hierarchy = new TreeNodeHierarchy();
const children = [];
const children = new Array<TreeNode>();
// act
hierarchy.setChildren(children);
// assert
@@ -55,7 +55,7 @@ describe('TreeNodeHierarchy', () => {
it('returns `false` without children', () => {
// arrange
const hierarchy = new TreeNodeHierarchy();
const children = [];
const children = new Array<TreeNode>();
// act
hierarchy.setChildren(children);
// assert

View File

@@ -237,7 +237,7 @@ describe('useGradualNodeRendering', () => {
});
it('skips scheduling when no nodes to render', () => {
// arrange
const nodes = [];
const nodes = new Array<TreeNode>();
const nodesStub = new UseCurrentTreeNodesStub()
.withQueryableNodes(new QueryableNodesStub().withFlattenedNodes(nodes));
const delaySchedulerStub = new DelaySchedulerStub();

View File

@@ -17,7 +17,7 @@ describe('parseTreeInput', () => {
it('returns an empty array if given an empty array', () => {
// arrange
const input = [];
const input = new Array<TreeInputNodeData>();
// act
const nodes = parseTreeInput(input);
// assert

View File

@@ -1,7 +1,7 @@
import { SingleNodeCollectionFocusManager } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/Focus/SingleNodeCollectionFocusManager';
import type { TreeNodeCollection } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeCollection';
import { TreeNodeInitializerAndUpdater } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeInitializerAndUpdater';
import { TreeRootManager } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRootManager';
import { TreeRootManager, type FocusManagerFactory } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRootManager';
import { SingleNodeFocusManagerStub } from '@tests/unit/shared/Stubs/SingleNodeFocusManagerStub';
import { TreeNodeCollectionStub } from '@tests/unit/shared/Stubs/TreeNodeCollectionStub';
@@ -19,9 +19,12 @@ describe('TreeRootManager', () => {
it('set by constructor as expected', () => {
// arrange
const expectedCollection = new TreeNodeCollectionStub();
const sut = new TreeRootManager();
const context = new TestContext()
.withNodeCollection(expectedCollection);
// act
const actualCollection = sut.collection;
const actualCollection = context
.build()
.collection;
// assert
expect(actualCollection).to.equal(expectedCollection);
});
@@ -39,15 +42,41 @@ describe('TreeRootManager', () => {
it('creates with same collection it uses', () => {
// arrange
let usedCollection: TreeNodeCollection | undefined;
const factoryMock = (collection) => {
const factoryMock: FocusManagerFactory = (collection) => {
usedCollection = collection;
return new SingleNodeFocusManagerStub();
};
const sut = new TreeRootManager(new TreeNodeCollectionStub(), factoryMock);
const context = new TestContext()
.withFocusManagerFactory(factoryMock);
// act
const expected = sut.collection;
const expected = context
.build()
.collection;
// assert
expect(usedCollection).to.equal(expected);
});
});
});
class TestContext {
private nodeCollection: TreeNodeCollection = new TreeNodeCollectionStub();
private focusManagerFactory: FocusManagerFactory = () => new SingleNodeFocusManagerStub();
public withFocusManagerFactory(focusManagerFactory: FocusManagerFactory): this {
this.focusManagerFactory = focusManagerFactory;
return this;
}
public withNodeCollection(nodeCollection: TreeNodeCollection): this {
this.nodeCollection = nodeCollection;
return this;
}
public build(): TreeRootManager {
return new TreeRootManager(
this.nodeCollection,
this.focusManagerFactory,
);
}
}

View File

@@ -7,6 +7,7 @@ import { UseUserSelectionStateStub } from '@tests/unit/shared/Stubs/UseUserSelec
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import type { TreeNodeId } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
import type { Executable } from '@/domain/Executables/Executable';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
describe('useSelectedScriptNodeIds', () => {
it('returns an empty array when no scripts are selected', () => {
@@ -44,7 +45,7 @@ describe('useSelectedScriptNodeIds', () => {
});
it('when the selection state changes', () => {
// arrange
const initialScripts = [];
const initialScripts = new Array<SelectedScript>();
const changedScripts = [
new SelectedScriptStub(new ScriptStub('id-1')),
new SelectedScriptStub(new ScriptStub('id-2')),

View File

@@ -67,7 +67,7 @@ function runSharedTestsForAnimation(
};
const element = document.createElement('div');
Object.entries(expectedStyleValues).forEach(([key, value]) => {
element.style[key] = value;
element.style[key as keyof MutatedStyleProperties] = value;
});
const timer = new TimerStub();
const hookResult = useExpandCollapseAnimation(timer);
@@ -78,7 +78,8 @@ function runSharedTestsForAnimation(
await promise;
// assert
Object.entries(expectedStyleValues).forEach(([key, expectedStyleValue]) => {
const actualStyleValue = element.style[key];
const styleProperty = key as keyof MutatedStyleProperties;
const actualStyleValue = element.style[styleProperty];
expect(actualStyleValue).to.equal(expectedStyleValue, formatAssertionMessage([
`Style key: ${key}`,
`Expected style value: ${expectedStyleValue}`,
@@ -86,7 +87,7 @@ function runSharedTestsForAnimation(
`Initial style value: ${expectedStyleValues}`,
'All styles:',
...Object.entries(expectedStyleValues)
.map(([k, value]) => indentText(`- ${k} > actual: "${element.style[k]}" | expected: "${value}"`)),
.map(([k, value]) => indentText(`- ${k} > actual: "${actualStyleValue}" | expected: "${value}"`)),
]));
});
});

View File

@@ -30,18 +30,19 @@ describe('useClipboard', () => {
} = {
copyText: ['text-arg'],
};
Object.entries(testScenarios).forEach(([functionName, testFunctionArgs]) => {
describe(functionName, () => {
Object.entries(testScenarios).forEach(([functionNameValue, testFunctionArgs]) => {
const functionName = functionNameValue as ClipboardFunction;
describe(functionNameValue, () => {
it('binds the method to the instance', () => {
// arrange
const expectedArgs = testFunctionArgs;
const clipboardStub = new ClipboardStub();
// act
const clipboard = useClipboard(clipboardStub);
const { [functionName as ClipboardFunction]: testFunction } = clipboard;
const { [functionName]: testFunction } = clipboard;
// assert
testFunction(...expectedArgs);
const call = clipboardStub.callHistory.find((c) => c.methodName === functionName);
const call = clipboardStub.callHistory.find((c) => c.methodName === functionNameValue);
expectExists(call);
expect(call.args).to.deep.equal(expectedArgs);
});
@@ -50,14 +51,15 @@ describe('useClipboard', () => {
const clipboardStub = new ClipboardStub();
const expectedThisContext = clipboardStub;
let actualThisContext: typeof expectedThisContext | undefined;
// eslint-disable-next-line func-names
clipboardStub[functionName] = function () {
// eslint-disable-next-line func-names, @typescript-eslint/no-unused-vars
clipboardStub[functionName] = function (_text) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
actualThisContext = this;
return Promise.resolve();
};
// act
const clipboard = useClipboard(clipboardStub);
const { [functionName as ClipboardFunction]: testFunction } = clipboard;
const { [functionNameValue as ClipboardFunction]: testFunction } = clipboard;
// assert
testFunction(...testFunctionArgs);
expect(expectedThisContext).to.equal(actualThisContext);

View File

@@ -9,6 +9,7 @@ import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub';
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
describe('useUserSelectionState', () => {
describe('currentSelection', () => {
@@ -170,8 +171,8 @@ describe('useUserSelectionState', () => {
describe('triggers change', () => {
it('with new selected scripts array reference', async () => {
// arrange
const oldSelectedScriptsArrayReference = [];
const newSelectedScriptsArrayReference = [];
const oldSelectedScriptsArrayReference = new Array<SelectedScript>();
const newSelectedScriptsArrayReference = new Array<SelectedScript>();
const scriptSelectionStub = new ScriptSelectionStub()
.withSelectedScripts(oldSelectedScriptsArrayReference);
const collectionStateStub = new UseCollectionStateStub()
@@ -191,7 +192,7 @@ describe('useUserSelectionState', () => {
});
it('with same selected scripts array reference', async () => {
// arrange
const sharedSelectedScriptsReference = [];
const sharedSelectedScriptsReference = new Array<SelectedScript>();
const scriptSelectionStub = new ScriptSelectionStub()
.withSelectedScripts(sharedSelectedScriptsReference);
const collectionStateStub = new UseCollectionStateStub()

View File

@@ -65,7 +65,7 @@ describe('IpcRegistration', () => {
// act
context.run();
// assert
const channel = IpcChannelDefinitions[key];
const channel = IpcChannelDefinitions[key as ChannelDefinitionKey] as IpcChannel<unknown>;
const actualInstance = getRegisteredInstance(channel);
expect(actualInstance).to.equal(expectedInstance);
});

View File

@@ -50,7 +50,7 @@ describe('RendererApiProvider', () => {
// act
const variables = testContext.provideWindowVariables();
// assert
const actualValue = variables[property];
const actualValue = variables[property as PropertyKeys<Required<WindowVariables>>];
expect(actualValue).to.equal(expectedValue);
});
});

View File

@@ -1,5 +1,4 @@
import { describe, it, expect } from 'vitest';
import type { IpcChannel } from '@/presentation/electron/shared/IpcBridging/IpcChannel';
import { type ChannelDefinitionKey, IpcChannelDefinitions } from '@/presentation/electron/shared/IpcBridging/IpcChannelDefinitions';
describe('IpcChannelDefinitions', () => {
@@ -25,7 +24,7 @@ describe('IpcChannelDefinitions', () => {
[definitionKey, { expectedNamespace, expectedAccessibleMembers }],
) => {
describe(`channel: "${definitionKey}"`, () => {
const ipcChannelUnderTest = IpcChannelDefinitions[definitionKey] as IpcChannel<unknown>;
const ipcChannelUnderTest = IpcChannelDefinitions[definitionKey as ChannelDefinitionKey];
it('has expected namespace', () => {
// act
const actualNamespace = ipcChannelUnderTest.namespace;

View File

@@ -38,7 +38,7 @@ export class ExpressionStub implements IExpression {
this.callHistory.push(context);
if (this.result === undefined /* not empty string */) {
const { args } = context;
return `[expression-stub] args: ${args ? Object.keys(args).map((key) => `${key}: ${args[key]}`).join('", "') : 'none'}`;
return `[expression-stub] args: ${args ? Object.entries(args).map((key, value) => `${key}: ${value}`).join('", "') : 'none'}`;
}
return this.result;
}

View File

@@ -1,6 +1,6 @@
import type { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions';
import type { SanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/SanityCheckOptions';
export class SanityCheckOptionsStub implements ISanityCheckOptions {
export class SanityCheckOptionsStub implements SanityCheckOptions {
public validateWindowVariables = false;
public validateEnvironmentVariables = false;

View File

@@ -1,8 +1,8 @@
import type { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions';
import type { ISanityValidator } from '@/infrastructure/RuntimeSanity/Common/ISanityValidator';
import type { SanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/SanityCheckOptions';
import type { SanityValidator } from '@/infrastructure/RuntimeSanity/Common/SanityValidator';
export class SanityValidatorStub implements ISanityValidator {
public shouldValidateArgs = new Array<ISanityCheckOptions>();
export class SanityValidatorStub implements SanityValidator {
public shouldValidateArgs = new Array<SanityCheckOptions>();
public name = 'sanity-validator-stub';
@@ -10,7 +10,7 @@ export class SanityValidatorStub implements ISanityValidator {
private shouldValidateResult = true;
public shouldValidate(options: ISanityCheckOptions): boolean {
public shouldValidate(options: SanityCheckOptions): boolean {
this.shouldValidateArgs.push(options);
return this.shouldValidateResult;
}

View File

@@ -11,7 +11,7 @@ export class VueDependencyInjectionApiStub implements VueDependencyInjectionApi
public inject<T>(key: InjectionKey<T>): T {
const providedValue = this.injections.get(key);
if (providedValue === undefined) {
throw new Error(`[VueDependencyInjectionApiStub] No value provided for key: ${String(key)}`);
throw new Error(`[${VueDependencyInjectionApiStub.name}] No value provided for key: ${String(key)}`);
}
return providedValue as T;
}