Refactor to enforce strictNullChecks

This commit applies `strictNullChecks` to the entire codebase to improve
maintainability and type safety. Key changes include:

- Remove some explicit null-checks where unnecessary.
- Add necessary null-checks.
- Refactor static factory functions for a more functional approach.
- Improve some test names and contexts for better debugging.
- Add unit tests for any additional logic introduced.
- Refactor `createPositionFromRegexFullMatch` to its own function as the
  logic is reused.
- Prefer `find` prefix on functions that may return `undefined` and
  `get` prefix for those that always return a value.
This commit is contained in:
undergroundwires
2023-11-12 22:54:00 +01:00
parent 7ab16ecccb
commit 949fac1a7c
294 changed files with 2477 additions and 2738 deletions

View File

@@ -1,48 +0,0 @@
import { expect } from 'vitest';
// `toThrowError` does not assert the error type (https://github.com/vitest-dev/vitest/blob/v0.34.2/docs/api/expect.md#tothrowerror)
export function expectDeepThrowsError<T extends Error>(delegate: () => void, expected: T) {
// arrange
if (!expected) {
throw new Error('missing expected');
}
let actual: T | undefined;
// act
try {
delegate();
} catch (error) {
actual = error;
}
// assert
expect(Boolean(actual)).to.equal(true, `Expected to throw "${expected.name}" but delegate did not throw at all.`);
expect(Boolean(actual?.stack)).to.equal(true, 'Empty stack trace.');
expect(expected.message).to.equal(actual.message);
expect(expected.name).to.equal(actual.name);
expectDeepEqualsIgnoringUndefined(expected, actual);
}
function expectDeepEqualsIgnoringUndefined(expected: unknown, actual: unknown) {
const actualClean = removeUndefinedProperties(actual);
const expectedClean = removeUndefinedProperties(expected);
expect(expectedClean).to.deep.equal(actualClean);
}
function removeUndefinedProperties(obj: unknown): unknown {
return Object.keys(obj ?? {})
.reduce((acc, key) => {
const value = obj[key];
switch (typeof value) {
case 'object': {
const cleanValue = removeUndefinedProperties(value); // recurse
if (!Object.keys(cleanValue).length) {
return { ...acc };
}
return { ...acc, [key]: cleanValue };
}
case 'undefined':
return { ...acc };
default:
return { ...acc, [key]: value };
}
}, {});
}

View File

@@ -0,0 +1,12 @@
export function collectExceptionMessage(action: () => unknown): string {
let message: string | undefined;
try {
action();
} catch (e) {
message = e.message;
}
if (!message) {
throw new Error('action did not throw');
}
return message;
}

View File

@@ -3,6 +3,7 @@ import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IProjectInformation } from '@/domain/IProjectInformation';
import { ProjectInformationStub } from './ProjectInformationStub';
import { CategoryCollectionStub } from './CategoryCollectionStub';
export class ApplicationStub implements IApplication {
public info: IProjectInformation = new ProjectInformationStub();
@@ -10,24 +11,25 @@ export class ApplicationStub implements IApplication {
public collections: ICategoryCollection[] = [];
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection {
return this.collections.find((collection) => collection.os === operatingSystem);
const collection = this.collections.find((c) => c.os === operatingSystem);
return collection ?? new CategoryCollectionStub();
}
public getSupportedOsList(): OperatingSystem[] {
return this.collections.map((collection) => collection.os);
}
public withCollection(collection: ICategoryCollection): ApplicationStub {
public withCollection(collection: ICategoryCollection): this {
this.collections.push(collection);
return this;
}
public withProjectInformation(info: IProjectInformation): ApplicationStub {
public withProjectInformation(info: IProjectInformation): this {
this.info = info;
return this;
}
public withCollections(...collections: readonly ICategoryCollection[]): ApplicationStub {
public withCollections(...collections: readonly ICategoryCollection[]): this {
this.collections.push(...collections);
return this;
}

View File

@@ -24,10 +24,11 @@ export class CategoryCollectionParserStub {
}
public getStub(): CategoryCollectionParserType {
return (data: CollectionData, info: IProjectInformation) => {
return (data: CollectionData, info: IProjectInformation): ICategoryCollection => {
this.arguments.push({ data, info });
if (this.returnValues.has(data)) {
return this.returnValues.get(data);
const foundReturnValue = this.returnValues.get(data);
if (foundReturnValue) {
return foundReturnValue;
}
// Get next OS with a unique OS so mock does not result in an invalid app due to duplicated OS
// collections.

View File

@@ -6,6 +6,7 @@ import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ScriptStub } from './ScriptStub';
import { ScriptingDefinitionStub } from './ScriptingDefinitionStub';
import { CategoryStub } from './CategoryStub';
export class CategoryCollectionStub implements ICategoryCollection {
public scripting: IScriptingDefinition = new ScriptingDefinitionStub();
@@ -20,34 +21,35 @@ export class CategoryCollectionStub implements ICategoryCollection {
public readonly actions = new Array<ICategory>();
public withAction(category: ICategory): CategoryCollectionStub {
public withAction(category: ICategory): this {
this.actions.push(category);
return this;
}
public withOs(os: OperatingSystem): CategoryCollectionStub {
public withOs(os: OperatingSystem): this {
this.os = os;
return this;
}
public withScripting(scripting: IScriptingDefinition): CategoryCollectionStub {
public withScripting(scripting: IScriptingDefinition): this {
this.scripting = scripting;
return this;
}
public withInitialScript(script: IScript): CategoryCollectionStub {
public withInitialScript(script: IScript): this {
this.initialScript = script;
return this;
}
public withTotalScripts(totalScripts: number) {
public withTotalScripts(totalScripts: number): this {
this.totalScripts = totalScripts;
return this;
}
public findCategory(categoryId: number): ICategory {
public getCategory(categoryId: number): ICategory {
return this.getAllCategories()
.find((category) => category.id === categoryId);
.find((category) => category.id === categoryId)
?? new CategoryStub(categoryId);
}
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
@@ -55,9 +57,10 @@ export class CategoryCollectionStub implements ICategoryCollection {
.filter((script) => script.level !== undefined && script.level <= level);
}
public findScript(scriptId: string): IScript {
public getScript(scriptId: string): IScript {
return this.getAllScripts()
.find((script) => scriptId === script.id);
.find((script) => scriptId === script.id)
?? new ScriptStub(scriptId);
}
public getAllScripts(): ReadonlyArray<IScript> {

View File

@@ -1,8 +1,8 @@
import type { CategoryData, CategoryOrScriptData, DocumentationData } from '@/application/collections/';
import { ScriptDataStub } from './ScriptDataStub';
import { createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
export class CategoryDataStub implements CategoryData {
public children: readonly CategoryOrScriptData[] = [ScriptDataStub.createWithCode()];
public children: readonly CategoryOrScriptData[] = [createScriptDataWithCode()];
public category = 'category name';

View File

@@ -1,5 +1,6 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { ICategory, IScript } from '@/domain/ICategory';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ScriptStub } from './ScriptStub';
export class CategoryStub extends BaseEntity<number> implements ICategory {
@@ -26,37 +27,44 @@ export class CategoryStub extends BaseEntity<number> implements ICategory {
];
}
public withScriptIds(...scriptIds: string[]): CategoryStub {
public withScriptIds(...scriptIds: string[]): this {
return this.withScripts(
...scriptIds.map((id) => new ScriptStub(id)),
);
}
public withScripts(...scripts: IScript[]): CategoryStub {
public withScripts(...scripts: IScript[]): this {
for (const script of scripts) {
this.withScript(script);
}
return this;
}
public withCategories(...categories: ICategory[]): CategoryStub {
public withMandatoryScripts(): this {
return this
.withScript(new ScriptStub(`[${CategoryStub.name}] script-1`).withLevel(RecommendationLevel.Standard))
.withScript(new ScriptStub(`[${CategoryStub.name}] script-2`).withLevel(RecommendationLevel.Strict))
.withScript(new ScriptStub(`[${CategoryStub.name}] script-3`).withLevel(undefined));
}
public withCategories(...categories: ICategory[]): this {
for (const category of categories) {
this.withCategory(category);
}
return this;
}
public withCategory(category: ICategory): CategoryStub {
public withCategory(category: ICategory): this {
this.subCategories.push(category);
return this;
}
public withScript(script: IScript): CategoryStub {
public withScript(script: IScript): this {
this.scripts.push(script);
return this;
}
public withName(categoryName: string) {
public withName(categoryName: string): this {
this.name = categoryName;
return this;
}

View File

@@ -20,7 +20,7 @@ export class CodeValidatorStub implements ICodeValidator {
}
public assertHistory(expected: {
validatedCodes: readonly string[],
validatedCodes: readonly (string | undefined)[],
rules: readonly Constructible<ICodeValidationRule>[],
}) {
expect(this.callHistory).to.have.lengthOf(expected.validatedCodes.length);

View File

@@ -3,6 +3,7 @@ import type {
} from '@/application/collections/';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { createScriptDataWithCode } from './ScriptDataStub';
export class CollectionDataStub implements CollectionData {
public os = 'windows';
@@ -13,17 +14,17 @@ export class CollectionDataStub implements CollectionData {
public functions?: ReadonlyArray<FunctionData>;
public withActions(actions: readonly CategoryData[]): CollectionDataStub {
public withActions(actions: readonly CategoryData[]): this {
this.actions = actions;
return this;
}
public withOs(os: string): CollectionDataStub {
public withOs(os: string): this {
this.os = os;
return this;
}
public withScripting(scripting: ScriptingDefinitionData): CollectionDataStub {
public withScripting(scripting: ScriptingDefinitionData): this {
this.scripting = scripting;
return this;
}
@@ -57,11 +58,7 @@ function getScriptStub(
scriptName: string,
level: RecommendationLevel = RecommendationLevel.Standard,
): ScriptData {
return {
name: scriptName,
code: 'script code',
revertCode: 'revert code',
recommend: RecommendationLevel[level].toLowerCase(),
call: undefined,
};
return createScriptDataWithCode()
.withName(scriptName)
.withRecommend(RecommendationLevel[level].toLowerCase());
}

View File

@@ -1,7 +1,7 @@
import { DelayScheduler } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/DelayScheduler';
export class DelaySchedulerStub implements DelayScheduler {
public nextCallback: () => void | undefined = undefined;
public nextCallback: (() => void) | undefined = undefined;
public scheduleNext(callback: () => void): void {
this.nextCallback = callback;

View File

@@ -20,8 +20,9 @@ export class ExpressionParserStub implements IExpressionParser {
public findExpressions(code: string): IExpression[] {
this.callHistory.push(code);
if (this.results.has(code)) {
return [...this.results.get(code)];
const foundResult = this.results.get(code);
if (foundResult) {
return [...foundResult];
}
return [];
}

View File

@@ -11,7 +11,7 @@ export class ExpressionStub implements IExpression {
public parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
private result: string = undefined;
private result = `[${ExpressionStub.name}] result`;
public withParameters(parameters: IReadOnlyFunctionParameterCollection) {
this.parameters = parameters;

View File

@@ -1,7 +1,7 @@
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { scrambledEqual } from '@/application/Common/Array';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
@@ -10,7 +10,7 @@ export class ExpressionsCompilerStub
implements IExpressionsCompiler {
private readonly scenarios = new Array<ExpressionCompilationScenario>();
public setup(scenario: ExpressionCompilationScenario): ExpressionsCompilerStub {
public setup(scenario: ExpressionCompilationScenario): this {
this.scenarios.push(scenario);
return this;
}
@@ -18,10 +18,22 @@ export class ExpressionsCompilerStub
public setupToReturnFunctionCode(
func: ISharedFunction,
givenArgs: FunctionCallArgumentCollectionStub,
) {
return this
.setup({ givenCode: func.body.code.execute, givenArgs, result: func.body.code.execute })
.setup({ givenCode: func.body.code.revert, givenArgs, result: func.body.code.revert });
): this {
if (func.body.type !== FunctionBodyType.Code) {
throw new Error('not a code body');
}
if (func.body.code.revert) {
this.setup({
givenCode: func.body.code.revert,
givenArgs,
result: func.body.code.revert,
});
}
return this.setup({
givenCode: func.body.code.execute,
givenArgs,
result: func.body.code.execute,
});
}
public compileExpressions(
@@ -42,7 +54,7 @@ export class ExpressionsCompilerStub
.getAllParameterNames()
.map((name) => `${name}=${parameters.getArgument(name).argumentValue}`)
.join('\n\t');
return `[ExpressionsCompilerStub]\ncode: "${code}"\nparameters: ${parametersAndValues}`;
return `[${ExpressionsCompilerStub.name}]\ncode: "${code}"\nparameters: ${parametersAndValues}`;
}
}

View File

@@ -12,12 +12,12 @@ export class FileSystemOpsStub
return Promise.resolve();
}
public createDirectory(directoryPath: string, isRecursive?: boolean): Promise<string> {
public createDirectory(directoryPath: string, isRecursive?: boolean): Promise<void> {
this.registerMethodCall({
methodName: 'createDirectory',
args: [directoryPath, isRecursive],
});
return Promise.resolve(directoryPath);
return Promise.resolve();
}
public writeToFile(filePath: string, data: string): Promise<void> {

View File

@@ -1,26 +1,35 @@
import { FilterActionType } from '@/application/Context/State/Filter/Event/FilterActionType';
import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
import { FilterAction, IFilterChangeDetails, IFilterChangeDetailsVisitor } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
export class FilterChangeDetailsStub implements IFilterChangeDetails {
public static forApply(filter: IFilterResult) {
return new FilterChangeDetailsStub(FilterActionType.Apply, filter);
return new FilterChangeDetailsStub({
type: FilterActionType.Apply,
filter,
});
}
public static forClear() {
return new FilterChangeDetailsStub(FilterActionType.Clear);
return new FilterChangeDetailsStub({
type: FilterActionType.Clear,
});
}
private constructor(
public actionType: FilterActionType,
public filter?: IFilterResult,
public readonly action: FilterAction,
) { /* Private constructor to enforce factory methods */ }
visit(visitor: IFilterChangeDetailsVisitor): void {
if (this.filter) {
visitor.onApply(this.filter);
} else {
visitor.onClear();
if (this.action.type === FilterActionType.Apply) {
if (visitor.onApply) {
visitor.onApply(this.action.filter);
}
}
if (this.action.type === FilterActionType.Clear) {
if (visitor.onClear) {
visitor.onClear();
}
}
}
}

View File

@@ -3,14 +3,14 @@ import { IFunctionCode } from '@/application/Parser/Script/Compiler/Function/ISh
export class FunctionCodeStub implements IFunctionCode {
public execute = 'execute code (function-code-stub)';
public revert? = 'revert code (function-code-stub)';
public revert: string | undefined = 'revert code (function-code-stub)';
public withExecute(code: string) {
this.execute = code;
return this;
}
public withRevert(revert: string) {
public withRevert(revert: string | undefined) {
this.revert = revert;
return this;
}

View File

@@ -1,39 +1,60 @@
import type { FunctionData, ParameterDefinitionData, FunctionCallsData } from '@/application/collections/';
import type {
ParameterDefinitionData, CodeFunctionData,
FunctionCallsData, CallFunctionData,
} from '@/application/collections/';
import { FunctionCallDataStub } from './FunctionCallDataStub';
export class FunctionDataStub implements FunctionData {
public static createWithCode() {
return new FunctionDataStub()
.withCode('stub-code')
.withRevertCode('stub-revert-code');
}
export function createFunctionDataWithCode(): FunctionDataStub {
const instance = new FunctionDataStub()
.withCode('stub-code')
.withRevertCode('stub-revert-code');
return instance;
}
public static createWithCall(call?: FunctionCallsData) {
let instance = new FunctionDataStub();
if (call) {
instance = instance.withCall(call);
} else {
instance = instance.withMockCall();
}
return instance;
export function createFunctionDataWithCall(
call?: FunctionCallsData,
): FunctionDataStub {
let instance = new FunctionDataStub();
if (call) {
instance = instance.withCall(call);
} else {
instance = instance.withMockCall();
}
return instance;
}
public static createWithoutCallOrCodes() {
return new FunctionDataStub();
}
export function createFunctionDataWithoutCallOrCode(): FunctionDataStub {
return new FunctionDataStub();
}
interface FunctionDataBuilder<T> {
withName(name: string): T;
withParameters(...parameters: readonly ParameterDefinitionData[]): T;
withParametersObject(parameters: readonly ParameterDefinitionData[]): T;
}
interface CodeFunctionDataBuilder extends FunctionDataBuilder<CodeFunctionDataBuilder> {
withCode(code: string): this;
withRevertCode(revertCode: string): this;
}
interface CallFunctionDataBuilder extends FunctionDataBuilder<CallFunctionDataBuilder> {
withCall(call: FunctionCallsData): this;
withMockCall(): this;
}
class FunctionDataStub
implements CodeFunctionDataBuilder, CallFunctionDataBuilder, CallFunctionData, CodeFunctionData {
public name = 'functionDataStub';
public code: string;
public revertCode: string;
public revertCode?: string;
public call?: FunctionCallsData;
public call: FunctionCallsData;
public parameters?: readonly ParameterDefinitionData[];
private constructor() { /* use static factory methods to create an instance */ }
public withName(name: string) {
this.name = name;
return this;

View File

@@ -1,9 +1,9 @@
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
export class LanguageSyntaxStub implements ILanguageSyntax {
public commentDelimiters = [];
public commentDelimiters: string[] = [];
public commonCodeParts = [];
public commonCodeParts: string[] = [];
public withCommentDelimiters(...delimiters: string[]) {
this.commentDelimiters = delimiters;

View File

@@ -24,14 +24,16 @@ export class LocationOpsStub
methodName: 'combinePaths',
args: pathSegments,
});
if (this.sequence.length > 0) {
return this.sequence.pop();
const nextInSequence = this.sequence.pop();
if (nextInSequence) {
return nextInSequence;
}
const key = LocationOpsStub.getScenarioKey(pathSegments);
if (!this.scenarios.has(key)) {
return pathSegments.join('/PATH-SEGMENT-SEPARATOR/');
const foundScenario = this.scenarios.get(key);
if (foundScenario) {
return foundScenario;
}
return this.scenarios.get(key);
return pathSegments.join('/PATH-SEGMENT-SEPARATOR/');
}
private static getScenarioKey(paths: string[]): string {

View File

@@ -9,7 +9,7 @@ export class NodeMetadataStub implements NodeMetadata {
public readonly docs: readonly string[] = [];
public children?: readonly NodeMetadata[] = [];
public children: readonly NodeMetadata[] = [];
public readonly type: NodeType = NodeType.Category;

View File

@@ -23,7 +23,7 @@ export class NodeStateChangeEventArgsStub implements NodeStateChangeEventArgs {
return this;
}
public withOldState(oldState: TreeNodeStateDescriptor): this {
public withOldState(oldState: TreeNodeStateDescriptor | undefined): this {
this.oldState = oldState;
return this;
}

View File

@@ -13,9 +13,6 @@ export class PipeFactoryStub implements IPipeFactory {
}
public withPipe(pipe: IPipe) {
if (!pipe) {
throw new Error('missing pipe');
}
this.pipes.push(pipe);
return this;
}

View File

@@ -4,7 +4,7 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
import { ProjectInformationStub } from './ProjectInformationStub';
export class ProjectInformationParserStub {
public readonly arguments = new Array<IAppMetadata>();
public readonly arguments = new Array<IAppMetadata | undefined>();
private returnValue: IProjectInformation = new ProjectInformationStub();

View File

@@ -6,9 +6,9 @@ export class RuntimeEnvironmentStub implements IRuntimeEnvironment {
public isDesktop = true;
public os = OperatingSystem.Windows;
public os: OperatingSystem | undefined = OperatingSystem.Windows;
public withOs(os: OperatingSystem): this {
public withOs(os: OperatingSystem | undefined): this {
this.os = os;
return this;
}

View File

@@ -1,22 +1,27 @@
import type { ScriptData } from '@/application/collections/';
import { IScriptCompiler } from '@/application/Parser/Script/Compiler/IScriptCompiler';
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCodeStub } from './ScriptCodeStub';
export class ScriptCompilerStub implements IScriptCompiler {
public compilables = new Map<ScriptData, IScriptCode>();
public compilableScripts = new Map<ScriptData, IScriptCode>();
public canCompile(script: ScriptData): boolean {
return this.compilables.has(script);
return this.compilableScripts.has(script);
}
public compile(script: ScriptData): IScriptCode {
return this.compilables.get(script);
const foundCode = this.compilableScripts.get(script);
if (foundCode) {
return foundCode;
}
return new ScriptCodeStub();
}
public withCompileAbility(script: ScriptData, result?: IScriptCode): ScriptCompilerStub {
this.compilables.set(
public withCompileAbility(script: ScriptData, result?: IScriptCode): this {
this.compilableScripts.set(
script,
result || { execute: `compiled code of ${script.name}`, revert: `compiled revert code of ${script.name}` },
result ?? { execute: `compiled code of ${script.name}`, revert: `compiled revert code of ${script.name}` },
);
return this;
}

View File

@@ -1,78 +1,81 @@
import type { FunctionCallData, ScriptData } from '@/application/collections/';
import type {
FunctionCallData, CallScriptData, CodeScriptData,
} from '@/application/collections/';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
export class ScriptDataStub implements ScriptData {
public static createWithCode(): ScriptDataStub {
return new ScriptDataStub()
.withCode('stub-code')
.withRevertCode('stub-revert-code');
}
export function createScriptDataWithCode(): ScriptDataStub & CodeScriptData {
return new ScriptDataStub()
.withCode('stub-code')
.withRevertCode('stub-revert-code');
}
public static createWithCall(call?: FunctionCallData): ScriptDataStub {
let instance = new ScriptDataStub();
if (call) {
instance = instance.withCall(call);
} else {
instance = instance.withMockCall();
}
return instance;
export function createScriptDataWithCall(
call?: FunctionCallData,
): ScriptDataStub & CallScriptData {
let instance = new ScriptDataStub();
if (call) {
instance = instance.withCall(call);
} else {
instance = instance.withMockCall();
}
return instance;
}
public static createWithoutCallOrCodes(): ScriptDataStub {
return new ScriptDataStub();
}
export function createScriptDataWithoutCallOrCodes(): ScriptDataStub {
return new ScriptDataStub();
}
class ScriptDataStub implements CallScriptData, CodeScriptData {
public name = 'valid-name';
public code = undefined;
public code: string;
public revertCode = undefined;
public revertCode: string | undefined = undefined;
public call = undefined;
public call: FunctionCallData | undefined = undefined;
public recommend = RecommendationLevel[RecommendationLevel.Standard].toLowerCase();
public recommend:
string | undefined = RecommendationLevel[RecommendationLevel.Standard].toLowerCase();
public docs?: readonly string[] = ['hello.com'];
private constructor() { /* use static methods for constructing */ }
public withName(name: string): ScriptDataStub {
public withName(name: string): this {
this.name = name;
return this;
}
public withDocs(docs: readonly string[]): ScriptDataStub {
public withDocs(docs: readonly string[]): this {
this.docs = docs;
return this;
}
public withCode(code: string): ScriptDataStub {
public withCode(code: string): this {
this.code = code;
return this;
}
public withRevertCode(revertCode: string): ScriptDataStub {
public withRevertCode(revertCode: string | undefined): this {
this.revertCode = revertCode;
return this;
}
public withMockCall(): ScriptDataStub {
public withMockCall(): this {
this.call = new FunctionCallDataStub();
return this;
}
public withCall(call: FunctionCallData): ScriptDataStub {
public withCall(call: FunctionCallData | undefined): this {
this.call = call;
return this;
}
public withRecommend(recommend: string): ScriptDataStub {
public withRecommend(recommend: string | undefined): this {
this.recommend = recommend;
return this;
}
public withRecommendationLevel(level: RecommendationLevel): ScriptDataStub {
public withRecommendationLevel(level: RecommendationLevel): this {
this.recommend = RecommendationLevel[level].toLowerCase();
return this;
}

View File

@@ -2,11 +2,12 @@ import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { IScript } from '@/domain/IScript';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IScriptCode } from '@/domain/IScriptCode';
export class ScriptStub extends BaseEntity<string> implements IScript {
public name = `name${this.id}`;
public code = {
public code: IScriptCode = {
execute: `REM execute-code (${this.id})`,
revert: `REM revert-code (${this.id})`,
};
@@ -23,23 +24,29 @@ export class ScriptStub extends BaseEntity<string> implements IScript {
return Boolean(this.code.revert);
}
public withLevel(value?: RecommendationLevel): ScriptStub {
public withLevel(value: RecommendationLevel | undefined): this {
this.level = value;
return this;
}
public withCode(value: string): ScriptStub {
this.code.execute = value;
public withCode(value: string): this {
this.code = {
execute: value,
revert: this.code.revert,
};
return this;
}
public withName(name: string): ScriptStub {
public withName(name: string): this {
this.name = name;
return this;
}
public withRevertCode(revertCode: string): ScriptStub {
this.code.revert = revertCode;
public withRevertCode(revertCode?: string): this {
this.code = {
execute: this.code.execute,
revert: revertCode,
};
return this;
}

View File

@@ -10,17 +10,17 @@ export class ScriptingDefinitionStub implements IScriptingDefinition {
public endCode = 'REM end code';
public withStartCode(startCode: string): ScriptingDefinitionStub {
public withStartCode(startCode: string): this {
this.startCode = startCode;
return this;
}
public withEndCode(endCode: string): ScriptingDefinitionStub {
public withEndCode(endCode: string): this {
this.endCode = endCode;
return this;
}
public withLanguage(language: ScriptingLanguage): ScriptingDefinitionStub {
public withLanguage(language: ScriptingLanguage): this {
this.language = language;
return this;
}

View File

@@ -1,6 +1,6 @@
import { ISharedFunction, FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { SharedFunctionStub } from './SharedFunctionStub';
import { createSharedFunctionStubWithCode } from './SharedFunctionStub';
export class SharedFunctionCollectionStub implements ISharedFunctionCollection {
private readonly functions = new Map<string, ISharedFunction>();
@@ -13,10 +13,11 @@ export class SharedFunctionCollectionStub implements ISharedFunctionCollection {
}
public getFunctionByName(name: string): ISharedFunction {
if (this.functions.has(name)) {
return this.functions.get(name);
const foundFunction = this.functions.get(name);
if (foundFunction) {
return foundFunction;
}
return new SharedFunctionStub(FunctionBodyType.Code)
return createSharedFunctionStubWithCode()
.withName(name)
.withCode('code by SharedFunctionCollectionStub')
.withRevertCode('revert-code by SharedFunctionCollectionStub');

View File

@@ -1,72 +1,103 @@
import { ISharedFunction, ISharedFunctionBody, FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import {
ISharedFunction, FunctionBodyType, CallFunctionBody, CodeFunctionBody,
} from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionParameterCollectionStub } from './FunctionParameterCollectionStub';
import { FunctionCallStub } from './FunctionCallStub';
import { FunctionCodeStub } from './FunctionCodeStub';
export class SharedFunctionStub implements ISharedFunction {
public name = 'shared-function-stub-name';
type CodeOrCallBody<T extends FunctionBodyType> = T extends FunctionBodyType.Calls
? CallFunctionBody : CodeFunctionBody;
export function createSharedFunctionStubWithCalls(): SharedFunctionStub<FunctionBodyType.Calls> {
return new SharedFunctionStub(FunctionBodyType.Calls);
}
export function createSharedFunctionStubWithCode(): SharedFunctionStub<FunctionBodyType.Code> {
return new SharedFunctionStub(FunctionBodyType.Code);
}
class SharedFunctionStub<T extends FunctionBodyType>
implements ISharedFunction {
public name = `${SharedFunctionStub.name}-name`;
public parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub()
.withParameterName('shared-function-stub-parameter-name');
.withParameterName(`${SharedFunctionStub.name}-parameter-name`);
private code = 'shared-function-stub-code';
private code = `${SharedFunctionStub.name}-code`;
private revertCode = 'shared-function-stub-revert-code';
private revertCode: string | undefined = `${SharedFunctionStub.name}-revert-code`;
private bodyType: FunctionBodyType = FunctionBodyType.Code;
private readonly bodyType: FunctionBodyType = FunctionBodyType.Code;
private calls: FunctionCall[] = [new FunctionCallStub()];
constructor(type: FunctionBodyType) {
constructor(type: T) {
this.bodyType = type;
}
public get body(): ISharedFunctionBody {
return {
type: this.bodyType,
code: this.bodyType === FunctionBodyType.Code ? {
execute: this.code,
revert: this.revertCode,
} : undefined,
calls: this.bodyType === FunctionBodyType.Calls ? this.calls : undefined,
};
public get body(): CodeOrCallBody<T> {
switch (this.bodyType) {
case FunctionBodyType.Code:
return this.getCodeBody() as CodeOrCallBody<T>;
case FunctionBodyType.Calls:
return this.getCallBody() as CodeOrCallBody<T>;
default:
throw new Error(`unknown body type: ${this.bodyType}`);
}
}
public withName(name: string) {
public withName(name: string): this {
this.name = name;
return this;
}
public withCode(code: string) {
public withCode(code: string): this {
this.code = code;
return this;
}
public withRevertCode(revertCode: string) {
public withRevertCode(revertCode: string | undefined): this {
this.revertCode = revertCode;
return this;
}
public withParameters(parameters: IReadOnlyFunctionParameterCollection) {
public withParameters(parameters: IReadOnlyFunctionParameterCollection): this {
this.parameters = parameters;
return this;
}
public withSomeCalls() {
public withSomeCalls(): this {
return this.withCalls(new FunctionCallStub(), new FunctionCallStub());
}
public withCalls(...calls: readonly FunctionCall[]) {
public withCalls(...calls: readonly FunctionCall[]): this {
this.calls = [...calls];
return this;
}
public withParameterNames(...parameterNames: readonly string[]) {
public withParameterNames(...parameterNames: readonly string[]): this {
let collection = new FunctionParameterCollectionStub();
for (const name of parameterNames) {
collection = collection.withParameterName(name);
}
return this.withParameters(collection);
}
private getCodeBody(): CodeFunctionBody {
return {
type: FunctionBodyType.Code,
code: new FunctionCodeStub()
.withExecute(this.code)
.withRevert(this.revertCode),
};
}
private getCallBody(): CallFunctionBody {
return {
type: FunctionBodyType.Calls,
calls: this.calls,
};
}
}

View File

@@ -32,7 +32,7 @@ export class SharedFunctionsParserStub implements ISharedFunctionsParser {
return result || new SharedFunctionCollectionStub();
}
private findResult(functions: readonly FunctionData[]): ISharedFunctionCollection {
private findResult(functions: readonly FunctionData[]): ISharedFunctionCollection | undefined {
return this.setupResults
.find((result) => sequenceEqual(result.functions, functions))
?.result;

View File

@@ -19,7 +19,7 @@ export abstract class StubWithObservableMethodCalls<T> {
type MethodCall<T> = {
[K in FunctionKeys<T>]: {
methodName: K;
args: T[K] extends (...args: infer A) => unknown ? A : never;
readonly methodName: K;
readonly args: T[K] extends (...args: infer A) => unknown ? A : never;
}
}[FunctionKeys<T>];

View File

@@ -24,7 +24,7 @@ export class TreeNodeStub implements TreeNode {
public metadata?: object = new NodeMetadataStub();
public withMetadata(metadata: object): this {
public withMetadata(metadata: object | undefined): this {
this.metadata = metadata;
return this;
}

View File

@@ -71,7 +71,7 @@ export class UseCollectionStateStub
if (scenario.immediateOnly) {
handlers = handlers.filter((args) => args[1]?.immediate === true);
}
const callbacks = handlers.map((args) => args[0] as NewStateEventHandler);
const callbacks = handlers.map((args) => args[0]);
if (!callbacks.length) {
throw new Error('No handler callbacks are registered to handle state change');
}

View File

@@ -2,6 +2,7 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'
import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
import { FilterActionType } from '@/application/Context/State/Filter/Event/FilterActionType';
import { FilterResultStub } from './FilterResultStub';
import { EventSourceStub } from './EventSourceStub';
@@ -21,7 +22,11 @@ export class UserFilterStub implements IUserFilter {
public notifyFilterChange(change: IFilterChangeDetails) {
this.filterChangedSource.notify(change);
this.currentFilter = change.filter;
if (change.action.type === FilterActionType.Apply) {
this.currentFilter = change.action.filter;
} else {
this.currentFilter = undefined;
}
}
public withNoCurrentFilter() {

View File

@@ -6,30 +6,30 @@ import { SystemOperationsStub } from './SystemOperationsStub';
import { LoggerStub } from './LoggerStub';
export class WindowVariablesStub implements WindowVariables {
public system: ISystemOperations = new SystemOperationsStub();
public system?: ISystemOperations = new SystemOperationsStub();
public isDesktop = false;
public isDesktop? = false;
public os: OperatingSystem = OperatingSystem.BlackBerryOS;
public os?: OperatingSystem = OperatingSystem.BlackBerryOS;
public log: ILogger = new LoggerStub();
public log?: ILogger = new LoggerStub();
public withLog(log: ILogger): this {
public withLog(log?: ILogger): this {
this.log = log;
return this;
}
public withIsDesktop(value: boolean): this {
public withIsDesktop(value?: boolean): this {
this.isDesktop = value;
return this;
}
public withOs(value: OperatingSystem): this {
public withOs(value: OperatingSystem | undefined): this {
this.os = value;
return this;
}
public withSystem(value: ISystemOperations): this {
public withSystem(value?: ISystemOperations): this {
this.system = value;
return this;
}

View File

@@ -1,28 +1,84 @@
import { it } from 'vitest';
export function itEachAbsentStringValue(
runner: (absentValue: string | null) => void,
options: { excludeUndefined: true, excludeNull?: false }
): void;
export function itEachAbsentStringValue(
runner: (absentValue: string | undefined) => void,
options: { excludeUndefined?: false, excludeNull: true }
): void;
export function itEachAbsentStringValue(
runner: (absentValue: string) => void,
options: IAbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
options: { excludeUndefined: true, excludeNull: true }
): void;
export function itEachAbsentStringValue(
runner: (absentValue: string | null | undefined) => void,
options?: { excludeUndefined?: false, excludeNull?: false },
): void;
export function itEachAbsentStringValue(
runner: ((absentValue: string) => void)
| ((absentValue: string | null) => void)
| ((absentValue: string | undefined) => void)
| ((absentValue: string | null | undefined) => void),
options: AbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
): void {
itEachAbsentTestCase(getAbsentStringTestCases(options), runner);
// Using `as any` due to limitation of TypeScript, this may be fixed in future versions.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
itEachAbsentTestCase(getAbsentStringTestCases(options as any), runner);
}
export function itEachAbsentCollectionValue<T>(
runner: (absentValue: T[] | null) => void,
options: { excludeUndefined: true, excludeNull?: false }
): void;
export function itEachAbsentCollectionValue<T>(
runner: (absentValue: T[] | undefined) => void,
options: { excludeUndefined?: false, excludeNull: true }
): void;
export function itEachAbsentCollectionValue<T>(
runner: (absentValue: T[]) => void,
options: { excludeUndefined: true, excludeNull: true }
): void;
export function itEachAbsentCollectionValue<T>(
runner: (absentValue: T[] | null | undefined) => void,
options?: { excludeUndefined?: false, excludeNull?: false },
): void;
export function itEachAbsentCollectionValue<T>(
runner: ((absentValue: T[]) => void)
| ((absentValue: T[] | null) => void)
| ((absentValue: T[] | undefined) => void)
| ((absentValue: T[] | null | undefined) => void),
options: AbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
): void {
// Using `as any` due to limitation of TypeScript, this may be fixed in future versions.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
itEachAbsentTestCase(getAbsentCollectionTestCases<T>(options as any), runner);
}
export function itEachAbsentObjectValue(
runner: (absentValue: AbsentObjectType) => void,
options: IAbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
runner: (absentValue: null) => void,
options: { excludeUndefined: true, excludeNull?: false },
): void;
export function itEachAbsentObjectValue(
runner: (absentValue: undefined) => void,
options: { excludeUndefined?: false, excludeNull: true },
): void;
export function itEachAbsentObjectValue(
runner: (absentValue: undefined | null) => void,
options?: { excludeUndefined?: false, excludeNull?: false } | AbsentTestCaseOptions,
): void;
export function itEachAbsentObjectValue(
runner: ((absentValue: null) => void)
| ((absentValue: undefined) => void)
| ((absentValue: null | undefined) => void),
options: AbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
): void {
itEachAbsentTestCase(getAbsentObjectTestCases(options), runner);
}
export function itEachAbsentCollectionValue<T>(
runner: (absentValue: []) => void,
options: IAbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
): void {
itEachAbsentTestCase(getAbsentCollectionTestCases<T>(options), runner);
}
export function itEachAbsentTestCase<T>(
testCases: readonly IAbsentTestCase<T>[],
testCases: readonly AbsentTestCase<T>[],
runner: (absentValue: T) => void,
): void {
for (const testCase of testCases) {
@@ -32,68 +88,106 @@ export function itEachAbsentTestCase<T>(
}
}
export function getAbsentObjectTestCases(
options: IAbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
): IAbsentTestCase<AbsentObjectType>[] {
return [
{
valueName: 'null',
absentValue: null,
},
...(options.excludeUndefined ? [] : [
{
valueName: 'undefined',
absentValue: undefined,
},
]),
];
interface AbsentTestCaseOptions {
readonly excludeUndefined?: boolean;
readonly excludeNull?: boolean;
}
export function getAbsentStringTestCases(
options: IAbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
): IAbsentStringCase[] {
return [
{
valueName: 'empty',
absentValue: '',
},
...getAbsentObjectTestCases(options),
];
}
export function getAbsentCollectionTestCases<T>(
options: IAbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
): readonly IAbsentCollectionCase<T>[] {
return [
...getAbsentObjectTestCases(options),
{
valueName: 'empty',
absentValue: new Array<T>(),
},
];
}
const DefaultAbsentTestCaseOptions: IAbsentTestCaseOptions = {
const DefaultAbsentTestCaseOptions: AbsentTestCaseOptions = {
excludeUndefined: false,
excludeNull: false,
};
interface IAbsentTestCaseOptions {
readonly excludeUndefined: boolean;
}
type AbsentObjectType = undefined | null;
interface IAbsentTestCase<T> {
interface AbsentTestCase<T> {
readonly valueName: string;
readonly absentValue: T;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface IAbsentStringCase extends IAbsentTestCase<string> {
// Marker interface
export function getAbsentObjectTestCases(
options: { excludeUndefined: true, excludeNull?: false },
): ReadonlyArray<AbsentTestCase<null>>;
export function getAbsentObjectTestCases(
options: { excludeUndefined?: false, excludeNull: true },
): ReadonlyArray<AbsentTestCase<undefined>>;
export function getAbsentObjectTestCases(
options: { excludeUndefined: true, excludeNull: true },
): ReadonlyArray<never>;
export function getAbsentObjectTestCases(
options?: { excludeUndefined?: false, excludeNull?: false } | AbsentTestCaseOptions,
): ReadonlyArray<AbsentTestCase<null> | AbsentTestCase<undefined>>;
export function getAbsentObjectTestCases(
options: AbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
): ReadonlyArray<AbsentTestCase<null> | AbsentTestCase<undefined>> {
const results: Array<AbsentTestCase<null> | AbsentTestCase<undefined>> = [];
if (!options.excludeNull) {
results.push({
valueName: 'null',
absentValue: null,
});
}
if (!options.excludeUndefined) {
results.push({
valueName: 'undefined',
absentValue: undefined,
});
}
return results;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface IAbsentCollectionCase<T> extends IAbsentTestCase<T[]> {
// Marker interface
export function getAbsentStringTestCases(
options?: { excludeUndefined: false, excludeNull: false },
): ReadonlyArray<AbsentTestCase<string> | AbsentTestCase<null> | AbsentTestCase<undefined>>;
export function getAbsentStringTestCases(
options: { excludeUndefined: true, excludeNull?: false }
): ReadonlyArray<AbsentTestCase<string> | AbsentTestCase<null>>;
export function getAbsentStringTestCases(
options: { excludeUndefined?: false, excludeNull: true }
): ReadonlyArray<AbsentTestCase<string> | AbsentTestCase<undefined>>;
export function getAbsentStringTestCases(
options: { excludeUndefined: true, excludeNull: true }
): ReadonlyArray<AbsentTestCase<string>>;
export function getAbsentStringTestCases(
options: AbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
): ReadonlyArray<AbsentTestCase<string> | AbsentTestCase<null> | AbsentTestCase<undefined>> {
const results: Array<(
AbsentTestCase<string> | AbsentTestCase<null> | AbsentTestCase<undefined>
)> = [];
results.push({
valueName: 'empty',
absentValue: '',
});
const objectTestCases = getAbsentObjectTestCases(options);
results.push(...objectTestCases);
return results;
}
export function getAbsentCollectionTestCases<T>(
options?: { excludeUndefined: false, excludeNull: false },
): ReadonlyArray<AbsentTestCase<T[]> | AbsentTestCase<null> | AbsentTestCase<undefined>>;
export function getAbsentCollectionTestCases<T>(
options: { excludeUndefined: true, excludeNull?: false }
): ReadonlyArray<AbsentTestCase<T[]> | AbsentTestCase<null>>;
export function getAbsentCollectionTestCases<T>(
options: { excludeUndefined?: false, excludeNull: true }
): ReadonlyArray<AbsentTestCase<T[]> | AbsentTestCase<undefined>>;
export function getAbsentCollectionTestCases<T>(
options: { excludeUndefined: true, excludeNull: true }
): ReadonlyArray<AbsentTestCase<T[]>>;
export function getAbsentCollectionTestCases<T>(
options: AbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
): ReadonlyArray<AbsentTestCase<T[]> | AbsentTestCase<null> | AbsentTestCase<undefined>> {
const results: Array<AbsentTestCase<T[]> | AbsentTestCase<null> | AbsentTestCase<undefined>> = [];
const objectTestCases = getAbsentObjectTestCases(options);
results.push(...objectTestCases);
results.push({
valueName: 'empty',
absentValue: [],
});
return results;
}