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:
@@ -63,9 +63,24 @@ async function determineLogPath(
|
||||
const logFilePaths: {
|
||||
readonly [K in SupportedPlatform]: () => string;
|
||||
} = {
|
||||
[SupportedPlatform.macOS]: () => join(process.env.HOME, 'Library', 'Logs', appName, `${logFileName}.log`),
|
||||
[SupportedPlatform.Linux]: () => join(process.env.HOME, '.config', appName, 'logs', `${logFileName}.log`),
|
||||
[SupportedPlatform.Windows]: () => join(process.env.USERPROFILE, 'AppData', 'Roaming', appName, 'logs', `${logFileName}.log`),
|
||||
[SupportedPlatform.macOS]: () => {
|
||||
if (!process.env.HOME) {
|
||||
throw new Error('HOME environment variable is not defined');
|
||||
}
|
||||
return join(process.env.HOME, 'Library', 'Logs', appName, `${logFileName}.log`);
|
||||
},
|
||||
[SupportedPlatform.Linux]: () => {
|
||||
if (!process.env.HOME) {
|
||||
throw new Error('HOME environment variable is not defined');
|
||||
}
|
||||
return join(process.env.HOME, '.config', appName, 'logs', `${logFileName}.log`);
|
||||
},
|
||||
[SupportedPlatform.Windows]: () => {
|
||||
if (!process.env.USERPROFILE) {
|
||||
throw new Error('USERPROFILE environment variable is not defined');
|
||||
}
|
||||
return join(process.env.USERPROFILE, 'AppData', 'Roaming', appName, 'logs', `${logFileName}.log`);
|
||||
},
|
||||
};
|
||||
const logFilePath = logFilePaths[CURRENT_PLATFORM]?.();
|
||||
if (!logFilePath) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { splitTextIntoLines, indentText } from '../utils/text';
|
||||
import { splitTextIntoLines, indentText, filterEmpty } from '../utils/text';
|
||||
import { log, die } from '../utils/log';
|
||||
import { readAppLogFile } from './app-logs';
|
||||
import { STDERR_IGNORE_PATTERNS } from './error-ignore-patterns';
|
||||
@@ -33,7 +33,7 @@ async function gatherErrors(
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
const { logFileContent: mainLogs, logFilePath: mainLogFile } = await readAppLogFile(projectDir, 'main');
|
||||
const { logFileContent: rendererLogs, logFilePath: rendererLogFile } = await readAppLogFile(projectDir, 'renderer');
|
||||
const allLogs = [mainLogs, rendererLogs, stderr].filter(Boolean).join('\n');
|
||||
const allLogs = filterEmpty([mainLogs, rendererLogs, stderr]).join('\n');
|
||||
return [
|
||||
verifyStdErr(stderr),
|
||||
verifyApplicationLogsExist('main', mainLogs, mainLogFile),
|
||||
@@ -43,7 +43,7 @@ async function gatherErrors(
|
||||
),
|
||||
verifyWindowTitle(windowTitles),
|
||||
verifyErrorsInLogs(allLogs),
|
||||
].filter(Boolean);
|
||||
].filter((error): error is ExecutionError => Boolean(error));
|
||||
}
|
||||
|
||||
interface ExecutionError {
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function findByFilePattern(
|
||||
pattern: string,
|
||||
directory: string,
|
||||
projectRootDir: string,
|
||||
): Promise<ArtifactLocation> {
|
||||
): Promise<ArtifactLocation | never> {
|
||||
if (!directory) { throw new Error('Missing directory'); }
|
||||
if (!pattern) { throw new Error('Missing file pattern'); }
|
||||
|
||||
@@ -42,5 +42,5 @@ function escapeRegExp(string: string) {
|
||||
}
|
||||
|
||||
interface ArtifactLocation {
|
||||
readonly absolutePath?: string;
|
||||
readonly absolutePath: string;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,10 @@ async function mountDmg(
|
||||
die(`Failed to mount DMG file at ${dmgFile}.\n${error}`);
|
||||
}
|
||||
const mountPathMatch = hdiutilOutput.match(/\/Volumes\/[^\n]+/);
|
||||
const mountPath = mountPathMatch ? mountPathMatch[0] : null;
|
||||
if (!mountPathMatch || mountPathMatch.length === 0) {
|
||||
die(`Could not find mount path from \`hdiutil\` output:\n${hdiutilOutput}`);
|
||||
}
|
||||
const mountPath = mountPathMatch[0];
|
||||
return {
|
||||
mountPath,
|
||||
};
|
||||
@@ -52,7 +55,10 @@ async function findMacAppExecutablePath(
|
||||
return die(`Failed to find executable path at mount path ${mountPath}\n${error}`);
|
||||
}
|
||||
const appFolder = findOutput.trim();
|
||||
const appName = appFolder.split('/').pop().replace('.app', '');
|
||||
const appName = appFolder.split('/').pop()?.replace('.app', '');
|
||||
if (!appName) {
|
||||
die(`Could not extract app path from \`find\` output: ${findOutput}`);
|
||||
}
|
||||
const appPath = `${appFolder}/Contents/MacOS/${appName}`;
|
||||
if (await exists(appPath)) {
|
||||
log(`Application is located at ${appPath}`);
|
||||
|
||||
@@ -20,6 +20,7 @@ export function runApplication(
|
||||
|
||||
logDetails(appFile, executionDurationInSeconds);
|
||||
|
||||
const process = spawn(appFile);
|
||||
const processDetails: ApplicationProcessDetails = {
|
||||
stderrData: '',
|
||||
stdoutData: '',
|
||||
@@ -27,15 +28,15 @@ export function runApplication(
|
||||
windowTitles: [],
|
||||
isCrashed: false,
|
||||
isDone: false,
|
||||
process: undefined,
|
||||
process,
|
||||
resolve: () => { /* NOOP */ },
|
||||
};
|
||||
|
||||
const process = spawn(appFile);
|
||||
processDetails.process = process;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
processDetails.resolve = resolve;
|
||||
if (process.pid === undefined) {
|
||||
throw new Error('Unknown PID');
|
||||
}
|
||||
beginCapturingTitles(process.pid, processDetails);
|
||||
handleProcessEvents(
|
||||
processDetails,
|
||||
@@ -54,13 +55,14 @@ interface ApplicationExecutionResult {
|
||||
}
|
||||
|
||||
interface ApplicationProcessDetails {
|
||||
readonly process: ChildProcess;
|
||||
|
||||
stderrData: string;
|
||||
stdoutData: string;
|
||||
explicitlyKilled: boolean;
|
||||
windowTitles: Array<string>;
|
||||
isCrashed: boolean;
|
||||
isDone: boolean;
|
||||
process: ChildProcess;
|
||||
resolve: (value: ApplicationExecutionResult) => void;
|
||||
}
|
||||
|
||||
@@ -85,7 +87,7 @@ function beginCapturingTitles(
|
||||
const titles = await captureWindowTitles(processId);
|
||||
|
||||
(titles || []).forEach((title) => {
|
||||
if (!title?.length) {
|
||||
if (!title) {
|
||||
return;
|
||||
}
|
||||
if (!processDetails.windowTitles.includes(title)) {
|
||||
@@ -109,10 +111,10 @@ function handleProcessEvents(
|
||||
executionDurationInSeconds: number,
|
||||
): void {
|
||||
const { process } = processDetails;
|
||||
process.stderr.on('data', (data) => {
|
||||
process.stderr?.on('data', (data) => {
|
||||
processDetails.stderrData += data.toString();
|
||||
});
|
||||
process.stdout.on('data', (data) => {
|
||||
process.stdout?.on('data', (data) => {
|
||||
processDetails.stdoutData += data.toString();
|
||||
});
|
||||
|
||||
@@ -130,7 +132,7 @@ function handleProcessEvents(
|
||||
}
|
||||
|
||||
async function onProcessExit(
|
||||
code: number,
|
||||
code: number | null,
|
||||
processDetails: ApplicationProcessDetails,
|
||||
enableScreenshot: boolean,
|
||||
screenshotPath: string,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { runCommand } from '../../utils/run-command';
|
||||
import { log, LogLevel } from '../../utils/log';
|
||||
import { SupportedPlatform, CURRENT_PLATFORM } from '../../utils/platform';
|
||||
import { filterEmpty } from '../../utils/text';
|
||||
|
||||
export async function captureWindowTitles(processId: number) {
|
||||
if (!processId) { throw new Error('Missing process ID.'); }
|
||||
@@ -67,7 +68,7 @@ async function captureTitlesOnLinux(processId: number): Promise<string[]> {
|
||||
return titleOutput.trim();
|
||||
}));
|
||||
|
||||
return titles.filter(Boolean);
|
||||
return filterEmpty(titles);
|
||||
}
|
||||
|
||||
let hasAssistiveAccessOnMac = true;
|
||||
|
||||
@@ -70,13 +70,14 @@ const appNameCache = new Map<string, string>();
|
||||
|
||||
export async function getAppName(projectDir: string): Promise<string> {
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
if (appNameCache.has(projectDir)) {
|
||||
return appNameCache.get(projectDir);
|
||||
const cachedName = appNameCache.get(projectDir);
|
||||
if (cachedName) {
|
||||
return cachedName;
|
||||
}
|
||||
const packageData = await readPackageJsonContents(projectDir);
|
||||
try {
|
||||
const packageJson = JSON.parse(packageData);
|
||||
const name = packageJson.name as string;
|
||||
const name = packageJson.name as string | undefined;
|
||||
if (!name) {
|
||||
return die(`The \`package.json\` file doesn't specify a name: ${packageData}`);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const NODE_PLATFORM_MAPPINGS: {
|
||||
[SupportedPlatform.Windows]: 'win32',
|
||||
};
|
||||
|
||||
function findCurrentPlatform(): SupportedPlatform | undefined {
|
||||
function getCurrentPlatform(): SupportedPlatform | never {
|
||||
const nodePlatform = platform();
|
||||
|
||||
for (const key of Object.keys(NODE_PLATFORM_MAPPINGS)) {
|
||||
@@ -28,4 +28,4 @@ function findCurrentPlatform(): SupportedPlatform | undefined {
|
||||
return die(`Unsupported platform: ${nodePlatform}`);
|
||||
}
|
||||
|
||||
export const CURRENT_PLATFORM: SupportedPlatform = findCurrentPlatform();
|
||||
export const CURRENT_PLATFORM: SupportedPlatform = getCurrentPlatform();
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface CommandResult {
|
||||
|
||||
function formatError(
|
||||
command: string,
|
||||
error: ExecException | undefined,
|
||||
error: ExecException | null,
|
||||
stdout: string | undefined,
|
||||
stderr: string | undefined,
|
||||
) {
|
||||
|
||||
@@ -15,6 +15,11 @@ export function splitTextIntoLines(text: string): string[] {
|
||||
.split(/[\r\n]+/);
|
||||
}
|
||||
|
||||
export function filterEmpty(texts: readonly (string | undefined | null)[]): string[] {
|
||||
return texts
|
||||
.filter((title): title is string => Boolean(title));
|
||||
}
|
||||
|
||||
function validateText(text: string): void {
|
||||
if (typeof text !== 'string') {
|
||||
throw new Error(`text is not a string. It is: ${typeof text}\n${text}`);
|
||||
|
||||
@@ -9,9 +9,9 @@ export async function getUrlStatusesInParallel(
|
||||
): Promise<IUrlStatus[]> {
|
||||
// urls = [ 'https://privacy.sexy' ]; // Here to comment out when testing
|
||||
const uniqueUrls = Array.from(new Set(urls));
|
||||
options = { ...DefaultOptions, ...options };
|
||||
console.log('Options: ', options);
|
||||
const results = await request(uniqueUrls, options);
|
||||
const defaultedOptions = { ...DefaultOptions, ...options };
|
||||
console.log('Options: ', defaultedOptions);
|
||||
const results = await request(uniqueUrls, defaultedOptions);
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ interface IDomainOptions {
|
||||
sameDomainDelayInMs?: number;
|
||||
}
|
||||
|
||||
const DefaultOptions: IBatchRequestOptions = {
|
||||
const DefaultOptions: Required<IBatchRequestOptions> = {
|
||||
domainOptions: {
|
||||
sameDomainParallelize: false,
|
||||
sameDomainDelayInMs: 3 /* sec */ * 1000,
|
||||
@@ -39,7 +39,7 @@ const DefaultOptions: IBatchRequestOptions = {
|
||||
|
||||
function request(
|
||||
urls: string[],
|
||||
options: IBatchRequestOptions,
|
||||
options: Required<IBatchRequestOptions>,
|
||||
): Promise<IUrlStatus[]> {
|
||||
if (!options.domainOptions.sameDomainParallelize) {
|
||||
return runOnEachDomainWithDelay(
|
||||
@@ -54,7 +54,7 @@ function request(
|
||||
async function runOnEachDomainWithDelay(
|
||||
urls: string[],
|
||||
action: (url: string) => Promise<IUrlStatus>,
|
||||
delayInMs: number,
|
||||
delayInMs: number | undefined,
|
||||
): Promise<IUrlStatus[]> {
|
||||
const grouped = groupUrlsByDomain(urls);
|
||||
const tasks = grouped.map(async (group) => {
|
||||
@@ -64,7 +64,9 @@ async function runOnEachDomainWithDelay(
|
||||
const status = await action(url);
|
||||
results.push(status);
|
||||
if (results.length !== group.length) {
|
||||
await sleep(delayInMs);
|
||||
if (delayInMs !== undefined) {
|
||||
await sleep(delayInMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
@@ -26,6 +26,9 @@ function shouldRetry(status: IUrlStatus) {
|
||||
if (status.error) {
|
||||
return true;
|
||||
}
|
||||
if (status.code === undefined) {
|
||||
return true;
|
||||
}
|
||||
return isTransientError(status.code)
|
||||
|| status.code === 429; // Too Many Requests
|
||||
}
|
||||
|
||||
@@ -4,19 +4,22 @@ export function fetchFollow(
|
||||
url: string,
|
||||
timeoutInMs: number,
|
||||
fetchOptions: RequestInit,
|
||||
followOptions: IFollowOptions,
|
||||
followOptions: IFollowOptions | undefined,
|
||||
): Promise<Response> {
|
||||
followOptions = { ...DefaultOptions, ...followOptions };
|
||||
if (followRedirects(followOptions)) {
|
||||
const defaultedFollowOptions = {
|
||||
...DefaultFollowOptions,
|
||||
...followOptions,
|
||||
};
|
||||
if (followRedirects(defaultedFollowOptions)) {
|
||||
return fetchWithTimeout(url, timeoutInMs, fetchOptions);
|
||||
}
|
||||
fetchOptions = { ...fetchOptions, redirect: 'manual' /* handled manually */ };
|
||||
const cookies = new CookieStorage(followOptions.enableCookies);
|
||||
const cookies = new CookieStorage(defaultedFollowOptions.enableCookies);
|
||||
return followRecursivelyWithCookies(
|
||||
url,
|
||||
timeoutInMs,
|
||||
fetchOptions,
|
||||
followOptions.maximumRedirectFollowDepth,
|
||||
defaultedFollowOptions.maximumRedirectFollowDepth,
|
||||
cookies,
|
||||
);
|
||||
}
|
||||
@@ -27,7 +30,7 @@ export interface IFollowOptions {
|
||||
enableCookies?: boolean;
|
||||
}
|
||||
|
||||
const DefaultOptions: IFollowOptions = {
|
||||
export const DefaultFollowOptions: Required<IFollowOptions> = {
|
||||
followRedirects: true,
|
||||
maximumRedirectFollowDepth: 20,
|
||||
enableCookies: true,
|
||||
@@ -53,9 +56,14 @@ async function followRecursivelyWithCookies(
|
||||
if (newFollowDepth < 0) {
|
||||
throw new Error(`[max-redirect] maximum redirect reached at: ${url}`);
|
||||
}
|
||||
const cookieHeader = response.headers.get('set-cookie');
|
||||
cookies.addHeader(cookieHeader);
|
||||
const nextUrl = response.headers.get('location');
|
||||
if (!nextUrl) {
|
||||
return response;
|
||||
}
|
||||
const cookieHeader = response.headers.get('set-cookie');
|
||||
if (cookieHeader) {
|
||||
cookies.addHeader(cookieHeader);
|
||||
}
|
||||
return followRecursivelyWithCookies(nextUrl, timeoutInMs, options, newFollowDepth, cookies);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,48 +1,49 @@
|
||||
import { retryWithExponentialBackOff } from './ExponentialBackOffRetryHandler';
|
||||
import { IUrlStatus } from './IUrlStatus';
|
||||
import { fetchFollow, IFollowOptions } from './FetchFollow';
|
||||
import { fetchFollow, IFollowOptions, DefaultFollowOptions } from './FetchFollow';
|
||||
import { getRandomUserAgent } from './UserAgents';
|
||||
|
||||
export function getUrlStatus(
|
||||
url: string,
|
||||
options: IRequestOptions = DefaultOptions,
|
||||
): Promise<IUrlStatus> {
|
||||
options = { ...DefaultOptions, ...options };
|
||||
const fetchOptions = getFetchOptions(url, options);
|
||||
const defaultedOptions = { ...DefaultOptions, ...options };
|
||||
const fetchOptions = getFetchOptions(url, defaultedOptions);
|
||||
return retryWithExponentialBackOff(async () => {
|
||||
console.log('Requesting', url);
|
||||
let result: IUrlStatus;
|
||||
try {
|
||||
const response = await fetchFollow(
|
||||
url,
|
||||
options.requestTimeoutInMs,
|
||||
defaultedOptions.requestTimeoutInMs,
|
||||
fetchOptions,
|
||||
options.followOptions,
|
||||
defaultedOptions.followOptions,
|
||||
);
|
||||
result = { url, code: response.status };
|
||||
} catch (err) {
|
||||
result = { url, error: JSON.stringify(err, null, '\t') };
|
||||
}
|
||||
return result;
|
||||
}, options.retryExponentialBaseInMs);
|
||||
}, defaultedOptions.retryExponentialBaseInMs);
|
||||
}
|
||||
|
||||
export interface IRequestOptions {
|
||||
retryExponentialBaseInMs?: number;
|
||||
additionalHeaders?: Record<string, string>;
|
||||
additionalHeadersUrlIgnore?: string[];
|
||||
followOptions?: IFollowOptions;
|
||||
requestTimeoutInMs: number;
|
||||
readonly retryExponentialBaseInMs?: number;
|
||||
readonly additionalHeaders?: Record<string, string>;
|
||||
readonly additionalHeadersUrlIgnore?: string[];
|
||||
readonly followOptions?: IFollowOptions;
|
||||
readonly requestTimeoutInMs: number;
|
||||
}
|
||||
|
||||
const DefaultOptions: IRequestOptions = {
|
||||
const DefaultOptions: Required<IRequestOptions> = {
|
||||
retryExponentialBaseInMs: 5000,
|
||||
additionalHeaders: {},
|
||||
additionalHeadersUrlIgnore: [],
|
||||
requestTimeoutInMs: 60 /* seconds */ * 1000,
|
||||
followOptions: DefaultFollowOptions,
|
||||
};
|
||||
|
||||
function getFetchOptions(url: string, options: IRequestOptions): RequestInit {
|
||||
function getFetchOptions(url: string, options: Required<IRequestOptions>): RequestInit {
|
||||
const additionalHeaders = options.additionalHeadersUrlIgnore
|
||||
.some((ignorePattern) => url.startsWith(ignorePattern))
|
||||
? {}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('revert toggle', () => {
|
||||
context('toggle switch', () => {
|
||||
beforeEach(() => {
|
||||
@@ -30,20 +32,24 @@ describe('revert toggle', () => {
|
||||
const font = getFont($label[0]);
|
||||
const expectedMinimumTextWidth = getTextWidth(text, font);
|
||||
const containerWidth = $label.parent().width();
|
||||
expectExists(containerWidth);
|
||||
expect(expectedMinimumTextWidth).to.be.lessThan(containerWidth);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getFont(element) {
|
||||
function getFont(element: Element): string {
|
||||
const computedStyle = window.getComputedStyle(element);
|
||||
return `${computedStyle.fontWeight} ${computedStyle.fontSize} ${computedStyle.fontFamily}`;
|
||||
}
|
||||
|
||||
function getTextWidth(text, font) {
|
||||
function getTextWidth(text: string, font: string): number {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Unable to get 2D context from canvas element');
|
||||
}
|
||||
ctx.font = font;
|
||||
const measuredWidth = ctx.measureText(text).width;
|
||||
return measuredWidth;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
@@ -8,7 +9,8 @@
|
||||
"types": [
|
||||
"cypress",
|
||||
"node"
|
||||
]
|
||||
],
|
||||
"sourceMap": false,
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
|
||||
@@ -28,7 +28,11 @@ function generateTestOptions(): ISanityCheckOptions[] {
|
||||
return generateBooleanPermutations(defaultOptions);
|
||||
}
|
||||
|
||||
function generateBooleanPermutations<T>(object: T): T[] {
|
||||
function generateBooleanPermutations<T>(object: T | undefined): T[] {
|
||||
if (!object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const keys = Object.keys(object) as (keyof T)[];
|
||||
|
||||
if (keys.length === 0) {
|
||||
@@ -47,7 +51,7 @@ function generateBooleanPermutations<T>(object: T): T[] {
|
||||
|
||||
const remainingKeys = Object.fromEntries(
|
||||
keys.slice(1).map((key) => [key, object[key]]),
|
||||
) as unknown as T;
|
||||
) as unknown as T | undefined;
|
||||
|
||||
const subPermutations = generateBooleanPermutations(remainingKeys);
|
||||
|
||||
|
||||
@@ -4,9 +4,6 @@ import { ISanityValidator } from '@/infrastructure/RuntimeSanity/Common/ISanityV
|
||||
export function itNoErrorsOnCurrentEnvironment(
|
||||
factory: () => ISanityValidator,
|
||||
) {
|
||||
if (!factory) {
|
||||
throw new Error('missing factory');
|
||||
}
|
||||
it('it does report errors on current environment', () => {
|
||||
// arrange
|
||||
const validator = factory();
|
||||
|
||||
@@ -23,7 +23,7 @@ function checkAllowedType(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.every(checkAllowedType);
|
||||
}
|
||||
if (type === 'object' && value !== null) {
|
||||
if (type === 'object' && value !== null && value !== undefined) {
|
||||
return (
|
||||
// Every key should be a string
|
||||
Object.keys(value).every((key) => typeof key === 'string')
|
||||
|
||||
51
tests/shared/Assertions/ExpectDeepThrowsError.ts
Normal file
51
tests/shared/Assertions/ExpectDeepThrowsError.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { expect } from 'vitest';
|
||||
import { expectExists } from './ExpectExists';
|
||||
|
||||
// `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
|
||||
let actual: T | undefined;
|
||||
// act
|
||||
try {
|
||||
delegate();
|
||||
} catch (error) {
|
||||
actual = error;
|
||||
}
|
||||
// assert
|
||||
expectExists(actual);
|
||||
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: object | undefined,
|
||||
actual: object | undefined,
|
||||
) {
|
||||
const actualClean = removeUndefinedProperties(actual);
|
||||
const expectedClean = removeUndefinedProperties(expected);
|
||||
expect(expectedClean).to.deep.equal(actualClean);
|
||||
}
|
||||
|
||||
function removeUndefinedProperties(obj: object | undefined): object | undefined {
|
||||
if (!obj) {
|
||||
return obj;
|
||||
}
|
||||
return Object.keys(obj).reduce((acc, key) => {
|
||||
const value = obj[key];
|
||||
switch (typeof value) {
|
||||
case 'object': {
|
||||
const cleanValue = removeUndefinedProperties(value); // recurse
|
||||
if (!cleanValue || !Object.keys(cleanValue).length) {
|
||||
return { ...acc };
|
||||
}
|
||||
return { ...acc, [key]: cleanValue };
|
||||
}
|
||||
case 'undefined':
|
||||
return { ...acc };
|
||||
default:
|
||||
return { ...acc, [key]: value };
|
||||
}
|
||||
}, {});
|
||||
}
|
||||
14
tests/shared/Assertions/ExpectExists.ts
Normal file
14
tests/shared/Assertions/ExpectExists.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Asserts that the provided value is neither null nor undefined.
|
||||
*
|
||||
* This function is a more explicit alternative to `expect(value).to.exist` in test cases.
|
||||
* It is particularly useful in situations where TypeScript's control flow analysis
|
||||
* does not recognize standard assertions, ensuring that `value` is treated as
|
||||
* non-nullable in the subsequent code. This can prevent potential type errors
|
||||
* and enhance code safety and clarity.
|
||||
*/
|
||||
export function expectExists<T>(value: T): asserts value is NonNullable<T> {
|
||||
if (value === null || value === undefined) {
|
||||
throw new Error('Expected value to exist');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect } from 'vitest';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
export async function expectThrowsAsync(
|
||||
method: () => Promise<unknown>,
|
||||
@@ -10,11 +11,9 @@ export async function expectThrowsAsync(
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expectExists(error);
|
||||
expect(error).to.be.an(Error.name);
|
||||
if (errorMessage) {
|
||||
expect(error.message).to.equal(errorMessage);
|
||||
}
|
||||
expect(error.message).to.equal(errorMessage);
|
||||
}
|
||||
|
||||
export async function expectDoesNotThrowAsync(
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ApplicationFactory, ApplicationGetterType } from '@/application/ApplicationFactory';
|
||||
import { ApplicationStub } from '@tests/unit/shared/Stubs/ApplicationStub';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('ApplicationFactory', () => {
|
||||
describe('ctor', () => {
|
||||
describe('throws if getter is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing getter';
|
||||
const getter: ApplicationGetterType = absentValue;
|
||||
// act
|
||||
const act = () => new SystemUnderTest(getter);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('getApp', () => {
|
||||
it('returns result from the getter', async () => {
|
||||
// arrange
|
||||
|
||||
@@ -1,36 +1,9 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { scrambledEqual, sequenceEqual } from '@/application/Common/Array';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { ComparerTestScenario } from './Array.ComparerTestScenario';
|
||||
|
||||
describe('Array', () => {
|
||||
describe('scrambledEqual', () => {
|
||||
describe('throws if arguments are absent', () => {
|
||||
describe('first argument is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing first array';
|
||||
const firstArray = absentValue;
|
||||
const secondArray = [];
|
||||
// act
|
||||
const act = () => scrambledEqual(firstArray, secondArray);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('second argument is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing second array';
|
||||
const firstArray = [];
|
||||
const secondArray = absentValue;
|
||||
// act
|
||||
const act = () => scrambledEqual(firstArray, secondArray);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('returns as expected', () => {
|
||||
// arrange
|
||||
const scenario = new ComparerTestScenario()
|
||||
@@ -49,43 +22,17 @@ describe('Array', () => {
|
||||
});
|
||||
});
|
||||
describe('sequenceEqual', () => {
|
||||
describe('throws if arguments are absent', () => {
|
||||
describe('first argument is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing first array';
|
||||
const firstArray = absentValue;
|
||||
const secondArray = [];
|
||||
// act
|
||||
const act = () => sequenceEqual(firstArray, secondArray);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('second argument is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing second array';
|
||||
const firstArray = [];
|
||||
const secondArray = absentValue;
|
||||
// act
|
||||
const act = () => sequenceEqual(firstArray, secondArray);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('returns as expected', () => {
|
||||
// arrange
|
||||
const scenario = new ComparerTestScenario()
|
||||
.addSameItemsWithSameOrder(true)
|
||||
.addSameItemsWithDifferentOrder(true)
|
||||
.addSameItemsWithDifferentOrder(false)
|
||||
.addDifferentItemsWithSameLength(false)
|
||||
.addDifferentItemsWithDifferentLength(false);
|
||||
// act
|
||||
scenario.forEachCase((testCase) => {
|
||||
it(testCase.name, () => {
|
||||
const actual = scrambledEqual(testCase.first, testCase.second);
|
||||
const actual = sequenceEqual(testCase.first, testCase.second);
|
||||
// assert
|
||||
expect(actual).to.equal(testCase.expected);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
describe, it, afterEach, expect,
|
||||
} from 'vitest';
|
||||
import { CustomError, Environment } from '@/application/Common/CustomError';
|
||||
import { CustomError, PlatformErrorPrototypeManipulation } from '@/application/Common/CustomError';
|
||||
|
||||
describe('CustomError', () => {
|
||||
afterEach(() => {
|
||||
Environment.getSetPrototypeOf = () => Object.setPrototypeOf;
|
||||
Environment.getCaptureStackTrace = () => Error.captureStackTrace;
|
||||
PlatformErrorPrototypeManipulation.getSetPrototypeOf = () => Object.setPrototypeOf;
|
||||
PlatformErrorPrototypeManipulation.getCaptureStackTrace = () => Error.captureStackTrace;
|
||||
});
|
||||
describe('sets members as expected', () => {
|
||||
it('`name`', () => {
|
||||
@@ -39,7 +39,7 @@ describe('CustomError', () => {
|
||||
it('sets using `getCaptureStackTrace` if available', () => {
|
||||
// arrange
|
||||
const mockStackTrace = 'mocked stack trace';
|
||||
Environment.getCaptureStackTrace = () => (error) => {
|
||||
PlatformErrorPrototypeManipulation.getCaptureStackTrace = () => (error) => {
|
||||
(error as Error).stack = mockStackTrace;
|
||||
};
|
||||
// act
|
||||
@@ -102,7 +102,7 @@ describe('CustomError', () => {
|
||||
describe('Object.setPrototypeOf', () => {
|
||||
it('does not throw if unavailable', () => {
|
||||
// arrange
|
||||
Environment.getSetPrototypeOf = () => undefined;
|
||||
PlatformErrorPrototypeManipulation.getSetPrototypeOf = () => undefined;
|
||||
|
||||
// act
|
||||
const act = () => new CustomErrorConcrete();
|
||||
@@ -114,7 +114,7 @@ describe('CustomError', () => {
|
||||
// arrange
|
||||
let wasCalled = false;
|
||||
const setPrototypeOf = () => { wasCalled = true; };
|
||||
Environment.getSetPrototypeOf = () => setPrototypeOf;
|
||||
PlatformErrorPrototypeManipulation.getSetPrototypeOf = () => setPrototypeOf;
|
||||
|
||||
// act
|
||||
// eslint-disable-next-line no-new
|
||||
@@ -127,7 +127,7 @@ describe('CustomError', () => {
|
||||
describe('Error.captureStackTrace', () => {
|
||||
it('does not throw if unavailable', () => {
|
||||
// arrange
|
||||
Environment.getCaptureStackTrace = () => undefined;
|
||||
PlatformErrorPrototypeManipulation.getCaptureStackTrace = () => undefined;
|
||||
|
||||
// act
|
||||
const act = () => new CustomErrorConcrete();
|
||||
@@ -139,7 +139,7 @@ describe('CustomError', () => {
|
||||
// arrange
|
||||
let wasCalled = false;
|
||||
const captureStackTrace = () => { wasCalled = true; };
|
||||
Environment.getCaptureStackTrace = () => captureStackTrace;
|
||||
PlatformErrorPrototypeManipulation.getCaptureStackTrace = () => captureStackTrace;
|
||||
|
||||
// act
|
||||
// eslint-disable-next-line no-new
|
||||
|
||||
@@ -36,8 +36,15 @@ describe('Enum', () => {
|
||||
describe('throws as expected', () => {
|
||||
// arrange
|
||||
const enumName = 'ParsableEnum';
|
||||
const testCases = [
|
||||
...getAbsentStringTestCases().map((test) => ({
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly name: string;
|
||||
readonly value: string;
|
||||
readonly expectedError: string;
|
||||
}> = [
|
||||
...getAbsentStringTestCases({
|
||||
excludeNull: true,
|
||||
excludeUndefined: true,
|
||||
}).map((test) => ({
|
||||
name: test.valueName,
|
||||
value: test.absentValue,
|
||||
expectedError: `missing ${enumName}`,
|
||||
@@ -59,7 +66,7 @@ describe('Enum', () => {
|
||||
},
|
||||
];
|
||||
// act
|
||||
for (const testCase of testCases) {
|
||||
for (const testCase of testScenarios) {
|
||||
it(testCase.name, () => {
|
||||
const parser = createEnumParser(ParsableEnum);
|
||||
const act = () => parser.parseEnum(testCase.value, enumName);
|
||||
@@ -100,7 +107,6 @@ describe('Enum', () => {
|
||||
// assert
|
||||
new EnumRangeTestRunner(act)
|
||||
.testOutOfRangeThrows()
|
||||
.testAbsentValueThrows()
|
||||
.testValidValueDoesNotThrow(validValue);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { it, expect } from 'vitest';
|
||||
import { EnumType } from '@/application/Common/Enum';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
export class EnumRangeTestRunner<TEnumValue extends EnumType> {
|
||||
constructor(private readonly runner: (value: TEnumValue) => void) {
|
||||
}
|
||||
|
||||
public testOutOfRangeThrows() {
|
||||
public testOutOfRangeThrows(errorMessageBuilder?: (outOfRangeValue: TEnumValue) => string) {
|
||||
it('throws when value is out of range', () => {
|
||||
// arrange
|
||||
const value = Number.MAX_SAFE_INTEGER as TEnumValue;
|
||||
const expectedError = `enum value "${value}" is out of range`;
|
||||
const expectedError = errorMessageBuilder
|
||||
? errorMessageBuilder(value)
|
||||
: `enum value "${value}" is out of range`;
|
||||
// act
|
||||
const act = () => this.runner(value);
|
||||
// assert
|
||||
@@ -19,23 +20,8 @@ export class EnumRangeTestRunner<TEnumValue extends EnumType> {
|
||||
return this;
|
||||
}
|
||||
|
||||
public testAbsentValueThrows() {
|
||||
describe('throws when value is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const value = absentValue;
|
||||
const expectedError = 'absent enum value';
|
||||
// act
|
||||
const act = () => this.runner(value);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public testInvalidValueThrows(invalidValue: TEnumValue, expectedError: string) {
|
||||
it(`throws ${expectedError}`, () => {
|
||||
it(`throws: \`${expectedError}\``, () => {
|
||||
// arrange
|
||||
const value = invalidValue;
|
||||
// act
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, it, expect } from 'vitest';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
||||
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { ScriptingLanguageFactoryTestRunner } from './ScriptingLanguageFactoryTestRunner';
|
||||
|
||||
class ScriptingLanguageConcrete extends ScriptingLanguageFactory<number> {
|
||||
@@ -23,22 +22,8 @@ describe('ScriptingLanguageFactory', () => {
|
||||
// assert
|
||||
new EnumRangeTestRunner(act)
|
||||
.testOutOfRangeThrows()
|
||||
.testAbsentValueThrows()
|
||||
.testValidValueDoesNotThrow(validValue);
|
||||
});
|
||||
describe('describe when getter is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing getter';
|
||||
const language = ScriptingLanguage.batchfile;
|
||||
const getter = absentValue;
|
||||
const sut = new ScriptingLanguageConcrete();
|
||||
// act
|
||||
const act = () => sut.registerGetter(language, getter);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('throw when language is already registered', () => {
|
||||
// arrange
|
||||
const language = ScriptingLanguage.batchfile;
|
||||
|
||||
@@ -21,7 +21,6 @@ export class ScriptingLanguageFactoryTestRunner<T> {
|
||||
}
|
||||
|
||||
public testCreateMethod(sut: IScriptingLanguageFactory<T>) {
|
||||
if (!sut) { throw new Error('missing sut'); }
|
||||
testLanguageValidation(sut);
|
||||
if (this.expectedLanguageTypes.size) {
|
||||
testExpectedInstanceTypes(sut, this.expectedLanguageTypes);
|
||||
@@ -36,7 +35,7 @@ function testExpectedInstanceTypes<T>(
|
||||
sut: IScriptingLanguageFactory<T>,
|
||||
expectedTypes: Map<ScriptingLanguage, Constructible<T>>,
|
||||
) {
|
||||
if (!expectedTypes?.size) {
|
||||
if (!expectedTypes.size) {
|
||||
throw new Error('No expected types provided.');
|
||||
}
|
||||
describe('`create` creates expected instances', () => {
|
||||
@@ -47,7 +46,7 @@ function testExpectedInstanceTypes<T>(
|
||||
const expected = expectedTypes.get(language);
|
||||
const result = sut.create(language);
|
||||
// assert
|
||||
expect(result).to.be.instanceOf(expected, `Actual was: ${result.constructor.name}`);
|
||||
expect(result).to.be.instanceOf(expected, `Actual was: ${result}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -57,7 +56,7 @@ function testExpectedValues<T>(
|
||||
sut: IScriptingLanguageFactory<T>,
|
||||
expectedValues: Map<ScriptingLanguage, T>,
|
||||
) {
|
||||
if (!expectedValues?.size) {
|
||||
if (!expectedValues.size) {
|
||||
throw new Error('No expected values provided.');
|
||||
}
|
||||
describe('`create` creates expected values', () => {
|
||||
@@ -83,7 +82,6 @@ function testLanguageValidation<T>(sut: IScriptingLanguageFactory<T>) {
|
||||
// assert
|
||||
new EnumRangeTestRunner(act)
|
||||
.testOutOfRangeThrows()
|
||||
.testAbsentValueThrows()
|
||||
.testValidValueDoesNotThrow(validValue);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ import { IApplicationContext, IApplicationContextChangedEvent } from '@/applicat
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { ApplicationStub } from '@tests/unit/shared/Stubs/ApplicationStub';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('ApplicationContext', () => {
|
||||
describe('changeContext', () => {
|
||||
@@ -51,6 +50,19 @@ describe('ApplicationContext', () => {
|
||||
// assert
|
||||
expectEmptyState(sut.state);
|
||||
});
|
||||
it('throws when OS is unknown to application', () => {
|
||||
// arrange
|
||||
const expectedError = 'expected error from application';
|
||||
const applicationStub = new ApplicationStub();
|
||||
const sut = new ObservableApplicationContextFactory()
|
||||
.withApp(applicationStub)
|
||||
.construct();
|
||||
// act
|
||||
applicationStub.getCollection = () => { throw new Error(expectedError); };
|
||||
const act = () => sut.changeContext(OperatingSystem.Android);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('remembers old state when changed backed to same os', () => {
|
||||
// arrange
|
||||
@@ -70,6 +82,7 @@ describe('ApplicationContext', () => {
|
||||
sut.state.filter.applyFilter('second-state');
|
||||
sut.changeContext(os);
|
||||
// assert
|
||||
expectExists(sut.state.filter.currentFilter);
|
||||
const actualFilter = sut.state.filter.currentFilter.query;
|
||||
expect(actualFilter).to.equal(expectedFilter);
|
||||
});
|
||||
@@ -125,35 +138,8 @@ describe('ApplicationContext', () => {
|
||||
expect(duplicates.length).to.be.equal(0);
|
||||
});
|
||||
});
|
||||
describe('throws with invalid os', () => {
|
||||
new EnumRangeTestRunner((os: OperatingSystem) => {
|
||||
// arrange
|
||||
const sut = new ObservableApplicationContextFactory()
|
||||
.construct();
|
||||
// act
|
||||
sut.changeContext(os);
|
||||
})
|
||||
// assert
|
||||
.testOutOfRangeThrows()
|
||||
.testAbsentValueThrows()
|
||||
.testInvalidValueThrows(OperatingSystem.Android, 'os "Android" is not defined in application');
|
||||
});
|
||||
});
|
||||
describe('ctor', () => {
|
||||
describe('app', () => {
|
||||
describe('throw when app is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing app';
|
||||
const app = absentValue;
|
||||
const os = OperatingSystem.Windows;
|
||||
// act
|
||||
const act = () => new ApplicationContext(app, os);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('collection', () => {
|
||||
it('returns right collection for expected OS', () => {
|
||||
// arrange
|
||||
@@ -195,16 +181,17 @@ describe('ApplicationContext', () => {
|
||||
const actual = sut.state.os;
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
describe('throws when OS is invalid', () => {
|
||||
it('throws when OS is unknown to application', () => {
|
||||
// arrange
|
||||
const expectedError = 'expected error from application';
|
||||
const applicationStub = new ApplicationStub();
|
||||
applicationStub.getCollection = () => { throw new Error(expectedError); };
|
||||
// act
|
||||
const act = (os: OperatingSystem) => new ObservableApplicationContextFactory()
|
||||
.withInitialOs(os)
|
||||
const act = () => new ObservableApplicationContextFactory()
|
||||
.withApp(applicationStub)
|
||||
.construct();
|
||||
// assert
|
||||
new EnumRangeTestRunner(act)
|
||||
.testOutOfRangeThrows()
|
||||
.testAbsentValueThrows()
|
||||
.testInvalidValueThrows(OperatingSystem.Android, 'os "Android" is not defined in application');
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('app', () => {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { IApplication } from '@/domain/IApplication';
|
||||
import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub';
|
||||
import { ApplicationStub } from '@tests/unit/shared/Stubs/ApplicationStub';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { expectThrowsAsync } from '@tests/shared/Assertions/ExpectThrowsAsync';
|
||||
|
||||
describe('ApplicationContextFactory', () => {
|
||||
describe('buildContext', () => {
|
||||
@@ -23,19 +22,10 @@ describe('ApplicationContextFactory', () => {
|
||||
// assert
|
||||
expect(expected).to.equal(context.app);
|
||||
});
|
||||
it('throws when null', async () => {
|
||||
// arrange
|
||||
const expectedError = 'missing factory';
|
||||
const factory = null;
|
||||
// act
|
||||
const act = async () => { await buildContext(factory); };
|
||||
// assert
|
||||
expectThrowsAsync(act, expectedError);
|
||||
});
|
||||
});
|
||||
describe('environment', () => {
|
||||
describe('sets initial OS as expected', () => {
|
||||
it('returns currentOs if it is supported', async () => {
|
||||
it('returns current OS if it is supported', async () => {
|
||||
// arrange
|
||||
const expected = OperatingSystem.Windows;
|
||||
const environment = new RuntimeEnvironmentStub().withOs(expected);
|
||||
@@ -47,7 +37,7 @@ describe('ApplicationContextFactory', () => {
|
||||
const actual = context.state.os;
|
||||
expect(expected).to.equal(actual);
|
||||
});
|
||||
it('fallbacks to other os if OS in environment is not supported', async () => {
|
||||
it('fallbacks to other OS if OS in environment is not supported', async () => {
|
||||
// arrange
|
||||
const expected = OperatingSystem.Windows;
|
||||
const currentOs = OperatingSystem.macOS;
|
||||
@@ -60,15 +50,34 @@ describe('ApplicationContextFactory', () => {
|
||||
const actual = context.state.os;
|
||||
expect(expected).to.equal(actual);
|
||||
});
|
||||
it('fallbacks to most supported os if current os is not supported', async () => {
|
||||
it('fallbacks to most supported OS if current OS is not supported', async () => {
|
||||
// arrange
|
||||
const runtimeOs = OperatingSystem.macOS;
|
||||
const expectedOs = OperatingSystem.Android;
|
||||
const allCollections = [
|
||||
new CategoryCollectionStub().withOs(OperatingSystem.Linux).withTotalScripts(3),
|
||||
new CategoryCollectionStub().withOs(expectedOs).withTotalScripts(5),
|
||||
new CategoryCollectionStub().withOs(OperatingSystem.Windows).withTotalScripts(4),
|
||||
];
|
||||
const environment = new RuntimeEnvironmentStub().withOs(OperatingSystem.macOS);
|
||||
const environment = new RuntimeEnvironmentStub().withOs(runtimeOs);
|
||||
const app = new ApplicationStub().withCollections(...allCollections);
|
||||
const factoryMock = mockFactoryWithApp(app);
|
||||
// act
|
||||
const context = await buildContext(factoryMock, environment);
|
||||
// assert
|
||||
const actual = context.state.os;
|
||||
expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`);
|
||||
});
|
||||
it('fallbacks to most supported OS if current OS is undefined', async () => {
|
||||
// arrange
|
||||
const runtimeOs = OperatingSystem.macOS;
|
||||
const expectedOs = OperatingSystem.Android;
|
||||
const allCollections = [
|
||||
new CategoryCollectionStub().withOs(OperatingSystem.Linux).withTotalScripts(3),
|
||||
new CategoryCollectionStub().withOs(expectedOs).withTotalScripts(5),
|
||||
new CategoryCollectionStub().withOs(OperatingSystem.Windows).withTotalScripts(4),
|
||||
];
|
||||
const environment = new RuntimeEnvironmentStub().withOs(runtimeOs);
|
||||
const app = new ApplicationStub().withCollections(...allCollections);
|
||||
const factoryMock = mockFactoryWithApp(app);
|
||||
// act
|
||||
@@ -77,16 +86,6 @@ describe('ApplicationContextFactory', () => {
|
||||
const actual = context.state.os;
|
||||
expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`);
|
||||
});
|
||||
});
|
||||
it('throws when null', async () => {
|
||||
// arrange
|
||||
const expectedError = 'missing environment';
|
||||
const factory = mockFactoryWithApp(undefined);
|
||||
const environment = null;
|
||||
// act
|
||||
const act = async () => { await buildContext(factory, environment); };
|
||||
// assert
|
||||
expectThrowsAsync(act, expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,7 +92,11 @@ describe('CategoryCollectionState', () => {
|
||||
// act
|
||||
let actualScript: IScript | undefined;
|
||||
sut.filter.filterChanged.on((result) => {
|
||||
[actualScript] = result.filter?.scriptMatches ?? [undefined];
|
||||
result.visit({
|
||||
onApply: (filter) => {
|
||||
[actualScript] = filter.scriptMatches;
|
||||
},
|
||||
});
|
||||
});
|
||||
sut.filter.applyFilter(scriptNameFilter);
|
||||
// assert
|
||||
|
||||
@@ -12,8 +12,7 @@ import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefin
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('ApplicationCode', () => {
|
||||
describe('ctor', () => {
|
||||
@@ -47,47 +46,12 @@ describe('ApplicationCode', () => {
|
||||
// assert
|
||||
expect(actual).to.equal(expected.code);
|
||||
});
|
||||
describe('throws when userSelection is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing userSelection';
|
||||
const userSelection = absentValue;
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
// act
|
||||
const act = () => new ApplicationCode(userSelection, definition);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('throws when scriptingDefinition is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing scriptingDefinition';
|
||||
const userSelection = new UserSelectionStub([]);
|
||||
const definition = absentValue;
|
||||
// act
|
||||
const act = () => new ApplicationCode(userSelection, definition);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('throws when generator is missing', () => {
|
||||
// arrange
|
||||
const expectedError = 'missing generator';
|
||||
const userSelection = new UserSelectionStub([]);
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
const generator = null;
|
||||
// act
|
||||
const act = () => new ApplicationCode(userSelection, definition, generator);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('changed event', () => {
|
||||
describe('code', () => {
|
||||
it('empty when nothing is selected', () => {
|
||||
// arrange
|
||||
let signaled: ICodeChangedEvent;
|
||||
let signaled: ICodeChangedEvent | undefined;
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(1).withScripts(...scripts));
|
||||
@@ -101,12 +65,13 @@ describe('ApplicationCode', () => {
|
||||
// act
|
||||
selection.changed.notify([]);
|
||||
// assert
|
||||
expectExists(signaled);
|
||||
expect(signaled.code).to.have.lengthOf(0);
|
||||
expect(signaled.code).to.equal(sut.current);
|
||||
});
|
||||
it('has code when some are selected', () => {
|
||||
// arrange
|
||||
let signaled: ICodeChangedEvent;
|
||||
let signaled: ICodeChangedEvent | undefined;
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(1).withScripts(...scripts));
|
||||
@@ -120,6 +85,7 @@ describe('ApplicationCode', () => {
|
||||
// act
|
||||
selection.changed.notify(scripts.map((s) => new SelectedScript(s, false)));
|
||||
// assert
|
||||
expectExists(signaled);
|
||||
expect(signaled.code).to.have.length.greaterThan(0);
|
||||
expect(signaled.code).to.equal(sut.current);
|
||||
});
|
||||
@@ -131,12 +97,12 @@ describe('ApplicationCode', () => {
|
||||
const collection = new CategoryCollectionStub();
|
||||
const selection = new UserSelection(collection, []);
|
||||
const generatorMock: IUserScriptGenerator = {
|
||||
buildCode: (selectedScripts, definition) => {
|
||||
buildCode: (_, definition) => {
|
||||
if (definition !== expectedDefinition) {
|
||||
throw new Error('Unexpected scripting definition');
|
||||
}
|
||||
return {
|
||||
code: '',
|
||||
code: 'non-important-code',
|
||||
scriptPositions: new Map<SelectedScript, ICodePosition>(),
|
||||
};
|
||||
},
|
||||
@@ -176,7 +142,7 @@ describe('ApplicationCode', () => {
|
||||
});
|
||||
it('sets positions from the generator', () => {
|
||||
// arrange
|
||||
let signaled: ICodeChangedEvent;
|
||||
let signaled: ICodeChangedEvent | undefined;
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(1).withScripts(...scripts));
|
||||
@@ -205,6 +171,7 @@ describe('ApplicationCode', () => {
|
||||
// act
|
||||
selection.changed.notify(scriptsToSelect);
|
||||
// assert
|
||||
expectExists(signaled);
|
||||
expect(signaled.getScriptPositionInCode(scripts[0]))
|
||||
.to.deep.equal(expected.get(scriptsToSelect[0]));
|
||||
expect(signaled.getScriptPositionInCode(scripts[1]))
|
||||
|
||||
@@ -166,6 +166,17 @@ describe('CodeChangedEvent', () => {
|
||||
});
|
||||
});
|
||||
describe('getScriptPositionInCode', () => {
|
||||
it('throws if script is unknown', () => {
|
||||
// arrange
|
||||
const expectedError = 'Unknown script: Position could not be found for the script';
|
||||
const unknownScript = new ScriptStub('1');
|
||||
const sut = new CodeChangedEventBuilder()
|
||||
.build();
|
||||
// act
|
||||
const act = () => sut.getScriptPositionInCode(unknownScript);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('returns expected position for existing script', () => {
|
||||
// arrange
|
||||
const script = new ScriptStub('1');
|
||||
|
||||
@@ -5,8 +5,8 @@ import { ICodeBuilderFactory } from '@/application/Context/State/Code/Generation
|
||||
import { ICodeBuilder } from '@/application/Context/State/Code/Generation/ICodeBuilder';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
||||
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('UserScriptGenerator', () => {
|
||||
describe('scriptingDefinition', () => {
|
||||
@@ -45,7 +45,7 @@ describe('UserScriptGenerator', () => {
|
||||
// assert
|
||||
const actual = code.code;
|
||||
expect(actual.startsWith(expectedStart));
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
});
|
||||
describe('endCode', () => {
|
||||
@@ -83,54 +83,62 @@ describe('UserScriptGenerator', () => {
|
||||
// assert
|
||||
const actual = code.code;
|
||||
expect(actual.endsWith(expectedEnd));
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
});
|
||||
describe('throws when absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
});
|
||||
describe('execute', () => {
|
||||
it('appends non-revert script', () => {
|
||||
const sut = new UserScriptGenerator();
|
||||
// arrange
|
||||
const scriptName = 'test non-revert script';
|
||||
const scriptCode = 'REM nop';
|
||||
const script = new ScriptStub('id').withName(scriptName).withCode(scriptCode);
|
||||
const selectedScripts = [new SelectedScript(script, false)];
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, definition);
|
||||
// assert
|
||||
expect(actual.code).to.include(scriptName);
|
||||
expect(actual.code).to.not.include(`${scriptName} (revert)`);
|
||||
expect(actual.code).to.include(scriptCode);
|
||||
});
|
||||
});
|
||||
describe('revert', () => {
|
||||
it('appends revert script', () => {
|
||||
// arrange
|
||||
const sut = new UserScriptGenerator();
|
||||
const scriptName = 'test non-revert script';
|
||||
const scriptCode = 'REM nop';
|
||||
const script = new ScriptStub('id')
|
||||
.withName(scriptName)
|
||||
.withRevertCode(scriptCode)
|
||||
.toSelectedScript(true);
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
// act
|
||||
const actual = sut.buildCode([script], definition);
|
||||
// assert
|
||||
expect(actual.code).to.include(`${scriptName} (revert)`);
|
||||
expect(actual.code).to.include(scriptCode);
|
||||
});
|
||||
describe('throws if revert script lacks revert code', () => {
|
||||
itEachAbsentStringValue((emptyRevertCode) => {
|
||||
// arrange
|
||||
const expectedError = 'missing definition';
|
||||
const expectedError = 'Reverted script lacks revert code.';
|
||||
const sut = new UserScriptGenerator();
|
||||
const scriptingDefinition = absentValue;
|
||||
const selectedScripts = [new SelectedScriptStub('a')];
|
||||
const script = new ScriptStub('id')
|
||||
.toSelectedScript(true);
|
||||
// Hack until SelectedScript is interface:
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(script.script.code as any).revert = emptyRevertCode;
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
// act
|
||||
const act = () => sut.buildCode(selectedScripts, scriptingDefinition);
|
||||
const act = () => sut.buildCode([script], definition);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true });
|
||||
});
|
||||
});
|
||||
it('appends revert script', () => {
|
||||
// arrange
|
||||
const sut = new UserScriptGenerator();
|
||||
const scriptName = 'test non-revert script';
|
||||
const scriptCode = 'REM nop';
|
||||
const script = new ScriptStub('id')
|
||||
.withName(scriptName)
|
||||
.withRevertCode(scriptCode)
|
||||
.toSelectedScript(true);
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
// act
|
||||
const actual = sut.buildCode([script], definition);
|
||||
// assert
|
||||
expect(actual.code).to.include(`${scriptName} (revert)`);
|
||||
expect(actual.code).to.include(scriptCode);
|
||||
});
|
||||
it('appends non-revert script', () => {
|
||||
const sut = new UserScriptGenerator();
|
||||
// arrange
|
||||
const scriptName = 'test non-revert script';
|
||||
const scriptCode = 'REM nop';
|
||||
const script = new ScriptStub('id').withName(scriptName).withCode(scriptCode);
|
||||
const selectedScripts = [new SelectedScript(script, false)];
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, definition);
|
||||
// assert
|
||||
expect(actual.code).to.include(scriptName);
|
||||
expect(actual.code).to.not.include(`${scriptName} (revert)`);
|
||||
expect(actual.code).to.include(scriptCode);
|
||||
});
|
||||
describe('scriptPositions', () => {
|
||||
it('without script; returns empty', () => {
|
||||
// arrange
|
||||
@@ -179,6 +187,7 @@ describe('UserScriptGenerator', () => {
|
||||
// expect
|
||||
expect(1).to.equal(actual.scriptPositions.size);
|
||||
const position = actual.scriptPositions.get(selectedScript);
|
||||
expectExists(position);
|
||||
expect(expectedStartLine).to.equal(position.startLine, 'Unexpected start line position');
|
||||
expect(expectedEndLine).to.equal(position.endLine, 'Unexpected end line position');
|
||||
});
|
||||
@@ -209,28 +218,15 @@ describe('UserScriptGenerator', () => {
|
||||
const firstPosition = actual.scriptPositions.get(selectedScripts[0]);
|
||||
const secondPosition = actual.scriptPositions.get(selectedScripts[1]);
|
||||
expect(actual.scriptPositions.size).to.equal(2);
|
||||
expectExists(firstPosition);
|
||||
expect(expectedFirstScriptStart).to.equal(firstPosition.startLine, 'Unexpected start line position (first script)');
|
||||
expect(expectedFirstScriptEnd).to.equal(firstPosition.endLine, 'Unexpected end line position (first script)');
|
||||
expectExists(secondPosition);
|
||||
expect(expectedSecondScriptStart).to.equal(secondPosition.startLine, 'Unexpected start line position (second script)');
|
||||
expect(expectedSecondScriptEnd).to.equal(secondPosition.endLine, 'Unexpected end line position (second script)');
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('selectedScripts', () => {
|
||||
describe('throws when absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing scripts';
|
||||
const sut = new UserScriptGenerator();
|
||||
const scriptingDefinition = new ScriptingDefinitionStub();
|
||||
const selectedScripts = absentValue;
|
||||
// act
|
||||
const act = () => sut.buildCode(selectedScripts, scriptingDefinition);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockCodeBuilderFactory(mock: ICodeBuilder): ICodeBuilderFactory {
|
||||
|
||||
@@ -1,30 +1,19 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { FilterChange } from '@/application/Context/State/Filter/Event/FilterChange';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { FilterResultStub } from '@tests/unit/shared/Stubs/FilterResultStub';
|
||||
import { FilterActionType } from '@/application/Context/State/Filter/Event/FilterActionType';
|
||||
import { FilterChangeDetailsVisitorStub } from '@tests/unit/shared/Stubs/FilterChangeDetailsVisitorStub';
|
||||
import { ApplyFilterAction } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
||||
|
||||
describe('FilterChange', () => {
|
||||
describe('forApply', () => {
|
||||
describe('throws when filter is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing filter';
|
||||
const filterValue = absentValue;
|
||||
// act
|
||||
const act = () => FilterChange.forApply(filterValue);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('sets filter result', () => {
|
||||
// arrange
|
||||
const expectedFilter = new FilterResultStub();
|
||||
// act
|
||||
const sut = FilterChange.forApply(expectedFilter);
|
||||
// assert
|
||||
const actualFilter = sut.filter;
|
||||
const actualFilter = (sut.action as ApplyFilterAction).filter;
|
||||
expect(actualFilter).to.equal(expectedFilter);
|
||||
});
|
||||
it('sets action as expected', () => {
|
||||
@@ -33,7 +22,7 @@ describe('FilterChange', () => {
|
||||
// act
|
||||
const sut = FilterChange.forApply(new FilterResultStub());
|
||||
// assert
|
||||
const actualAction = sut.actionType;
|
||||
const actualAction = sut.action.type;
|
||||
expect(actualAction).to.equal(expectedAction);
|
||||
});
|
||||
});
|
||||
@@ -44,7 +33,7 @@ describe('FilterChange', () => {
|
||||
// act
|
||||
const sut = FilterChange.forClear();
|
||||
// assert
|
||||
const actualFilter = sut.filter;
|
||||
const actualFilter = (sut.action as ApplyFilterAction).filter;
|
||||
expect(actualFilter).to.equal(expectedFilter);
|
||||
});
|
||||
it('sets action as expected', () => {
|
||||
@@ -53,23 +42,11 @@ describe('FilterChange', () => {
|
||||
// act
|
||||
const sut = FilterChange.forClear();
|
||||
// assert
|
||||
const actualAction = sut.actionType;
|
||||
const actualAction = sut.action.type;
|
||||
expect(actualAction).to.equal(expectedAction);
|
||||
});
|
||||
});
|
||||
describe('visit', () => {
|
||||
describe('throws when visitor is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing visitor';
|
||||
const visitorValue = absentValue;
|
||||
const sut = FilterChange.forClear();
|
||||
// act
|
||||
const act = () => sut.visit(visitorValue);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('onClear', () => {
|
||||
itVisitsOnce(
|
||||
() => FilterChange.forClear(),
|
||||
@@ -99,7 +76,7 @@ function itVisitsOnce(sutFactory: () => FilterChange) {
|
||||
it('visits', () => {
|
||||
// arrange
|
||||
const sut = sutFactory();
|
||||
const expectedType = sut.actionType;
|
||||
const expectedType = sut.action.type;
|
||||
const visitor = new FilterChangeDetailsVisitorStub();
|
||||
// act
|
||||
sut.visit(visitor);
|
||||
@@ -109,7 +86,7 @@ function itVisitsOnce(sutFactory: () => FilterChange) {
|
||||
it('visits once', () => {
|
||||
// arrange
|
||||
const sut = sutFactory();
|
||||
const expectedType = sut.actionType;
|
||||
const expectedType = sut.action.type;
|
||||
const visitor = new FilterChangeDetailsVisitorStub();
|
||||
// act
|
||||
sut.visit(visitor);
|
||||
|
||||
@@ -7,13 +7,14 @@ import { FilterChangeDetailsStub } from '@tests/unit/shared/Stubs/FilterChangeDe
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('UserFilter', () => {
|
||||
describe('clearFilter', () => {
|
||||
it('signals when removing filter', () => {
|
||||
// arrange
|
||||
const expectedChange = FilterChangeDetailsStub.forClear();
|
||||
let actualChange: IFilterChangeDetails;
|
||||
let actualChange: IFilterChangeDetails | undefined;
|
||||
const sut = new UserFilter(new CategoryCollectionStub());
|
||||
sut.filterChanged.on((change) => {
|
||||
actualChange = change;
|
||||
@@ -21,6 +22,7 @@ describe('UserFilter', () => {
|
||||
// act
|
||||
sut.clearFilter();
|
||||
// assert
|
||||
expectExists(actualChange);
|
||||
expect(actualChange).to.deep.equal(expectedChange);
|
||||
});
|
||||
it('sets currentFilter to undefined', () => {
|
||||
@@ -160,6 +162,7 @@ describe('UserFilter', () => {
|
||||
sut.applyFilter(filter);
|
||||
// assert
|
||||
const actual = sut.currentFilter;
|
||||
expectExists(actual);
|
||||
assert(actual);
|
||||
});
|
||||
});
|
||||
@@ -171,13 +174,18 @@ describe('UserFilter', () => {
|
||||
it(name, () => {
|
||||
// arrange
|
||||
const sut = new UserFilter(collection);
|
||||
let actualFilterResult: IFilterResult;
|
||||
let actualFilterResult: IFilterResult | undefined;
|
||||
sut.filterChanged.on((filterResult) => {
|
||||
actualFilterResult = filterResult.filter;
|
||||
filterResult.visit({
|
||||
onApply: (result) => {
|
||||
actualFilterResult = result;
|
||||
},
|
||||
});
|
||||
});
|
||||
// act
|
||||
sut.applyFilter(filter);
|
||||
// assert
|
||||
expectExists(actualFilterResult);
|
||||
assert(actualFilterResult);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ function expectSameScripts(actual: readonly SelectedScript[], expected: readonly
|
||||
`Scripts with different statuses:\n${
|
||||
scriptsWithDifferentStatus
|
||||
.map((s) => `[id: ${s.id}, actual status: ${s.revert}, `
|
||||
+ `expected status: ${expected.find((existing) => existing.id === s.id).revert}]`)
|
||||
+ `expected status: ${expected.find((existing) => existing.id === s.id)?.revert ?? 'unknown'}]`)
|
||||
.join(' , ')
|
||||
}`,
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import LinuxData from '@/application/collections/linux.yaml';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub';
|
||||
import { getAbsentCollectionTestCases, getAbsentObjectTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { getAbsentCollectionTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { AppMetadataStub } from '@tests/unit/shared/Stubs/AppMetadataStub';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { CategoryCollectionParserStub } from '@tests/unit/shared/Stubs/CategoryCollectionParserStub';
|
||||
@@ -65,8 +65,8 @@ describe('ApplicationParser', () => {
|
||||
sut.parseApplication();
|
||||
// assert
|
||||
expect(collectionParserStub.arguments).to.have.length.above(0);
|
||||
const actualyUsedInfos = collectionParserStub.arguments.map((arg) => arg.info);
|
||||
expect(actualyUsedInfos.every((info) => info === expectedInformation));
|
||||
const actuallyUsedInfos = collectionParserStub.arguments.map((arg) => arg.info);
|
||||
expect(actuallyUsedInfos.every((info) => info === expectedInformation));
|
||||
});
|
||||
});
|
||||
describe('metadata', () => {
|
||||
@@ -152,15 +152,15 @@ describe('ApplicationParser', () => {
|
||||
describe('throws when data is invalid', () => {
|
||||
// arrange
|
||||
const testCases = [
|
||||
...getAbsentCollectionTestCases<CollectionData>().map((testCase) => ({
|
||||
...getAbsentCollectionTestCases<CollectionData>(
|
||||
{
|
||||
excludeUndefined: true,
|
||||
excludeNull: true,
|
||||
},
|
||||
).map((testCase) => ({
|
||||
name: `given absent collection "${testCase.valueName}"`,
|
||||
value: testCase.absentValue,
|
||||
expectedError: 'missing collections',
|
||||
})).filter((test) => test.value !== undefined /* the default value is set */),
|
||||
...getAbsentObjectTestCases().map((testCase) => ({
|
||||
name: `given absent item "${testCase.valueName}"`,
|
||||
value: [testCase.absentValue],
|
||||
expectedError: 'missing collection provided',
|
||||
})),
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
@@ -185,9 +185,9 @@ class ApplicationParserBuilder {
|
||||
private projectInformationParser
|
||||
: typeof parseProjectInformation = new ProjectInformationParserStub().getStub();
|
||||
|
||||
private metadata: IAppMetadata = new AppMetadataStub();
|
||||
private metadata: IAppMetadata | undefined = new AppMetadataStub();
|
||||
|
||||
private collectionsData: CollectionData[] = [new CollectionDataStub()];
|
||||
private collectionsData: CollectionData[] | undefined = [new CollectionDataStub()];
|
||||
|
||||
public withCategoryCollectionParser(
|
||||
categoryCollectionParser: CategoryCollectionParserType,
|
||||
@@ -204,13 +204,13 @@ class ApplicationParserBuilder {
|
||||
}
|
||||
|
||||
public withMetadata(
|
||||
metadata: IAppMetadata,
|
||||
metadata: IAppMetadata | undefined,
|
||||
): this {
|
||||
this.metadata = metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCollectionsData(collectionsData: CollectionData[]): this {
|
||||
public withCollectionsData(collectionsData: CollectionData[] | undefined): this {
|
||||
this.collectionsData = collectionsData;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -10,27 +10,17 @@ import { ProjectInformationStub } from '@tests/unit/shared/Stubs/ProjectInformat
|
||||
import { getCategoryStub, CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub';
|
||||
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
|
||||
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
||||
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import type { CategoryData } from '@/application/collections/';
|
||||
|
||||
describe('CategoryCollectionParser', () => {
|
||||
describe('parseCategoryCollection', () => {
|
||||
describe('throws with absent content', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing content';
|
||||
const info = new ProjectInformationStub();
|
||||
// act
|
||||
const act = () => parseCategoryCollection(absentValue, info);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('actions', () => {
|
||||
describe('throws with absent actions', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
itEachAbsentCollectionValue<CategoryData>((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'content does not define any action';
|
||||
const collection = new CollectionDataStub()
|
||||
@@ -40,18 +30,7 @@ describe('CategoryCollectionParser', () => {
|
||||
const act = () => parseCategoryCollection(collection, info);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('throws when has no actions', () => {
|
||||
// arrange
|
||||
const expectedError = 'content does not define any action';
|
||||
const collection = new CollectionDataStub()
|
||||
.withActions([]);
|
||||
const info = new ProjectInformationStub();
|
||||
// act
|
||||
const act = () => parseCategoryCollection(collection, info);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
it('parses actions', () => {
|
||||
// arrange
|
||||
@@ -110,17 +89,18 @@ describe('CategoryCollectionParser', () => {
|
||||
const expectedCode = 'code-from-the-function';
|
||||
const functionName = 'function-name';
|
||||
const scriptName = 'script-name';
|
||||
const script = ScriptDataStub.createWithCall()
|
||||
const script = createScriptDataWithCall()
|
||||
.withCall(new FunctionCallDataStub().withName(functionName).withParameters({}))
|
||||
.withName(scriptName);
|
||||
const func = FunctionDataStub.createWithCode().withParametersObject([])
|
||||
const func = createFunctionDataWithCode()
|
||||
.withParametersObject([])
|
||||
.withName(functionName)
|
||||
.withCode(expectedCode);
|
||||
const category = new CategoryDataStub()
|
||||
.withChildren([script,
|
||||
ScriptDataStub.createWithCode().withName('2')
|
||||
createScriptDataWithCode().withName('2')
|
||||
.withRecommendationLevel(RecommendationLevel.Standard),
|
||||
ScriptDataStub.createWithCode()
|
||||
createScriptDataWithCode()
|
||||
.withName('3').withRecommendationLevel(RecommendationLevel.Strict),
|
||||
]);
|
||||
const collection = new CollectionDataStub()
|
||||
@@ -130,7 +110,7 @@ describe('CategoryCollectionParser', () => {
|
||||
// act
|
||||
const actual = parseCategoryCollection(collection, info);
|
||||
// assert
|
||||
const actualScript = actual.findScript(scriptName);
|
||||
const actualScript = actual.getScript(scriptName);
|
||||
const actualCode = actualScript.code.execute;
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
});
|
||||
|
||||
@@ -4,15 +4,15 @@ import { CategoryFactoryType, parseCategory } from '@/application/Parser/Categor
|
||||
import { parseScript } from '@/application/Parser/Script/ScriptParser';
|
||||
import { parseDocs } from '@/application/Parser/DocumentationParser';
|
||||
import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub';
|
||||
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
||||
import { itEachAbsentCollectionValue, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||
import { expectThrowsNodeError, ITestScenario, NodeValidationTestRunner } from '@tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner';
|
||||
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { createScriptDataWithCall, createScriptDataWithCode, createScriptDataWithoutCallOrCodes } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
|
||||
describe('CategoryParser', () => {
|
||||
describe('parseCategory', () => {
|
||||
@@ -30,7 +30,7 @@ describe('CategoryParser', () => {
|
||||
});
|
||||
});
|
||||
describe('throws when category children is absent', () => {
|
||||
itEachAbsentCollectionValue((absentValue) => {
|
||||
itEachAbsentCollectionValue<CategoryOrScriptData>((absentValue) => {
|
||||
// arrange
|
||||
const categoryName = 'test';
|
||||
const expectedMessage = `"${categoryName}" has no children.`;
|
||||
@@ -41,7 +41,7 @@ describe('CategoryParser', () => {
|
||||
const test = createTest(category);
|
||||
// assert
|
||||
expectThrowsNodeError(test, expectedMessage);
|
||||
});
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
describe('throws when category child is missing', () => {
|
||||
new NodeValidationTestRunner()
|
||||
@@ -140,19 +140,6 @@ describe('CategoryParser', () => {
|
||||
}, expectedError);
|
||||
});
|
||||
});
|
||||
describe('throws when context is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing context';
|
||||
const context = absentValue;
|
||||
// act
|
||||
const act = () => new TestBuilder()
|
||||
.withContext(context)
|
||||
.parseCategory();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('returns expected docs', () => {
|
||||
// arrange
|
||||
const url = 'https://privacy.sexy';
|
||||
@@ -170,7 +157,7 @@ describe('CategoryParser', () => {
|
||||
describe('parses expected subscript', () => {
|
||||
it('single script with code', () => {
|
||||
// arrange
|
||||
const script = ScriptDataStub.createWithCode();
|
||||
const script = createScriptDataWithCode();
|
||||
const context = new CategoryCollectionParseContextStub();
|
||||
const expected = [parseScript(script, context)];
|
||||
const category = new CategoryDataStub()
|
||||
@@ -186,7 +173,7 @@ describe('CategoryParser', () => {
|
||||
});
|
||||
it('single script with function call', () => {
|
||||
// arrange
|
||||
const script = ScriptDataStub.createWithCall();
|
||||
const script = createScriptDataWithCall();
|
||||
const compiler = new ScriptCompilerStub()
|
||||
.withCompileAbility(script);
|
||||
const context = new CategoryCollectionParseContextStub()
|
||||
@@ -205,8 +192,8 @@ describe('CategoryParser', () => {
|
||||
});
|
||||
it('multiple scripts with function call and code', () => {
|
||||
// arrange
|
||||
const callableScript = ScriptDataStub.createWithCall();
|
||||
const scripts = [callableScript, ScriptDataStub.createWithCode()];
|
||||
const callableScript = createScriptDataWithCall();
|
||||
const scripts = [callableScript, createScriptDataWithCode()];
|
||||
const category = new CategoryDataStub()
|
||||
.withChildren(scripts);
|
||||
const compiler = new ScriptCompilerStub()
|
||||
@@ -234,8 +221,7 @@ describe('CategoryParser', () => {
|
||||
new CategoryDataStub()
|
||||
.withName('sub-category')
|
||||
.withChildren([
|
||||
ScriptDataStub
|
||||
.createWithoutCallOrCodes()
|
||||
createScriptDataWithoutCallOrCodes()
|
||||
.withCode(duplicatedCode),
|
||||
]),
|
||||
]);
|
||||
@@ -253,7 +239,7 @@ describe('CategoryParser', () => {
|
||||
// arrange
|
||||
const expected = [new CategoryDataStub()
|
||||
.withName('test category')
|
||||
.withChildren([ScriptDataStub.createWithCode()]),
|
||||
.withChildren([createScriptDataWithCode()]),
|
||||
];
|
||||
const category = new CategoryDataStub()
|
||||
.withName('category name')
|
||||
@@ -276,7 +262,7 @@ class TestBuilder {
|
||||
|
||||
private context: ICategoryCollectionParseContext = new CategoryCollectionParseContextStub();
|
||||
|
||||
private factory: CategoryFactoryType = undefined;
|
||||
private factory?: CategoryFactoryType = undefined;
|
||||
|
||||
public withData(data: CategoryData) {
|
||||
this.data = data;
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { DocumentableData } from '@/application/collections/';
|
||||
import { parseDocs } from '@/application/Parser/DocumentationParser';
|
||||
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('DocumentationParser', () => {
|
||||
describe('parseDocs', () => {
|
||||
describe('throws when node is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing documentable';
|
||||
// act
|
||||
const act = () => parseDocs(absentValue);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('throws when single documentation is missing', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
@@ -24,7 +14,7 @@ describe('DocumentationParser', () => {
|
||||
const act = () => parseDocs(node);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
describe('throws when type is unexpected', () => {
|
||||
// arrange
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { NodeDataError } from '@/application/Parser/NodeValidation/NodeDataError';
|
||||
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
||||
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
|
||||
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
|
||||
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
||||
import { NodeDataErrorContextStub } from '@tests/unit/shared/Stubs/NodeDataErrorContextStub';
|
||||
import { NodeData } from '@/application/Parser/NodeValidation/NodeData';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it } from 'vitest';
|
||||
import { NodeDataError, INodeDataErrorContext } from '@/application/Parser/NodeValidation/NodeDataError';
|
||||
import { NodeData } from '@/application/Parser/NodeValidation/NodeData';
|
||||
import { getAbsentObjectTestCases, getAbsentStringTestCases, itEachAbsentTestCase } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
|
||||
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
|
||||
|
||||
export interface ITestScenario {
|
||||
readonly act: () => void;
|
||||
|
||||
@@ -5,43 +5,30 @@ import { CategoryCollectionParseContext } from '@/application/Parser/Script/Cate
|
||||
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
||||
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
import { itEachAbsentCollectionValue, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import type { FunctionData } from '@/application/collections/';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
|
||||
describe('CategoryCollectionParseContext', () => {
|
||||
describe('ctor', () => {
|
||||
describe('functionsData', () => {
|
||||
describe('can create with absent data', () => {
|
||||
itEachAbsentCollectionValue((absentValue) => {
|
||||
itEachAbsentCollectionValue<FunctionData>((absentValue) => {
|
||||
// arrange
|
||||
const scripting = new ScriptingDefinitionStub();
|
||||
// act
|
||||
const act = () => new CategoryCollectionParseContext(absentValue, scripting);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('scripting', () => {
|
||||
describe('throws when missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing scripting';
|
||||
const scripting = absentValue;
|
||||
const functionsData = [FunctionDataStub.createWithCode()];
|
||||
// act
|
||||
const act = () => new CategoryCollectionParseContext(functionsData, scripting);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('compiler', () => {
|
||||
it('constructed as expected', () => {
|
||||
// arrange
|
||||
const functionsData = [FunctionDataStub.createWithCode()];
|
||||
const functionsData = [createFunctionDataWithCode()];
|
||||
const syntax = new LanguageSyntaxStub();
|
||||
const expected = new ScriptCompiler(functionsData, syntax);
|
||||
const language = ScriptingLanguage.shellscript;
|
||||
|
||||
@@ -9,25 +9,13 @@ import { ExpressionEvaluationContextStub } from '@tests/unit/shared/Stubs/Expres
|
||||
import { IPipelineCompiler } from '@/application/Parser/Script/Compiler/Expressions/Pipes/IPipelineCompiler';
|
||||
import { PipelineCompilerStub } from '@tests/unit/shared/Stubs/PipelineCompilerStub';
|
||||
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
|
||||
import { getAbsentObjectTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('Expression', () => {
|
||||
describe('ctor', () => {
|
||||
describe('position', () => {
|
||||
describe('throws when missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing position';
|
||||
const position = absentValue;
|
||||
// act
|
||||
const act = () => new ExpressionBuilder()
|
||||
.withPosition(position)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
const expected = new ExpressionPosition(0, 5);
|
||||
@@ -52,7 +40,7 @@ describe('Expression', () => {
|
||||
expect(actual.parameters);
|
||||
expect(actual.parameters.all);
|
||||
expect(actual.parameters.all.length).to.equal(0);
|
||||
});
|
||||
}, { excludeNull: true });
|
||||
});
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
@@ -67,21 +55,6 @@ describe('Expression', () => {
|
||||
expect(actual.parameters).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
describe('evaluator', () => {
|
||||
describe('throws if missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing evaluator';
|
||||
const evaluator = absentValue;
|
||||
// act
|
||||
const act = () => new ExpressionBuilder()
|
||||
.withEvaluator(evaluator)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('evaluate', () => {
|
||||
describe('throws with invalid arguments', () => {
|
||||
@@ -91,11 +64,6 @@ describe('Expression', () => {
|
||||
expectedError: string,
|
||||
sutBuilder?: (builder: ExpressionBuilder) => ExpressionBuilder,
|
||||
}[] = [
|
||||
...getAbsentObjectTestCases().map((testCase) => ({
|
||||
name: `throws if arguments is ${testCase.valueName}`,
|
||||
context: testCase.absentValue,
|
||||
expectedError: 'missing context',
|
||||
})),
|
||||
{
|
||||
name: 'throws when some of the required args are not provided',
|
||||
sutBuilder: (i: ExpressionBuilder) => i.withParameterNames(['a', 'b', 'c'], false),
|
||||
@@ -159,7 +127,7 @@ describe('Expression', () => {
|
||||
const expected = new PipelineCompilerStub();
|
||||
const context = new ExpressionEvaluationContextStub()
|
||||
.withPipelineCompiler(expected);
|
||||
let actual: IPipelineCompiler;
|
||||
let actual: IPipelineCompiler | undefined;
|
||||
const evaluatorMock: ExpressionEvaluator = (c) => {
|
||||
actual = c.pipelineCompiler;
|
||||
return '';
|
||||
@@ -170,6 +138,7 @@ describe('Expression', () => {
|
||||
// arrange
|
||||
sut.evaluate(context);
|
||||
// assert
|
||||
expectExists(actual);
|
||||
expect(expected).to.equal(actual);
|
||||
});
|
||||
describe('filters unused parameters', () => {
|
||||
@@ -202,7 +171,7 @@ describe('Expression', () => {
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
let actual: IReadOnlyFunctionCallArgumentCollection;
|
||||
let actual: IReadOnlyFunctionCallArgumentCollection | undefined;
|
||||
const evaluatorMock: ExpressionEvaluator = (c) => {
|
||||
actual = c.args;
|
||||
return '';
|
||||
@@ -216,8 +185,10 @@ describe('Expression', () => {
|
||||
// act
|
||||
sut.evaluate(context);
|
||||
// assert
|
||||
const actualArguments = actual.getAllParameterNames()
|
||||
.map((name) => actual.getArgument(name));
|
||||
expectExists(actual);
|
||||
const collection = actual;
|
||||
const actualArguments = collection.getAllParameterNames()
|
||||
.map((name) => collection.getArgument(name));
|
||||
expect(actualArguments).to.deep.equal(testCase.expectedArguments);
|
||||
});
|
||||
}
|
||||
@@ -228,7 +199,7 @@ describe('Expression', () => {
|
||||
class ExpressionBuilder {
|
||||
private position: ExpressionPosition = new ExpressionPosition(0, 5);
|
||||
|
||||
private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
|
||||
private parameters?: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
|
||||
|
||||
public withPosition(position: ExpressionPosition) {
|
||||
this.position = position;
|
||||
@@ -240,7 +211,7 @@ class ExpressionBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withParameters(parameters: IReadOnlyFunctionParameterCollection) {
|
||||
public withParameters(parameters: IReadOnlyFunctionParameterCollection | undefined) {
|
||||
this.parameters = parameters;
|
||||
return this;
|
||||
}
|
||||
@@ -261,5 +232,5 @@ class ExpressionBuilder {
|
||||
return new Expression(this.position, this.evaluator, this.parameters);
|
||||
}
|
||||
|
||||
private evaluator: ExpressionEvaluator = () => '';
|
||||
private evaluator: ExpressionEvaluator = () => `[${ExpressionBuilder.name}] evaluated-expression`;
|
||||
}
|
||||
|
||||
@@ -4,24 +4,10 @@ import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Sc
|
||||
import { IPipelineCompiler } from '@/application/Parser/Script/Compiler/Expressions/Pipes/IPipelineCompiler';
|
||||
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
|
||||
import { PipelineCompilerStub } from '@tests/unit/shared/Stubs/PipelineCompilerStub';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('ExpressionEvaluationContext', () => {
|
||||
describe('ctor', () => {
|
||||
describe('args', () => {
|
||||
describe('throws if args is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing args, send empty collection instead.';
|
||||
const args = absentValue;
|
||||
// act
|
||||
const act = () => new ExpressionEvaluationContextBuilder()
|
||||
.withArgs(args)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
const expected = new FunctionCallArgumentCollectionStub()
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createPositionFromRegexFullMatch } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory';
|
||||
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
||||
|
||||
describe('ExpressionPositionFactory', () => {
|
||||
describe('createPositionFromRegexFullMatch', () => {
|
||||
it(`creates ${ExpressionPosition.name} instance`, () => {
|
||||
// arrange
|
||||
const expectedType = ExpressionPosition;
|
||||
const fakeMatch = createRegexMatch({
|
||||
fullMatch: 'matched string',
|
||||
matchIndex: 5,
|
||||
});
|
||||
// act
|
||||
const position = createPositionFromRegexFullMatch(fakeMatch);
|
||||
// assert
|
||||
expect(position).to.be.instanceOf(expectedType);
|
||||
});
|
||||
|
||||
it('creates a position with the correct start position', () => {
|
||||
// arrange
|
||||
const expectedStartPosition = 5;
|
||||
const fakeMatch = createRegexMatch({
|
||||
fullMatch: 'matched string',
|
||||
matchIndex: expectedStartPosition,
|
||||
});
|
||||
// act
|
||||
const position = createPositionFromRegexFullMatch(fakeMatch);
|
||||
// assert
|
||||
expect(position.start).toBe(expectedStartPosition);
|
||||
});
|
||||
|
||||
it('creates a position with the correct end position', () => {
|
||||
// arrange
|
||||
const startPosition = 3;
|
||||
const matchedString = 'matched string';
|
||||
const expectedEndPosition = startPosition + matchedString.length;
|
||||
const fakeMatch = createRegexMatch({
|
||||
fullMatch: matchedString,
|
||||
matchIndex: startPosition,
|
||||
});
|
||||
// act
|
||||
const position = createPositionFromRegexFullMatch(fakeMatch);
|
||||
// assert
|
||||
expect(position.end).to.equal(expectedEndPosition);
|
||||
});
|
||||
|
||||
it('creates correct position with capturing groups', () => {
|
||||
// arrange
|
||||
const startPosition = 20;
|
||||
const fakeMatch = createRegexMatch({
|
||||
fullMatch: 'matched string',
|
||||
capturingGroups: ['group1', 'group2'],
|
||||
matchIndex: startPosition,
|
||||
});
|
||||
// act
|
||||
const position = createPositionFromRegexFullMatch(fakeMatch);
|
||||
// assert
|
||||
expect(position.start).toBe(startPosition);
|
||||
expect(position.end).toBe(startPosition + fakeMatch[0].length);
|
||||
});
|
||||
|
||||
describe('invalid values', () => {
|
||||
it('throws an error if match.index is undefined', () => {
|
||||
// arrange
|
||||
const fakeMatch = createRegexMatch({
|
||||
fullMatch: 'matched string',
|
||||
matchIndex: undefined,
|
||||
});
|
||||
const expectedError = `Regex match did not yield any results: ${JSON.stringify(fakeMatch)}`;
|
||||
// act
|
||||
const act = () => createPositionFromRegexFullMatch(fakeMatch);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws an error for empty match', () => {
|
||||
// arrange
|
||||
const fakeMatch = createRegexMatch({
|
||||
fullMatch: '',
|
||||
matchIndex: 0,
|
||||
});
|
||||
const expectedError = `Regex match is empty: ${JSON.stringify(fakeMatch)}`;
|
||||
// act
|
||||
const act = () => createPositionFromRegexFullMatch(fakeMatch);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createRegexMatch(options?: {
|
||||
readonly fullMatch?: string,
|
||||
readonly capturingGroups?: readonly string[],
|
||||
readonly matchIndex?: number,
|
||||
}): RegExpMatchArray {
|
||||
const fullMatch = options?.fullMatch ?? 'fake match';
|
||||
const capturingGroups = options?.capturingGroups ?? [];
|
||||
const fakeMatch: RegExpMatchArray = [fullMatch, ...capturingGroups];
|
||||
fakeMatch.index = options?.matchIndex;
|
||||
return fakeMatch;
|
||||
}
|
||||
@@ -4,22 +4,23 @@ import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressi
|
||||
import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub';
|
||||
import { ExpressionParserStub } from '@tests/unit/shared/Stubs/ExpressionParserStub';
|
||||
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
|
||||
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { IExpression } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
|
||||
|
||||
describe('ExpressionsCompiler', () => {
|
||||
describe('compileExpressions', () => {
|
||||
describe('returns code when code is absent', () => {
|
||||
describe('returns empty string when code is absent', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const expected = absentValue;
|
||||
const expected = '';
|
||||
const code = absentValue;
|
||||
const sut = new SystemUnderTest();
|
||||
const args = new FunctionCallArgumentCollectionStub();
|
||||
// act
|
||||
const value = sut.compileExpressions(absentValue, args);
|
||||
const value = sut.compileExpressions(code, args);
|
||||
// assert
|
||||
expect(value).to.equal(expected);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
describe('can compile nested expressions', () => {
|
||||
it('when one expression is evaluated to a text that contains another expression', () => {
|
||||
@@ -189,19 +190,6 @@ describe('ExpressionsCompiler', () => {
|
||||
+ `Not equal: ${actualArgs.filter((arg) => arg !== expected)}`,
|
||||
);
|
||||
});
|
||||
describe('throws if arguments is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing args, send empty collection instead.';
|
||||
const args = absentValue;
|
||||
const expressionParserMock = new ExpressionParserStub();
|
||||
const sut = new SystemUnderTest(expressionParserMock);
|
||||
// act
|
||||
const act = () => sut.compileExpressions('code', args);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('throws when expressions are invalid', () => {
|
||||
describe('throws when expected argument is not provided but used in code', () => {
|
||||
|
||||
@@ -3,29 +3,20 @@ import { IExpression } from '@/application/Parser/Script/Compiler/Expressions/Ex
|
||||
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
|
||||
import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser';
|
||||
import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('CompositeExpressionParser', () => {
|
||||
describe('ctor', () => {
|
||||
it('throws if null parsers given', () => {
|
||||
// arrange
|
||||
const expectedError = 'missing leafs';
|
||||
const parsers = null;
|
||||
// act
|
||||
const act = () => new CompositeExpressionParser(parsers);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
describe('throws if one of the parsers is undefined', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
describe('throws when parsers are missing', () => {
|
||||
itEachAbsentCollectionValue<IExpressionParser>((absentCollection) => {
|
||||
// arrange
|
||||
const expectedError = 'missing leaf';
|
||||
const parsers: readonly IExpressionParser[] = [absentValue, mockParser()];
|
||||
const expectedError = 'missing leafs';
|
||||
const parsers = absentCollection;
|
||||
// act
|
||||
const act = () => new CompositeExpressionParser(parsers);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
});
|
||||
describe('findExpressions', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { IPrimitiveExpression, RegexParser } from '@/application/Parser/Script/C
|
||||
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
||||
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('RegexParser', () => {
|
||||
describe('findExpressions', () => {
|
||||
@@ -16,7 +17,7 @@ describe('RegexParser', () => {
|
||||
const act = () => sut.findExpressions(absentValue);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
it('throws when position is invalid', () => {
|
||||
// arrange
|
||||
@@ -30,17 +31,19 @@ describe('RegexParser', () => {
|
||||
];
|
||||
const sut = new RegexParserConcrete(regexMatchingEmpty);
|
||||
// act
|
||||
let error: string;
|
||||
let errorMessage: string | undefined;
|
||||
try {
|
||||
sut.findExpressions(code);
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
errorMessage = err.message;
|
||||
}
|
||||
// assert
|
||||
expectExists(errorMessage);
|
||||
const error = errorMessage; // workaround for ts(18048): possibly 'undefined'
|
||||
expect(
|
||||
expectedErrorParts.every((part) => error.includes(part)),
|
||||
`Expected parts: ${expectedErrorParts.join(', ')}`
|
||||
+ `Actual error: ${error}`,
|
||||
+ `Actual error: ${errorMessage}`,
|
||||
);
|
||||
});
|
||||
describe('matches regex as expected', () => {
|
||||
@@ -139,7 +142,7 @@ function mockBuilder(): (match: RegExpMatchArray) => IPrimitiveExpression {
|
||||
});
|
||||
}
|
||||
function getEvaluatorStub(): ExpressionEvaluator {
|
||||
return () => undefined;
|
||||
return () => `[${getEvaluatorStub.name}] evaluated code`;
|
||||
}
|
||||
|
||||
function mockPrimitiveExpression(): IPrimitiveExpression {
|
||||
|
||||
@@ -8,6 +8,12 @@ describe('EscapeDoubleQuotes', () => {
|
||||
const sut = new EscapeDoubleQuotes();
|
||||
// act
|
||||
runPipeTests(sut, [
|
||||
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
|
||||
.map((testCase) => ({
|
||||
name: 'returns as it is when if input is missing',
|
||||
input: testCase.absentValue,
|
||||
expectedOutput: testCase.absentValue,
|
||||
})),
|
||||
{
|
||||
name: 'using "',
|
||||
input: 'hello "world"',
|
||||
@@ -23,10 +29,5 @@ describe('EscapeDoubleQuotes', () => {
|
||||
input: '""hello world""',
|
||||
expectedOutput: '"^"""^""hello world"^"""^""',
|
||||
},
|
||||
...getAbsentStringTestCases().map((testCase) => ({
|
||||
name: 'returns as it is when if input is missing',
|
||||
input: testCase.absentValue,
|
||||
expectedOutput: testCase.absentValue,
|
||||
})),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe } from 'vitest';
|
||||
import { InlinePowerShell } from '@/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell';
|
||||
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { IPipeTestCase, runPipeTests } from './PipeTestRunner';
|
||||
|
||||
describe('InlinePowerShell', () => {
|
||||
@@ -7,11 +8,12 @@ describe('InlinePowerShell', () => {
|
||||
const sut = new InlinePowerShell();
|
||||
// act
|
||||
runPipeTests(sut, [
|
||||
{
|
||||
name: 'returns undefined when if input is undefined',
|
||||
input: undefined,
|
||||
expectedOutput: undefined,
|
||||
},
|
||||
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
|
||||
.map((testCase) => ({
|
||||
name: 'returns as it is when if input is missing',
|
||||
input: testCase.absentValue,
|
||||
expectedOutput: '',
|
||||
})),
|
||||
...prefixTests('newline', getNewLineCases()),
|
||||
...prefixTests('comment', getCommentCases()),
|
||||
...prefixTests('here-string', hereStringCases()),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { PipeFactory } from '@/application/Parser/Script/Compiler/Expressions/Pipes/PipeFactory';
|
||||
import { PipeStub } from '@tests/unit/shared/Stubs/PipeStub';
|
||||
import { getAbsentStringTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('PipeFactory', () => {
|
||||
describe('ctor', () => {
|
||||
@@ -19,26 +19,6 @@ describe('PipeFactory', () => {
|
||||
// expect
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
describe('throws when a pipe is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing pipe in list';
|
||||
const pipes = [new PipeStub(), absentValue];
|
||||
// act
|
||||
const act = () => new PipeFactory(pipes);
|
||||
// expect
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('throws when pipes are null', () => {
|
||||
// arrange
|
||||
const expectedError = 'missing pipes';
|
||||
const pipes = null;
|
||||
// act
|
||||
const act = () => new PipeFactory(pipes);
|
||||
// expect
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
describe('throws when name is invalid', () => {
|
||||
// act
|
||||
const act = (invalidName: string) => new PipeFactory([new PipeStub().withName(invalidName)]);
|
||||
@@ -82,11 +62,12 @@ describe('PipeFactory', () => {
|
||||
function testPipeNameValidation(testRunner: (invalidName: string) => void) {
|
||||
const testCases = [
|
||||
// Validate missing value
|
||||
...getAbsentStringTestCases().map((testCase) => ({
|
||||
name: `empty pipe name (${testCase.valueName})`,
|
||||
value: testCase.absentValue,
|
||||
expectedError: 'empty pipe name',
|
||||
})),
|
||||
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
|
||||
.map((testCase) => ({
|
||||
name: `empty pipe name (${testCase.valueName})`,
|
||||
value: testCase.absentValue,
|
||||
expectedError: 'empty pipe name',
|
||||
})),
|
||||
// Validate camelCase
|
||||
...[
|
||||
'PascalCase',
|
||||
|
||||
@@ -10,21 +10,23 @@ describe('PipelineCompiler', () => {
|
||||
describe('compile', () => {
|
||||
describe('throws for invalid arguments', () => {
|
||||
interface ITestCase {
|
||||
name: string;
|
||||
act: (test: PipelineTestRunner) => PipelineTestRunner;
|
||||
expectedError: string;
|
||||
readonly name: string;
|
||||
readonly act: (test: PipelineTestRunner) => PipelineTestRunner;
|
||||
readonly expectedError: string;
|
||||
}
|
||||
const testCases: ITestCase[] = [
|
||||
...getAbsentStringTestCases().map((testCase) => ({
|
||||
name: `"value" is ${testCase.valueName}`,
|
||||
act: (test) => test.withValue(testCase.absentValue),
|
||||
expectedError: 'missing value',
|
||||
})),
|
||||
...getAbsentStringTestCases().map((testCase) => ({
|
||||
name: `"pipeline" is ${testCase.valueName}`,
|
||||
act: (test) => test.withPipeline(testCase.absentValue),
|
||||
expectedError: 'missing pipeline',
|
||||
})),
|
||||
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
|
||||
.map((testCase) => ({
|
||||
name: `"value" is ${testCase.valueName}`,
|
||||
act: (test) => test.withValue(testCase.absentValue),
|
||||
expectedError: 'missing value',
|
||||
})),
|
||||
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
|
||||
.map((testCase) => ({
|
||||
name: `"pipeline" is ${testCase.valueName}`,
|
||||
act: (test) => test.withPipeline(testCase.absentValue),
|
||||
expectedError: 'missing pipeline',
|
||||
})),
|
||||
{
|
||||
name: '"pipeline" does not start with pipe',
|
||||
act: (test) => test.withPipeline('pipeline |'),
|
||||
|
||||
@@ -119,13 +119,14 @@ describe('WithParser', () => {
|
||||
describe('conditional rendering based on argument value', () => {
|
||||
describe('does not render scope', () => {
|
||||
runner.expectResults(
|
||||
...getAbsentStringTestCases().map((testCase) => ({
|
||||
name: `does not render when value is "${testCase.valueName}"`,
|
||||
code: '{{ with $parameter }}dark{{ end }} ',
|
||||
args: (args) => args
|
||||
.withArgument('parameter', testCase.absentValue),
|
||||
expected: [''],
|
||||
})),
|
||||
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
|
||||
.map((testCase) => ({
|
||||
name: `does not render when value is "${testCase.valueName}"`,
|
||||
code: '{{ with $parameter }}dark{{ end }} ',
|
||||
args: (args) => args
|
||||
.withArgument('parameter', testCase.absentValue),
|
||||
expected: [''],
|
||||
})),
|
||||
{
|
||||
name: 'does not render when argument is not provided',
|
||||
code: '{{ with $parameter }}dark{{ end }}',
|
||||
@@ -136,13 +137,14 @@ describe('WithParser', () => {
|
||||
});
|
||||
describe('renders scope', () => {
|
||||
runner.expectResults(
|
||||
...getAbsentStringTestCases().map((testCase) => ({
|
||||
name: `does not render when value is "${testCase.valueName}"`,
|
||||
code: '{{ with $parameter }}dark{{ end }} ',
|
||||
args: (args) => args
|
||||
.withArgument('parameter', testCase.absentValue),
|
||||
expected: [''],
|
||||
})),
|
||||
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
|
||||
.map((testCase) => ({
|
||||
name: `does not render when value is "${testCase.valueName}"`,
|
||||
code: '{{ with $parameter }}dark{{ end }} ',
|
||||
args: (args) => args
|
||||
.withArgument('parameter', testCase.absentValue),
|
||||
expected: [''],
|
||||
})),
|
||||
{
|
||||
name: 'does not render when argument is not provided',
|
||||
code: '{{ with $parameter }}dark{{ end }}',
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('FunctionCallArgument', () => {
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
||||
import { FunctionCallArgumentStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentStub';
|
||||
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('FunctionCallArgumentCollection', () => {
|
||||
describe('addArgument', () => {
|
||||
describe('throws if argument is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const errorMessage = 'missing argument';
|
||||
const arg = absentValue;
|
||||
const sut = new FunctionCallArgumentCollection();
|
||||
// act
|
||||
const act = () => sut.addArgument(arg);
|
||||
// assert
|
||||
expect(act).to.throw(errorMessage);
|
||||
});
|
||||
});
|
||||
it('throws if parameter value is already provided', () => {
|
||||
// arrange
|
||||
const duplicateParameterName = 'duplicateParam';
|
||||
@@ -70,7 +58,7 @@ describe('FunctionCallArgumentCollection', () => {
|
||||
const act = () => sut.getArgument(parameterName);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
it('throws if argument does not exist', () => {
|
||||
// arrange
|
||||
@@ -106,7 +94,7 @@ describe('FunctionCallArgumentCollection', () => {
|
||||
const act = () => sut.hasArgument(parameterName);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
describe('returns as expected', () => {
|
||||
// arrange
|
||||
|
||||
@@ -2,11 +2,12 @@ import { expect, describe, it } from 'vitest';
|
||||
import { NewlineCodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger';
|
||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||
import { getAbsentStringTestCases, itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
|
||||
describe('NewlineCodeSegmentMerger', () => {
|
||||
describe('mergeCodeParts', () => {
|
||||
describe('throws given empty segments', () => {
|
||||
itEachAbsentCollectionValue((absentValue) => {
|
||||
itEachAbsentCollectionValue<CompiledCode>((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing segments';
|
||||
const segments = absentValue;
|
||||
@@ -15,7 +16,7 @@ describe('NewlineCodeSegmentMerger', () => {
|
||||
const act = () => merger.mergeCodeParts(segments);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
describe('merges correctly', () => {
|
||||
const testCases: ReadonlyArray<{
|
||||
@@ -38,29 +39,31 @@ describe('NewlineCodeSegmentMerger', () => {
|
||||
revertCode: 'revert1\nrevert2\nrevert3',
|
||||
},
|
||||
},
|
||||
...getAbsentStringTestCases().map((absentTestCase) => ({
|
||||
description: `filter out ${absentTestCase.valueName} \`revertCode\``,
|
||||
segments: [
|
||||
new CompiledCodeStub().withCode('code1').withRevertCode('revert1'),
|
||||
new CompiledCodeStub().withCode('code2').withRevertCode(absentTestCase.absentValue),
|
||||
new CompiledCodeStub().withCode('code3').withRevertCode('revert3'),
|
||||
],
|
||||
expected: {
|
||||
code: 'code1\ncode2\ncode3',
|
||||
revertCode: 'revert1\nrevert3',
|
||||
},
|
||||
})),
|
||||
{
|
||||
description: 'given only `code` in segments',
|
||||
segments: [
|
||||
new CompiledCodeStub().withCode('code1').withRevertCode(''),
|
||||
new CompiledCodeStub().withCode('code2').withRevertCode(''),
|
||||
],
|
||||
expected: {
|
||||
code: 'code1\ncode2',
|
||||
revertCode: '',
|
||||
},
|
||||
},
|
||||
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
|
||||
.map((absentTestCase) => ({
|
||||
description: `filter out ${absentTestCase.valueName} \`revertCode\``,
|
||||
segments: [
|
||||
new CompiledCodeStub().withCode('code1').withRevertCode('revert1'),
|
||||
new CompiledCodeStub().withCode('code2').withRevertCode(absentTestCase.absentValue),
|
||||
new CompiledCodeStub().withCode('code3').withRevertCode('revert3'),
|
||||
],
|
||||
expected: {
|
||||
code: 'code1\ncode2\ncode3',
|
||||
revertCode: 'revert1\nrevert3',
|
||||
},
|
||||
})),
|
||||
...getAbsentStringTestCases({ excludeNull: true })
|
||||
.map((emptyRevertCode) => ({
|
||||
description: `given only \`code\` in segments with "${emptyRevertCode.valueName}" \`revertCode\``,
|
||||
segments: [
|
||||
new CompiledCodeStub().withCode('code1').withRevertCode(emptyRevertCode.absentValue),
|
||||
new CompiledCodeStub().withCode('code2').withRevertCode(emptyRevertCode.absentValue),
|
||||
],
|
||||
expected: {
|
||||
code: 'code1\ncode2',
|
||||
revertCode: '',
|
||||
},
|
||||
})),
|
||||
{
|
||||
description: 'given mix of segments with only `code` or `revertCode`',
|
||||
segments: [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { FunctionCallSequenceCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler';
|
||||
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
|
||||
import { CodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger';
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
@@ -13,6 +13,7 @@ import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompi
|
||||
import { CodeSegmentMergerStub } from '@tests/unit/shared/Stubs/CodeSegmentMergerStub';
|
||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('FunctionCallSequenceCompiler', () => {
|
||||
describe('instance', () => {
|
||||
@@ -25,7 +26,7 @@ describe('FunctionCallSequenceCompiler', () => {
|
||||
describe('parameter validation', () => {
|
||||
describe('calls', () => {
|
||||
describe('throws with missing call', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
itEachAbsentCollectionValue<FunctionCall>((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing calls';
|
||||
const calls = absentValue;
|
||||
@@ -35,38 +36,7 @@ describe('FunctionCallSequenceCompiler', () => {
|
||||
const act = () => builder.compileFunctionCalls();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('throws if call sequence has absent call', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing function call';
|
||||
const calls = [
|
||||
new FunctionCallStub(),
|
||||
absentValue,
|
||||
];
|
||||
const builder = new FunctionCallSequenceCompilerBuilder()
|
||||
.withCalls(calls);
|
||||
// act
|
||||
const act = () => builder.compileFunctionCalls();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('functions', () => {
|
||||
describe('throws with missing functions', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing functions';
|
||||
const functions = absentValue;
|
||||
const builder = new FunctionCallSequenceCompilerBuilder()
|
||||
.withFunctions(functions);
|
||||
// act
|
||||
const act = () => builder.compileFunctionCalls();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -84,7 +54,7 @@ describe('FunctionCallSequenceCompiler', () => {
|
||||
// assert
|
||||
expect(singleCallCompilerStub.callHistory).to.have.lengthOf(1);
|
||||
const calledMethod = singleCallCompilerStub.callHistory.find((m) => m.methodName === 'compileSingleCall');
|
||||
expect(calledMethod).toBeDefined();
|
||||
expectExists(calledMethod);
|
||||
expect(calledMethod.args[0]).to.equal(expectedCall);
|
||||
});
|
||||
it('with every call', () => {
|
||||
@@ -118,7 +88,7 @@ describe('FunctionCallSequenceCompiler', () => {
|
||||
// assert
|
||||
expect(singleCallCompilerStub.callHistory).to.have.lengthOf(1);
|
||||
const calledMethod = singleCallCompilerStub.callHistory.find((m) => m.methodName === 'compileSingleCall');
|
||||
expect(calledMethod).toBeDefined();
|
||||
expectExists(calledMethod);
|
||||
const actualFunctions = calledMethod.args[1].allFunctions;
|
||||
expect(actualFunctions).to.equal(expectedFunctions);
|
||||
});
|
||||
@@ -173,7 +143,9 @@ describe('FunctionCallSequenceCompiler', () => {
|
||||
// act
|
||||
builder.compileFunctionCalls();
|
||||
// assert
|
||||
const [actualSegments] = codeSegmentMergerStub.callHistory.find((c) => c.methodName === 'mergeCodeParts').args;
|
||||
const calledMethod = codeSegmentMergerStub.callHistory.find((c) => c.methodName === 'mergeCodeParts');
|
||||
expectExists(calledMethod);
|
||||
const [actualSegments] = calledMethod.args;
|
||||
expect(expectedFlattenedSegments).to.have.lengthOf(actualSegments.length);
|
||||
expect(expectedFlattenedSegments).to.have.deep.members(actualSegments);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { expect, describe, it } from 'vitest';
|
||||
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
||||
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { createSharedFunctionStubWithCalls, createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
||||
import { NestedFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler';
|
||||
import { ArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler';
|
||||
import { ArgumentCompilerStub } from '@tests/unit/shared/Stubs/ArgumentCompilerStub';
|
||||
@@ -10,14 +9,14 @@ import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompi
|
||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
|
||||
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
|
||||
|
||||
describe('NestedFunctionCallCompiler', () => {
|
||||
describe('canCompile', () => {
|
||||
it('returns `true` for code body function', () => {
|
||||
// arrange
|
||||
const expected = true;
|
||||
const func = new SharedFunctionStub(FunctionBodyType.Calls)
|
||||
const func = createSharedFunctionStubWithCalls()
|
||||
.withSomeCalls();
|
||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
||||
.build();
|
||||
@@ -29,7 +28,7 @@ describe('NestedFunctionCallCompiler', () => {
|
||||
it('returns `false` for non-code body function', () => {
|
||||
// arrange
|
||||
const expected = false;
|
||||
const func = new SharedFunctionStub(FunctionBodyType.Code);
|
||||
const func = createSharedFunctionStubWithCode();
|
||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
||||
.build();
|
||||
// act
|
||||
@@ -129,8 +128,8 @@ describe('NestedFunctionCallCompiler', () => {
|
||||
});
|
||||
it('flattens re-compiled functions', () => {
|
||||
// arrange
|
||||
const deepFunc1 = new SharedFunctionStub(FunctionBodyType.Code);
|
||||
const deepFunc2 = new SharedFunctionStub(FunctionBodyType.Code);
|
||||
const deepFunc1 = createSharedFunctionStubWithCode();
|
||||
const deepFunc2 = createSharedFunctionStubWithCalls();
|
||||
const callToDeepFunc1 = new FunctionCallStub().withFunctionName(deepFunc1.name);
|
||||
const callToDeepFunc2 = new FunctionCallStub().withFunctionName(deepFunc2.name);
|
||||
const singleCallCompilationScenario = new Map<FunctionCall, CompiledCode[]>([
|
||||
@@ -141,7 +140,7 @@ describe('NestedFunctionCallCompiler', () => {
|
||||
.withScenario({ givenNestedFunctionCall: callToDeepFunc1, result: callToDeepFunc1 })
|
||||
.withScenario({ givenNestedFunctionCall: callToDeepFunc2, result: callToDeepFunc2 });
|
||||
const expectedFlattenedCodes = [...singleCallCompilationScenario.values()].flat();
|
||||
const frontFunc = new SharedFunctionStub(FunctionBodyType.Calls)
|
||||
const frontFunc = createSharedFunctionStubWithCalls()
|
||||
.withCalls(callToDeepFunc1, callToDeepFunc2);
|
||||
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.name);
|
||||
const singleCallCompilerStub = new SingleCallCompilerStub()
|
||||
@@ -212,9 +211,9 @@ describe('NestedFunctionCallCompiler', () => {
|
||||
});
|
||||
|
||||
function createSingleFuncCallingAnotherFunc() {
|
||||
const deepFunc = new SharedFunctionStub(FunctionBodyType.Code);
|
||||
const deepFunc = createSharedFunctionStubWithCode();
|
||||
const callToDeepFunc = new FunctionCallStub().withFunctionName(deepFunc.name);
|
||||
const frontFunc = new SharedFunctionStub(FunctionBodyType.Calls).withCalls(callToDeepFunc);
|
||||
const frontFunc = createSharedFunctionStubWithCalls().withCalls(callToDeepFunc);
|
||||
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.name);
|
||||
return {
|
||||
deepFunc,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { expect, describe, it } from 'vitest';
|
||||
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
||||
import { createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
||||
import type { FunctionCallParametersData } from '@/application/collections/';
|
||||
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
|
||||
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
|
||||
@@ -70,7 +69,7 @@ describe('AdaptiveFunctionCallCompiler', () => {
|
||||
}) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const func = new SharedFunctionStub(FunctionBodyType.Code)
|
||||
const func = createSharedFunctionStubWithCode()
|
||||
.withName('test-function-name')
|
||||
.withParameterNames(...functionParameters);
|
||||
const params = callParameters
|
||||
@@ -137,7 +136,7 @@ describe('AdaptiveFunctionCallCompiler', () => {
|
||||
describe('strategy invocation', () => {
|
||||
it('passes correct function for compilation ability check', () => {
|
||||
// arrange
|
||||
const expectedFunction = new SharedFunctionStub(FunctionBodyType.Code);
|
||||
const expectedFunction = createSharedFunctionStubWithCode();
|
||||
const strategy = new SingleCallCompilerStrategyStub()
|
||||
.withCanCompileResult(true);
|
||||
const builder = new AdaptiveFunctionCallCompilerBuilder()
|
||||
@@ -157,7 +156,7 @@ describe('AdaptiveFunctionCallCompiler', () => {
|
||||
describe('compilation arguments', () => {
|
||||
it('uses correct function', () => {
|
||||
// arrange
|
||||
const expectedFunction = new SharedFunctionStub(FunctionBodyType.Code);
|
||||
const expectedFunction = createSharedFunctionStubWithCode();
|
||||
const strategy = new SingleCallCompilerStrategyStub()
|
||||
.withCanCompileResult(true);
|
||||
const builder = new AdaptiveFunctionCallCompilerBuilder()
|
||||
|
||||
@@ -7,12 +7,11 @@ import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expre
|
||||
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
|
||||
import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub';
|
||||
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
|
||||
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
|
||||
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
|
||||
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
|
||||
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
||||
import { createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
||||
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
||||
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
|
||||
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
|
||||
describe('NestedFunctionArgumentCompiler', () => {
|
||||
describe('createCompiledNestedCall', () => {
|
||||
@@ -132,7 +131,7 @@ describe('NestedFunctionArgumentCompiler', () => {
|
||||
givenArgs: parentCall.args,
|
||||
result: testParameterScenarios.find(
|
||||
(r) => r.rawArgumentValue === rawArgumentValue,
|
||||
).compiledArgumentValue,
|
||||
)?.compiledArgumentValue ?? 'unexpected arguments',
|
||||
});
|
||||
});
|
||||
const nestedCallArgs = new FunctionCallArgumentCollectionStub()
|
||||
@@ -166,7 +165,7 @@ describe('NestedFunctionArgumentCompiler', () => {
|
||||
// arrange
|
||||
const parameterName = 'requiredParameter';
|
||||
const initialValue = 'initial-value';
|
||||
const compiledValue = undefined;
|
||||
const emptyCompiledExpression = '';
|
||||
const expectedError = `Compilation resulted in empty value for required parameter: "${parameterName}"`;
|
||||
const nestedCall = new FunctionCallStub()
|
||||
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
|
||||
@@ -183,7 +182,7 @@ describe('NestedFunctionArgumentCompiler', () => {
|
||||
.setup({
|
||||
givenCode: initialValue,
|
||||
givenArgs: parentCall.args,
|
||||
result: compiledValue,
|
||||
result: emptyCompiledExpression,
|
||||
});
|
||||
const builder = new NestedFunctionArgumentCompilerBuilder()
|
||||
.withExpressionsCompiler(expressionsCompilerStub)
|
||||
@@ -199,7 +198,7 @@ describe('NestedFunctionArgumentCompiler', () => {
|
||||
// arrange
|
||||
const parameterName = 'optionalParameter';
|
||||
const initialValue = 'initial-value';
|
||||
const compiledValue = undefined;
|
||||
const emptyValue = '';
|
||||
const nestedCall = new FunctionCallStub()
|
||||
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
|
||||
.withArgument(parameterName, initialValue));
|
||||
@@ -215,7 +214,7 @@ describe('NestedFunctionArgumentCompiler', () => {
|
||||
.setup({
|
||||
givenCode: initialValue,
|
||||
givenArgs: parentCall.args,
|
||||
result: compiledValue,
|
||||
result: emptyValue,
|
||||
});
|
||||
const builder = new NestedFunctionArgumentCompilerBuilder()
|
||||
.withExpressionsCompiler(expressionsCompilerStub)
|
||||
@@ -240,7 +239,7 @@ function createContextWithParameter(options: {
|
||||
}): FunctionCallCompilationContext {
|
||||
const parameters = new FunctionParameterCollectionStub()
|
||||
.withParameterName(options.existingParameterName, options.isExistingParameterOptional);
|
||||
const func = new SharedFunctionStub(FunctionBodyType.Code)
|
||||
const func = createSharedFunctionStubWithCode()
|
||||
.withName(options.existingFunctionName)
|
||||
.withParameters(parameters);
|
||||
const functions = new SharedFunctionCollectionStub()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { expect, describe, it } from 'vitest';
|
||||
import { InlineFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler';
|
||||
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
||||
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { createSharedFunctionStubWithCode, createSharedFunctionStubWithCalls } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
||||
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
|
||||
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
|
||||
@@ -12,7 +11,7 @@ describe('InlineFunctionCallCompiler', () => {
|
||||
it('returns `true` if function has code body', () => {
|
||||
// arrange
|
||||
const expected = true;
|
||||
const func = new SharedFunctionStub(FunctionBodyType.Code);
|
||||
const func = createSharedFunctionStubWithCode();
|
||||
const compiler = new InlineFunctionCallCompilerBuilder()
|
||||
.build();
|
||||
// act
|
||||
@@ -23,7 +22,7 @@ describe('InlineFunctionCallCompiler', () => {
|
||||
it('returns `false` if function does not have code body', () => {
|
||||
// arrange
|
||||
const expected = false;
|
||||
const func = new SharedFunctionStub(FunctionBodyType.Calls);
|
||||
const func = createSharedFunctionStubWithCalls();
|
||||
const compiler = new InlineFunctionCallCompilerBuilder()
|
||||
.build();
|
||||
// act
|
||||
@@ -33,6 +32,19 @@ describe('InlineFunctionCallCompiler', () => {
|
||||
});
|
||||
});
|
||||
describe('compile', () => {
|
||||
it('throws if function body is not code', () => {
|
||||
// arrange
|
||||
const expectedError = 'Unexpected function body type.';
|
||||
const compiler = new InlineFunctionCallCompilerBuilder()
|
||||
.build();
|
||||
// act
|
||||
const act = () => compiler.compileFunction(
|
||||
createSharedFunctionStubWithCalls(),
|
||||
new FunctionCallStub(),
|
||||
);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('compiles expressions with correct arguments', () => {
|
||||
// arrange
|
||||
const expressionsCompilerStub = new ExpressionsCompilerStub();
|
||||
@@ -42,7 +54,7 @@ describe('InlineFunctionCallCompiler', () => {
|
||||
.build();
|
||||
// act
|
||||
compiler.compileFunction(
|
||||
new SharedFunctionStub(FunctionBodyType.Code),
|
||||
createSharedFunctionStubWithCode(),
|
||||
new FunctionCallStub()
|
||||
.withArgumentCollection(expectedArgs),
|
||||
);
|
||||
@@ -50,49 +62,71 @@ describe('InlineFunctionCallCompiler', () => {
|
||||
const actualArgs = expressionsCompilerStub.callHistory.map((call) => call.args[1]);
|
||||
expect(actualArgs.every((arg) => arg === expectedArgs));
|
||||
});
|
||||
it('creates compiled code with compiled `execute`', () => {
|
||||
// arrange
|
||||
const func = new SharedFunctionStub(FunctionBodyType.Code);
|
||||
const args = new FunctionCallArgumentCollectionStub();
|
||||
const expectedCode = 'expected-code';
|
||||
const expressionsCompilerStub = new ExpressionsCompilerStub()
|
||||
.setup({
|
||||
givenCode: func.body.code.execute,
|
||||
givenArgs: args,
|
||||
result: expectedCode,
|
||||
});
|
||||
const compiler = new InlineFunctionCallCompilerBuilder()
|
||||
.withExpressionsCompiler(expressionsCompilerStub)
|
||||
.build();
|
||||
// act
|
||||
const compiledCodes = compiler
|
||||
.compileFunction(func, new FunctionCallStub().withArgumentCollection(args));
|
||||
// assert
|
||||
expect(compiledCodes).to.have.lengthOf(1);
|
||||
const actualCode = compiledCodes[0].code;
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
describe('execute', () => {
|
||||
it('creates compiled code with compiled `execute`', () => {
|
||||
// arrange
|
||||
const func = createSharedFunctionStubWithCode();
|
||||
const args = new FunctionCallArgumentCollectionStub();
|
||||
const expectedCode = 'expected-code';
|
||||
const expressionsCompilerStub = new ExpressionsCompilerStub()
|
||||
.setup({
|
||||
givenCode: func.body.code.execute,
|
||||
givenArgs: args,
|
||||
result: expectedCode,
|
||||
});
|
||||
const compiler = new InlineFunctionCallCompilerBuilder()
|
||||
.withExpressionsCompiler(expressionsCompilerStub)
|
||||
.build();
|
||||
// act
|
||||
const compiledCodes = compiler
|
||||
.compileFunction(func, new FunctionCallStub().withArgumentCollection(args));
|
||||
// assert
|
||||
expect(compiledCodes).to.have.lengthOf(1);
|
||||
const actualCode = compiledCodes[0].code;
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
});
|
||||
});
|
||||
it('creates compiled revert code with compiled `revert`', () => {
|
||||
// arrange
|
||||
const func = new SharedFunctionStub(FunctionBodyType.Code);
|
||||
const args = new FunctionCallArgumentCollectionStub();
|
||||
const expectedRevertCode = 'expected-revert-code';
|
||||
const expressionsCompilerStub = new ExpressionsCompilerStub()
|
||||
.setup({
|
||||
givenCode: func.body.code.revert,
|
||||
givenArgs: args,
|
||||
result: expectedRevertCode,
|
||||
});
|
||||
const compiler = new InlineFunctionCallCompilerBuilder()
|
||||
.withExpressionsCompiler(expressionsCompilerStub)
|
||||
.build();
|
||||
// act
|
||||
const compiledCodes = compiler
|
||||
.compileFunction(func, new FunctionCallStub().withArgumentCollection(args));
|
||||
// assert
|
||||
expect(compiledCodes).to.have.lengthOf(1);
|
||||
const actualRevertCode = compiledCodes[0].revertCode;
|
||||
expect(actualRevertCode).to.equal(expectedRevertCode);
|
||||
describe('revert', () => {
|
||||
it('compiles to `undefined` when given `undefined`', () => {
|
||||
// arrange
|
||||
const expected = undefined;
|
||||
const revertCode = undefined;
|
||||
const func = createSharedFunctionStubWithCode()
|
||||
.withRevertCode(revertCode);
|
||||
const compiler = new InlineFunctionCallCompilerBuilder()
|
||||
.build();
|
||||
// act
|
||||
const compiledCodes = compiler
|
||||
.compileFunction(func, new FunctionCallStub());
|
||||
// assert
|
||||
expect(compiledCodes).to.have.lengthOf(1);
|
||||
const actualRevertCode = compiledCodes[0].revertCode;
|
||||
expect(actualRevertCode).to.equal(expected);
|
||||
});
|
||||
it('creates compiled revert code with compiled `revert`', () => {
|
||||
// arrange
|
||||
const revertCode = 'revert-code-input';
|
||||
const func = createSharedFunctionStubWithCode()
|
||||
.withRevertCode(revertCode);
|
||||
const args = new FunctionCallArgumentCollectionStub();
|
||||
const expectedRevertCode = 'expected-revert-code';
|
||||
const expressionsCompilerStub = new ExpressionsCompilerStub()
|
||||
.setup({
|
||||
givenCode: revertCode,
|
||||
givenArgs: args,
|
||||
result: expectedRevertCode,
|
||||
});
|
||||
const compiler = new InlineFunctionCallCompilerBuilder()
|
||||
.withExpressionsCompiler(expressionsCompilerStub)
|
||||
.build();
|
||||
// act
|
||||
const compiledCodes = compiler
|
||||
.compileFunction(func, new FunctionCallStub().withArgumentCollection(args));
|
||||
// assert
|
||||
expect(compiledCodes).to.have.lengthOf(1);
|
||||
const actualRevertCode = compiledCodes[0].revertCode;
|
||||
expect(actualRevertCode).to.equal(expectedRevertCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseFunctionCalls } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCallParser';
|
||||
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
||||
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('FunctionCallParser', () => {
|
||||
describe('parseFunctionCalls', () => {
|
||||
describe('throws with missing call data', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing call data';
|
||||
const call = absentValue;
|
||||
// act
|
||||
const act = () => parseFunctionCalls(call);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('throws if call is not an object', () => {
|
||||
// arrange
|
||||
const expectedError = 'called function(s) must be an object';
|
||||
@@ -27,20 +16,6 @@ describe('FunctionCallParser', () => {
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('throws if call sequence has undefined call', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing call data';
|
||||
const data = [
|
||||
new FunctionCallDataStub(),
|
||||
absentValue,
|
||||
];
|
||||
// act
|
||||
const act = () => parseFunctionCalls(data);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('throws if call sequence has undefined function name', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
@@ -53,7 +28,7 @@ describe('FunctionCallParser', () => {
|
||||
const act = () => parseFunctionCalls(data);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
it('parses single call as expected', () => {
|
||||
// arrange
|
||||
|
||||
@@ -2,24 +2,11 @@ import { describe, it, expect } from 'vitest';
|
||||
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
|
||||
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('ParsedFunctionCall', () => {
|
||||
describe('ctor', () => {
|
||||
describe('args', () => {
|
||||
describe('throws when args is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing args';
|
||||
const args = absentValue;
|
||||
// act
|
||||
const act = () => new FunctionCallBuilder()
|
||||
.withArgs(args)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('sets args as expected', () => {
|
||||
// arrange
|
||||
const expected = new FunctionCallArgumentCollectionStub()
|
||||
@@ -44,7 +31,7 @@ describe('ParsedFunctionCall', () => {
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
it('sets function name as expected', () => {
|
||||
// arrange
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
CallFunctionBody, CodeFunctionBody, FunctionBodyType, SharedFunctionBody,
|
||||
} from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
|
||||
export function expectCodeFunctionBody(
|
||||
body: SharedFunctionBody,
|
||||
): asserts body is CodeFunctionBody {
|
||||
expectBodyType(body, FunctionBodyType.Code);
|
||||
}
|
||||
|
||||
export function expectCallsFunctionBody(
|
||||
body: SharedFunctionBody,
|
||||
): asserts body is CallFunctionBody {
|
||||
expectBodyType(body, FunctionBodyType.Calls);
|
||||
}
|
||||
|
||||
function expectBodyType(body: SharedFunctionBody, expectedType: FunctionBodyType) {
|
||||
const actualType = body.type;
|
||||
expect(actualType).to.equal(
|
||||
expectedType,
|
||||
[
|
||||
'\n---',
|
||||
`Actual: ${FunctionBodyType[actualType]}`,
|
||||
`Expected: ${FunctionBodyType[expectedType]}`,
|
||||
`Body: ${JSON.stringify(body)}`,
|
||||
'---\n\n',
|
||||
].join('\n'),
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('FunctionParameterCollection', () => {
|
||||
it('all returns added parameters as expected', () => {
|
||||
@@ -33,18 +32,4 @@ describe('FunctionParameterCollection', () => {
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
describe('addParameter', () => {
|
||||
describe('throws if parameter is undefined', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing parameter';
|
||||
const value = absentValue;
|
||||
const sut = new FunctionParameterCollection();
|
||||
// act
|
||||
const act = () => sut.addParameter(value);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,12 @@ import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/Functi
|
||||
import { createCallerFunction, createFunctionWithInlineCode } from '@/application/Parser/Script/Compiler/Function/SharedFunction';
|
||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
|
||||
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { CallFunctionBody, FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import {
|
||||
getAbsentStringTestCases, itEachAbsentCollectionValue, itEachAbsentObjectValue,
|
||||
getAbsentStringTestCases, itEachAbsentCollectionValue,
|
||||
itEachAbsentStringValue,
|
||||
} from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
|
||||
|
||||
describe('SharedFunction', () => {
|
||||
describe('SharedFunction', () => {
|
||||
@@ -34,7 +35,7 @@ describe('SharedFunction', () => {
|
||||
const act = () => build(builder);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -51,19 +52,6 @@ describe('SharedFunction', () => {
|
||||
// assert
|
||||
expect(sut.parameters).equal(expected);
|
||||
});
|
||||
describe('throws if missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing parameters';
|
||||
const parameters = absentValue;
|
||||
const builder = new SharedFunctionBuilder()
|
||||
.withParameters(parameters);
|
||||
// act
|
||||
const act = () => build(builder);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -77,6 +65,7 @@ describe('SharedFunction', () => {
|
||||
.withCode(expected)
|
||||
.createFunctionWithInlineCode();
|
||||
// assert
|
||||
expectCodeFunctionBody(sut.body);
|
||||
expect(sut.body.code.execute).equal(expected);
|
||||
});
|
||||
describe('throws if absent', () => {
|
||||
@@ -92,23 +81,26 @@ describe('SharedFunction', () => {
|
||||
.createFunctionWithInlineCode();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
});
|
||||
describe('revertCode', () => {
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
const testData = [
|
||||
const revertCodeTestValues: readonly (string | undefined)[] = [
|
||||
'expected-revert-code',
|
||||
...getAbsentStringTestCases().map((testCase) => testCase.absentValue),
|
||||
...getAbsentStringTestCases({
|
||||
excludeNull: true,
|
||||
}).map((testCase) => testCase.absentValue),
|
||||
];
|
||||
for (const data of testData) {
|
||||
for (const revertCode of revertCodeTestValues) {
|
||||
// act
|
||||
const sut = new SharedFunctionBuilder()
|
||||
.withRevertCode(data)
|
||||
.withRevertCode(revertCode)
|
||||
.createFunctionWithInlineCode();
|
||||
// assert
|
||||
expect(sut.body.code.revert).equal(data);
|
||||
expectCodeFunctionBody(sut.body);
|
||||
expect(sut.body.code.revert).equal(revertCode);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -128,7 +120,7 @@ describe('SharedFunction', () => {
|
||||
const sut = new SharedFunctionBuilder()
|
||||
.createFunctionWithInlineCode();
|
||||
// assert
|
||||
expect(sut.body.calls).equal(expectedCalls);
|
||||
expect((sut.body as CallFunctionBody).calls).equal(expectedCalls);
|
||||
});
|
||||
});
|
||||
describe('createCallerFunction', () => {
|
||||
@@ -144,10 +136,11 @@ describe('SharedFunction', () => {
|
||||
.withRootCallSequence(expected)
|
||||
.createCallerFunction();
|
||||
// assert
|
||||
expectCallsFunctionBody(sut.body);
|
||||
expect(sut.body.calls).equal(expected);
|
||||
});
|
||||
describe('throws if missing', () => {
|
||||
itEachAbsentCollectionValue((absentValue) => {
|
||||
itEachAbsentCollectionValue<FunctionCall>((absentValue) => {
|
||||
// arrange
|
||||
const functionName = 'invalidFunction';
|
||||
const rootCallSequence = absentValue;
|
||||
@@ -159,7 +152,7 @@ describe('SharedFunction', () => {
|
||||
.createCallerFunction();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
});
|
||||
it('sets type as expected', () => {
|
||||
@@ -171,15 +164,6 @@ describe('SharedFunction', () => {
|
||||
// assert
|
||||
expect(sut.body.type).equal(expectedType);
|
||||
});
|
||||
it('code is undefined', () => {
|
||||
// arrange
|
||||
const expectedCode = undefined;
|
||||
// act
|
||||
const sut = new SharedFunctionBuilder()
|
||||
.createCallerFunction();
|
||||
// assert
|
||||
expect(sut.body.code).equal(expectedCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,9 +192,9 @@ class SharedFunctionBuilder {
|
||||
|
||||
private callSequence: readonly FunctionCall[] = [new FunctionCallStub()];
|
||||
|
||||
private code = 'code';
|
||||
private code = `[${SharedFunctionBuilder.name}] code`;
|
||||
|
||||
private revertCode = 'revert-code';
|
||||
private revertCode: string | undefined = `[${SharedFunctionBuilder.name}] revert-code`;
|
||||
|
||||
public createCallerFunction(): ISharedFunction {
|
||||
return createCallerFunction(
|
||||
@@ -244,7 +228,7 @@ class SharedFunctionBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withRevertCode(revertCode: string) {
|
||||
public withRevertCode(revertCode: string | undefined) {
|
||||
this.revertCode = revertCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,16 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/SharedFunctionCollection';
|
||||
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
||||
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { createSharedFunctionStubWithCode, createSharedFunctionStubWithCalls } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
||||
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
|
||||
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('SharedFunctionCollection', () => {
|
||||
describe('addFunction', () => {
|
||||
describe('throws if function is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing function';
|
||||
const func = absentValue;
|
||||
const sut = new SharedFunctionCollection();
|
||||
// act
|
||||
const act = () => sut.addFunction(func);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('throws if function with same name already exists', () => {
|
||||
// arrange
|
||||
const functionName = 'duplicate-function';
|
||||
const expectedError = `function with name ${functionName} already exists`;
|
||||
const func = new SharedFunctionStub(FunctionBodyType.Code)
|
||||
const func = createSharedFunctionStubWithCode()
|
||||
.withName('duplicate-function');
|
||||
const sut = new SharedFunctionCollection();
|
||||
sut.addFunction(func);
|
||||
@@ -43,13 +30,13 @@ describe('SharedFunctionCollection', () => {
|
||||
const act = () => sut.getFunctionByName(absentValue);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
it('throws if function does not exist', () => {
|
||||
// arrange
|
||||
const name = 'unique-name';
|
||||
const expectedError = `called function is not defined "${name}"`;
|
||||
const func = new SharedFunctionStub(FunctionBodyType.Code)
|
||||
const func = createSharedFunctionStubWithCode()
|
||||
.withName('unexpected-name');
|
||||
const sut = new SharedFunctionCollection();
|
||||
sut.addFunction(func);
|
||||
@@ -61,7 +48,7 @@ describe('SharedFunctionCollection', () => {
|
||||
describe('returns existing function', () => {
|
||||
it('when function with inline code is added', () => {
|
||||
// arrange
|
||||
const expected = new SharedFunctionStub(FunctionBodyType.Code)
|
||||
const expected = createSharedFunctionStubWithCode()
|
||||
.withName('expected-function-name');
|
||||
const sut = new SharedFunctionCollection();
|
||||
// act
|
||||
@@ -72,9 +59,9 @@ describe('SharedFunctionCollection', () => {
|
||||
});
|
||||
it('when calling function is added', () => {
|
||||
// arrange
|
||||
const callee = new SharedFunctionStub(FunctionBodyType.Code)
|
||||
const callee = createSharedFunctionStubWithCode()
|
||||
.withName('calleeFunction');
|
||||
const caller = new SharedFunctionStub(FunctionBodyType.Calls)
|
||||
const caller = createSharedFunctionStubWithCalls()
|
||||
.withName('callerFunction')
|
||||
.withCalls(new FunctionCallStub().withFunctionName(callee.name));
|
||||
const sut = new SharedFunctionCollection();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { FunctionData } from '@/application/collections/';
|
||||
import type { FunctionData, CodeInstruction } from '@/application/collections/';
|
||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { SharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/SharedFunctionsParser';
|
||||
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
import { createFunctionDataWithCode, createFunctionDataWithoutCallOrCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterDefinitionDataStub';
|
||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
||||
import { itEachAbsentCollectionValue, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
|
||||
@@ -14,6 +14,8 @@ import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
|
||||
|
||||
describe('SharedFunctionsParser', () => {
|
||||
describe('instance', () => {
|
||||
@@ -23,41 +25,14 @@ describe('SharedFunctionsParser', () => {
|
||||
});
|
||||
});
|
||||
describe('parseFunctions', () => {
|
||||
describe('throws if syntax is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing syntax';
|
||||
const syntax = absentValue;
|
||||
// act
|
||||
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||
.withSyntax(syntax)
|
||||
.parseFunctions();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('validates functions', () => {
|
||||
describe('throws if one of the functions is undefined', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'some functions are undefined';
|
||||
const functions = [FunctionDataStub.createWithCode(), absentValue];
|
||||
const sut = new ParseFunctionsCallerWithDefaults();
|
||||
// act
|
||||
const act = () => sut
|
||||
.withFunctions(functions)
|
||||
.parseFunctions();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('throws when functions have same names', () => {
|
||||
// arrange
|
||||
const name = 'same-func-name';
|
||||
const expectedError = `duplicate function name: "${name}"`;
|
||||
const functions = [
|
||||
FunctionDataStub.createWithCode().withName(name),
|
||||
FunctionDataStub.createWithCode().withName(name),
|
||||
createFunctionDataWithCode().withName(name),
|
||||
createFunctionDataWithCode().withName(name),
|
||||
];
|
||||
// act
|
||||
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||
@@ -72,8 +47,8 @@ describe('SharedFunctionsParser', () => {
|
||||
const code = 'duplicate-code';
|
||||
const expectedError = `duplicate "code" in functions: "${code}"`;
|
||||
const functions = [
|
||||
FunctionDataStub.createWithoutCallOrCodes().withName('func-1').withCode(code),
|
||||
FunctionDataStub.createWithoutCallOrCodes().withName('func-2').withCode(code),
|
||||
createFunctionDataWithoutCallOrCode().withName('func-1').withCode(code),
|
||||
createFunctionDataWithoutCallOrCode().withName('func-2').withCode(code),
|
||||
];
|
||||
// act
|
||||
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||
@@ -87,9 +62,9 @@ describe('SharedFunctionsParser', () => {
|
||||
const revertCode = 'duplicate-revert-code';
|
||||
const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`;
|
||||
const functions = [
|
||||
FunctionDataStub.createWithoutCallOrCodes()
|
||||
createFunctionDataWithoutCallOrCode()
|
||||
.withName('func-1').withCode('code-1').withRevertCode(revertCode),
|
||||
FunctionDataStub.createWithoutCallOrCodes()
|
||||
createFunctionDataWithoutCallOrCode()
|
||||
.withName('func-2').withCode('code-2').withRevertCode(revertCode),
|
||||
];
|
||||
// act
|
||||
@@ -105,7 +80,7 @@ describe('SharedFunctionsParser', () => {
|
||||
// arrange
|
||||
const functionName = 'invalid-function';
|
||||
const expectedError = `both "code" and "call" are defined in "${functionName}"`;
|
||||
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
|
||||
const invalidFunction = createFunctionDataWithoutCallOrCode()
|
||||
.withName(functionName)
|
||||
.withCode('code')
|
||||
.withMockCall();
|
||||
@@ -120,7 +95,7 @@ describe('SharedFunctionsParser', () => {
|
||||
// arrange
|
||||
const functionName = 'invalid-function';
|
||||
const expectedError = `neither "code" or "call" is defined in "${functionName}"`;
|
||||
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
|
||||
const invalidFunction = createFunctionDataWithoutCallOrCode()
|
||||
.withName(functionName);
|
||||
// act
|
||||
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||
@@ -144,8 +119,7 @@ describe('SharedFunctionsParser', () => {
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.state, () => {
|
||||
// arrange
|
||||
const func = FunctionDataStub
|
||||
.createWithCall()
|
||||
const func = createFunctionDataWithCode()
|
||||
.withParametersObject(testCase.invalidType as never);
|
||||
const expectedError = `parameters must be an array of objects in function(s) "${func.name}"`;
|
||||
// act
|
||||
@@ -160,8 +134,7 @@ describe('SharedFunctionsParser', () => {
|
||||
it('validates function code as expected when code is defined', () => {
|
||||
// arrange
|
||||
const expectedRules = [NoEmptyLines, NoDuplicatedLines];
|
||||
const functionData = FunctionDataStub
|
||||
.createWithCode()
|
||||
const functionData = createFunctionDataWithCode()
|
||||
.withCode('expected code to be validated')
|
||||
.withRevertCode('expected revert code to be validated');
|
||||
const validator = new CodeValidatorStub();
|
||||
@@ -180,13 +153,11 @@ describe('SharedFunctionsParser', () => {
|
||||
// arrange
|
||||
const invalidParameterName = 'invalid function p@r4meter name';
|
||||
const functionName = 'functionName';
|
||||
let parameterException: Error;
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new FunctionParameter(invalidParameterName, false);
|
||||
} catch (e) { parameterException = e; }
|
||||
const expectedError = `"${functionName}": ${parameterException.message}`;
|
||||
const functionData = FunctionDataStub.createWithCode()
|
||||
const message = collectExceptionMessage(
|
||||
() => new FunctionParameter(invalidParameterName, false),
|
||||
);
|
||||
const expectedError = `"${functionName}": ${message}`;
|
||||
const functionData = createFunctionDataWithCode()
|
||||
.withName(functionName)
|
||||
.withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName));
|
||||
|
||||
@@ -200,21 +171,20 @@ describe('SharedFunctionsParser', () => {
|
||||
});
|
||||
});
|
||||
describe('given empty functions, returns empty collection', () => {
|
||||
itEachAbsentCollectionValue((absentValue) => {
|
||||
itEachAbsentCollectionValue<FunctionData>((absentValue) => {
|
||||
// act
|
||||
const actual = new ParseFunctionsCallerWithDefaults()
|
||||
.withFunctions(absentValue)
|
||||
.parseFunctions();
|
||||
// assert
|
||||
expect(actual).to.not.equal(undefined);
|
||||
});
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
describe('function with inline code', () => {
|
||||
it('parses single function with code as expected', () => {
|
||||
// arrange
|
||||
const name = 'function-name';
|
||||
const expected = FunctionDataStub
|
||||
.createWithoutCallOrCodes()
|
||||
const expected = createFunctionDataWithoutCallOrCode()
|
||||
.withName(name)
|
||||
.withCode('expected-code')
|
||||
.withRevertCode('expected-revert-code')
|
||||
@@ -239,7 +209,7 @@ describe('SharedFunctionsParser', () => {
|
||||
const call = new FunctionCallDataStub()
|
||||
.withName('calleeFunction')
|
||||
.withParameters({ test: 'value' });
|
||||
const data = FunctionDataStub.createWithoutCallOrCodes()
|
||||
const data = createFunctionDataWithoutCallOrCode()
|
||||
.withName('caller-function')
|
||||
.withCall(call);
|
||||
// act
|
||||
@@ -260,10 +230,10 @@ describe('SharedFunctionsParser', () => {
|
||||
const call2 = new FunctionCallDataStub()
|
||||
.withName('calleeFunction2')
|
||||
.withParameters({ param2: 'value2' });
|
||||
const caller1 = FunctionDataStub.createWithoutCallOrCodes()
|
||||
const caller1 = createFunctionDataWithoutCallOrCode()
|
||||
.withName('caller-function')
|
||||
.withCall(call1);
|
||||
const caller2 = FunctionDataStub.createWithoutCallOrCodes()
|
||||
const caller2 = createFunctionDataWithoutCallOrCode()
|
||||
.withName('caller-function-2')
|
||||
.withCall([call1, call2]);
|
||||
// act
|
||||
@@ -289,7 +259,7 @@ class ParseFunctionsCallerWithDefaults {
|
||||
|
||||
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
||||
|
||||
private functions: readonly FunctionData[] = [FunctionDataStub.createWithCode()];
|
||||
private functions: readonly FunctionData[] = [createFunctionDataWithCode()];
|
||||
|
||||
public withSyntax(syntax: ILanguageSyntax) {
|
||||
this.syntax = syntax;
|
||||
@@ -312,11 +282,11 @@ class ParseFunctionsCallerWithDefaults {
|
||||
}
|
||||
}
|
||||
|
||||
function expectEqualName(expected: FunctionDataStub, actual: ISharedFunction): void {
|
||||
function expectEqualName(expected: FunctionData, actual: ISharedFunction): void {
|
||||
expect(actual.name).to.equal(expected.name);
|
||||
}
|
||||
|
||||
function expectEqualParameters(expected: FunctionDataStub, actual: ISharedFunction): void {
|
||||
function expectEqualParameters(expected: FunctionData, actual: ISharedFunction): void {
|
||||
const actualSimplifiedParameters = actual.parameters.all.map((parameter) => ({
|
||||
name: parameter.name,
|
||||
optional: parameter.isOptional,
|
||||
@@ -329,10 +299,11 @@ function expectEqualParameters(expected: FunctionDataStub, actual: ISharedFuncti
|
||||
}
|
||||
|
||||
function expectEqualFunctionWithInlineCode(
|
||||
expected: FunctionData,
|
||||
expected: CodeInstruction,
|
||||
actual: ISharedFunction,
|
||||
): void {
|
||||
expect(actual.body, `function "${actual.name}" has no body`);
|
||||
expectCodeFunctionBody(actual.body);
|
||||
expect(actual.body.code, `function "${actual.name}" has no code`);
|
||||
expect(actual.body.code.execute).to.equal(expected.code);
|
||||
expect(actual.body.code.revert).to.equal(expected.revertCode);
|
||||
@@ -343,6 +314,7 @@ function expectEqualCalls(
|
||||
actual: ISharedFunction,
|
||||
) {
|
||||
expect(actual.body, `function "${actual.name}" has no body`);
|
||||
expectCallsFunctionBody(actual.body);
|
||||
expect(actual.body.calls, `function "${actual.name}" has no calls`);
|
||||
const actualSimplifiedCalls = actual.body.calls
|
||||
.map((call) => ({
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
export function testParameterName(action: (parameterName: string) => string) {
|
||||
describe('name', () => {
|
||||
describe('sets as expected', () => {
|
||||
// arrange
|
||||
const expectedValues = [
|
||||
const expectedValues: readonly string[] = [
|
||||
'lowercase',
|
||||
'onlyLetters',
|
||||
'l3tt3rsW1thNumb3rs',
|
||||
@@ -21,29 +20,33 @@ export function testParameterName(action: (parameterName: string) => string) {
|
||||
});
|
||||
describe('throws if invalid', () => {
|
||||
// arrange
|
||||
const testCases = [
|
||||
...getAbsentStringTestCases().map((test) => ({
|
||||
name: test.valueName,
|
||||
value: test.absentValue,
|
||||
expectedError: 'missing parameter name',
|
||||
})),
|
||||
const testScenarios: readonly {
|
||||
readonly description: string;
|
||||
readonly value: string;
|
||||
readonly expectedError: string;
|
||||
}[] = [
|
||||
{
|
||||
name: 'has @',
|
||||
description: 'empty Name',
|
||||
value: '',
|
||||
expectedError: 'missing parameter name',
|
||||
},
|
||||
{
|
||||
description: 'has @',
|
||||
value: 'b@d',
|
||||
expectedError: 'parameter name must be alphanumeric but it was "b@d"',
|
||||
},
|
||||
{
|
||||
name: 'has {',
|
||||
description: 'has {',
|
||||
value: 'b{a}d',
|
||||
expectedError: 'parameter name must be alphanumeric but it was "b{a}d"',
|
||||
},
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
for (const { description, value, expectedError } of testScenarios) {
|
||||
it(description, () => {
|
||||
// act
|
||||
const act = () => action(testCase.value);
|
||||
const act = () => action(value);
|
||||
// assert
|
||||
expect(act).to.throw(testCase.expectedError);
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,57 +6,28 @@ import { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Fun
|
||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
import { FunctionCallCompilerStub } from '@tests/unit/shared/Stubs/FunctionCallCompilerStub';
|
||||
import { SharedFunctionsParserStub } from '@tests/unit/shared/Stubs/SharedFunctionsParserStub';
|
||||
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
|
||||
import { parseFunctionCalls } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCallParser';
|
||||
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
|
||||
describe('ScriptCompiler', () => {
|
||||
describe('ctor', () => {
|
||||
describe('throws if syntax is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing syntax';
|
||||
const syntax = absentValue;
|
||||
// act
|
||||
const act = () => new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.withSyntax(syntax)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('canCompile', () => {
|
||||
describe('throws if script is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing script';
|
||||
const argument = absentValue;
|
||||
const builder = new ScriptCompilerBuilder()
|
||||
.withEmptyFunctions()
|
||||
.build();
|
||||
// act
|
||||
const act = () => builder.canCompile(argument);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('returns true if "call" is defined', () => {
|
||||
// arrange
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withEmptyFunctions()
|
||||
.build();
|
||||
const script = ScriptDataStub.createWithCall();
|
||||
const script = createScriptDataWithCall();
|
||||
// act
|
||||
const actual = sut.canCompile(script);
|
||||
// assert
|
||||
@@ -67,7 +38,7 @@ describe('ScriptCompiler', () => {
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withEmptyFunctions()
|
||||
.build();
|
||||
const script = ScriptDataStub.createWithCode();
|
||||
const script = createScriptDataWithCode();
|
||||
// act
|
||||
const actual = sut.canCompile(script);
|
||||
// assert
|
||||
@@ -75,19 +46,17 @@ describe('ScriptCompiler', () => {
|
||||
});
|
||||
});
|
||||
describe('compile', () => {
|
||||
describe('throws if script is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing script';
|
||||
const argument = absentValue;
|
||||
const builder = new ScriptCompilerBuilder()
|
||||
.withEmptyFunctions()
|
||||
.build();
|
||||
// act
|
||||
const act = () => builder.compile(argument);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws if script does not have body', () => {
|
||||
// arrange
|
||||
const expectedError = 'Script does include any calls.';
|
||||
const scriptData = createScriptDataWithCode();
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
.build();
|
||||
// act
|
||||
const act = () => sut.compile(scriptData);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('returns code as expected', () => {
|
||||
// arrange
|
||||
@@ -96,8 +65,8 @@ describe('ScriptCompiler', () => {
|
||||
revertCode: 'expected-revert-code',
|
||||
};
|
||||
const call = new FunctionCallDataStub();
|
||||
const script = ScriptDataStub.createWithCall(call);
|
||||
const functions = [FunctionDataStub.createWithCode().withName('existing-func')];
|
||||
const script = createScriptDataWithCall(call);
|
||||
const functions = [createFunctionDataWithCode().withName('existing-func')];
|
||||
const compiledFunctions = new SharedFunctionCollectionStub();
|
||||
const functionParserMock = new SharedFunctionsParserStub();
|
||||
functionParserMock.setup(functions, compiledFunctions);
|
||||
@@ -124,7 +93,7 @@ describe('ScriptCompiler', () => {
|
||||
.withSyntax(expected)
|
||||
.withSharedFunctionsParser(parser)
|
||||
.build();
|
||||
const scriptData = ScriptDataStub.createWithCall();
|
||||
const scriptData = createScriptDataWithCall();
|
||||
// act
|
||||
sut.compile(scriptData);
|
||||
// assert
|
||||
@@ -133,13 +102,13 @@ describe('ScriptCompiler', () => {
|
||||
});
|
||||
it('parses given functions', () => {
|
||||
// arrange
|
||||
const expectedFunctions = [FunctionDataStub.createWithCode().withName('existing-func')];
|
||||
const expectedFunctions = [createFunctionDataWithCode().withName('existing-func')];
|
||||
const parser = new SharedFunctionsParserStub();
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withFunctions(...expectedFunctions)
|
||||
.withSharedFunctionsParser(parser)
|
||||
.build();
|
||||
const scriptData = ScriptDataStub.createWithCall();
|
||||
const scriptData = createScriptDataWithCall();
|
||||
// act
|
||||
sut.compile(scriptData);
|
||||
// assert
|
||||
@@ -155,7 +124,7 @@ describe('ScriptCompiler', () => {
|
||||
const callCompiler: FunctionCallCompiler = {
|
||||
compileFunctionCalls: () => { throw new Error(innerError); },
|
||||
};
|
||||
const scriptData = ScriptDataStub.createWithCall()
|
||||
const scriptData = createScriptDataWithCall()
|
||||
.withName(scriptName);
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
@@ -170,7 +139,8 @@ describe('ScriptCompiler', () => {
|
||||
// arrange
|
||||
const scriptName = 'scriptName';
|
||||
const syntax = new LanguageSyntaxStub();
|
||||
const invalidCode: CompiledCode = { code: undefined, revertCode: undefined };
|
||||
const invalidCode = new CompiledCodeStub()
|
||||
.withCode('' /* invalid code (empty string) */);
|
||||
const realExceptionMessage = collectExceptionMessage(
|
||||
() => new ScriptCode(invalidCode.code, invalidCode.revertCode),
|
||||
);
|
||||
@@ -178,7 +148,7 @@ describe('ScriptCompiler', () => {
|
||||
const callCompiler: FunctionCallCompiler = {
|
||||
compileFunctionCalls: () => invalidCode,
|
||||
};
|
||||
const scriptData = ScriptDataStub.createWithCall()
|
||||
const scriptData = createScriptDataWithCall()
|
||||
.withName(scriptName);
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
@@ -196,7 +166,7 @@ describe('ScriptCompiler', () => {
|
||||
NoEmptyLines,
|
||||
// Allow duplicated lines to enable calling same function multiple times
|
||||
];
|
||||
const scriptData = ScriptDataStub.createWithCall();
|
||||
const scriptData = createScriptDataWithCall();
|
||||
const validator = new CodeValidatorStub();
|
||||
const sut = new ScriptCompilerBuilder()
|
||||
.withSomeFunctions()
|
||||
@@ -216,11 +186,11 @@ describe('ScriptCompiler', () => {
|
||||
class ScriptCompilerBuilder {
|
||||
private static createFunctions(...names: string[]): FunctionData[] {
|
||||
return names.map((functionName) => {
|
||||
return FunctionDataStub.createWithCode().withName(functionName);
|
||||
return createFunctionDataWithCode().withName(functionName);
|
||||
});
|
||||
}
|
||||
|
||||
private functions: FunctionData[];
|
||||
private functions: FunctionData[] | undefined;
|
||||
|
||||
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
|
||||
|
||||
@@ -230,46 +200,46 @@ class ScriptCompilerBuilder {
|
||||
|
||||
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
||||
|
||||
public withFunctions(...functions: FunctionData[]): ScriptCompilerBuilder {
|
||||
public withFunctions(...functions: FunctionData[]): this {
|
||||
this.functions = functions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSomeFunctions(): ScriptCompilerBuilder {
|
||||
public withSomeFunctions(): this {
|
||||
this.functions = ScriptCompilerBuilder.createFunctions('test-function');
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFunctionNames(...functionNames: string[]): ScriptCompilerBuilder {
|
||||
public withFunctionNames(...functionNames: string[]): this {
|
||||
this.functions = ScriptCompilerBuilder.createFunctions(...functionNames);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withEmptyFunctions(): ScriptCompilerBuilder {
|
||||
public withEmptyFunctions(): this {
|
||||
this.functions = [];
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSyntax(syntax: ILanguageSyntax): ScriptCompilerBuilder {
|
||||
public withSyntax(syntax: ILanguageSyntax): this {
|
||||
this.syntax = syntax;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSharedFunctionsParser(
|
||||
sharedFunctionsParser: ISharedFunctionsParser,
|
||||
): ScriptCompilerBuilder {
|
||||
): this {
|
||||
this.sharedFunctionsParser = sharedFunctionsParser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCodeValidator(
|
||||
codeValidator: ICodeValidator,
|
||||
): ScriptCompilerBuilder {
|
||||
): this {
|
||||
this.codeValidator = codeValidator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFunctionCallCompiler(callCompiler: FunctionCallCompiler): ScriptCompilerBuilder {
|
||||
public withFunctionCallCompiler(callCompiler: FunctionCallCompiler): this {
|
||||
this.callCompiler = callCompiler;
|
||||
return this;
|
||||
}
|
||||
@@ -287,13 +257,3 @@ class ScriptCompilerBuilder {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function collectExceptionMessage(action: () => unknown) {
|
||||
let message = '';
|
||||
try {
|
||||
action();
|
||||
} catch (e) {
|
||||
message = e.message;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import { parseScript, ScriptFactoryType } from '@/application/Parser/Script/Scri
|
||||
import { parseDocs } from '@/application/Parser/DocumentationParser';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
||||
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub';
|
||||
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import { createScriptDataWithCall, createScriptDataWithCode, createScriptDataWithoutCallOrCodes } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
||||
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
|
||||
@@ -25,7 +25,7 @@ describe('ScriptParser', () => {
|
||||
it('parses name as expected', () => {
|
||||
// arrange
|
||||
const expected = 'test-expected-name';
|
||||
const script = ScriptDataStub.createWithCode()
|
||||
const script = createScriptDataWithCode()
|
||||
.withName(expected);
|
||||
// act
|
||||
const actual = new TestBuilder()
|
||||
@@ -37,7 +37,7 @@ describe('ScriptParser', () => {
|
||||
it('parses docs as expected', () => {
|
||||
// arrange
|
||||
const docs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
|
||||
const script = ScriptDataStub.createWithCode()
|
||||
const script = createScriptDataWithCode()
|
||||
.withDocs(docs);
|
||||
const expected = parseDocs(script);
|
||||
// act
|
||||
@@ -51,7 +51,7 @@ describe('ScriptParser', () => {
|
||||
describe('accepts absent level', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const script = ScriptDataStub.createWithCode()
|
||||
const script = createScriptDataWithCode()
|
||||
.withRecommend(absentValue);
|
||||
// act
|
||||
const actual = new TestBuilder()
|
||||
@@ -59,14 +59,14 @@ describe('ScriptParser', () => {
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(actual.level).to.equal(undefined);
|
||||
});
|
||||
}, { excludeNull: true });
|
||||
});
|
||||
it('parses level as expected', () => {
|
||||
// arrange
|
||||
const expectedLevel = RecommendationLevel.Standard;
|
||||
const expectedName = 'level';
|
||||
const levelText = 'standard';
|
||||
const script = ScriptDataStub.createWithCode()
|
||||
const script = createScriptDataWithCode()
|
||||
.withRecommend(levelText);
|
||||
const parserMock = new EnumParserStub<RecommendationLevel>()
|
||||
.setup(expectedName, levelText, expectedLevel);
|
||||
@@ -83,8 +83,7 @@ describe('ScriptParser', () => {
|
||||
it('parses "execute" as expected', () => {
|
||||
// arrange
|
||||
const expected = 'expected-code';
|
||||
const script = ScriptDataStub
|
||||
.createWithCode()
|
||||
const script = createScriptDataWithCode()
|
||||
.withCode(expected);
|
||||
// act
|
||||
const parsed = new TestBuilder()
|
||||
@@ -97,8 +96,7 @@ describe('ScriptParser', () => {
|
||||
it('parses "revert" as expected', () => {
|
||||
// arrange
|
||||
const expected = 'expected-revert-code';
|
||||
const script = ScriptDataStub
|
||||
.createWithCode()
|
||||
const script = createScriptDataWithCode()
|
||||
.withRevertCode(expected);
|
||||
// act
|
||||
const parsed = new TestBuilder()
|
||||
@@ -109,23 +107,10 @@ describe('ScriptParser', () => {
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
describe('compiler', () => {
|
||||
describe('throws when context is not defined', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedMessage = 'missing context';
|
||||
const context: ICategoryCollectionParseContext = absentValue;
|
||||
// act
|
||||
const act = () => new TestBuilder()
|
||||
.withContext(context)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(act).to.throw(expectedMessage);
|
||||
});
|
||||
});
|
||||
it('gets code from compiler', () => {
|
||||
// arrange
|
||||
const expected = new ScriptCodeStub();
|
||||
const script = ScriptDataStub.createWithCode();
|
||||
const script = createScriptDataWithCode();
|
||||
const compiler = new ScriptCompilerStub()
|
||||
.withCompileAbility(script, expected);
|
||||
const parseContext = new CategoryCollectionParseContextStub()
|
||||
@@ -147,8 +132,7 @@ describe('ScriptParser', () => {
|
||||
const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`;
|
||||
const parseContext = new CategoryCollectionParseContextStub()
|
||||
.withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter));
|
||||
const script = ScriptDataStub
|
||||
.createWithoutCallOrCodes()
|
||||
const script = createScriptDataWithoutCallOrCodes()
|
||||
.withCode(duplicatedCode);
|
||||
// act
|
||||
const act = () => new TestBuilder()
|
||||
@@ -166,8 +150,7 @@ describe('ScriptParser', () => {
|
||||
NoDuplicatedLines,
|
||||
];
|
||||
const validator = new CodeValidatorStub();
|
||||
const script = ScriptDataStub
|
||||
.createWithCode()
|
||||
const script = createScriptDataWithCode()
|
||||
.withCode('expected code to be validated')
|
||||
.withRevertCode('expected revert code to be validated');
|
||||
// act
|
||||
@@ -186,8 +169,7 @@ describe('ScriptParser', () => {
|
||||
const expectedRules = [];
|
||||
const expectedCodeCalls = [];
|
||||
const validator = new CodeValidatorStub();
|
||||
const script = ScriptDataStub
|
||||
.createWithCall();
|
||||
const script = createScriptDataWithCall();
|
||||
const compiler = new ScriptCompilerStub()
|
||||
.withCompileAbility(script, new ScriptCodeStub());
|
||||
const parseContext = new CategoryCollectionParseContextStub()
|
||||
@@ -222,7 +204,7 @@ describe('ScriptParser', () => {
|
||||
new NodeValidationTestRunner()
|
||||
.testInvalidNodeName((invalidName) => {
|
||||
return createTest(
|
||||
ScriptDataStub.createWithCall().withName(invalidName),
|
||||
createScriptDataWithCall().withName(invalidName),
|
||||
);
|
||||
})
|
||||
.testMissingNodeData((node) => {
|
||||
@@ -231,30 +213,30 @@ describe('ScriptParser', () => {
|
||||
.runThrowingCase({
|
||||
name: 'throws when both function call and code are defined',
|
||||
scenario: createTest(
|
||||
ScriptDataStub.createWithCall().withCode('code'),
|
||||
createScriptDataWithCall().withCode('code'),
|
||||
),
|
||||
expectedMessage: 'Cannot define both "call" and "code".',
|
||||
expectedMessage: 'Both "call" and "code" are defined.',
|
||||
})
|
||||
.runThrowingCase({
|
||||
name: 'throws when both function call and revertCode are defined',
|
||||
scenario: createTest(
|
||||
ScriptDataStub.createWithCall().withRevertCode('revert-code'),
|
||||
createScriptDataWithCall().withRevertCode('revert-code'),
|
||||
),
|
||||
expectedMessage: 'Cannot define "revertCode" if "call" is defined.',
|
||||
expectedMessage: 'Both "call" and "revertCode" are defined.',
|
||||
})
|
||||
.runThrowingCase({
|
||||
name: 'throws when neither call or revertCode are defined',
|
||||
scenario: createTest(
|
||||
ScriptDataStub.createWithoutCallOrCodes(),
|
||||
createScriptDataWithoutCallOrCodes(),
|
||||
),
|
||||
expectedMessage: 'Must define either "call" or "code".',
|
||||
expectedMessage: 'Neither "call" or "code" is defined.',
|
||||
});
|
||||
});
|
||||
it(`rethrows exception if ${Script.name} cannot be constructed`, () => {
|
||||
// arrange
|
||||
const expectedError = 'script creation failed';
|
||||
const factoryMock: ScriptFactoryType = () => { throw new Error(expectedError); };
|
||||
const data = ScriptDataStub.createWithCode();
|
||||
const data = createScriptDataWithCode();
|
||||
// act
|
||||
const act = () => new TestBuilder()
|
||||
.withData(data)
|
||||
@@ -274,14 +256,14 @@ describe('ScriptParser', () => {
|
||||
});
|
||||
|
||||
class TestBuilder {
|
||||
private data: ScriptData = ScriptDataStub.createWithCode();
|
||||
private data: ScriptData = createScriptDataWithCode();
|
||||
|
||||
private context: ICategoryCollectionParseContext = new CategoryCollectionParseContextStub();
|
||||
|
||||
private parser: IEnumParser<RecommendationLevel> = new EnumParserStub<RecommendationLevel>()
|
||||
.setupDefaultValue(RecommendationLevel.Standard);
|
||||
|
||||
private factory: ScriptFactoryType = undefined;
|
||||
private factory?: ScriptFactoryType = undefined;
|
||||
|
||||
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CodeValidationRuleStub } from '@tests/unit/shared/Stubs/CodeValidationR
|
||||
import { itEachAbsentCollectionValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
|
||||
import { ICodeLine } from '@/application/Parser/Script/Validation/ICodeLine';
|
||||
import { IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
|
||||
import { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
|
||||
|
||||
describe('CodeValidator', () => {
|
||||
describe('instance', () => {
|
||||
@@ -23,10 +23,10 @@ describe('CodeValidator', () => {
|
||||
const act = () => sut.throwIfInvalid(code, [new CodeValidationRuleStub()]);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
describe('throws if rules are empty', () => {
|
||||
itEachAbsentCollectionValue((absentValue) => {
|
||||
itEachAbsentCollectionValue<ICodeValidationRule>((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing rules';
|
||||
const rules = absentValue;
|
||||
@@ -35,7 +35,7 @@ describe('CodeValidator', () => {
|
||||
const act = () => sut.throwIfInvalid('code', rules);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
describe('splits lines as expected', () => {
|
||||
it('supports all line separators', () => {
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
import { describe, expect } from 'vitest';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { describe } from 'vitest';
|
||||
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
|
||||
import { testCodeValidationRule } from './CodeValidationRuleTestRunner';
|
||||
|
||||
describe('NoDuplicatedLines', () => {
|
||||
describe('ctor', () => {
|
||||
describe('throws if syntax is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing syntax';
|
||||
const syntax = absentValue;
|
||||
// act
|
||||
const act = () => new NoDuplicatedLines(syntax);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('analyze', () => {
|
||||
testCodeValidationRule([
|
||||
{
|
||||
|
||||
@@ -23,12 +23,6 @@ describe('NoEmptyLines', () => {
|
||||
expected: [2, 4].map((index) => ({ index, error: 'Empty line' })),
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
{
|
||||
testName: 'shows error for undefined and null lines',
|
||||
codeLines: ['first line', undefined, 'third line', null],
|
||||
expected: [2, 4].map((index) => ({ index, error: 'Empty line' })),
|
||||
sut: new NoEmptyLines(),
|
||||
},
|
||||
{
|
||||
testName: 'shows error for whitespace-only lines',
|
||||
codeLines: ['first line', ' ', 'third line'],
|
||||
|
||||
@@ -3,40 +3,21 @@ import { CodeSubstituter } from '@/application/Parser/ScriptingDefinition/CodeSu
|
||||
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||
import { ProjectInformationStub } from '@tests/unit/shared/Stubs/ProjectInformationStub';
|
||||
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
|
||||
import { getAbsentObjectTestCases, getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('CodeSubstituter', () => {
|
||||
describe('throws with invalid parameters', () => {
|
||||
// arrange
|
||||
const testCases = [
|
||||
...getAbsentStringTestCases().map((testCase) => ({
|
||||
name: `given code: ${testCase.valueName}`,
|
||||
expectedError: 'missing code',
|
||||
parameters: {
|
||||
code: testCase.absentValue,
|
||||
info: new ProjectInformationStub(),
|
||||
},
|
||||
})),
|
||||
...getAbsentObjectTestCases().map((testCase) => ({
|
||||
name: `given info: ${testCase.valueName}`,
|
||||
expectedError: 'missing info',
|
||||
parameters: {
|
||||
code: 'non empty code',
|
||||
info: testCase.absentValue,
|
||||
},
|
||||
})),
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(`${testCase.name} throws "${testCase.expectedError}"`, () => {
|
||||
// arrange
|
||||
const sut = new CodeSubstituterBuilder().build();
|
||||
const { code, info } = testCase.parameters;
|
||||
// act
|
||||
const act = () => sut.substitute(code, info);
|
||||
// assert
|
||||
expect(act).to.throw(testCase.expectedError);
|
||||
});
|
||||
}
|
||||
describe('throws if code is empty', () => {
|
||||
itEachAbsentStringValue((emptyCode) => {
|
||||
// arrange
|
||||
const expectedError = 'missing code';
|
||||
const code = emptyCode;
|
||||
const info = new ProjectInformationStub();
|
||||
const sut = new CodeSubstituterBuilder().build();
|
||||
// act
|
||||
const act = () => sut.substitute(code, info);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
describe('substitutes parameters as expected values', () => {
|
||||
// arrange
|
||||
|
||||
@@ -8,38 +8,9 @@ import { ProjectInformationStub } from '@tests/unit/shared/Stubs/ProjectInformat
|
||||
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
||||
import { ScriptingDefinitionDataStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionDataStub';
|
||||
import { CodeSubstituterStub } from '@tests/unit/shared/Stubs/CodeSubstituterStub';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('ScriptingDefinitionParser', () => {
|
||||
describe('parseScriptingDefinition', () => {
|
||||
describe('throws when info is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing info';
|
||||
const info = absentValue;
|
||||
const definition = new ScriptingDefinitionDataStub();
|
||||
const sut = new ScriptingDefinitionParserBuilder()
|
||||
.build();
|
||||
// act
|
||||
const act = () => sut.parse(definition, info);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('throws when definition is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing definition';
|
||||
const info = new ProjectInformationStub();
|
||||
const definition = absentValue;
|
||||
const sut = new ScriptingDefinitionParserBuilder()
|
||||
.build();
|
||||
// act
|
||||
const act = () => sut.parse(definition, info);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('language', () => {
|
||||
it('parses as expected', () => {
|
||||
// arrange
|
||||
|
||||
@@ -3,21 +3,22 @@ import { Application } from '@/domain/Application';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { ProjectInformationStub } from '@tests/unit/shared/Stubs/ProjectInformationStub';
|
||||
import { getAbsentObjectTestCases, getAbsentCollectionTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { getAbsentCollectionTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('Application', () => {
|
||||
describe('getCollection', () => {
|
||||
it('returns undefined if not found', () => {
|
||||
it('throws if not found', () => {
|
||||
// arrange
|
||||
const expected = undefined;
|
||||
const missingOs = OperatingSystem.Android;
|
||||
const expectedError = `Operating system "${OperatingSystem[missingOs]}" is not defined in application`;
|
||||
const info = new ProjectInformationStub();
|
||||
const collections = [new CategoryCollectionStub().withOs(OperatingSystem.Windows)];
|
||||
// act
|
||||
const sut = new Application(info, collections);
|
||||
const actual = sut.getCollection(OperatingSystem.Android);
|
||||
const act = () => sut.getCollection(missingOs);
|
||||
// assert
|
||||
expect(actual).to.equals(expected);
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('returns expected when multiple collections exist', () => {
|
||||
// arrange
|
||||
@@ -34,18 +35,6 @@ describe('Application', () => {
|
||||
});
|
||||
describe('ctor', () => {
|
||||
describe('info', () => {
|
||||
describe('throws if missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing project information';
|
||||
const info = absentValue;
|
||||
const collections = [new CategoryCollectionStub()];
|
||||
// act
|
||||
const act = () => new Application(info, collections);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
const expected = new ProjectInformationStub();
|
||||
@@ -60,20 +49,20 @@ describe('Application', () => {
|
||||
describe('throws on invalid value', () => {
|
||||
// arrange
|
||||
const testCases: readonly {
|
||||
name: string,
|
||||
expectedError: string,
|
||||
value: readonly ICategoryCollection[],
|
||||
readonly name: string,
|
||||
readonly expectedError: string,
|
||||
readonly value: readonly ICategoryCollection[],
|
||||
}[] = [
|
||||
...getAbsentCollectionTestCases<ICategoryCollection>().map((testCase) => ({
|
||||
name: testCase.valueName,
|
||||
...getAbsentCollectionTestCases<ICategoryCollection>(
|
||||
{
|
||||
excludeUndefined: true,
|
||||
excludeNull: true,
|
||||
},
|
||||
).map((testCase) => ({
|
||||
name: `empty collection: ${testCase.valueName}`,
|
||||
expectedError: 'missing collections',
|
||||
value: testCase.absentValue,
|
||||
})),
|
||||
...getAbsentObjectTestCases().map((testCase) => ({
|
||||
name: `${testCase.valueName} value in list`,
|
||||
expectedError: 'missing collection in the list',
|
||||
value: [new CategoryCollectionStub(), testCase.absentValue],
|
||||
})),
|
||||
{
|
||||
name: 'two collections with same OS',
|
||||
expectedError: 'multiple collections with same os: windows',
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('Category', () => {
|
||||
const construct = () => new Category(5, name, [], [new CategoryStub(5)], []);
|
||||
// assert
|
||||
expect(construct).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
it('throws when has no children', () => {
|
||||
const expectedError = 'A category must have at least one sub-category or script';
|
||||
|
||||
@@ -9,7 +9,6 @@ import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('CategoryCollection', () => {
|
||||
describe('getScriptsByLevel', () => {
|
||||
@@ -81,7 +80,6 @@ describe('CategoryCollection', () => {
|
||||
sut.getScriptsByLevel(level);
|
||||
})
|
||||
// assert
|
||||
.testAbsentValueThrows()
|
||||
.testOutOfRangeThrows()
|
||||
.testValidValueDoesNotThrow(RecommendationLevel.Standard);
|
||||
});
|
||||
@@ -214,8 +212,7 @@ describe('CategoryCollection', () => {
|
||||
.construct();
|
||||
// assert
|
||||
new EnumRangeTestRunner(act)
|
||||
.testOutOfRangeThrows()
|
||||
.testAbsentValueThrows();
|
||||
.testOutOfRangeThrows();
|
||||
});
|
||||
});
|
||||
describe('scriptingDefinition', () => {
|
||||
@@ -229,20 +226,60 @@ describe('CategoryCollection', () => {
|
||||
// assert
|
||||
expect(sut.scripting).to.deep.equal(expected);
|
||||
});
|
||||
describe('cannot construct without initial script', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing scripting definition';
|
||||
const scriptingDefinition = absentValue;
|
||||
// act
|
||||
function construct() {
|
||||
return new CategoryCollectionBuilder()
|
||||
.withScripting(scriptingDefinition)
|
||||
.construct();
|
||||
}
|
||||
// assert
|
||||
expect(construct).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('getCategory', () => {
|
||||
it('throws if category is not found', () => {
|
||||
// arrange
|
||||
const categoryId = 123;
|
||||
const expectedError = `Missing category with ID: "${categoryId}"`;
|
||||
const collection = new CategoryCollectionBuilder()
|
||||
.withActions([new CategoryStub(456).withMandatoryScripts()])
|
||||
.construct();
|
||||
// act
|
||||
const act = () => collection.getCategory(categoryId);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('finds correct category', () => {
|
||||
// arrange
|
||||
const categoryId = 123;
|
||||
const expectedCategory = new CategoryStub(categoryId).withMandatoryScripts();
|
||||
const collection = new CategoryCollectionBuilder()
|
||||
.withActions([expectedCategory])
|
||||
.construct();
|
||||
// act
|
||||
const actualCategory = collection.getCategory(categoryId);
|
||||
// assert
|
||||
expect(actualCategory).to.equal(expectedCategory);
|
||||
});
|
||||
});
|
||||
describe('getScript', () => {
|
||||
it('throws if script is not found', () => {
|
||||
// arrange
|
||||
const scriptId = 'missingScript';
|
||||
const expectedError = `missing script: ${scriptId}`;
|
||||
const collection = new CategoryCollectionBuilder()
|
||||
.withActions([new CategoryStub(456).withMandatoryScripts()])
|
||||
.construct();
|
||||
// act
|
||||
const act = () => collection.getScript(scriptId);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('finds correct script', () => {
|
||||
// arrange
|
||||
const scriptId = 'existingScript';
|
||||
const expectedScript = new ScriptStub(scriptId);
|
||||
const parentCategory = new CategoryStub(123)
|
||||
.withMandatoryScripts()
|
||||
.withScript(expectedScript);
|
||||
const collection = new CategoryCollectionBuilder()
|
||||
.withActions([parentCategory])
|
||||
.construct();
|
||||
// act
|
||||
const actualScript = collection.getScript(scriptId);
|
||||
// assert
|
||||
expect(actualScript).to.equal(expectedScript);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -260,30 +297,27 @@ class CategoryCollectionBuilder {
|
||||
private os = OperatingSystem.Windows;
|
||||
|
||||
private actions: readonly ICategory[] = [
|
||||
new CategoryStub(1).withScripts(
|
||||
new ScriptStub('S1').withLevel(RecommendationLevel.Standard),
|
||||
new ScriptStub('S2').withLevel(RecommendationLevel.Strict),
|
||||
),
|
||||
new CategoryStub(1).withMandatoryScripts(),
|
||||
];
|
||||
|
||||
private script: IScriptingDefinition = getValidScriptingDefinition();
|
||||
private scriptingDefinition: IScriptingDefinition = getValidScriptingDefinition();
|
||||
|
||||
public withOs(os: OperatingSystem): CategoryCollectionBuilder {
|
||||
public withOs(os: OperatingSystem): this {
|
||||
this.os = os;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withActions(actions: readonly ICategory[]): CategoryCollectionBuilder {
|
||||
public withActions(actions: readonly ICategory[]): this {
|
||||
this.actions = actions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withScripting(script: IScriptingDefinition): CategoryCollectionBuilder {
|
||||
this.script = script;
|
||||
public withScripting(scriptingDefinition: IScriptingDefinition): this {
|
||||
this.scriptingDefinition = scriptingDefinition;
|
||||
return this;
|
||||
}
|
||||
|
||||
public construct(): CategoryCollection {
|
||||
return new CategoryCollection(this.os, this.actions, this.script);
|
||||
return new CategoryCollection(this.os, this.actions, this.scriptingDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('ProjectInformation', () => {
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
}
|
||||
it('should throw an error when provided with an invalid operating system', () => {
|
||||
describe('should throw an error when provided with an invalid operating system', () => {
|
||||
// arrange
|
||||
const sut = new ProjectInformationBuilder()
|
||||
.build();
|
||||
@@ -168,7 +168,6 @@ describe('ProjectInformation', () => {
|
||||
// assert
|
||||
new EnumRangeTestRunner(act)
|
||||
.testOutOfRangeThrows()
|
||||
.testAbsentValueThrows()
|
||||
.testInvalidValueThrows(OperatingSystem.KaiOS, `Unsupported os: ${OperatingSystem[OperatingSystem.KaiOS]}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Script } from '@/domain/Script';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('Script', () => {
|
||||
describe('ctor', () => {
|
||||
@@ -20,19 +19,6 @@ describe('Script', () => {
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
describe('throws when missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing code';
|
||||
const code: IScriptCode = absentValue;
|
||||
// act
|
||||
const construct = () => new ScriptBuilder()
|
||||
.withCode(code)
|
||||
.build();
|
||||
// assert
|
||||
expect(construct).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('canRevert', () => {
|
||||
it('returns false without revert code', () => {
|
||||
@@ -112,34 +98,34 @@ class ScriptBuilder {
|
||||
|
||||
private code: IScriptCode = new ScriptCodeStub();
|
||||
|
||||
private level = RecommendationLevel.Standard;
|
||||
private level? = RecommendationLevel.Standard;
|
||||
|
||||
private docs: readonly string[] = undefined;
|
||||
private docs: readonly string[] = [];
|
||||
|
||||
public withCodes(code: string, revertCode = ''): ScriptBuilder {
|
||||
public withCodes(code: string, revertCode = ''): this {
|
||||
this.code = new ScriptCodeStub()
|
||||
.withExecute(code)
|
||||
.withRevert(revertCode);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCode(code: IScriptCode): ScriptBuilder {
|
||||
public withCode(code: IScriptCode): this {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withName(name: string): ScriptBuilder {
|
||||
public withName(name: string): this {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withRecommendationLevel(level: RecommendationLevel): ScriptBuilder {
|
||||
public withRecommendationLevel(level: RecommendationLevel | undefined): this {
|
||||
this.level = level;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withDocs(urls: readonly string[]): ScriptBuilder {
|
||||
this.docs = urls;
|
||||
public withDocs(docs: readonly string[]): this {
|
||||
this.docs = docs;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,26 +6,34 @@ describe('ScriptCode', () => {
|
||||
describe('code', () => {
|
||||
describe('throws with invalid code', () => {
|
||||
// arrange
|
||||
const testCases = [
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly code: {
|
||||
readonly execute: string;
|
||||
readonly revert?: string;
|
||||
},
|
||||
readonly expectedError: string;
|
||||
}> = [
|
||||
{
|
||||
name: 'throws when "execute" and "revert" are same',
|
||||
description: 'throws when "execute" and "revert" are same',
|
||||
code: {
|
||||
execute: 'same',
|
||||
revert: 'same',
|
||||
},
|
||||
expectedError: '(revert): Code itself and its reverting code cannot be the same',
|
||||
},
|
||||
...getAbsentStringTestCases().map((testCase) => ({
|
||||
name: `cannot construct with ${testCase.valueName} "execute"`,
|
||||
code: {
|
||||
execute: testCase.absentValue,
|
||||
revert: 'code',
|
||||
},
|
||||
expectedError: 'missing code',
|
||||
})),
|
||||
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
|
||||
.map((testCase) => ({
|
||||
description: `cannot construct with ${testCase.valueName} "execute"`,
|
||||
code: {
|
||||
execute: testCase.absentValue,
|
||||
revert: 'code',
|
||||
},
|
||||
expectedError: 'missing code',
|
||||
})),
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
for (const testCase of testScenarios) {
|
||||
it(testCase.description, () => {
|
||||
// act
|
||||
const act = () => new ScriptCodeBuilder()
|
||||
.withExecute(testCase.code.execute)
|
||||
@@ -38,21 +46,27 @@ describe('ScriptCode', () => {
|
||||
});
|
||||
describe('sets as expected with valid "execute" or "revert"', () => {
|
||||
// arrange
|
||||
const testCases = [
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly code: string;
|
||||
readonly revertCode: string | undefined;
|
||||
}> = [
|
||||
{
|
||||
testName: 'code and revert code is given',
|
||||
description: 'code and revert code is given',
|
||||
code: 'valid code',
|
||||
revertCode: 'valid revert-code',
|
||||
},
|
||||
{
|
||||
testName: 'only code is given but not revert code',
|
||||
code: 'valid code',
|
||||
revertCode: undefined,
|
||||
},
|
||||
...getAbsentStringTestCases({ excludeNull: true })
|
||||
.map((testCase) => ({
|
||||
description: `only code is given but not revert code (given ${testCase.valueName})`,
|
||||
code: 'valid code',
|
||||
revertCode: testCase.absentValue,
|
||||
expectedError: 'missing code',
|
||||
})),
|
||||
];
|
||||
// assert
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.testName, () => {
|
||||
for (const testCase of testScenarios) {
|
||||
it(testCase.description, () => {
|
||||
// act
|
||||
const sut = new ScriptCodeBuilder()
|
||||
.withExecute(testCase.code)
|
||||
@@ -70,14 +84,14 @@ describe('ScriptCode', () => {
|
||||
class ScriptCodeBuilder {
|
||||
public execute = 'default-execute-code';
|
||||
|
||||
public revert = '';
|
||||
public revert: string | undefined = '';
|
||||
|
||||
public withExecute(execute: string) {
|
||||
this.execute = execute;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withRevert(revert: string) {
|
||||
public withRevert(revert: string | undefined) {
|
||||
this.revert = revert;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -68,14 +68,13 @@ describe('ScriptingDefinition', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing start code';
|
||||
const undefinedValue = absentValue;
|
||||
// act
|
||||
const act = () => new ScriptingDefinitionBuilder()
|
||||
.withStartCode(undefinedValue)
|
||||
.withStartCode(absentValue)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
});
|
||||
describe('endCode', () => {
|
||||
@@ -89,18 +88,17 @@ describe('ScriptingDefinition', () => {
|
||||
// assert
|
||||
expect(sut.endCode).to.equal(expected);
|
||||
});
|
||||
describe('throws when undefined', () => {
|
||||
describe('throws when absent', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing end code';
|
||||
const undefinedValue = absentValue;
|
||||
// act
|
||||
const act = () => new ScriptingDefinitionBuilder()
|
||||
.withEndCode(undefinedValue)
|
||||
.withEndCode(absentValue)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -108,21 +106,21 @@ describe('ScriptingDefinition', () => {
|
||||
class ScriptingDefinitionBuilder {
|
||||
private language = ScriptingLanguage.shellscript;
|
||||
|
||||
private startCode = 'REM start-code';
|
||||
private startCode = `# [${ScriptingDefinitionBuilder.name}]: start-code`;
|
||||
|
||||
private endCode = 'REM end-code';
|
||||
private endCode = `# [${ScriptingDefinitionBuilder.name}]: end-code`;
|
||||
|
||||
public withLanguage(language: ScriptingLanguage): ScriptingDefinitionBuilder {
|
||||
public withLanguage(language: ScriptingLanguage): this {
|
||||
this.language = language;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withStartCode(startCode: string): ScriptingDefinitionBuilder {
|
||||
public withStartCode(startCode: string): this {
|
||||
this.startCode = startCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withEndCode(endCode: string): ScriptingDefinitionBuilder {
|
||||
public withEndCode(endCode: string): this {
|
||||
this.endCode = endCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Version } from '@/domain/Version';
|
||||
import { itEachAbsentStringValue } from '../shared/TestCases/AbsentTests';
|
||||
|
||||
describe('Version', () => {
|
||||
describe('invalid versions', () => {
|
||||
@@ -19,20 +20,15 @@ describe('Version', () => {
|
||||
}
|
||||
});
|
||||
describe('throws with empty string', () => {
|
||||
// arrange
|
||||
const expectedError = 'empty version';
|
||||
const testCases = [
|
||||
{ name: 'empty', value: '' },
|
||||
{ name: 'undefined', value: undefined },
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(`given ${testCase.name}`, () => {
|
||||
// act
|
||||
const act = () => new Version(testCase.value);
|
||||
//
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'empty version';
|
||||
const value = absentValue;
|
||||
// act
|
||||
const act = () => new Version(value);
|
||||
//
|
||||
expect(act).to.throw(expectedError);
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
});
|
||||
describe('valid versions', () => {
|
||||
|
||||
@@ -183,16 +183,37 @@ describe('CodeRunner', () => {
|
||||
.filter((command) => expectedOrder.includes(command));
|
||||
expect(expectedOrder).to.deep.equal(actualOrder);
|
||||
});
|
||||
it('throws with unsupported os', async () => {
|
||||
// arrange
|
||||
const unknownOs = OperatingSystem.Android;
|
||||
const expectedError = `unsupported os: ${OperatingSystem[unknownOs]}`;
|
||||
const context = new TestContext()
|
||||
.withOs(unknownOs);
|
||||
// act
|
||||
const act = async () => { await context.runCode(); };
|
||||
// assert
|
||||
expectThrowsAsync(act, expectedError);
|
||||
describe('throws with invalid OS', () => {
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly invalidOs: OperatingSystem | undefined;
|
||||
readonly expectedError: string;
|
||||
}> = [
|
||||
(() => {
|
||||
const unsupportedOs = OperatingSystem.Android;
|
||||
return {
|
||||
description: 'unsupported OS',
|
||||
invalidOs: unsupportedOs,
|
||||
expectedError: `unsupported os: ${OperatingSystem[unsupportedOs]}`,
|
||||
};
|
||||
})(),
|
||||
{
|
||||
description: 'unknown OS',
|
||||
invalidOs: undefined,
|
||||
expectedError: 'Unidentified operating system',
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({ description, invalidOs, expectedError }) => {
|
||||
it(description, async () => {
|
||||
// arrange
|
||||
const context = new TestContext()
|
||||
.withOs(invalidOs);
|
||||
// act
|
||||
const act = async () => { await context.runCode(); };
|
||||
// assert
|
||||
await expectThrowsAsync(act, expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -204,7 +225,7 @@ class TestContext {
|
||||
|
||||
private fileExtension = 'fileExtension';
|
||||
|
||||
private os = OperatingSystem.Windows;
|
||||
private os: OperatingSystem | undefined = OperatingSystem.Windows;
|
||||
|
||||
private systemOperations: ISystemOperations = new SystemOperationsStub();
|
||||
|
||||
@@ -229,7 +250,7 @@ class TestContext {
|
||||
return this.withSystemOperations(stub);
|
||||
}
|
||||
|
||||
public withOs(os: OperatingSystem) {
|
||||
public withOs(os: OperatingSystem | undefined) {
|
||||
this.os = os;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
|
||||
import { EnvironmentVariablesFactory, EnvironmentVariablesValidator } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { ViteEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables';
|
||||
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('EnvironmentVariablesFactory', () => {
|
||||
describe('instance', () => {
|
||||
@@ -23,7 +24,7 @@ describe('EnvironmentVariablesFactory', () => {
|
||||
});
|
||||
it('validates its instance', () => {
|
||||
// arrange
|
||||
let validatedInstance: IEnvironmentVariables;
|
||||
let validatedInstance: IEnvironmentVariables | undefined;
|
||||
const validatorMock = (instanceToValidate: IEnvironmentVariables) => {
|
||||
validatedInstance = instanceToValidate;
|
||||
};
|
||||
@@ -31,6 +32,7 @@ describe('EnvironmentVariablesFactory', () => {
|
||||
const sut = new TestableEnvironmentVariablesFactory(validatorMock);
|
||||
const actualInstance = sut.instance;
|
||||
// assert
|
||||
expectExists(validatedInstance);
|
||||
expect(actualInstance).to.equal(validatedInstance);
|
||||
});
|
||||
it('throws error if validator fails', () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { EnvironmentVariablesStub } from '@tests/unit/shared/Stubs/EnvironmentVariablesStub';
|
||||
import { validateEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator';
|
||||
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
||||
@@ -28,17 +27,6 @@ describe('EnvironmentVariablesValidator', () => {
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
describe('throws as expected', () => {
|
||||
describe('"missing environment" if environment is not provided', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing environment';
|
||||
const environment = absentValue;
|
||||
// act
|
||||
const act = () => validateEnvironmentVariables(environment);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('"missing keys" if environment has properties with missing values', () => {
|
||||
// arrange
|
||||
const expectedError = 'Environment keys missing: name, homepageUrl';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
|
||||
import { EventSubscriptionStub } from '@tests/unit/shared/Stubs/EventSubscriptionStub';
|
||||
import { itEachAbsentCollectionValue, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||
|
||||
describe('EventSubscriptionCollection', () => {
|
||||
@@ -140,7 +140,7 @@ function describeSubscriptionValidations(
|
||||
handleValue: (subscriptions: IEventSubscription[]) => void,
|
||||
) {
|
||||
describe('throws error if no subscriptions are provided', () => {
|
||||
itEachAbsentCollectionValue((absentValue) => {
|
||||
itEachAbsentCollectionValue<IEventSubscription>((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing subscriptions';
|
||||
|
||||
@@ -149,24 +149,6 @@ function describeSubscriptionValidations(
|
||||
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('throws error if nullish subscriptions are provided', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing subscription in list';
|
||||
const subscriptions = [
|
||||
new EventSubscriptionStub(),
|
||||
absentValue,
|
||||
new EventSubscriptionStub(),
|
||||
];
|
||||
|
||||
// act
|
||||
const act = () => handleValue(subscriptions);
|
||||
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { NumericEntityStub } from '@tests/unit/shared/Stubs/NumericEntityStub';
|
||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('InMemoryRepository', () => {
|
||||
describe('exists', () => {
|
||||
@@ -52,20 +51,6 @@ describe('InMemoryRepository', () => {
|
||||
expect(actual.length).to.equal(expected.length);
|
||||
expect(actual.item).to.deep.equal(expected.item);
|
||||
});
|
||||
describe('throws when item is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing item';
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>();
|
||||
const item = absentValue;
|
||||
|
||||
// act
|
||||
const act = () => sut.addItem(item);
|
||||
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('removeItem removes', () => {
|
||||
// arrange
|
||||
@@ -116,20 +101,6 @@ describe('InMemoryRepository', () => {
|
||||
const actual = sut.getItems();
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
describe('throws when item is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing item';
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>();
|
||||
const item = absentValue;
|
||||
|
||||
// act
|
||||
const act = () => sut.addOrUpdateItem(item);
|
||||
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('getById', () => {
|
||||
it('returns entity if it exists', () => {
|
||||
@@ -144,13 +115,15 @@ describe('InMemoryRepository', () => {
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
it('returns undefined if it does not exist', () => {
|
||||
it('throws if item does not exist', () => {
|
||||
// arrange
|
||||
const id = 31;
|
||||
const expectedError = `missing item: ${id}`;
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>([]);
|
||||
// act
|
||||
const actual = sut.getById(31);
|
||||
const act = () => sut.getById(id);
|
||||
// assert
|
||||
expect(actual).to.equal(undefined);
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('ConsoleLogger', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing console';
|
||||
const console = absentValue;
|
||||
const console = absentValue as never;
|
||||
// act
|
||||
const act = () => new ConsoleLogger(console);
|
||||
// assert
|
||||
@@ -32,10 +32,25 @@ describe('ConsoleLogger', () => {
|
||||
expect(consoleMock.callHistory[0].args).to.deep.equal(expectedParams);
|
||||
});
|
||||
});
|
||||
describe('throws if log function is missing', () => {
|
||||
itEachLoggingMethod((functionName, testParameters) => {
|
||||
// arrange
|
||||
const expectedError = `missing "${functionName}" function`;
|
||||
const consoleMock = {} as Partial<Console>;
|
||||
consoleMock[functionName] = undefined;
|
||||
const logger = new ConsoleLogger(consoleMock);
|
||||
|
||||
// act
|
||||
const act = () => logger[functionName](...testParameters);
|
||||
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class MockConsole
|
||||
extends StubWithObservableMethodCalls<Partial<Console>>
|
||||
extends StubWithObservableMethodCalls<Console>
|
||||
implements Partial<Console> {
|
||||
public info(...args: unknown[]) {
|
||||
this.registerMethodCall({
|
||||
|
||||
@@ -10,31 +10,48 @@ describe('ElectronLogger', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing logger';
|
||||
const electronLog = absentValue;
|
||||
const electronLog = absentValue as never;
|
||||
// act
|
||||
const act = () => createElectronLogger(electronLog);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}, { excludeUndefined: true });
|
||||
});
|
||||
itEachLoggingMethod((functionName, testParameters) => {
|
||||
// arrange
|
||||
const expectedParams = testParameters;
|
||||
const electronLogMock = new MockElectronLog();
|
||||
const logger = createElectronLogger(electronLogMock);
|
||||
describe('throws if log function is missing', () => {
|
||||
itEachLoggingMethod((functionName, testParameters) => {
|
||||
// arrange
|
||||
const expectedError = `missing "${functionName}" function`;
|
||||
const electronLogMock = {} as Partial<ElectronLog>;
|
||||
electronLogMock[functionName] = undefined;
|
||||
const logger = createElectronLogger(electronLogMock);
|
||||
|
||||
// act
|
||||
logger[functionName](...expectedParams);
|
||||
// act
|
||||
const act = () => logger[functionName](...testParameters);
|
||||
|
||||
// assert
|
||||
expect(electronLogMock.callHistory).to.have.lengthOf(1);
|
||||
expect(electronLogMock.callHistory[0].methodName).to.equal(functionName);
|
||||
expect(electronLogMock.callHistory[0].args).to.deep.equal(expectedParams);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('methods log the provided params', () => {
|
||||
itEachLoggingMethod((functionName, testParameters) => {
|
||||
// arrange
|
||||
const expectedParams = testParameters;
|
||||
const electronLogMock = new MockElectronLog();
|
||||
const logger = createElectronLogger(electronLogMock);
|
||||
|
||||
// act
|
||||
logger[functionName](...expectedParams);
|
||||
|
||||
// assert
|
||||
expect(electronLogMock.callHistory).to.have.lengthOf(1);
|
||||
expect(electronLogMock.callHistory[0].methodName).to.equal(functionName);
|
||||
expect(electronLogMock.callHistory[0].args).to.deep.equal(expectedParams);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class MockElectronLog
|
||||
extends StubWithObservableMethodCalls<Partial<ElectronLog>>
|
||||
extends StubWithObservableMethodCalls<ElectronLog>
|
||||
implements Partial<ElectronLog> {
|
||||
public info(...args: unknown[]) {
|
||||
this.registerMethodCall({
|
||||
|
||||
@@ -2,13 +2,15 @@ import { it } from 'vitest';
|
||||
import { FunctionKeys } from '@/TypeHelpers';
|
||||
import { ILogger } from '@/infrastructure/Log/ILogger';
|
||||
|
||||
type TestParameters = [string, number, { some: string }];
|
||||
|
||||
export function itEachLoggingMethod(
|
||||
handler: (
|
||||
functionName: keyof ILogger,
|
||||
testParameters?: unknown[]
|
||||
testParameters: TestParameters,
|
||||
) => void,
|
||||
) {
|
||||
const testParameters = ['test', 123, { some: 'object' }];
|
||||
const testParameters: TestParameters = ['test', 123, { some: 'object' }];
|
||||
const loggerMethods: Array<FunctionKeys<ILogger>> = [
|
||||
'info',
|
||||
];
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('WindowInjectedLogger', () => {
|
||||
// arrange
|
||||
const expectedError = 'missing log';
|
||||
const windowVariables = new WindowVariablesStub()
|
||||
.withLog(absentValue);
|
||||
.withLog(absentValue as never);
|
||||
// act
|
||||
const act = () => new WindowInjectedLogger(windowVariables);
|
||||
// assert
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('BrowserOsDetector', () => {
|
||||
const actual = sut.detect(userAgent);
|
||||
// assert
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
it('detects as expected', () => {
|
||||
BrowserOsTestCases.forEach((testCase) => {
|
||||
@@ -27,7 +27,7 @@ describe('BrowserOsDetector', () => {
|
||||
expect(actual).to.equal(testCase.expectedOs, printMessage());
|
||||
function printMessage(): string {
|
||||
return `Expected: "${OperatingSystem[testCase.expectedOs]}"\n`
|
||||
+ `Actual: "${OperatingSystem[actual]}"\n`
|
||||
+ `Actual: "${actual === undefined ? 'undefined' : OperatingSystem[actual]}"\n`
|
||||
+ `UserAgent: "${testCase.userAgent}"`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('RuntimeEnvironment', () => {
|
||||
const absentWindow = absentValue;
|
||||
// act
|
||||
const act = () => createEnvironment({
|
||||
window: absentWindow,
|
||||
window: absentWindow as never,
|
||||
});
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
@@ -123,7 +123,7 @@ describe('RuntimeEnvironment', () => {
|
||||
// arrange
|
||||
const expectedValue = undefined;
|
||||
const windowWithAbsentOs = {
|
||||
os: absentValue,
|
||||
os: absentValue as never,
|
||||
};
|
||||
// act
|
||||
const sut = createEnvironment({
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('WindowVariablesValidator', () => {
|
||||
itEachInvalidObjectValue((invalidObjectValue) => {
|
||||
// arrange
|
||||
const expectedError = 'window is not an object';
|
||||
const window: Partial<WindowVariables> = invalidObjectValue;
|
||||
const window: Partial<WindowVariables> = invalidObjectValue as never;
|
||||
// act
|
||||
const act = () => validateWindowVariables(window);
|
||||
// assert
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
import { describe } from 'vitest';
|
||||
import { FactoryValidator, FactoryFunction } from '@/infrastructure/RuntimeSanity/Common/FactoryValidator';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('FactoryValidator', () => {
|
||||
describe('ctor', () => {
|
||||
describe('throws when factory is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing factory';
|
||||
const factory = absentValue;
|
||||
// act
|
||||
const act = () => new TestableFactoryValidator(factory);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('collectErrors', () => {
|
||||
it('reports error thrown by factory function', () => {
|
||||
// arrange
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user