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

17
package-lock.json generated
View File

@@ -6,7 +6,7 @@
"packages": { "packages": {
"": { "": {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.13.5", "version": "0.13.6",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@floating-ui/vue": "^1.1.1", "@floating-ui/vue": "^1.1.1",
@@ -53,7 +53,7 @@
"start-server-and-test": "^2.0.4", "start-server-and-test": "^2.0.4",
"terser": "^5.31.3", "terser": "^5.31.3",
"tslib": "^2.6.3", "tslib": "^2.6.3",
"typescript": "^5.4.5", "typescript": "~5.5.4",
"vite": "^5.3.4", "vite": "^5.3.4",
"vitest": "^2.0.3", "vitest": "^2.0.3",
"vue-tsc": "^2.0.26", "vue-tsc": "^2.0.26",
@@ -15689,10 +15689,11 @@
"dev": true "dev": true
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.4.5", "version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -28699,9 +28700,9 @@
"dev": true "dev": true
}, },
"typescript": { "typescript": {
"version": "5.4.5", "version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"devOptional": true "devOptional": true
}, },
"uc.micro": { "uc.micro": {

View File

@@ -78,7 +78,7 @@
"start-server-and-test": "^2.0.4", "start-server-and-test": "^2.0.4",
"terser": "^5.31.3", "terser": "^5.31.3",
"tslib": "^2.6.3", "tslib": "^2.6.3",
"typescript": "^5.4.5", "typescript": "~5.5.4",
"vite": "^5.3.4", "vite": "^5.3.4",
"vitest": "^2.0.3", "vitest": "^2.0.3",
"vue-tsc": "^2.0.26", "vue-tsc": "^2.0.26",

View File

@@ -33,23 +33,25 @@ function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
if (!casedValue) { if (!casedValue) {
throw new Error(`unknown ${enumName}: "${value}"`); throw new Error(`unknown ${enumName}: "${value}"`);
} }
return enumVariable[casedValue as keyof typeof enumVariable]; return enumVariable[casedValue as keyof EnumVariable<T, TEnumValue>];
} }
export function getEnumNames export function getEnumNames
<T extends EnumType, TEnumValue extends EnumType>( <T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>, enumVariable: EnumVariable<T, TEnumValue>,
): string[] { ): (string & keyof EnumVariable<T, TEnumValue>)[] {
return Object return Object
.values(enumVariable) .values(enumVariable)
.filter((enumMember): enumMember is string => isString(enumMember)); .filter((
enumMember,
): enumMember is string & (keyof EnumVariable<T, TEnumValue>) => isString(enumMember));
} }
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>( export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>, enumVariable: EnumVariable<T, TEnumValue>,
): TEnumValue[] { ): TEnumValue[] {
return getEnumNames(enumVariable) return getEnumNames(enumVariable)
.map((level) => enumVariable[level]) as TEnumValue[]; .map((name) => enumVariable[name]) as TEnumValue[];
} }
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>( export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(

View File

@@ -17,7 +17,7 @@ export class ApplicationContext implements IApplicationContext {
public currentOs: OperatingSystem; public currentOs: OperatingSystem;
public get state(): ICategoryCollectionState { public get state(): ICategoryCollectionState {
return this.states[this.collection.os]; return this.getState(this.collection.os);
} }
private readonly states: StateMachine; private readonly states: StateMachine;
@@ -26,30 +26,51 @@ export class ApplicationContext implements IApplicationContext {
public readonly app: IApplication, public readonly app: IApplication,
initialContext: OperatingSystem, initialContext: OperatingSystem,
) { ) {
this.setContext(initialContext);
this.states = initializeStates(app); this.states = initializeStates(app);
this.changeContext(initialContext);
} }
public changeContext(os: OperatingSystem): void { public changeContext(os: OperatingSystem): void {
assertInRange(os, OperatingSystem);
if (this.currentOs === os) { if (this.currentOs === os) {
return; return;
} }
const collection = this.app.getCollection(os);
this.collection = collection;
const event: IApplicationContextChangedEvent = { const event: IApplicationContextChangedEvent = {
newState: this.states[os], newState: this.getState(os),
oldState: this.states[this.currentOs], oldState: this.getState(this.currentOs),
}; };
this.setContext(os);
this.contextChanged.notify(event); this.contextChanged.notify(event);
}
private setContext(os: OperatingSystem): void {
validateOperatingSystem(os, this.app);
this.collection = this.app.getCollection(os);
this.currentOs = os; this.currentOs = os;
} }
private getState(os: OperatingSystem): ICategoryCollectionState {
const state = this.states.get(os);
if (!state) {
throw new Error(`Operating system "${OperatingSystem[os]}" state is unknown.`);
}
return state;
}
}
function validateOperatingSystem(
os: OperatingSystem,
app: IApplication,
): void {
assertInRange(os, OperatingSystem);
if (!app.getSupportedOsList().includes(os)) {
throw new Error(`Operating system "${OperatingSystem[os]}" is not supported.`);
}
} }
function initializeStates(app: IApplication): StateMachine { function initializeStates(app: IApplication): StateMachine {
const machine = new Map<OperatingSystem, ICategoryCollectionState>(); const machine = new Map<OperatingSystem, ICategoryCollectionState>();
for (const collection of app.collections) { for (const collection of app.collections) {
machine[collection.os] = new CategoryCollectionState(collection); machine.set(collection.os, new CategoryCollectionState(collection));
} }
return machine; return machine;
} }

View File

@@ -82,7 +82,7 @@ function ensureValidCategory(
}); });
validator.assertType((v) => v.assertObject({ validator.assertType((v) => v.assertObject({
value: category, value: category,
valueName: `Category '${category.category}'` ?? 'Category', valueName: category.category ? `Category '${category.category}'` : 'Category',
allowedProperties: [ allowedProperties: [
'docs', 'children', 'category', 'docs', 'children', 'category',
], ],

View File

@@ -106,7 +106,7 @@ function validateScript(
): asserts script is NonNullable<ScriptData> { ): asserts script is NonNullable<ScriptData> {
validator.assertType((v) => v.assertObject<CallScriptData & CodeScriptData>({ validator.assertType((v) => v.assertObject<CallScriptData & CodeScriptData>({
value: script, value: script,
valueName: `Script '${script.name}'` ?? 'Script', valueName: script.name ? `Script '${script.name}'` : 'Script',
allowedProperties: [ allowedProperties: [
'name', 'recommend', 'code', 'revertCode', 'call', 'docs', 'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
], ],

View File

@@ -15,7 +15,7 @@ export type DuplicateLinesAnalyzer = CodeValidationAnalyzer & {
export const analyzeDuplicateLines: DuplicateLinesAnalyzer = ( export const analyzeDuplicateLines: DuplicateLinesAnalyzer = (
lines: readonly CodeLine[], lines: readonly CodeLine[],
language: ScriptingLanguage, language: ScriptingLanguage,
syntaxFactory = createSyntax, syntaxFactory: SyntaxFactory = createSyntax,
) => { ) => {
const syntax = syntaxFactory(language); const syntax = syntaxFactory(language);
return lines return lines

View File

@@ -1,16 +1,16 @@
import type { ISanityValidator } from './ISanityValidator'; import type { SanityValidator } from './SanityValidator';
import type { ISanityCheckOptions } from './ISanityCheckOptions'; import type { SanityCheckOptions } from './SanityCheckOptions';
export type FactoryFunction<T> = () => T; export type FactoryFunction<T> = () => T;
export abstract class FactoryValidator<T> implements ISanityValidator { export abstract class FactoryValidator<T> implements SanityValidator {
private readonly factory: FactoryFunction<T>; private readonly factory: FactoryFunction<T>;
protected constructor(factory: FactoryFunction<T>) { protected constructor(factory: FactoryFunction<T>) {
this.factory = factory; this.factory = factory;
} }
public abstract shouldValidate(options: ISanityCheckOptions): boolean; public abstract shouldValidate(options: SanityCheckOptions): boolean;
public abstract name: string; public abstract name: string;

View File

@@ -1,7 +0,0 @@
import type { ISanityCheckOptions } from './ISanityCheckOptions';
export interface ISanityValidator {
readonly name: string;
shouldValidate(options: ISanityCheckOptions): boolean;
collectErrors(): Iterable<string>;
}

View File

@@ -1,4 +1,4 @@
export interface ISanityCheckOptions { export interface SanityCheckOptions {
readonly validateEnvironmentVariables: boolean; readonly validateEnvironmentVariables: boolean;
readonly validateWindowVariables: boolean; readonly validateWindowVariables: boolean;
} }

View File

@@ -0,0 +1,7 @@
import type { SanityCheckOptions } from './SanityCheckOptions';
export interface SanityValidator {
readonly name: string;
shouldValidate(options: SanityCheckOptions): boolean;
collectErrors(): Iterable<string>;
}

View File

@@ -1,16 +1,23 @@
import { EnvironmentVariablesValidator } from './Validators/EnvironmentVariablesValidator'; import { EnvironmentVariablesValidator } from './Validators/EnvironmentVariablesValidator';
import type { ISanityCheckOptions } from './Common/ISanityCheckOptions'; import type { SanityCheckOptions } from './Common/SanityCheckOptions';
import type { ISanityValidator } from './Common/ISanityValidator'; import type { SanityValidator } from './Common/SanityValidator';
const DefaultSanityValidators: ISanityValidator[] = [ const DefaultSanityValidators: SanityValidator[] = [
new EnvironmentVariablesValidator(), new EnvironmentVariablesValidator(),
]; ];
export interface RuntimeSanityValidator {
(
options: SanityCheckOptions,
validators?: readonly SanityValidator[],
): void;
}
/* Helps to fail-fast on errors */ /* Helps to fail-fast on errors */
export function validateRuntimeSanity( export const validateRuntimeSanity: RuntimeSanityValidator = (
options: ISanityCheckOptions, options: SanityCheckOptions,
validators: readonly ISanityValidator[] = DefaultSanityValidators, validators: readonly SanityValidator[] = DefaultSanityValidators,
): void { ) => {
if (!validators.length) { if (!validators.length) {
throw new Error('missing validators'); throw new Error('missing validators');
} }
@@ -26,9 +33,9 @@ export function validateRuntimeSanity(
if (errorMessages.length > 0) { if (errorMessages.length > 0) {
throw new Error(`Sanity check failed.\n${errorMessages.join('\n---\n')}`); throw new Error(`Sanity check failed.\n${errorMessages.join('\n---\n')}`);
} }
} };
function getErrorMessage(validator: ISanityValidator): string | undefined { function getErrorMessage(validator: SanityValidator): string | undefined {
const errorMessages = [...validator.collectErrors()]; const errorMessages = [...validator.collectErrors()];
if (!errorMessages.length) { if (!errorMessages.length) {
return undefined; return undefined;

View File

@@ -1,7 +1,7 @@
import type { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables'; import type { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory'; import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
import { FactoryValidator, type FactoryFunction } from '../Common/FactoryValidator'; import { FactoryValidator, type FactoryFunction } from '../Common/FactoryValidator';
import type { ISanityCheckOptions } from '../Common/ISanityCheckOptions'; import type { SanityCheckOptions } from '../Common/SanityCheckOptions';
export class EnvironmentVariablesValidator extends FactoryValidator<IEnvironmentVariables> { export class EnvironmentVariablesValidator extends FactoryValidator<IEnvironmentVariables> {
constructor( constructor(
@@ -14,7 +14,7 @@ export class EnvironmentVariablesValidator extends FactoryValidator<IEnvironment
public override name = 'environment variables'; public override name = 'environment variables';
public override shouldValidate(options: ISanityCheckOptions): boolean { public override shouldValidate(options: SanityCheckOptions): boolean {
return options.validateEnvironmentVariables; return options.validateEnvironmentVariables;
} }
} }

View File

@@ -1,6 +1,6 @@
import type { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables'; import type { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
import { FactoryValidator, type FactoryFunction } from '../Common/FactoryValidator'; import { FactoryValidator, type FactoryFunction } from '../Common/FactoryValidator';
import type { ISanityCheckOptions } from '../Common/ISanityCheckOptions'; import type { SanityCheckOptions } from '../Common/SanityCheckOptions';
export class WindowVariablesValidator extends FactoryValidator<WindowVariables> { export class WindowVariablesValidator extends FactoryValidator<WindowVariables> {
constructor(factory: FactoryFunction<WindowVariables> = () => window) { constructor(factory: FactoryFunction<WindowVariables> = () => window) {
@@ -9,7 +9,7 @@ export class WindowVariablesValidator extends FactoryValidator<WindowVariables>
public override name = 'window variables'; public override name = 'window variables';
public override shouldValidate(options: ISanityCheckOptions): boolean { public override shouldValidate(options: SanityCheckOptions): boolean {
return options.validateWindowVariables; return options.validateWindowVariables;
} }
} }

View File

@@ -1,4 +1,4 @@
import { RuntimeSanityValidator } from './Modules/RuntimeSanityValidator'; import { RuntimeSanityBootstrapper } from './Modules/RuntimeSanityBootstrapper';
import { AppInitializationLogger } from './Modules/AppInitializationLogger'; import { AppInitializationLogger } from './Modules/AppInitializationLogger';
import { DependencyBootstrapper } from './Modules/DependencyBootstrapper'; import { DependencyBootstrapper } from './Modules/DependencyBootstrapper';
import { MobileSafariActivePseudoClassEnabler } from './Modules/MobileSafariActivePseudoClassEnabler'; import { MobileSafariActivePseudoClassEnabler } from './Modules/MobileSafariActivePseudoClassEnabler';
@@ -17,7 +17,7 @@ export class ApplicationBootstrapper implements Bootstrapper {
private static getAllBootstrappers(): Bootstrapper[] { private static getAllBootstrappers(): Bootstrapper[] {
return [ return [
new RuntimeSanityValidator(), new RuntimeSanityBootstrapper(),
new DependencyBootstrapper(), new DependencyBootstrapper(),
new AppInitializationLogger(), new AppInitializationLogger(),
new MobileSafariActivePseudoClassEnabler(), new MobileSafariActivePseudoClassEnabler(),

View File

@@ -0,0 +1,15 @@
import { validateRuntimeSanity, type RuntimeSanityValidator } from '@/infrastructure/RuntimeSanity/SanityChecks';
import type { Bootstrapper } from '../Bootstrapper';
export class RuntimeSanityBootstrapper implements Bootstrapper {
constructor(private readonly validator: RuntimeSanityValidator = validateRuntimeSanity) {
}
public async bootstrap(): Promise<void> {
this.validator({
validateEnvironmentVariables: true,
validateWindowVariables: true,
});
}
}

View File

@@ -1,15 +0,0 @@
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
import type { Bootstrapper } from '../Bootstrapper';
export class RuntimeSanityValidator implements Bootstrapper {
constructor(private readonly validator = validateRuntimeSanity) {
}
public async bootstrap(): Promise<void> {
this.validator({
validateEnvironmentVariables: true,
validateWindowVariables: true,
});
}
}

View File

@@ -39,7 +39,7 @@
:tree-root="treeRoot" :tree-root="treeRoot"
:rendering-strategy="renderingStrategy" :rendering-strategy="renderingStrategy"
> >
<template #node-content="slotProps"> <template #node-content="slotProps: NodeMetadata">
<slot name="node-content" v-bind="slotProps" /> <slot name="node-content" v-bind="slotProps" />
</template> </template>
</HierarchicalTreeNode> </HierarchicalTreeNode>
@@ -55,6 +55,7 @@ import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { useNodeState } from './UseNodeState'; import { useNodeState } from './UseNodeState';
import LeafTreeNode from './LeafTreeNode.vue'; import LeafTreeNode from './LeafTreeNode.vue';
import InteractableNode from './InteractableNode.vue'; import InteractableNode from './InteractableNode.vue';
import type { NodeMetadata } from '../../NodeContent/NodeMetadata';
import type { TreeRoot } from '../TreeRoot/TreeRoot'; import type { TreeRoot } from '../TreeRoot/TreeRoot';
import type { TreeNode, TreeNodeId } from './TreeNode'; import type { TreeNode, TreeNodeId } from './TreeNode';
import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy'; import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
@@ -107,6 +108,7 @@ export default defineComponent({
); );
return { return {
NodeMetadata: Object as PropType<NodeMetadata>,
renderedNodeIds, renderedNodeIds,
isExpanded, isExpanded,
toggleExpand, toggleExpand,

View File

@@ -11,11 +11,17 @@ export class TreeRootManager implements TreeRoot {
constructor( constructor(
collection: TreeNodeCollection = new TreeNodeInitializerAndUpdater(), collection: TreeNodeCollection = new TreeNodeInitializerAndUpdater(),
createFocusManager: ( createFocusManager: FocusManagerFactory = (
collection: TreeNodeCollection nodes,
) => SingleNodeFocusManager = (nodes) => new SingleNodeCollectionFocusManager(nodes), ) => new SingleNodeCollectionFocusManager(nodes),
) { ) {
this.collection = collection; this.collection = collection;
this.focus = createFocusManager(this.collection); this.focus = createFocusManager(this.collection);
} }
} }
export interface FocusManagerFactory {
(
collection: TreeNodeCollection
): SingleNodeFocusManager;
}

View File

@@ -120,17 +120,15 @@ function getArrowPositionStyles(
coordinations: Partial<Coords>, coordinations: Partial<Coords>,
placement: Placement, placement: Placement,
): CSSProperties { ): CSSProperties {
const style: CSSProperties = {}; const { x, y } = coordinations; // either X or Y is calculated
style.position = 'absolute';
const { x, y } = coordinations;
if (x) {
style.left = `${x}px`;
} else if (y) { // either X or Y is calculated
style.top = `${y}px`;
}
const oppositeSide = getCounterpartBoxOffsetProperty(placement); const oppositeSide = getCounterpartBoxOffsetProperty(placement);
style[oppositeSide.toString()] = `-${ARROW_SIZE_IN_PX}px`; const newStyle: CSSProperties = {
return style; [oppositeSide]: `-${ARROW_SIZE_IN_PX}px`,
position: 'absolute',
left: x ? `${x}px` : undefined,
top: y ? `${y}px` : undefined,
};
return newStyle;
} }
function getCounterpartBoxOffsetProperty(placement: Placement): keyof CSSProperties { function getCounterpartBoxOffsetProperty(placement: Placement): keyof CSSProperties {

View File

@@ -22,7 +22,8 @@ export function registerAllIpcChannels(
}; };
Object.entries(ipcInstanceCreators).forEach(([name, instanceFactory]) => { Object.entries(ipcInstanceCreators).forEach(([name, instanceFactory]) => {
try { try {
const definition = IpcChannelDefinitions[name]; const definitionKey = name as keyof typeof IpcChannelDefinitions;
const definition = IpcChannelDefinitions[definitionKey] as IpcChannel<unknown>;
const instance = instanceFactory(); const instance = instanceFactory();
registrar(definition, instance); registrar(definition, instance);
} catch (err) { } catch (err) {

View File

@@ -1,6 +1,6 @@
import { app, dialog } from 'electron/main'; import { app, dialog } from 'electron/main';
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import { UpdateProgressBar } from './UpdateProgressBar'; import { UpdateProgressBar } from './ProgressBar/UpdateProgressBar';
import { getAutoUpdater } from './ElectronAutoUpdaterFactory'; import { getAutoUpdater } from './ElectronAutoUpdaterFactory';
import type { AppUpdater, UpdateInfo } from 'electron-updater'; import type { AppUpdater, UpdateInfo } from 'electron-updater';
import type { ProgressInfo } from 'electron-builder'; import type { ProgressInfo } from 'electron-builder';
@@ -26,11 +26,13 @@ function startHandlingUpdateProgress(autoUpdater: AppUpdater) {
So the indeterminate progress will continue until download is finished. So the indeterminate progress will continue until download is finished.
*/ */
ElectronLogger.debug('@download-progress@\n', progress); ElectronLogger.debug('@download-progress@\n', progress);
if (progressBar.isOpen) { // May be closed by the user
progressBar.showProgress(progress); progressBar.showProgress(progress);
}
}); });
autoUpdater.on('update-downloaded', async (info: UpdateInfo) => { autoUpdater.on('update-downloaded', async (info: UpdateInfo) => {
ElectronLogger.info('@update-downloaded@\n', info); ElectronLogger.info('@update-downloaded@\n', info);
progressBar.close(); progressBar.closeIfOpen();
await handleUpdateDownloaded(autoUpdater); await handleUpdateDownloaded(autoUpdater);
}); });
} }

View File

@@ -3,7 +3,7 @@ import { unlink, mkdir } from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { app } from 'electron/main'; import { app } from 'electron/main';
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import { UpdateProgressBar } from '../UpdateProgressBar'; import { UpdateProgressBar } from '../ProgressBar/UpdateProgressBar';
import { retryFileSystemAccess } from './RetryFileSystemAccess'; import { retryFileSystemAccess } from './RetryFileSystemAccess';
import type { UpdateInfo } from 'electron-updater'; import type { UpdateInfo } from 'electron-updater';
import type { ReadableStream } from 'node:stream/web'; import type { ReadableStream } from 'node:stream/web';

View File

@@ -4,7 +4,7 @@ import { GitHubProjectDetails } from '@/domain/Project/GitHubProjectDetails';
import { Version } from '@/domain/Version'; import { Version } from '@/domain/Version';
import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser'; import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { UpdateProgressBar } from '../UpdateProgressBar'; import { UpdateProgressBar } from '../ProgressBar/UpdateProgressBar';
import { import {
promptForManualUpdate, promptInstallerOpenError, promptForManualUpdate, promptInstallerOpenError,
promptIntegrityCheckFailure, promptDownloadError, promptIntegrityCheckFailure, promptDownloadError,
@@ -100,7 +100,7 @@ async function withProgressBar(
) { ) {
const progressBar = new UpdateProgressBar(); const progressBar = new UpdateProgressBar();
await action(progressBar); await action(progressBar);
progressBar.close(); progressBar.closeIfOpen();
} }
async function isIntegrityPreserved( async function isIntegrityPreserved(

View File

@@ -0,0 +1,158 @@
// @ts-expect-error Outdated `@types/electron-progressbar` causes build failure on macOS
import ProgressBar from 'electron-progressbar';
import { BrowserWindow } from 'electron/main';
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import type { Logger } from '@/application/Common/Log/Logger';
import { EventSource } from '@/infrastructure/Events/EventSource';
import type {
InitialProgressBarWindowOptions,
ProgressBarUpdater,
ProgressBarStatus,
ProgressBarUpdateCallback, ProgressBarWithLifecycle,
} from './ProgressBarWithLifecycle';
/**
* It provides a type-safe way to manage `electron-progressbar` instance,
* through its lifecycle, ensuring correct usage, state transitions, and cleanup.
*/
export class ElectronProgressBarWithLifecycle implements ProgressBarWithLifecycle {
private state: ProgressBarWithState = { status: 'closed' };
public readonly statusChanged = new EventSource<ProgressBarStatus>();
private readyCallbacks = new Array<ProgressBarUpdateCallback>();
constructor(
private readonly logger: Logger = ElectronLogger,
) { }
public update(handler: ProgressBarUpdateCallback): void {
switch (this.state.status) { // eslint-disable-line default-case
case 'closed':
// Throwing an error here helps catch bugs early in the development process.
throw new Error('Cannot update the progress bar because it is not currently open.');
case 'ready':
handler(wrapForUpdate(this.state));
break;
case 'loading':
this.readyCallbacks.push(handler);
break;
}
}
public resetAndOpen(options: InitialProgressBarWindowOptions): void {
this.closeIfOpen();
const bar = createElectronProgressBar(options);
this.state = { status: 'loading', progressBar: bar };
bar.on('ready', () => this.handleReadyEvent(bar));
bar.on('aborted' /* closed by user */, (value: number) => {
this.changeState({ status: 'closed' });
this.logger.info(`Progress bar window closed by user. State: ${this.state.status}, Value: ${value}.`);
});
}
public closeIfOpen() {
if (this.state.status === 'closed') {
return;
}
this.state.progressBar.close();
this.changeState({
status: 'closed',
});
}
private handleReadyEvent(bar: ProgressBar): void {
if (this.state.status !== 'loading' || this.state.progressBar !== bar) {
// Handle race conditions if `open` called rapidly without closing to avoid leaks
this.logger.warn('Unexpected state when handling ready event. Closing the progress bar.');
bar.close();
return;
}
const readyBar: ReadyProgressBar = {
status: 'ready',
progressBar: bar,
browserWindow: getWindow(bar),
};
this.readyCallbacks.forEach((callback) => callback(wrapForUpdate(readyBar)));
this.changeState(readyBar);
}
private changeState(newState: ProgressBarWithState): void {
if (isSameState(this.state, newState)) {
return;
}
this.readyCallbacks = [];
this.state = newState;
this.statusChanged.notify(newState.status);
}
}
type ProgressBarWithState = { readonly status: 'closed' }
| { readonly status: 'loading', readonly progressBar: ProgressBar }
| ReadyProgressBar;
interface ReadyProgressBar {
readonly status: 'ready';
readonly progressBar: ProgressBar;
readonly browserWindow: BrowserWindow;
}
function getWindow(bar: ProgressBar): BrowserWindow {
// Note: The ProgressBar library does not provide a public method or event
// to access the BrowserWindow, so we access the internal `_window` property directly.
if (!('_window' in bar)) {
throw new Error('Unable to access the progress bar window.');
}
const browserWindow = bar._window as BrowserWindow; // eslint-disable-line no-underscore-dangle
if (!browserWindow) {
throw new Error('Missing internal browser window');
}
return browserWindow;
}
function isSameState( // eslint-disable-line consistent-return
first: ProgressBarWithState,
second: ProgressBarWithState,
): boolean {
switch (first.status) { // eslint-disable-line default-case
case 'closed':
return second.status === 'closed';
case 'loading':
return second.status === 'loading'
&& second.progressBar === first.progressBar;
case 'ready':
return second.status === 'ready'
&& second.progressBar === first.progressBar
&& second.browserWindow === first.browserWindow;
}
}
function wrapForUpdate(bar: ReadyProgressBar): ProgressBarUpdater {
return {
setText: (text: string) => {
bar.progressBar.detail = text;
},
setClosable: (closable: boolean) => {
bar.browserWindow.setClosable(closable);
},
setProgress: (progress: number) => {
bar.progressBar.value = progress;
},
};
}
function createElectronProgressBar(
options: InitialProgressBarWindowOptions,
): ProgressBar {
const bar = new ProgressBar({
indeterminate: options.type === 'indeterminate',
title: options.title,
text: options.initialText,
});
if (options.type === 'percentile') { // Indeterminate progress bar does not fire `completed` event, see `electron-progressbar` docs
bar.on('completed', () => {
bar.detail = options.textOnCompleted;
});
}
return bar;
}

View File

@@ -0,0 +1,31 @@
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
/*
Defines interfaces to abstract the progress bar implementation,
serving as an anti-corruption layer. This approach allows for
flexibility in switching implementations if needed in the future,
while maintaining a consistent API for the rest of the application.
*/
export interface ProgressBarWithLifecycle {
resetAndOpen(options: InitialProgressBarWindowOptions): void;
closeIfOpen(): void;
update(handler: ProgressBarUpdateCallback): void;
readonly statusChanged: IEventSource<ProgressBarStatus>;
}
export type ProgressBarStatus = 'closed' | 'loading' | 'ready';
export type ProgressBarUpdateCallback = (bar: ProgressBarUpdater) => void;
export interface InitialProgressBarWindowOptions {
readonly type: 'indeterminate' | 'percentile';
readonly title: string,
readonly initialText: string;
readonly textOnCompleted: string;
}
export interface ProgressBarUpdater {
setText(text: string): void;
setClosable(closable: boolean): void;
setProgress(progress: number): void;
}

View File

@@ -0,0 +1,92 @@
import { app } from 'electron/main';
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import type { Logger } from '@/application/Common/Log/Logger';
import { ElectronProgressBarWithLifecycle } from './ElectronProgressBarWithLifecycle';
import type { ProgressInfo } from 'electron-builder';
import type { InitialProgressBarWindowOptions, ProgressBarWithLifecycle } from './ProgressBarWithLifecycle';
export class UpdateProgressBar {
private visibilityState: ProgressBarVisibilityState = 'closed';
constructor(
private readonly logger: Logger = ElectronLogger,
private readonly currentApp: Electron.App = app,
private readonly progressBar: ProgressBarWithLifecycle = new ElectronProgressBarWithLifecycle(),
) {
this.progressBar.statusChanged.on((status) => {
if (status === 'closed') {
this.visibilityState = 'closed';
}
});
}
public get isOpen(): boolean {
return this.visibilityState !== 'closed';
}
public showIndeterminateState() {
if (this.visibilityState === 'showingIndeterminate') {
return;
}
this.progressBar.resetAndOpen(createInitialProgressBarWindowOptions('indeterminate', this.currentApp.name));
this.visibilityState = 'showingIndeterminate';
}
public showProgress(progress: ProgressInfo) {
const percentage = getUpdatePercent(progress);
this.showPercentage(percentage);
}
public showPercentage(percentage: number) {
if (this.visibilityState !== 'showingPercentile') {
this.progressBar.resetAndOpen(createInitialProgressBarWindowOptions('percentile', this.currentApp.name));
this.visibilityState = 'showingPercentile';
}
this.progressBar.update((bar) => {
bar.setProgress(percentage);
bar.setText(`${percentage}% ...`);
});
}
public showError(e: Error) {
this.logger.warn(`Error displayed in progress bar. Visibility state: ${this.visibilityState}. Error message: ${e.message}`);
if (this.visibilityState === 'closed') {
throw new Error('Cannot display error because the progress bar is not visible.');
}
this.progressBar.update((bar) => {
bar.setText('An error occurred while downloading updates.'
+ `\n${e && e.message ? e.message : e}`);
bar.setClosable(true);
});
}
public closeIfOpen() {
if (this.visibilityState === 'closed') {
return;
}
this.progressBar.closeIfOpen();
this.visibilityState = 'closed';
}
}
type ProgressBarVisibilityState = 'closed' | 'showingIndeterminate' | 'showingPercentile';
function getUpdatePercent(progress: ProgressInfo) {
let { percent } = progress;
if (percent) {
percent = Math.round(percent * 100) / 100;
}
return percent;
}
function createInitialProgressBarWindowOptions(
type: 'indeterminate' | 'percentile',
appName: string,
): InitialProgressBarWindowOptions {
return {
type,
title: `${appName} Update`,
initialText: `Downloading ${appName} update...`,
textOnCompleted: 'Download completed.',
};
}

View File

@@ -1,95 +0,0 @@
import ProgressBar from 'electron-progressbar';
import { app, BrowserWindow } from 'electron/main';
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import type { ProgressInfo } from 'electron-builder';
export class UpdateProgressBar {
private progressBar: ProgressBar | undefined;
private get innerProgressBarWindow(): BrowserWindow {
// eslint-disable-next-line no-underscore-dangle
return this.progressBar._window;
}
private showingProgress = false;
public showIndeterminateState() {
this.progressBar?.close();
this.progressBar = progressBarFactory.createWithIndeterminateState();
}
public showProgress(progress: ProgressInfo) {
const percentage = getUpdatePercent(progress);
this.showPercentage(percentage);
}
public showPercentage(percentage: number) {
if (!this.showingProgress) { // First time showing progress
this.progressBar?.close();
this.showingProgress = true;
this.progressBar = progressBarFactory.createWithPercentile(percentage);
} else {
this.progressBar.value = percentage;
}
}
public showError(e: Error) {
const reportUpdateError = () => {
this.progressBar.detail = 'An error occurred while fetching updates.'
+ `\n${e && e.message ? e.message : e}`;
this.innerProgressBarWindow.setClosable(true);
};
if (this.progressBar?.innerProgressBarWindow) {
reportUpdateError();
} else {
this.progressBar?.on('ready', () => reportUpdateError());
}
}
public close() {
if (!this.progressBar?.isCompleted()) {
this.progressBar?.close();
}
}
}
function getUpdatePercent(progress: ProgressInfo) {
let { percent } = progress;
if (percent) {
percent = Math.round(percent * 100) / 100;
}
return percent;
}
const progressBarFactory = {
createWithIndeterminateState: () => {
return new ProgressBar({
title: `${app.name} Update`,
text: `Downloading ${app.name} update...`,
});
},
createWithPercentile: (initialPercentage: number) => {
const progressBar = new ProgressBar({
indeterminate: false,
title: `${app.name} Update`,
text: `Downloading ${app.name} update...`,
detail: `${initialPercentage}% ...`,
initialValue: initialPercentage,
});
progressBar
.on('completed', () => {
progressBar.detail = 'Download completed.';
})
.on('aborted', (value: number) => {
ElectronLogger.info(`Progress aborted... ${value}`);
})
.on('progress', (value: number) => {
progressBar.detail = `${value}% ...`;
})
.on('ready', () => {
// initialValue doesn't set the UI, so this is needed to render it correctly
progressBar.value = initialPercentage;
});
return progressBar;
},
};

View File

@@ -43,9 +43,10 @@ function bindMethodsOfObject<T>(obj: T): T {
if (!prototype.hasOwnProperty.call(obj, property)) { if (!prototype.hasOwnProperty.call(obj, property)) {
return; // Skip properties not directly on the prototype return; // Skip properties not directly on the prototype
} }
const value = obj[property]; const propertyKey = property as keyof (typeof obj);
const value = obj[propertyKey];
if (isFunction(value)) { if (isFunction(value)) {
(obj as object)[property] = value.bind(obj); obj[propertyKey] = value.bind(obj);
} }
}); });
return obj; return obj;

View File

@@ -7,9 +7,9 @@ export function createIpcConsumerProxy<T>(
channel: IpcChannel<T>, channel: IpcChannel<T>,
electronIpcRenderer: Electron.IpcRenderer = ipcRenderer, electronIpcRenderer: Electron.IpcRenderer = ipcRenderer,
): AsyncMethods<T> { ): AsyncMethods<T> {
const facade: Partial<T> = {}; const facade: Partial<AsyncMethods<T>> = {};
channel.accessibleMembers.forEach((member) => { channel.accessibleMembers.forEach((member) => {
const functionKey = member as string; const functionKey = member as (keyof T & string);
const ipcChannel = getIpcChannelIdentifier(channel.namespace, functionKey); const ipcChannel = getIpcChannelIdentifier(channel.namespace, functionKey);
facade[functionKey] = ((...args: unknown[]) => { facade[functionKey] = ((...args: unknown[]) => {
return electronIpcRenderer.invoke(ipcChannel, ...args); return electronIpcRenderer.invoke(ipcChannel, ...args);

View File

@@ -78,10 +78,12 @@ function setCommandLineFlagsFromEnvironmentVariables() {
}; };
Object.entries(flagEnvironmentVariableKeyMappings) Object.entries(flagEnvironmentVariableKeyMappings)
.forEach(([flag, environmentVariableKey]) => { .forEach(([flag, environmentVariableKey]) => {
const flagValue = Number.parseInt(flag, 10) as CommandLineFlag;
const flagDefinition = COMMAND_LINE_FLAGS[flagValue];
if (process.env[environmentVariableKey] !== undefined) { if (process.env[environmentVariableKey] !== undefined) {
process.argv = [ process.argv = [
...process.argv, ...process.argv,
COMMAND_LINE_FLAGS[flag], flagDefinition,
]; ];
} }
}); });

View File

@@ -1,5 +1,5 @@
import { describe } from 'vitest'; import { describe } from 'vitest';
import type { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions'; import type { SanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/SanityCheckOptions';
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks'; import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
import { isBoolean } from '@/TypeHelpers'; import { isBoolean } from '@/TypeHelpers';
@@ -21,8 +21,8 @@ describe('SanityChecks', () => {
}); });
}); });
function generateTestOptions(): ISanityCheckOptions[] { function generateTestOptions(): SanityCheckOptions[] {
const defaultOptions: ISanityCheckOptions = { const defaultOptions: SanityCheckOptions = {
validateEnvironmentVariables: true, validateEnvironmentVariables: true,
validateWindowVariables: true, validateWindowVariables: true,
}; };

View File

@@ -1,8 +1,8 @@
import { it, expect } from 'vitest'; import { it, expect } from 'vitest';
import type { ISanityValidator } from '@/infrastructure/RuntimeSanity/Common/ISanityValidator'; import type { SanityValidator } from '@/infrastructure/RuntimeSanity/Common/SanityValidator';
export function itNoErrorsOnCurrentEnvironment( export function itNoErrorsOnCurrentEnvironment(
factory: () => ISanityValidator, factory: () => SanityValidator,
) { ) {
it('it does report errors on current environment', () => { it('it does report errors on current environment', () => {
// arrange // arrange

View File

@@ -129,7 +129,11 @@ function createSampleNodes(): TreeInputNodeDataWithMetadata[] {
]; ];
} }
function waitForStableDom(rootElement, timeout = 3000, interval = 200): Promise<void> { function waitForStableDom(
rootElement: Node,
timeout = 3000,
interval = 200,
): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let lastTimeoutId: ReturnType<typeof setTimeout>; let lastTimeoutId: ReturnType<typeof setTimeout>;
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {

View File

@@ -33,7 +33,7 @@ function removeUndefinedProperties(obj: object | undefined): object | undefined
return obj; return obj;
} }
return Object.keys(obj).reduce((acc, key) => { return Object.keys(obj).reduce((acc, key) => {
const value = obj[key]; const value = obj[key as keyof (typeof obj)];
switch (typeof value) { switch (typeof value) {
case 'object': { case 'object': {
const cleanValue = removeUndefinedProperties(value); // recurse const cleanValue = removeUndefinedProperties(value); // recurse

View File

@@ -4,7 +4,9 @@ export type SupportedOperatingSystem = OperatingSystem.Windows
| OperatingSystem.Linux | OperatingSystem.Linux
| OperatingSystem.macOS; | OperatingSystem.macOS;
export const AllSupportedOperatingSystems: readonly OperatingSystem[] = [ export const AllSupportedOperatingSystems: readonly (
OperatingSystem & SupportedOperatingSystem
)[] = [
OperatingSystem.Windows, OperatingSystem.Windows,
OperatingSystem.Linux, OperatingSystem.Linux,
OperatingSystem.macOS, OperatingSystem.macOS,

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ describe('ApplicationCode', () => {
describe('ctor', () => { describe('ctor', () => {
it('empty when selection is empty', () => { it('empty when selection is empty', () => {
// arrange // arrange
const selectedScripts = []; const selectedScripts: readonly SelectedScript[] = [];
const selection = new ScriptSelectionStub() const selection = new ScriptSelectionStub()
.withSelectedScripts(selectedScripts); .withSelectedScripts(selectedScripts);
const definition = new ScriptingDefinitionStub(); 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 { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { expectExists } from '@tests/shared/Assertions/ExpectExists'; import { expectExists } from '@tests/shared/Assertions/ExpectExists';
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub'; import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
describe('UserScriptGenerator', () => { describe('UserScriptGenerator', () => {
describe('scriptingDefinition', () => { describe('scriptingDefinition', () => {
@@ -143,7 +144,7 @@ describe('UserScriptGenerator', () => {
it('without script; returns empty', () => { it('without script; returns empty', () => {
// arrange // arrange
const sut = new UserScriptGenerator(); const sut = new UserScriptGenerator();
const selectedScripts = []; const selectedScripts: readonly SelectedScript[] = [];
const definition = new ScriptingDefinitionStub(); const definition = new ScriptingDefinitionStub();
// act // act
const actual = sut.buildCode(selectedScripts, definition); const actual = sut.buildCode(selectedScripts, definition);

View File

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

View File

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

View File

@@ -383,7 +383,7 @@ function createExpressionFactorySpy() {
}; };
return { return {
createExpression, 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 { PipeFactory } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeFactory';
import { PipeStub } from '@tests/unit/shared/Stubs/PipeStub'; import { PipeStub } from '@tests/unit/shared/Stubs/PipeStub';
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests'; import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import type { Pipe } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/Pipe';
describe('PipeFactory', () => { describe('PipeFactory', () => {
describe('ctor', () => { describe('ctor', () => {
@@ -49,7 +50,7 @@ describe('PipeFactory', () => {
// arrange // arrange
const missingName = 'missingName'; const missingName = 'missingName';
const expectedError = `Unknown pipe: "${missingName}"`; const expectedError = `Unknown pipe: "${missingName}"`;
const pipes = []; const pipes: readonly Pipe[] = [];
const sut = new PipeFactory(pipes); const sut = new PipeFactory(pipes);
// act // act
const act = () => sut.get(missingName); const act = () => sut.get(missingName);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,23 @@
import type { PropertyKeys } from '@/TypeHelpers'; import type { PropertyKeys } from '@/TypeHelpers';
import type { FactoryFunction, FactoryValidator } from '@/infrastructure/RuntimeSanity/Common/FactoryValidator'; 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'; import { SanityCheckOptionsStub } from '@tests/unit/shared/Stubs/SanityCheckOptionsStub';
interface ITestOptions<T> { interface TestOptions<T> {
createValidator: (factory?: FactoryFunction<T>) => FactoryValidator<T>; readonly createValidator: (factory?: FactoryFunction<T>) => FactoryValidator<T>;
enablingOptionProperty: PropertyKeys<ISanityCheckOptions>; readonly enablingOptionProperty: PropertyKeys<SanityCheckOptions>;
factoryFunctionStub: FactoryFunction<T>; readonly factoryFunctionStub: FactoryFunction<T>;
expectedValidatorName: string; readonly expectedValidatorName: string;
} }
export function runFactoryValidatorTests<T>( export function runFactoryValidatorTests<T>(
testOptions: ITestOptions<T>, testOptions: TestOptions<T>,
) { ) {
describe('shouldValidate', () => { describe('shouldValidate', () => {
it('returns true when option is true', () => { it('returns true when option is true', () => {
// arrange // arrange
const expectedValue = true; const expectedValue = true;
const options: ISanityCheckOptions = { const options: SanityCheckOptions = {
...new SanityCheckOptionsStub(), ...new SanityCheckOptionsStub(),
[testOptions.enablingOptionProperty]: true, [testOptions.enablingOptionProperty]: true,
}; };
@@ -31,7 +31,7 @@ export function runFactoryValidatorTests<T>(
it('returns false when option is false', () => { it('returns false when option is false', () => {
// arrange // arrange
const expectedValue = false; const expectedValue = false;
const options: ISanityCheckOptions = { const options: SanityCheckOptions = {
...new SanityCheckOptionsStub(), ...new SanityCheckOptionsStub(),
[testOptions.enablingOptionProperty]: false, [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 type { IApplicationContext } from '@/application/Context/IApplicationContext';
import { itIsTransientFactory } from '@tests/unit/shared/TestCases/TransientFactoryTests'; import { itIsTransientFactory } from '@tests/unit/shared/TestCases/TransientFactoryTests';
import { executeInComponentSetupContext } from '@tests/shared/Vue/ExecuteInComponentSetupContext'; 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('DependencyProvider', () => {
describe('provideDependencies', () => { describe('provideDependencies', () => {
const testCases: { const testCases: Record<InjectionKeyType, DependencyInjectionTestFunction> = {
readonly [K in keyof typeof InjectionKeys]: (injectionKey: symbol) => void;
} = {
useCollectionState: createTransientTests(), useCollectionState: createTransientTests(),
useApplication: createSingletonTests(), useApplication: createSingletonTests(),
useRuntimeEnvironment: createSingletonTests(), useRuntimeEnvironment: createSingletonTests(),
@@ -27,7 +29,8 @@ describe('DependencyProvider', () => {
useAutoUnsubscribedEventListener: createTransientTests(), useAutoUnsubscribedEventListener: createTransientTests(),
}; };
Object.entries(testCases).forEach(([key, runTests]) => { 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()}"`, () => { describe(`Key: "${registeredKey.toString()}"`, () => {
runTests(registeredKey); runTests(registeredKey);
}); });
@@ -35,7 +38,7 @@ describe('DependencyProvider', () => {
}); });
}); });
function createTransientTests() { function createTransientTests(): DependencyInjectionTestFunction {
return (injectionKey: symbol) => { return (injectionKey: symbol) => {
it('should register a function when transient dependency is resolved', () => { it('should register a function when transient dependency is resolved', () => {
// arrange // arrange
@@ -73,7 +76,7 @@ function createTransientTests() {
}; };
} }
function createSingletonTests() { function createSingletonTests(): DependencyInjectionTestFunction {
return (injectionKey: symbol) => { return (injectionKey: symbol) => {
it('should register an object when singleton dependency is resolved', () => { it('should register an object when singleton dependency is resolved', () => {
// arrange // arrange

View File

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

View File

@@ -17,7 +17,8 @@ describe('PlatformInstructionSteps', () => {
[OperatingSystem.macOS]: MacOsInstructions, [OperatingSystem.macOS]: MacOsInstructions,
[OperatingSystem.Linux]: LinuxInstructions, [OperatingSystem.Linux]: LinuxInstructions,
}; };
AllSupportedOperatingSystems.forEach((operatingSystem) => { AllSupportedOperatingSystems.forEach((operatingSystemKey) => {
const operatingSystem = operatingSystemKey as SupportedOperatingSystem;
it(`renders the correct component for ${OperatingSystem[operatingSystem]}`, () => { it(`renders the correct component for ${OperatingSystem[operatingSystem]}`, () => {
// arrange // arrange
const expectedComponent = testScenarios[operatingSystem]; const expectedComponent = testScenarios[operatingSystem];
@@ -47,7 +48,9 @@ describe('PlatformInstructionSteps', () => {
// assert // assert
const componentWrapper = wrapper.findComponent(wrappedComponent); 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', () => { it('throws error without renderers', () => {
// arrange // arrange
const expectedError = 'missing renderers'; const expectedError = 'missing renderers';
const renderers = []; const renderers = new Array<MarkdownRenderer>();
const context = new MarkdownRendererTestBuilder() const context = new MarkdownRendererTestBuilder()
.withMarkdownRenderers(renderers); .withMarkdownRenderers(renderers);
// act // act

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { SingleNodeCollectionFocusManager } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/Focus/SingleNodeCollectionFocusManager'; 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 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 { 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 { SingleNodeFocusManagerStub } from '@tests/unit/shared/Stubs/SingleNodeFocusManagerStub';
import { TreeNodeCollectionStub } from '@tests/unit/shared/Stubs/TreeNodeCollectionStub'; import { TreeNodeCollectionStub } from '@tests/unit/shared/Stubs/TreeNodeCollectionStub';
@@ -19,9 +19,12 @@ describe('TreeRootManager', () => {
it('set by constructor as expected', () => { it('set by constructor as expected', () => {
// arrange // arrange
const expectedCollection = new TreeNodeCollectionStub(); const expectedCollection = new TreeNodeCollectionStub();
const sut = new TreeRootManager(); const context = new TestContext()
.withNodeCollection(expectedCollection);
// act // act
const actualCollection = sut.collection; const actualCollection = context
.build()
.collection;
// assert // assert
expect(actualCollection).to.equal(expectedCollection); expect(actualCollection).to.equal(expectedCollection);
}); });
@@ -39,15 +42,41 @@ describe('TreeRootManager', () => {
it('creates with same collection it uses', () => { it('creates with same collection it uses', () => {
// arrange // arrange
let usedCollection: TreeNodeCollection | undefined; let usedCollection: TreeNodeCollection | undefined;
const factoryMock = (collection) => { const factoryMock: FocusManagerFactory = (collection) => {
usedCollection = collection; usedCollection = collection;
return new SingleNodeFocusManagerStub(); return new SingleNodeFocusManagerStub();
}; };
const sut = new TreeRootManager(new TreeNodeCollectionStub(), factoryMock); const context = new TestContext()
.withFocusManagerFactory(factoryMock);
// act // act
const expected = sut.collection; const expected = context
.build()
.collection;
// assert // assert
expect(usedCollection).to.equal(expected); 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 { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import type { TreeNodeId } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode'; import type { TreeNodeId } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
import type { Executable } from '@/domain/Executables/Executable'; import type { Executable } from '@/domain/Executables/Executable';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
describe('useSelectedScriptNodeIds', () => { describe('useSelectedScriptNodeIds', () => {
it('returns an empty array when no scripts are selected', () => { it('returns an empty array when no scripts are selected', () => {
@@ -44,7 +45,7 @@ describe('useSelectedScriptNodeIds', () => {
}); });
it('when the selection state changes', () => { it('when the selection state changes', () => {
// arrange // arrange
const initialScripts = []; const initialScripts = new Array<SelectedScript>();
const changedScripts = [ const changedScripts = [
new SelectedScriptStub(new ScriptStub('id-1')), new SelectedScriptStub(new ScriptStub('id-1')),
new SelectedScriptStub(new ScriptStub('id-2')), new SelectedScriptStub(new ScriptStub('id-2')),

View File

@@ -67,7 +67,7 @@ function runSharedTestsForAnimation(
}; };
const element = document.createElement('div'); const element = document.createElement('div');
Object.entries(expectedStyleValues).forEach(([key, value]) => { Object.entries(expectedStyleValues).forEach(([key, value]) => {
element.style[key] = value; element.style[key as keyof MutatedStyleProperties] = value;
}); });
const timer = new TimerStub(); const timer = new TimerStub();
const hookResult = useExpandCollapseAnimation(timer); const hookResult = useExpandCollapseAnimation(timer);
@@ -78,7 +78,8 @@ function runSharedTestsForAnimation(
await promise; await promise;
// assert // assert
Object.entries(expectedStyleValues).forEach(([key, expectedStyleValue]) => { 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([ expect(actualStyleValue).to.equal(expectedStyleValue, formatAssertionMessage([
`Style key: ${key}`, `Style key: ${key}`,
`Expected style value: ${expectedStyleValue}`, `Expected style value: ${expectedStyleValue}`,
@@ -86,7 +87,7 @@ function runSharedTestsForAnimation(
`Initial style value: ${expectedStyleValues}`, `Initial style value: ${expectedStyleValues}`,
'All styles:', 'All styles:',
...Object.entries(expectedStyleValues) ...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'], copyText: ['text-arg'],
}; };
Object.entries(testScenarios).forEach(([functionName, testFunctionArgs]) => { Object.entries(testScenarios).forEach(([functionNameValue, testFunctionArgs]) => {
describe(functionName, () => { const functionName = functionNameValue as ClipboardFunction;
describe(functionNameValue, () => {
it('binds the method to the instance', () => { it('binds the method to the instance', () => {
// arrange // arrange
const expectedArgs = testFunctionArgs; const expectedArgs = testFunctionArgs;
const clipboardStub = new ClipboardStub(); const clipboardStub = new ClipboardStub();
// act // act
const clipboard = useClipboard(clipboardStub); const clipboard = useClipboard(clipboardStub);
const { [functionName as ClipboardFunction]: testFunction } = clipboard; const { [functionName]: testFunction } = clipboard;
// assert // assert
testFunction(...expectedArgs); testFunction(...expectedArgs);
const call = clipboardStub.callHistory.find((c) => c.methodName === functionName); const call = clipboardStub.callHistory.find((c) => c.methodName === functionNameValue);
expectExists(call); expectExists(call);
expect(call.args).to.deep.equal(expectedArgs); expect(call.args).to.deep.equal(expectedArgs);
}); });
@@ -50,14 +51,15 @@ describe('useClipboard', () => {
const clipboardStub = new ClipboardStub(); const clipboardStub = new ClipboardStub();
const expectedThisContext = clipboardStub; const expectedThisContext = clipboardStub;
let actualThisContext: typeof expectedThisContext | undefined; let actualThisContext: typeof expectedThisContext | undefined;
// eslint-disable-next-line func-names // eslint-disable-next-line func-names, @typescript-eslint/no-unused-vars
clipboardStub[functionName] = function () { clipboardStub[functionName] = function (_text) {
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
actualThisContext = this; actualThisContext = this;
return Promise.resolve();
}; };
// act // act
const clipboard = useClipboard(clipboardStub); const clipboard = useClipboard(clipboardStub);
const { [functionName as ClipboardFunction]: testFunction } = clipboard; const { [functionNameValue as ClipboardFunction]: testFunction } = clipboard;
// assert // assert
testFunction(...testFunctionArgs); testFunction(...testFunctionArgs);
expect(expectedThisContext).to.equal(actualThisContext); 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 { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub';
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub'; import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub'; import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
describe('useUserSelectionState', () => { describe('useUserSelectionState', () => {
describe('currentSelection', () => { describe('currentSelection', () => {
@@ -170,8 +171,8 @@ describe('useUserSelectionState', () => {
describe('triggers change', () => { describe('triggers change', () => {
it('with new selected scripts array reference', async () => { it('with new selected scripts array reference', async () => {
// arrange // arrange
const oldSelectedScriptsArrayReference = []; const oldSelectedScriptsArrayReference = new Array<SelectedScript>();
const newSelectedScriptsArrayReference = []; const newSelectedScriptsArrayReference = new Array<SelectedScript>();
const scriptSelectionStub = new ScriptSelectionStub() const scriptSelectionStub = new ScriptSelectionStub()
.withSelectedScripts(oldSelectedScriptsArrayReference); .withSelectedScripts(oldSelectedScriptsArrayReference);
const collectionStateStub = new UseCollectionStateStub() const collectionStateStub = new UseCollectionStateStub()
@@ -191,7 +192,7 @@ describe('useUserSelectionState', () => {
}); });
it('with same selected scripts array reference', async () => { it('with same selected scripts array reference', async () => {
// arrange // arrange
const sharedSelectedScriptsReference = []; const sharedSelectedScriptsReference = new Array<SelectedScript>();
const scriptSelectionStub = new ScriptSelectionStub() const scriptSelectionStub = new ScriptSelectionStub()
.withSelectedScripts(sharedSelectedScriptsReference); .withSelectedScripts(sharedSelectedScriptsReference);
const collectionStateStub = new UseCollectionStateStub() const collectionStateStub = new UseCollectionStateStub()

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ export class ExpressionStub implements IExpression {
this.callHistory.push(context); this.callHistory.push(context);
if (this.result === undefined /* not empty string */) { if (this.result === undefined /* not empty string */) {
const { args } = context; 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; 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 validateWindowVariables = false;
public validateEnvironmentVariables = false; public validateEnvironmentVariables = false;

View File

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

View File

@@ -11,7 +11,7 @@ export class VueDependencyInjectionApiStub implements VueDependencyInjectionApi
public inject<T>(key: InjectionKey<T>): T { public inject<T>(key: InjectionKey<T>): T {
const providedValue = this.injections.get(key); const providedValue = this.injections.get(key);
if (providedValue === undefined) { 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; return providedValue as T;
} }

View File

@@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2018",
"lib": ["ESNext", "DOM", "DOM.Iterable"], "lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext", "module": "ESNext",
"importHelpers": true, "importHelpers": true,
@@ -8,6 +8,7 @@
"jsx": "preserve", "jsx": "preserve",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"strictNullChecks": true, "strictNullChecks": true,
"noImplicitAny": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"esModuleInterop": true, "esModuleInterop": true,
"resolveJsonModule": true, "resolveJsonModule": true,

View File

@@ -44,14 +44,14 @@ export function getClientEnvironmentVariables(): ViteGlobalVariableReplacementDe
function getPathAliasesFromTsConfig(): ViteAliasDefinitions { function getPathAliasesFromTsConfig(): ViteAliasDefinitions {
const { paths } = tsconfigJson.compilerOptions; const { paths } = tsconfigJson.compilerOptions;
return Object.keys(paths).reduce((aliases, pathName) => { return Object.keys(paths).reduce((aliases, pathName: keyof typeof paths) => {
const pathFolder = paths[pathName][0]; const pathFolder = paths[pathName][0];
const aliasFolder = pathFolder.substring(0, pathFolder.length - 1); // trim * from end const aliasFolder = pathFolder.substring(0, pathFolder.length - 1); // trim * from end
const aliasName = pathName.substring(0, pathName.length - 2); // trim /* from end const aliasName = pathName.substring(0, pathName.length - 2); // trim /* from end
const aliasPath = resolve(getSelfDirectoryAbsolutePath(), aliasFolder); const aliasPath = resolve(getSelfDirectoryAbsolutePath(), aliasFolder);
aliases[aliasName] = aliasPath; aliases[aliasName] = aliasPath;
return aliases; return aliases;
}, {}); }, {} as ViteAliasDefinitions);
} }
function getElectronProcessSpecificModuleAliases(): ViteAliasDefinitions { function getElectronProcessSpecificModuleAliases(): ViteAliasDefinitions {
@@ -64,5 +64,5 @@ function getElectronProcessSpecificModuleAliases(): ViteAliasDefinitions {
return electronProcessScopedModuleAliases.reduce((aliases, alias) => { return electronProcessScopedModuleAliases.reduce((aliases, alias) => {
aliases[alias] = 'electron'; aliases[alias] = 'electron';
return aliases; return aliases;
}, {}); }, {} as ViteAliasDefinitions);
} }