Skip to content

Commit

Permalink
Fixed Decider specification compatibility in browser
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed Jun 12, 2024
1 parent ba04864 commit 7d41295
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 9 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ jobs:
- name: Test
run: npm run test

- name: Copy packed Emmett bundle to browser compatibility tests folder
run: cp -r packages/emmett/dist /e2e/browserCompatibility/

- name: Install Playwright Browsers
run: npx playwright install --with-deps

- name: Run Playwright tests
run: npx playwright test

- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 1

- name: Run browser compatibility test
run: npm run test:compatibility:browser

- name: Pack Emmett locally to tar file
shell: bash
run: echo "PACKAGE_FILENAME=$(npm pack --json --pack-destination './e2e/esmCompatibility' -w @event-driven-io/emmett | jq -r '.[] | .filename')" >> $GITHUB_ENV
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
216 changes: 214 additions & 2 deletions src/packages/emmett/src/testing/assertions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { DefaultRecord } from '../typing';
import { deepEquals } from '../utils';

export class AssertionError extends Error {
constructor(message: string) {
super(message);
}
}
export const isSubset = (superObj: unknown, subObj: unknown): boolean => {
const sup = superObj as DefaultRecord;
const sub = subObj as DefaultRecord;
Expand All @@ -12,9 +18,215 @@ export const isSubset = (superObj: unknown, subObj: unknown): boolean => {
});
};

export const assertMatches = (actual: unknown, expected: unknown) => {
export const assertMatches = (
actual: unknown,
expected: unknown,
message?: string,
) => {
if (!isSubset(actual, expected))
throw Error(
`subObj:\n${JSON.stringify(expected)}\nis not subset of\n${JSON.stringify(actual)}`,
message ??
`subObj:\n${JSON.stringify(expected)}\nis not subset of\n${JSON.stringify(actual)}`,
);
};

export const assertDeepEquals = (
actual: unknown,
expected: unknown,
message?: string,
) => {
if (!deepEquals(actual, expected))
throw Error(
message ??
`subObj:\n${JSON.stringify(expected)}\nis equals to\n${JSON.stringify(actual)}`,
);
};

export const assertThat = <T>(item: T) => {
return {
isEqualTo: (other: T) => assertTrue(deepEquals(item, other)),
};
};

export function assertFalse(
condition: boolean,
message?: string,
): asserts condition is false {
if (condition) throw Error(message ?? `Condition is false`);
}

export function assertTrue(
condition: boolean,
message?: string,
): asserts condition is true {
if (!condition) throw Error(message ?? `Condition is false`);
}

export function assertOk<T extends object>(
obj: T | null | undefined,
message?: string,
): asserts obj is T {
if (!obj) throw Error(message ?? `Condition is not truthy`);
}

export function assertEqual<T>(
obj: T | null | undefined,
other: T | null | undefined,
message?: string,
): void {
if (!obj || !other || obj != other)
throw Error(message ?? `Objects are not equal`);
}

export function assertNotEqual<T>(
obj: T | null | undefined,
other: T | null | undefined,
message?: string,
): void {
if (obj === other) throw Error(message ?? `Objects are equal`);
}

export function assertIsNotNull<T extends object>(
result: T | null,
): asserts result is T {
assertNotEqual(result, null);
assertOk(result);
}

export function assertIsNull<T extends object>(
result: T | null,
): asserts result is null {
assertEqual(result, null);
}

type Call = {
arguments: unknown[];
result: unknown;
target: unknown;
this: unknown;
};

export type ArgumentMatcher = (arg: unknown) => boolean;

export const argValue =
<T>(value: T): ArgumentMatcher =>
(arg) =>
deepEquals(arg, value);

export const argMatches =
<T>(matches: (arg: T) => boolean): ArgumentMatcher =>
(arg) =>
matches(arg as T);

// eslint-disable-next-line @typescript-eslint/ban-types
export type MockedFunction = Function & { mock?: { calls: Call[] } };

export function verifyThat(fn: MockedFunction) {
return {
calledTimes: (times: number) => {
assertEqual(fn.mock?.calls?.length, times);
},
notCalled: () => {
assertEqual(fn?.mock?.calls?.length, 0);
},
called: () => {
assertTrue(
fn.mock?.calls.length !== undefined && fn.mock.calls.length > 0,
);
},
calledWith: (...args: unknown[]) => {
assertTrue(
fn.mock?.calls.length !== undefined &&
fn.mock.calls.length >= 1 &&
fn.mock.calls.some((call) => deepEquals(call.arguments, args)),
);
},
calledOnceWith: (...args: unknown[]) => {
assertTrue(
fn.mock?.calls.length !== undefined &&
fn.mock.calls.length === 1 &&
fn.mock.calls.some((call) => deepEquals(call.arguments, args)),
);
},
calledWithArgumentMatching: (...matches: ArgumentMatcher[]) => {
assertTrue(
fn.mock?.calls.length !== undefined && fn.mock.calls.length >= 1,
);
assertTrue(
fn.mock?.calls.length !== undefined &&
fn.mock.calls.length >= 1 &&
fn.mock.calls.some(
(call) =>
call.arguments &&
call.arguments.length >= matches.length &&
matches.every((match, index) => match(call.arguments[index])),
),
);
},
notCalledWithArgumentMatching: (...matches: ArgumentMatcher[]) => {
assertFalse(
fn.mock?.calls.length !== undefined &&
fn.mock.calls.length >= 1 &&
fn.mock.calls[0]!.arguments &&
fn.mock.calls[0]!.arguments.length >= matches.length &&
matches.every((match, index) =>
match(fn.mock!.calls[0]!.arguments[index]),
),
);
},
};
}

export const assertThatArray = <T>(array: T[]) => {
return {
isEmpty: () => assertEqual(array.length, 0),
hasSize: (length: number) => assertEqual(array.length, length),
containsElements: (...other: T[]) => {
assertTrue(other.every((ts) => other.some((o) => deepEquals(ts, o))));
},
containsExactlyInAnyOrder: (...other: T[]) => {
assertEqual(array.length, other.length);
assertTrue(array.every((ts) => other.some((o) => deepEquals(ts, o))));
},
containsExactlyInAnyOrderElementsOf: (other: T[]) => {
assertEqual(array.length, other.length);
assertTrue(array.every((ts) => other.some((o) => deepEquals(ts, o))));
},
containsExactlyElementsOf: (other: T[]) => {
assertEqual(array.length, other.length);
for (let i = 0; i < array.length; i++) {
assertTrue(deepEquals(array[i], other[i]));
}
},
containsExactly: (elem: T) => {
assertEqual(array.length, 1);
assertTrue(deepEquals(array[0], elem));
},
contains: (elem: T) => {
assertTrue(array.some((a) => deepEquals(a, elem)));
},
containsOnlyOnceElementsOf: (other: T[]) => {
assertTrue(
other
.map((o) => array.filter((a) => deepEquals(a, o)).length)
.filter((a) => a === 1).length === other.length,
);
},
containsAnyOf: (...other: T[]) => {
assertTrue(array.some((a) => other.some((o) => deepEquals(a, o))));
},
allMatch: (matches: (item: T) => boolean) => {
assertTrue(array.every(matches));
},
anyMatches: (matches: (item: T) => boolean) => {
assertTrue(array.some(matches));
},
allMatchAsync: async (
matches: (item: T) => Promise<boolean>,
): Promise<void> => {
for (const item of array) {
assertTrue(await matches(item));
}
},
};
};
14 changes: 7 additions & 7 deletions src/packages/emmett/src/testing/deciderSpecification.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert, { AssertionError } from 'assert';
import { isErrorConstructor, type ErrorConstructor } from '../errors';
import { AssertionError, assertMatches, assertTrue } from './assertions';

type ErrorCheck<ErrorType> = (error: ErrorType) => boolean;

Expand Down Expand Up @@ -58,35 +58,35 @@ export const DeciderSpecification = {
? expectedEvents
: [expectedEvents];

assert.deepEqual(resultEventsArray, expectedEventsArray);
assertMatches(resultEventsArray, expectedEventsArray);
},
thenThrows: <ErrorType extends Error>(
...args: Parameters<ThenThrows<ErrorType>>
): void => {
try {
handle();
assert.fail('Handler did not fail as expected');
throw new Error('Handler did not fail as expected');
} catch (error) {
if (error instanceof AssertionError) throw error;

if (args.length === 0) return;

if (!isErrorConstructor(args[0])) {
assert.ok(
assertTrue(
args[0](error as ErrorType),
`Error didn't match the error condition: ${error?.toString()}`,
);
return;
}

assert.ok(
assertTrue(
error instanceof args[0],
`Caught error is not an instance of the expected type: ${error?.toString()}`,
);

if (args[1]) {
assert.ok(
args[1](error),
assertTrue(
args[1](error as ErrorType),
`Error didn't match the error condition: ${error?.toString()}`,
);
}
Expand Down
56 changes: 56 additions & 0 deletions src/packages/emmett/src/utils/deepEquals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export const deepEquals = <T>(left: T, right: T): boolean => {
if (isEquatable(left)) {
return left.equals(right);
}

if (Array.isArray(left)) {
return (
Array.isArray(right) &&
left.length === right.length &&
left.every((val, index) => deepEquals(val, right[index]))
);
}

if (
typeof left !== 'object' ||
typeof right !== 'object' ||
left === null ||
right === null
) {
return left === right;
}

if (Array.isArray(right)) return false;

const keys1 = Object.keys(left);
const keys2 = Object.keys(right);

if (
keys1.length !== keys2.length ||
!keys1.every((key) => keys2.includes(key))
)
return false;

for (const key in left) {
if (left[key] instanceof Function && right[key] instanceof Function)
continue;

const isEqual = deepEquals(left[key], right[key]);
if (!isEqual) {
return false;
}
}

return true;
};

export type Equatable<T> = { equals: (right: T) => boolean } & T;

export const isEquatable = <T>(left: T): left is Equatable<T> => {
return (
left &&
typeof left === 'object' &&
'equals' in left &&
typeof left['equals'] === 'function'
);
};
1 change: 1 addition & 0 deletions src/packages/emmett/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './deepEquals';
export * from './iterators';
export * from './merge';

Expand Down

0 comments on commit 7d41295

Please sign in to comment.