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:
17
package-lock.json
generated
17
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { ISanityCheckOptions } from './ISanityCheckOptions';
|
||||
|
||||
export interface ISanityValidator {
|
||||
readonly name: string;
|
||||
shouldValidate(options: ISanityCheckOptions): boolean;
|
||||
collectErrors(): Iterable<string>;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ISanityCheckOptions {
|
||||
export interface SanityCheckOptions {
|
||||
readonly validateEnvironmentVariables: boolean;
|
||||
readonly validateWindowVariables: boolean;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { SanityCheckOptions } from './SanityCheckOptions';
|
||||
|
||||
export interface SanityValidator {
|
||||
readonly name: string;
|
||||
shouldValidate(options: SanityCheckOptions): boolean;
|
||||
collectErrors(): Iterable<string>;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.',
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
});
|
||||
|
||||
@@ -383,7 +383,7 @@ function createExpressionFactorySpy() {
|
||||
};
|
||||
return {
|
||||
createExpression,
|
||||
getInitParameters: (expression) => createdExpressions.get(expression),
|
||||
getInitParameters: (expression: IExpression) => createdExpressions.get(expression),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -114,7 +114,7 @@ export class SyntaxParserTestsRunner {
|
||||
}
|
||||
}
|
||||
|
||||
interface ExpectResultTestScenario {
|
||||
export interface ExpectResultTestScenario {
|
||||
readonly name: string;
|
||||
readonly code: string;
|
||||
readonly args: (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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}"`)),
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user