Skip to content

Commit 28a9126

Browse files
committed
Fixed Decider specification compatibility in browser
1 parent ba04864 commit 28a9126

File tree

5 files changed

+297
-9
lines changed

5 files changed

+297
-9
lines changed

.github/workflows/build_and_test.yml

+19
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,25 @@ jobs:
5353
- name: Test
5454
run: npm run test
5555

56+
- name: Copy packed Emmett bundle to browser compatibility tests folder
57+
run: cp -r packages/emmett/dist /e2e/browserCompatibility/
58+
59+
- name: Install Playwright Browsers
60+
run: npx playwright install --with-deps
61+
62+
- name: Run Playwright tests
63+
run: npx playwright test
64+
65+
- uses: actions/upload-artifact@v4
66+
if: always()
67+
with:
68+
name: playwright-report
69+
path: playwright-report/
70+
retention-days: 1
71+
72+
- name: Run browser compatibility test
73+
run: npm run test:compatibility:browser
74+
5675
- name: Pack Emmett locally to tar file
5776
shell: bash
5877
run: echo "PACKAGE_FILENAME=$(npm pack --json --pack-destination './e2e/esmCompatibility' -w @event-driven-io/emmett | jq -r '.[] | .filename')" >> $GITHUB_ENV

src/packages/emmett/src/testing/assertions.ts

+214-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { DefaultRecord } from '../typing';
2+
import { deepEquals } from '../utils';
23

4+
export class AssertionError extends Error {
5+
constructor(message: string) {
6+
super(message);
7+
}
8+
}
39
export const isSubset = (superObj: unknown, subObj: unknown): boolean => {
410
const sup = superObj as DefaultRecord;
511
const sub = subObj as DefaultRecord;
@@ -12,9 +18,215 @@ export const isSubset = (superObj: unknown, subObj: unknown): boolean => {
1218
});
1319
};
1420

15-
export const assertMatches = (actual: unknown, expected: unknown) => {
21+
export const assertMatches = (
22+
actual: unknown,
23+
expected: unknown,
24+
message?: string,
25+
) => {
1626
if (!isSubset(actual, expected))
1727
throw Error(
18-
`subObj:\n${JSON.stringify(expected)}\nis not subset of\n${JSON.stringify(actual)}`,
28+
message ??
29+
`subObj:\n${JSON.stringify(expected)}\nis not subset of\n${JSON.stringify(actual)}`,
1930
);
2031
};
32+
33+
export const assertDeepEquals = (
34+
actual: unknown,
35+
expected: unknown,
36+
message?: string,
37+
) => {
38+
if (!deepEquals(actual, expected))
39+
throw Error(
40+
message ??
41+
`subObj:\n${JSON.stringify(expected)}\nis equals to\n${JSON.stringify(actual)}`,
42+
);
43+
};
44+
45+
export const assertThat = <T>(item: T) => {
46+
return {
47+
isEqualTo: (other: T) => assertTrue(deepEquals(item, other)),
48+
};
49+
};
50+
51+
export function assertFalse(
52+
condition: boolean,
53+
message?: string,
54+
): asserts condition is false {
55+
if (condition) throw Error(message ?? `Condition is false`);
56+
}
57+
58+
export function assertTrue(
59+
condition: boolean,
60+
message?: string,
61+
): asserts condition is true {
62+
if (!condition) throw Error(message ?? `Condition is false`);
63+
}
64+
65+
export function assertOk<T extends object>(
66+
obj: T | null | undefined,
67+
message?: string,
68+
): asserts obj is T {
69+
if (!obj) throw Error(message ?? `Condition is not truthy`);
70+
}
71+
72+
export function assertEqual<T>(
73+
obj: T | null | undefined,
74+
other: T | null | undefined,
75+
message?: string,
76+
): void {
77+
if (!obj || !other || obj != other)
78+
throw Error(message ?? `Objects are not equal`);
79+
}
80+
81+
export function assertNotEqual<T>(
82+
obj: T | null | undefined,
83+
other: T | null | undefined,
84+
message?: string,
85+
): void {
86+
if (obj === other) throw Error(message ?? `Objects are equal`);
87+
}
88+
89+
export function assertIsNotNull<T extends object>(
90+
result: T | null,
91+
): asserts result is T {
92+
assertNotEqual(result, null);
93+
assertOk(result);
94+
}
95+
96+
export function assertIsNull<T extends object>(
97+
result: T | null,
98+
): asserts result is null {
99+
assertEqual(result, null);
100+
}
101+
102+
type Call = {
103+
arguments: unknown[];
104+
result: unknown;
105+
target: unknown;
106+
this: unknown;
107+
};
108+
109+
export type ArgumentMatcher = (arg: unknown) => boolean;
110+
111+
export const argValue =
112+
<T>(value: T): ArgumentMatcher =>
113+
(arg) =>
114+
deepEquals(arg, value);
115+
116+
export const argMatches =
117+
<T>(matches: (arg: T) => boolean): ArgumentMatcher =>
118+
(arg) =>
119+
matches(arg as T);
120+
121+
// eslint-disable-next-line @typescript-eslint/ban-types
122+
export type MockedFunction = Function & { mock?: { calls: Call[] } };
123+
124+
export function verifyThat(fn: MockedFunction) {
125+
return {
126+
calledTimes: (times: number) => {
127+
assertEqual(fn.mock?.calls?.length, times);
128+
},
129+
notCalled: () => {
130+
assertEqual(fn?.mock?.calls?.length, 0);
131+
},
132+
called: () => {
133+
assertTrue(
134+
fn.mock?.calls.length !== undefined && fn.mock.calls.length > 0,
135+
);
136+
},
137+
calledWith: (...args: unknown[]) => {
138+
assertTrue(
139+
fn.mock?.calls.length !== undefined &&
140+
fn.mock.calls.length >= 1 &&
141+
fn.mock.calls.some((call) => deepEquals(call.arguments, args)),
142+
);
143+
},
144+
calledOnceWith: (...args: unknown[]) => {
145+
assertTrue(
146+
fn.mock?.calls.length !== undefined &&
147+
fn.mock.calls.length === 1 &&
148+
fn.mock.calls.some((call) => deepEquals(call.arguments, args)),
149+
);
150+
},
151+
calledWithArgumentMatching: (...matches: ArgumentMatcher[]) => {
152+
assertTrue(
153+
fn.mock?.calls.length !== undefined && fn.mock.calls.length >= 1,
154+
);
155+
assertTrue(
156+
fn.mock?.calls.length !== undefined &&
157+
fn.mock.calls.length >= 1 &&
158+
fn.mock.calls.some(
159+
(call) =>
160+
call.arguments &&
161+
call.arguments.length >= matches.length &&
162+
matches.every((match, index) => match(call.arguments[index])),
163+
),
164+
);
165+
},
166+
notCalledWithArgumentMatching: (...matches: ArgumentMatcher[]) => {
167+
assertFalse(
168+
fn.mock?.calls.length !== undefined &&
169+
fn.mock.calls.length >= 1 &&
170+
fn.mock.calls[0]!.arguments &&
171+
fn.mock.calls[0]!.arguments.length >= matches.length &&
172+
matches.every((match, index) =>
173+
match(fn.mock!.calls[0]!.arguments[index]),
174+
),
175+
);
176+
},
177+
};
178+
}
179+
180+
export const assertThatArray = <T>(array: T[]) => {
181+
return {
182+
isEmpty: () => assertEqual(array.length, 0),
183+
hasSize: (length: number) => assertEqual(array.length, length),
184+
containsElements: (...other: T[]) => {
185+
assertTrue(other.every((ts) => other.some((o) => deepEquals(ts, o))));
186+
},
187+
containsExactlyInAnyOrder: (...other: T[]) => {
188+
assertEqual(array.length, other.length);
189+
assertTrue(array.every((ts) => other.some((o) => deepEquals(ts, o))));
190+
},
191+
containsExactlyInAnyOrderElementsOf: (other: T[]) => {
192+
assertEqual(array.length, other.length);
193+
assertTrue(array.every((ts) => other.some((o) => deepEquals(ts, o))));
194+
},
195+
containsExactlyElementsOf: (other: T[]) => {
196+
assertEqual(array.length, other.length);
197+
for (let i = 0; i < array.length; i++) {
198+
assertTrue(deepEquals(array[i], other[i]));
199+
}
200+
},
201+
containsExactly: (elem: T) => {
202+
assertEqual(array.length, 1);
203+
assertTrue(deepEquals(array[0], elem));
204+
},
205+
contains: (elem: T) => {
206+
assertTrue(array.some((a) => deepEquals(a, elem)));
207+
},
208+
containsOnlyOnceElementsOf: (other: T[]) => {
209+
assertTrue(
210+
other
211+
.map((o) => array.filter((a) => deepEquals(a, o)).length)
212+
.filter((a) => a === 1).length === other.length,
213+
);
214+
},
215+
containsAnyOf: (...other: T[]) => {
216+
assertTrue(array.some((a) => other.some((o) => deepEquals(a, o))));
217+
},
218+
allMatch: (matches: (item: T) => boolean) => {
219+
assertTrue(array.every(matches));
220+
},
221+
anyMatches: (matches: (item: T) => boolean) => {
222+
assertTrue(array.some(matches));
223+
},
224+
allMatchAsync: async (
225+
matches: (item: T) => Promise<boolean>,
226+
): Promise<void> => {
227+
for (const item of array) {
228+
assertTrue(await matches(item));
229+
}
230+
},
231+
};
232+
};

src/packages/emmett/src/testing/deciderSpecification.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import assert, { AssertionError } from 'assert';
21
import { isErrorConstructor, type ErrorConstructor } from '../errors';
2+
import { AssertionError, assertMatches, assertTrue } from './assertions';
33

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

@@ -58,35 +58,35 @@ export const DeciderSpecification = {
5858
? expectedEvents
5959
: [expectedEvents];
6060

61-
assert.deepEqual(resultEventsArray, expectedEventsArray);
61+
assertMatches(resultEventsArray, expectedEventsArray);
6262
},
6363
thenThrows: <ErrorType extends Error>(
6464
...args: Parameters<ThenThrows<ErrorType>>
6565
): void => {
6666
try {
6767
handle();
68-
assert.fail('Handler did not fail as expected');
68+
throw new Error('Handler did not fail as expected');
6969
} catch (error) {
7070
if (error instanceof AssertionError) throw error;
7171

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

7474
if (!isErrorConstructor(args[0])) {
75-
assert.ok(
75+
assertTrue(
7676
args[0](error as ErrorType),
7777
`Error didn't match the error condition: ${error?.toString()}`,
7878
);
7979
return;
8080
}
8181

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

8787
if (args[1]) {
88-
assert.ok(
89-
args[1](error),
88+
assertTrue(
89+
args[1](error as ErrorType),
9090
`Error didn't match the error condition: ${error?.toString()}`,
9191
);
9292
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
export const deepEquals = <T>(left: T, right: T): boolean => {
2+
if (isEquatable(left)) {
3+
return left.equals(right);
4+
}
5+
6+
if (Array.isArray(left)) {
7+
return (
8+
Array.isArray(right) &&
9+
left.length === right.length &&
10+
left.every((val, index) => deepEquals(val, right[index]))
11+
);
12+
}
13+
14+
if (
15+
typeof left !== 'object' ||
16+
typeof right !== 'object' ||
17+
left === null ||
18+
right === null
19+
) {
20+
return left === right;
21+
}
22+
23+
if (Array.isArray(right)) return false;
24+
25+
const keys1 = Object.keys(left);
26+
const keys2 = Object.keys(right);
27+
28+
if (
29+
keys1.length !== keys2.length ||
30+
!keys1.every((key) => keys2.includes(key))
31+
)
32+
return false;
33+
34+
for (const key in left) {
35+
if (left[key] instanceof Function && right[key] instanceof Function)
36+
continue;
37+
38+
const isEqual = deepEquals(left[key], right[key]);
39+
if (!isEqual) {
40+
return false;
41+
}
42+
}
43+
44+
return true;
45+
};
46+
47+
export type Equatable<T> = { equals: (right: T) => boolean } & T;
48+
49+
export const isEquatable = <T>(left: T): left is Equatable<T> => {
50+
return (
51+
left &&
52+
typeof left === 'object' &&
53+
'equals' in left &&
54+
typeof left['equals'] === 'function'
55+
);
56+
};

src/packages/emmett/src/utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './deepEquals';
12
export * from './iterators';
23
export * from './merge';
34

0 commit comments

Comments
 (0)