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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ export class ApplicationContext implements IApplicationContext {
public currentOs: OperatingSystem;
public get state(): ICategoryCollectionState {
return this.states[this.collection.os];
return this.getState(this.collection.os);
}
private readonly states: StateMachine;
@@ -26,30 +26,51 @@ export class ApplicationContext implements IApplicationContext {
public readonly app: IApplication,
initialContext: OperatingSystem,
) {
this.setContext(initialContext);
this.states = initializeStates(app);
this.changeContext(initialContext);
}
public changeContext(os: OperatingSystem): void {
assertInRange(os, OperatingSystem);
if (this.currentOs === os) {
return;
}
const collection = this.app.getCollection(os);
this.collection = collection;
const event: IApplicationContextChangedEvent = {
newState: this.states[os],
oldState: this.states[this.currentOs],
newState: this.getState(os),
oldState: this.getState(this.currentOs),
};
this.setContext(os);
this.contextChanged.notify(event);
}
private setContext(os: OperatingSystem): void {
validateOperatingSystem(os, this.app);
this.collection = this.app.getCollection(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 {
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
for (const collection of app.collections) {
machine[collection.os] = new CategoryCollectionState(collection);
machine.set(collection.os, new CategoryCollectionState(collection));
}
return machine;
}

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
import type { ISanityValidator } from './ISanityValidator';
import type { ISanityCheckOptions } from './ISanityCheckOptions';
import type { SanityValidator } from './SanityValidator';
import type { SanityCheckOptions } from './SanityCheckOptions';
export type FactoryFunction<T> = () => T;
export abstract class FactoryValidator<T> implements ISanityValidator {
export abstract class FactoryValidator<T> implements SanityValidator {
private readonly factory: FactoryFunction<T>;
protected constructor(factory: FactoryFunction<T>) {
this.factory = factory;
}
public abstract shouldValidate(options: ISanityCheckOptions): boolean;
public abstract shouldValidate(options: SanityCheckOptions): boolean;
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 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 type { ISanityCheckOptions } from './Common/ISanityCheckOptions';
import type { ISanityValidator } from './Common/ISanityValidator';
import type { SanityCheckOptions } from './Common/SanityCheckOptions';
import type { SanityValidator } from './Common/SanityValidator';
const DefaultSanityValidators: ISanityValidator[] = [
const DefaultSanityValidators: SanityValidator[] = [
new EnvironmentVariablesValidator(),
];
export interface RuntimeSanityValidator {
(
options: SanityCheckOptions,
validators?: readonly SanityValidator[],
): void;
}
/* Helps to fail-fast on errors */
export function validateRuntimeSanity(
options: ISanityCheckOptions,
validators: readonly ISanityValidator[] = DefaultSanityValidators,
): void {
export const validateRuntimeSanity: RuntimeSanityValidator = (
options: SanityCheckOptions,
validators: readonly SanityValidator[] = DefaultSanityValidators,
) => {
if (!validators.length) {
throw new Error('missing validators');
}
@@ -26,9 +33,9 @@ export function validateRuntimeSanity(
if (errorMessages.length > 0) {
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()];
if (!errorMessages.length) {
return undefined;

View File

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

View File

@@ -1,6 +1,6 @@
import type { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
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> {
constructor(factory: FactoryFunction<WindowVariables> = () => window) {
@@ -9,7 +9,7 @@ export class WindowVariablesValidator extends FactoryValidator<WindowVariables>
public override name = 'window variables';
public override shouldValidate(options: ISanityCheckOptions): boolean {
public override shouldValidate(options: SanityCheckOptions): boolean {
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 { DependencyBootstrapper } from './Modules/DependencyBootstrapper';
import { MobileSafariActivePseudoClassEnabler } from './Modules/MobileSafariActivePseudoClassEnabler';
@@ -17,7 +17,7 @@ export class ApplicationBootstrapper implements Bootstrapper {
private static getAllBootstrappers(): Bootstrapper[] {
return [
new RuntimeSanityValidator(),
new RuntimeSanityBootstrapper(),
new DependencyBootstrapper(),
new AppInitializationLogger(),
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"
:rendering-strategy="renderingStrategy"
>
<template #node-content="slotProps">
<template #node-content="slotProps: NodeMetadata">
<slot name="node-content" v-bind="slotProps" />
</template>
</HierarchicalTreeNode>
@@ -55,6 +55,7 @@ import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { useNodeState } from './UseNodeState';
import LeafTreeNode from './LeafTreeNode.vue';
import InteractableNode from './InteractableNode.vue';
import type { NodeMetadata } from '../../NodeContent/NodeMetadata';
import type { TreeRoot } from '../TreeRoot/TreeRoot';
import type { TreeNode, TreeNodeId } from './TreeNode';
import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
@@ -107,6 +108,7 @@ export default defineComponent({
);
return {
NodeMetadata: Object as PropType<NodeMetadata>,
renderedNodeIds,
isExpanded,
toggleExpand,

View File

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

View File

@@ -120,17 +120,15 @@ function getArrowPositionStyles(
coordinations: Partial<Coords>,
placement: Placement,
): CSSProperties {
const style: CSSProperties = {};
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 { x, y } = coordinations; // either X or Y is calculated
const oppositeSide = getCounterpartBoxOffsetProperty(placement);
style[oppositeSide.toString()] = `-${ARROW_SIZE_IN_PX}px`;
return style;
const newStyle: CSSProperties = {
[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 {

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import { unlink, mkdir } from 'node:fs/promises';
import path from 'node:path';
import { app } from 'electron/main';
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import { UpdateProgressBar } from '../UpdateProgressBar';
import { UpdateProgressBar } from '../ProgressBar/UpdateProgressBar';
import { retryFileSystemAccess } from './RetryFileSystemAccess';
import type { UpdateInfo } from 'electron-updater';
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 { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { UpdateProgressBar } from '../UpdateProgressBar';
import { UpdateProgressBar } from '../ProgressBar/UpdateProgressBar';
import {
promptForManualUpdate, promptInstallerOpenError,
promptIntegrityCheckFailure, promptDownloadError,
@@ -100,7 +100,7 @@ async function withProgressBar(
) {
const progressBar = new UpdateProgressBar();
await action(progressBar);
progressBar.close();
progressBar.closeIfOpen();
}
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)) {
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)) {
(obj as object)[property] = value.bind(obj);
obj[propertyKey] = value.bind(obj);
}
});
return obj;

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
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 { isBoolean } from '@/TypeHelpers';
@@ -21,8 +21,8 @@ describe('SanityChecks', () => {
});
});
function generateTestOptions(): ISanityCheckOptions[] {
const defaultOptions: ISanityCheckOptions = {
function generateTestOptions(): SanityCheckOptions[] {
const defaultOptions: SanityCheckOptions = {
validateEnvironmentVariables: true,
validateWindowVariables: true,
};

View File

@@ -1,8 +1,8 @@
import { it, expect } from 'vitest';
import type { ISanityValidator } from '@/infrastructure/RuntimeSanity/Common/ISanityValidator';
import type { SanityValidator } from '@/infrastructure/RuntimeSanity/Common/SanityValidator';
export function itNoErrorsOnCurrentEnvironment(
factory: () => ISanityValidator,
factory: () => SanityValidator,
) {
it('it does report errors on current environment', () => {
// 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) => {
let lastTimeoutId: ReturnType<typeof setTimeout>;
const observer = new MutationObserver(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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