Skip to content

Commit 4f60290

Browse files
committed
fix: handle edge cases
1 parent 4240a45 commit 4f60290

5 files changed

Lines changed: 614 additions & 318 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"test": "vp test",
2626
"test:coverage": "vp test run --coverage",
2727
"test:bun": "bun test src",
28-
"test:deno": "deno test --unstable-sloppy-imports --allow-net src",
28+
"test:deno": "deno test --unstable-sloppy-imports --allow-net --allow-env src",
29+
"test:live": "RUN_LIVE_CEP_TESTS=1 vp test src/get-address-info-by-cep/get-address-info-by-cep.test.ts",
2930
"test:edge-browser": "vp test --browser=edge",
3031
"test:chrome-browser": "vp test --browser=chrome",
3132
"test:safari-browser": "vp test --browser=safari --browser.headless=false",

src/_internals/test/globals.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
declare const Deno: {
2+
readonly env: {
3+
get(key: string): string | undefined;
4+
};
5+
readonly test: (options: {
6+
name: string;
7+
fn: () => void | Promise<void>;
8+
sanitizeOps?: boolean;
9+
sanitizeResources?: boolean;
10+
sanitizeExit?: boolean;
11+
}) => void;
12+
};
13+
14+
declare global {
15+
var RUN_LIVE_CEP_TESTS: string | number | undefined;
16+
interface GlobalThis {
17+
RUN_LIVE_CEP_TESTS?: string | number;
18+
}
19+
}

src/_internals/test/runtime-deno.ts

Lines changed: 161 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import { assertEquals, assertExists, assertInstanceOf, assertObjectMatch } from "jsr:@std/assert";
2-
import { afterEach, beforeEach, describe, it } from "jsr:@std/testing/bdd";
1+
/// <reference path="./globals.d.ts" />
2+
3+
type TestCallback = () => void | Promise<void>;
4+
5+
type Suite = {
6+
name: string;
7+
afterEach: TestCallback[];
8+
beforeEach: TestCallback[];
9+
};
310

411
type MockImplementation<TArgs extends unknown[] = unknown[], TResult = unknown> = (
512
...args: TArgs
@@ -9,6 +16,7 @@ type MockFunction<TArgs extends unknown[] = unknown[], TResult = unknown> = ((
916
...args: TArgs
1017
) => TResult) & {
1118
mockClear: () => void;
19+
mockRejectedValueOnce: (value: unknown) => MockFunction<TArgs, TResult>;
1220
mockResolvedValueOnce: (value: Awaited<TResult>) => MockFunction<TArgs, TResult>;
1321
mockImplementation: (
1422
implementation: MockImplementation<TArgs, TResult>,
@@ -18,6 +26,7 @@ type MockFunction<TArgs extends unknown[] = unknown[], TResult = unknown> = ((
1826
type AnyMockFunction = MockFunction<any[], any>;
1927

2028
const registeredMocks = new Set<AnyMockFunction>();
29+
const suiteStack: Suite[] = [];
2130

2231
function isRecord(value: unknown): value is Record<string, unknown> {
2332
return typeof value === "object" && value !== null;
@@ -27,6 +36,51 @@ function createAssertionError(message: string): Error {
2736
return new Error(message);
2837
}
2938

39+
function deepEqual(a: unknown, b: unknown): boolean {
40+
if (Object.is(a, b)) {
41+
return true;
42+
}
43+
44+
if (a instanceof Date && b instanceof Date) {
45+
return a.getTime() === b.getTime();
46+
}
47+
48+
if (Array.isArray(a) && Array.isArray(b)) {
49+
return a.length === b.length && a.every((value, index) => deepEqual(value, b[index]));
50+
}
51+
52+
if (isRecord(a) && isRecord(b)) {
53+
const aKeys = Object.keys(a);
54+
const bKeys = Object.keys(b);
55+
56+
return (
57+
aKeys.length === bKeys.length &&
58+
aKeys.every((key) => bKeys.includes(key) && deepEqual(a[key], b[key]))
59+
);
60+
}
61+
62+
return false;
63+
}
64+
65+
function objectMatches(
66+
actual: Record<string, unknown>,
67+
expected: Record<string, unknown>,
68+
): boolean {
69+
return Object.entries(expected).every(([key, value]) => {
70+
if (!(key in actual)) {
71+
return false;
72+
}
73+
74+
const actualValue = actual[key];
75+
76+
if (isRecord(value) && isRecord(actualValue)) {
77+
return objectMatches(actualValue, value);
78+
}
79+
80+
return deepEqual(actualValue, value);
81+
});
82+
}
83+
3084
function createMock<TArgs extends unknown[] = unknown[], TResult = unknown>(
3185
implementation?: MockImplementation<TArgs, TResult>,
3286
): MockFunction<TArgs, TResult> {
@@ -55,6 +109,12 @@ function createMock<TArgs extends unknown[] = unknown[], TResult = unknown>(
55109
return mockFn;
56110
};
57111

112+
mockFn.mockRejectedValueOnce = (value: unknown) => {
113+
queue.push(() => Promise.reject(value) as TResult);
114+
115+
return mockFn;
116+
};
117+
58118
mockFn.mockImplementation = (nextImplementation: MockImplementation<TArgs, TResult>) => {
59119
implementation = nextImplementation;
60120

@@ -74,10 +134,14 @@ function createExpect(actual: unknown) {
74134
}
75135
},
76136
toEqual(expected: unknown) {
77-
assertEquals(actual, expected);
137+
if (!deepEqual(actual, expected)) {
138+
throw createAssertionError("Expected values to be deeply equal");
139+
}
78140
},
79141
toStrictEqual(expected: unknown) {
80-
assertEquals(actual, expected);
142+
if (!deepEqual(actual, expected)) {
143+
throw createAssertionError("Expected values to be strictly equal");
144+
}
81145
},
82146
toContain(expected: unknown) {
83147
if (typeof actual === "string") {
@@ -97,17 +161,7 @@ function createExpect(actual: unknown) {
97161
throw createAssertionError("Expected value to be an array");
98162
}
99163

100-
const found = actual.some((value) => {
101-
try {
102-
assertEquals(value, expected);
103-
104-
return true;
105-
} catch {
106-
return false;
107-
}
108-
});
109-
110-
if (!found) {
164+
if (!actual.some((value) => deepEqual(value, expected))) {
111165
throw createAssertionError("Expected array to contain a deeply equal value");
112166
}
113167
},
@@ -128,10 +182,24 @@ function createExpect(actual: unknown) {
128182
}
129183
},
130184
toBeDefined() {
131-
assertExists(actual);
185+
if (actual === undefined || actual === null) {
186+
throw createAssertionError("Expected value to be defined");
187+
}
188+
},
189+
toBeUndefined() {
190+
if (actual !== undefined) {
191+
throw createAssertionError(`Expected ${String(actual)} to be undefined`);
192+
}
193+
},
194+
toBeTruthy() {
195+
if (!actual) {
196+
throw createAssertionError(`Expected ${String(actual)} to be truthy`);
197+
}
132198
},
133199
toBeInstanceOf(expected: new (...args: any[]) => unknown) {
134-
assertInstanceOf(actual, expected);
200+
if (!(actual instanceof expected)) {
201+
throw createAssertionError(`Expected value to be instance of ${expected.name}`);
202+
}
135203
},
136204
toBeGreaterThan(expected: number) {
137205
if (!(typeof actual === "number" && actual > expected)) {
@@ -153,20 +221,18 @@ function createExpect(actual: unknown) {
153221
}
154222
},
155223
toMatchObject(expected: Record<string, unknown>) {
156-
if (!isRecord(actual)) {
157-
throw createAssertionError("Expected value to be an object");
224+
if (!isRecord(actual) || !objectMatches(actual, expected)) {
225+
throw createAssertionError("Expected object to match");
158226
}
159-
160-
assertObjectMatch(actual, expected);
161227
},
162228
get rejects() {
163229
return {
164230
async toThrow(expected?: new (...args: any[]) => unknown) {
165231
try {
166232
await (actual as Promise<unknown>);
167233
} catch (error) {
168-
if (expected) {
169-
assertInstanceOf(error, expected);
234+
if (expected && !(error instanceof expected)) {
235+
throw createAssertionError(`Expected error to be instance of ${expected.name}`);
170236
}
171237

172238
return;
@@ -179,7 +245,76 @@ function createExpect(actual: unknown) {
179245
};
180246
}
181247

182-
export { afterEach, beforeEach, describe, it };
248+
async function runHooks(hooks: TestCallback[]) {
249+
for (const hook of hooks) {
250+
await hook();
251+
}
252+
}
253+
254+
function currentSuiteChain() {
255+
return [...suiteStack];
256+
}
257+
258+
type DescribeFunction = ((name: string, callback: TestCallback) => void) & {
259+
skip: (name: string, callback: TestCallback) => void;
260+
};
261+
262+
const describe: DescribeFunction = (name, callback) => {
263+
suiteStack.push({
264+
afterEach: [],
265+
beforeEach: [],
266+
name,
267+
});
268+
269+
try {
270+
callback();
271+
} finally {
272+
suiteStack.pop();
273+
}
274+
};
275+
276+
describe.skip = () => {};
277+
278+
export function beforeEach(callback: TestCallback) {
279+
const currentSuite = suiteStack.at(-1);
280+
281+
if (!currentSuite) {
282+
throw new Error("beforeEach must be used inside describe");
283+
}
284+
285+
currentSuite.beforeEach.push(callback);
286+
}
287+
288+
export function afterEach(callback: TestCallback) {
289+
const currentSuite = suiteStack.at(-1);
290+
291+
if (!currentSuite) {
292+
throw new Error("afterEach must be used inside describe");
293+
}
294+
295+
currentSuite.afterEach.push(callback);
296+
}
297+
298+
export function it(name: string, callback: TestCallback, timeout?: number) {
299+
const suites = currentSuiteChain();
300+
const testName = [...suites.map((suite) => suite.name), name].join(" > ");
301+
302+
Deno.test({
303+
fn: async () => {
304+
await runHooks(suites.flatMap((suite) => suite.beforeEach));
305+
306+
try {
307+
await callback();
308+
} finally {
309+
await runHooks([...suites].reverse().flatMap((suite) => suite.afterEach));
310+
}
311+
},
312+
name: testName,
313+
sanitizeOps: false,
314+
sanitizeResources: false,
315+
...(timeout ? { sanitizeExit: false } : {}),
316+
});
317+
}
183318

184319
export const test = it;
185320

@@ -193,3 +328,5 @@ export const vi = {
193328
}
194329
},
195330
};
331+
332+
export { describe };

0 commit comments

Comments
 (0)