@digitaldefiance/express-suite-test-utils 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Digital Defiance
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -157,12 +157,194 @@ describe('User model', () => {
157
157
 
158
158
  **Note:** Requires `mongoose` as a peer dependency and `mongodb-memory-server` as a dependency (already included).
159
159
 
160
+ ## Testing Approach
161
+
162
+ This package provides comprehensive testing utilities for Express Suite projects, including custom Jest matchers, console mocks, database helpers, and more.
163
+
164
+ ### Test Utilities Overview
165
+
166
+ **Custom Matchers**: `toThrowType` for type-safe error testing
167
+ **Console Mocks**: Mock and spy on console methods
168
+ **Direct Log Mocks**: Mock `fs.writeSync` for stdout/stderr testing
169
+ **Database Helpers**: MongoDB Memory Server integration
170
+ **React Mocks**: Mock React components and hooks
171
+
172
+ ### Usage Patterns
173
+
174
+ #### Using toThrowType Matcher
175
+
176
+ ```typescript
177
+ import '@digitaldefiance/express-suite-test-utils';
178
+
179
+ class CustomError extends Error {
180
+ constructor(public code: number) {
181
+ super('Custom error');
182
+ }
183
+ }
184
+
185
+ describe('Error Testing', () => {
186
+ it('should throw specific error type', () => {
187
+ expect(() => {
188
+ throw new CustomError(404);
189
+ }).toThrowType(CustomError);
190
+ });
191
+
192
+ it('should validate error properties', () => {
193
+ expect(() => {
194
+ throw new CustomError(404);
195
+ }).toThrowType(CustomError, (error) => {
196
+ expect(error.code).toBe(404);
197
+ });
198
+ });
199
+ });
200
+ ```
201
+
202
+ #### Using Console Mocks
203
+
204
+ ```typescript
205
+ import { withConsoleMocks, spyContains } from '@digitaldefiance/express-suite-test-utils';
206
+
207
+ describe('Console Output', () => {
208
+ it('should capture console.log', async () => {
209
+ await withConsoleMocks({ mute: true }, async (spies) => {
210
+ console.log('test message');
211
+
212
+ expect(spies.log).toHaveBeenCalledWith('test message');
213
+ expect(spyContains(spies.log, 'test', 'message')).toBe(true);
214
+ });
215
+ });
216
+
217
+ it('should capture console.error', async () => {
218
+ await withConsoleMocks({ mute: true }, async (spies) => {
219
+ console.error('error message');
220
+
221
+ expect(spies.error).toHaveBeenCalledWith('error message');
222
+ });
223
+ });
224
+ });
225
+ ```
226
+
227
+ #### Using Direct Log Mocks
228
+
229
+ ```typescript
230
+ import { withDirectLogMocks, directLogContains, getDirectLogMessages } from '@digitaldefiance/express-suite-test-utils';
231
+ import * as fs from 'fs';
232
+
233
+ // Mock fs at module level
234
+ jest.mock('fs', () => ({
235
+ ...jest.requireActual('fs'),
236
+ writeSync: jest.fn(),
237
+ }));
238
+
239
+ describe('Direct Logging', () => {
240
+ it('should capture stdout writes', async () => {
241
+ await withDirectLogMocks({ mute: true }, async (spies) => {
242
+ const buffer = Buffer.from('hello world\n', 'utf8');
243
+ fs.writeSync(1, buffer); // stdout
244
+
245
+ expect(directLogContains(spies.writeSync, 1, 'hello', 'world')).toBe(true);
246
+ expect(getDirectLogMessages(spies.writeSync, 1)).toEqual(['hello world\n']);
247
+ });
248
+ });
249
+ });
250
+ ```
251
+
252
+ #### Using MongoDB Memory Server
253
+
254
+ ```typescript
255
+ import { connectMemoryDB, disconnectMemoryDB, clearMemoryDB } from '@digitaldefiance/express-suite-test-utils';
256
+ import { User } from './models/user';
257
+
258
+ describe('Database Tests', () => {
259
+ beforeAll(async () => {
260
+ await connectMemoryDB();
261
+ });
262
+
263
+ afterAll(async () => {
264
+ await disconnectMemoryDB();
265
+ });
266
+
267
+ afterEach(async () => {
268
+ await clearMemoryDB();
269
+ });
270
+
271
+ it('should validate user schema', async () => {
272
+ const user = new User({
273
+ username: 'test',
274
+ email: 'test@example.com'
275
+ });
276
+
277
+ await user.validate(); // Real Mongoose validation!
278
+ await user.save();
279
+
280
+ const found = await User.findOne({ username: 'test' });
281
+ expect(found).toBeDefined();
282
+ });
283
+
284
+ it('should reject invalid data', async () => {
285
+ const invalid = new User({ username: 'ab' }); // too short
286
+
287
+ await expect(invalid.validate()).rejects.toThrow();
288
+ });
289
+ });
290
+ ```
291
+
292
+ ### Testing Best Practices
293
+
294
+ 1. **Always clean up** after tests (disconnect DB, restore mocks)
295
+ 2. **Use memory database** for fast, isolated tests
296
+ 3. **Mock external dependencies** to avoid side effects
297
+ 4. **Test error conditions** with `toThrowType` matcher
298
+ 5. **Capture console output** when testing logging behavior
299
+
300
+ ### Cross-Package Testing
301
+
302
+ These utilities are designed to work seamlessly with all Express Suite packages:
303
+
304
+ ```typescript
305
+ import { connectMemoryDB, disconnectMemoryDB } from '@digitaldefiance/express-suite-test-utils';
306
+ import { Application } from '@digitaldefiance/node-express-suite';
307
+ import { UserService } from '@digitaldefiance/node-express-suite';
308
+
309
+ describe('Integration Tests', () => {
310
+ let app: Application;
311
+
312
+ beforeAll(async () => {
313
+ await connectMemoryDB();
314
+ app = new Application({
315
+ mongoUri: global.__MONGO_URI__,
316
+ jwtSecret: 'test-secret'
317
+ });
318
+ });
319
+
320
+ afterAll(async () => {
321
+ await app.stop();
322
+ await disconnectMemoryDB();
323
+ });
324
+
325
+ it('should create and find user', async () => {
326
+ const userService = new UserService(app);
327
+ const user = await userService.create({
328
+ username: 'alice',
329
+ email: 'alice@example.com'
330
+ });
331
+
332
+ const found = await userService.findByUsername('alice');
333
+ expect(found).toBeDefined();
334
+ });
335
+ });
336
+ ```
337
+
160
338
  ## License
161
339
 
162
340
  MIT
163
341
 
164
342
  ## ChangeLog
165
343
 
344
+ ### v1.0.11
345
+
346
+ - Fix mongoose to use @digitaldefiance/mongoose-types
347
+
166
348
  ### v1.0.10
167
349
 
168
350
  - Fix direct-log mocks to work with non-configurable fs.writeSync in newer Node.js versions
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@digitaldefiance/express-suite-test-utils",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
+ "homepage": "https://github.com/Digital-Defiance/test-utils",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Digital-Defiance/test-utils.git"
8
+ },
4
9
  "description": "Test utilities for Digital Defiance Express Suite",
5
10
  "main": "src/index.js",
6
11
  "types": "src/index.d.ts",
@@ -46,6 +51,5 @@
46
51
  "devDependencies": {
47
52
  "@types/node": "^22.0.0",
48
53
  "mongoose": "^8.9.3"
49
- },
50
- "type": "commonjs"
51
- }
54
+ }
55
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './lib/to-throw-type';
2
+ export * from './lib/console';
3
+ export * from './lib/direct-log';
4
+ export * from './lib/localStorage-mock';
5
+ export * from './lib/bson-mock';
6
+ export * from './lib/react-mocks';
7
+ export * from './lib/mongoose-memory';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Mock bson and crypto modules for Jest tests
3
+ */
4
+
5
+ export class ObjectId {
6
+ constructor(public id: string = '000000000000000000000000') {}
7
+ toString(): string {
8
+ return this.id;
9
+ }
10
+ toHexString(): string {
11
+ return this.id;
12
+ }
13
+ }
14
+
15
+ export class Binary {}
16
+ export class Code {}
17
+ export class DBRef {}
18
+ export class Decimal128 {}
19
+ export class Double {}
20
+ export class Int32 {}
21
+ export class Long {}
22
+ export class MaxKey {}
23
+ export class MinKey {}
24
+ export class Timestamp {}
25
+ export class UUID {}
26
+
27
+ // Mock secp256k1 for ethereum crypto
28
+ export const secp256k1 = {
29
+ CURVE: {
30
+ p: BigInt(0),
31
+ n: BigInt(0),
32
+ },
33
+ };
34
+
35
+ // Default export for ethereum-cryptography
36
+ export default { secp256k1 };
@@ -0,0 +1,69 @@
1
+ /* Reusable console mock helpers for e2e tests */
2
+
3
+ export type ConsoleSpies = {
4
+ log: jest.SpyInstance;
5
+ info: jest.SpyInstance;
6
+ warn: jest.SpyInstance;
7
+ error: jest.SpyInstance;
8
+ debug: jest.SpyInstance;
9
+ };
10
+
11
+ export interface WithConsoleOptions {
12
+ mute?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Wrap a test body with console spies that are always restored.
17
+ * By default mutes console output to keep test logs clean.
18
+ */
19
+ export async function withConsoleMocks<T = unknown>(
20
+ options: WithConsoleOptions,
21
+ fn: (spies: ConsoleSpies) => Promise<T> | T,
22
+ ): Promise<T> {
23
+ const mute = options?.mute !== false; // default true
24
+ const noop = () => undefined;
25
+ const spies: ConsoleSpies = {
26
+ log: jest
27
+ .spyOn(console, 'log')
28
+ .mockImplementation(mute ? noop : console.log),
29
+ info: jest
30
+ .spyOn(console, 'info')
31
+ .mockImplementation(mute ? noop : console.info),
32
+ warn: jest
33
+ .spyOn(console, 'warn')
34
+ .mockImplementation(mute ? noop : console.warn),
35
+ error: jest
36
+ .spyOn(console, 'error')
37
+ .mockImplementation(mute ? noop : console.error),
38
+ debug: jest
39
+ .spyOn(console, 'debug')
40
+ .mockImplementation(
41
+ mute
42
+ ? noop
43
+ : (console.debug as ((...args: unknown[]) => void) | undefined) ??
44
+ noop,
45
+ ),
46
+ };
47
+ try {
48
+ return await fn(spies);
49
+ } finally {
50
+ spies.log.mockRestore();
51
+ spies.info.mockRestore();
52
+ spies.warn.mockRestore();
53
+ spies.error.mockRestore();
54
+ spies.debug.mockRestore();
55
+ }
56
+ }
57
+
58
+ /**
59
+ * True if any call to the spy contains all provided substrings, in order-agnostic check.
60
+ */
61
+ export function spyContains(
62
+ spy: jest.SpyInstance,
63
+ ...needles: string[]
64
+ ): boolean {
65
+ return spy.mock.calls.some((args: unknown[]) => {
66
+ const text = args.map((a) => (typeof a === 'string' ? a : '')).join(' ');
67
+ return needles.every((n) => text.includes(n));
68
+ });
69
+ }
@@ -0,0 +1,105 @@
1
+ import * as fs from 'fs';
2
+
3
+ /**
4
+ * Spies returned from withDirectLogMocks
5
+ */
6
+ export interface DirectLogSpies {
7
+ writeSync: jest.Mock;
8
+ }
9
+
10
+ /**
11
+ * Options for withDirectLogMocks
12
+ */
13
+ export interface WithDirectLogOptions {
14
+ /**
15
+ * If false, do not mute the output (let it pass through).
16
+ * By default (true), mutes output.
17
+ */
18
+ mute?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Wrap a test body with fs.writeSync spy for directLog testing.
23
+ * By default mutes output (does nothing on write).
24
+ * The spy will capture calls with file descriptor and buffer.
25
+ *
26
+ * Note: Requires fs module to be mocked at module level with jest.mock('fs')
27
+ */
28
+ export async function withDirectLogMocks<T = unknown>(
29
+ options: WithDirectLogOptions,
30
+ fn: (spies: DirectLogSpies) => Promise<T> | T,
31
+ ): Promise<T> {
32
+ const mute = options?.mute !== false; // default true
33
+
34
+ // Get the mocked writeSync function
35
+ const writeSync = fs.writeSync as unknown as jest.Mock;
36
+
37
+ // Store previous mock implementation if any
38
+ const previousImpl = writeSync.getMockImplementation();
39
+
40
+ // Set implementation based on mute option
41
+ if (mute) {
42
+ writeSync.mockImplementation(() => undefined);
43
+ } else {
44
+ // Pass through - requires original implementation to be available
45
+ writeSync.mockImplementation((...args: any[]) => {
46
+ // In test environment, we can't easily call the real fs.writeSync
47
+ // so we just no-op but track the calls
48
+ return args[1]?.length || 0;
49
+ });
50
+ }
51
+
52
+ const spies: DirectLogSpies = { writeSync };
53
+
54
+ try {
55
+ return await fn(spies);
56
+ } finally {
57
+ // Clear calls and restore previous implementation
58
+ writeSync.mockClear();
59
+ if (previousImpl) {
60
+ writeSync.mockImplementation(previousImpl);
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Helper to check if writeSync was called with a specific file descriptor and message.
67
+ * @param spy The writeSync mock
68
+ * @param fd The file descriptor (1 for stdout, 2 for stderr)
69
+ * @param needles Substrings to search for in the buffer content
70
+ */
71
+ export function directLogContains(
72
+ spy: jest.Mock,
73
+ fd: number,
74
+ ...needles: string[]
75
+ ): boolean {
76
+ return spy.mock.calls.some((args: unknown[]) => {
77
+ const [calledFd, buffer] = args;
78
+ if (calledFd !== fd) return false;
79
+
80
+ const text = buffer instanceof Buffer
81
+ ? buffer.toString('utf8')
82
+ : String(buffer);
83
+
84
+ return needles.every((n) => text.includes(n));
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Get all messages written to a specific file descriptor.
90
+ * @param spy The writeSync mock
91
+ * @param fd The file descriptor (1 for stdout, 2 for stderr)
92
+ */
93
+ export function getDirectLogMessages(
94
+ spy: jest.Mock,
95
+ fd: number,
96
+ ): string[] {
97
+ return spy.mock.calls
98
+ .filter((args: unknown[]) => args[0] === fd)
99
+ .map((args: unknown[]) => {
100
+ const buffer = args[1];
101
+ return buffer instanceof Buffer
102
+ ? buffer.toString('utf8')
103
+ : String(buffer);
104
+ });
105
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Simple localStorage mock for Node.js test environment
3
+ */
4
+ export class LocalStorageMock implements Storage {
5
+ private store: Map<string, string> = new Map();
6
+
7
+ get length(): number {
8
+ return this.store.size;
9
+ }
10
+
11
+ clear(): void {
12
+ this.store.clear();
13
+ }
14
+
15
+ getItem(key: string): string | null {
16
+ return this.store.get(key) ?? null;
17
+ }
18
+
19
+ key(index: number): string | null {
20
+ const keys = Array.from(this.store.keys());
21
+ return keys[index] ?? null;
22
+ }
23
+
24
+ removeItem(key: string): void {
25
+ this.store.delete(key);
26
+ }
27
+
28
+ setItem(key: string, value: string): void {
29
+ this.store.set(key, value);
30
+ }
31
+ }
32
+
33
+ // Set up global localStorage mock
34
+ if (typeof globalThis.localStorage === 'undefined') {
35
+ Object.defineProperty(globalThis, 'localStorage', {
36
+ value: new LocalStorageMock(),
37
+ writable: true,
38
+ configurable: true,
39
+ });
40
+ }
@@ -1,4 +1,4 @@
1
- import { Connection } from 'mongoose';
1
+ import { Connection } from '@digitaldefiance/mongoose-types';
2
2
  /**
3
3
  * Connect to in-memory MongoDB for testing
4
4
  * @returns Object with both the connection and URI
@@ -1 +1 @@
1
- {"version":3,"file":"mongoose-memory.d.ts","sourceRoot":"","sources":["../../../../../packages/digitaldefiance-express-suite-test-utils/src/lib/mongoose-memory.ts"],"names":[],"mappings":"AACA,OAAiB,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAKhD;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC;IAAE,UAAU,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAsBxF;AAED;;GAEG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAWxD;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAOnD"}
1
+ {"version":3,"file":"mongoose-memory.d.ts","sourceRoot":"","sources":["../../../../../packages/digitaldefiance-express-suite-test-utils/src/lib/mongoose-memory.ts"],"names":[],"mappings":"AAAA,OAAiB,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAMvE;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC;IAC/C,UAAU,EAAE,UAAU,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC,CAsBD;AAED;;GAEG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAWxD;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAOnD"}
@@ -4,8 +4,8 @@ exports.connectMemoryDB = connectMemoryDB;
4
4
  exports.disconnectMemoryDB = disconnectMemoryDB;
5
5
  exports.clearMemoryDB = clearMemoryDB;
6
6
  const tslib_1 = require("tslib");
7
+ const mongoose_types_1 = tslib_1.__importDefault(require("@digitaldefiance/mongoose-types"));
7
8
  const mongodb_memory_server_1 = require("mongodb-memory-server");
8
- const mongoose_1 = tslib_1.__importDefault(require("mongoose"));
9
9
  let mongoServer;
10
10
  let connection;
11
11
  /**
@@ -14,8 +14,8 @@ let connection;
14
14
  */
15
15
  async function connectMemoryDB() {
16
16
  // If mongoose is connected but we don't have our server, disconnect first
17
- if (mongoose_1.default.connection.readyState !== 0 && !mongoServer) {
18
- await mongoose_1.default.disconnect();
17
+ if (mongoose_types_1.default.connection.readyState !== 0 && !mongoServer) {
18
+ await mongoose_types_1.default.disconnect();
19
19
  connection = undefined;
20
20
  }
21
21
  // Create new server if needed
@@ -24,10 +24,10 @@ async function connectMemoryDB() {
24
24
  }
25
25
  const uri = mongoServer.getUri();
26
26
  // Connect if not already connected
27
- if (mongoose_1.default.connection.readyState === 0) {
28
- await mongoose_1.default.connect(uri);
27
+ if (mongoose_types_1.default.connection.readyState === 0) {
28
+ await mongoose_types_1.default.connect(uri);
29
29
  }
30
- connection = mongoose_1.default.connection;
30
+ connection = mongoose_types_1.default.connection;
31
31
  return { connection, uri };
32
32
  }
33
33
  /**
@@ -36,7 +36,7 @@ async function connectMemoryDB() {
36
36
  async function disconnectMemoryDB() {
37
37
  if (connection) {
38
38
  await connection.dropDatabase();
39
- await mongoose_1.default.disconnect();
39
+ await mongoose_types_1.default.disconnect();
40
40
  connection = undefined;
41
41
  }
42
42
  if (mongoServer) {
@@ -0,0 +1,64 @@
1
+ import mongoose, { Connection } from '@digitaldefiance/mongoose-types';
2
+ import { MongoMemoryServer } from 'mongodb-memory-server';
3
+
4
+ let mongoServer: MongoMemoryServer | undefined;
5
+ let connection: Connection | undefined;
6
+
7
+ /**
8
+ * Connect to in-memory MongoDB for testing
9
+ * @returns Object with both the connection and URI
10
+ */
11
+ export async function connectMemoryDB(): Promise<{
12
+ connection: Connection;
13
+ uri: string;
14
+ }> {
15
+ // If mongoose is connected but we don't have our server, disconnect first
16
+ if (mongoose.connection.readyState !== 0 && !mongoServer) {
17
+ await mongoose.disconnect();
18
+ connection = undefined;
19
+ }
20
+
21
+ // Create new server if needed
22
+ if (!mongoServer) {
23
+ mongoServer = await MongoMemoryServer.create();
24
+ }
25
+
26
+ const uri = mongoServer.getUri();
27
+
28
+ // Connect if not already connected
29
+ if (mongoose.connection.readyState === 0) {
30
+ await mongoose.connect(uri);
31
+ }
32
+
33
+ connection = mongoose.connection;
34
+
35
+ return { connection, uri };
36
+ }
37
+
38
+ /**
39
+ * Drop all collections and disconnect
40
+ */
41
+ export async function disconnectMemoryDB(): Promise<void> {
42
+ if (connection) {
43
+ await connection.dropDatabase();
44
+ await mongoose.disconnect();
45
+ connection = undefined;
46
+ }
47
+
48
+ if (mongoServer) {
49
+ await mongoServer.stop();
50
+ mongoServer = undefined;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Clear all collections without disconnecting
56
+ */
57
+ export async function clearMemoryDB(): Promise<void> {
58
+ if (connection) {
59
+ const collections = connection.collections;
60
+ for (const key in collections) {
61
+ await collections[key].deleteMany({});
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Mock utilities for React component testing
3
+ */
4
+
5
+ /**
6
+ * Mock timezones for testing
7
+ */
8
+ export const mockTimezones = [
9
+ 'America/New_York',
10
+ 'America/Los_Angeles',
11
+ 'Europe/London',
12
+ 'Europe/Paris',
13
+ 'Asia/Tokyo',
14
+ 'UTC',
15
+ ];
16
+
17
+ /**
18
+ * Get initial timezone mock
19
+ */
20
+ export const mockGetInitialTimezone = (): string => 'UTC';
21
+
22
+ /**
23
+ * Mock RegisterForm props
24
+ */
25
+ export const mockRegisterFormProps = {
26
+ timezones: mockTimezones,
27
+ getInitialTimezone: mockGetInitialTimezone,
28
+ onSubmit: jest.fn().mockResolvedValue({ success: true, message: 'Success' }),
29
+ };
@@ -0,0 +1,200 @@
1
+ import { expect } from '@jest/globals';
2
+ import type { MatcherContext } from 'expect';
3
+ import { readFileSync } from 'fs';
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ export type ErrorClass<E extends Error> = new (...args: any[]) => E;
7
+
8
+ declare global {
9
+ namespace jest {
10
+ interface Matchers<R> {
11
+ toThrowType<E extends Error>(
12
+ errorType: ErrorClass<E>,
13
+ validator?: (error: E) => void,
14
+ ): R;
15
+ }
16
+ }
17
+ }
18
+
19
+ interface MatcherError extends Error {
20
+ matcherResult?: {
21
+ expected: unknown;
22
+ received: unknown;
23
+ };
24
+ }
25
+
26
+ function isMatcherError(error: unknown): error is MatcherError {
27
+ return error instanceof Error && 'matcherResult' in error;
28
+ }
29
+
30
+ function extractTestInfo(stackTrace: string) {
31
+ const stackLines = stackTrace.split('\n');
32
+ const anonymousLine = stackLines.find((line) =>
33
+ line.includes('Object.<anonymous>'),
34
+ );
35
+ const match = anonymousLine?.match(/\((.+?\.spec\.ts):(\d+):(\d+)\)/);
36
+
37
+ if (!match) {
38
+ return { testHierarchy: ['Unknown Test'], location: '' };
39
+ }
40
+
41
+ const fullTestPath = match[1];
42
+ const lineNumber = parseInt(match[2]);
43
+ const testFile = fullTestPath.split('/').pop() || '';
44
+
45
+ try {
46
+ const fileContent = readFileSync(fullTestPath, 'utf8');
47
+ const lines = fileContent.split('\n');
48
+ const testLineContent = lines[lineNumber - 1];
49
+ const testNameMatch = testLineContent.match(/it\(['"](.+?)['"]/);
50
+ const testName = testNameMatch?.[1];
51
+
52
+ const testHierarchy = ['Test'];
53
+ if (testName) {
54
+ testHierarchy.push(testName);
55
+ }
56
+
57
+ return {
58
+ testHierarchy,
59
+ location: ` (${testFile}:${lineNumber})`,
60
+ };
61
+ } catch {
62
+ return {
63
+ testHierarchy: ['Test'],
64
+ location: ` (${testFile}:${lineNumber})`,
65
+ };
66
+ }
67
+ }
68
+
69
+ export const toThrowType = async function <E extends Error>(
70
+ this: MatcherContext,
71
+ received: (() => unknown | Promise<unknown>) | Promise<unknown>,
72
+ errorType: ErrorClass<E>,
73
+ validator?: (error: E) => void,
74
+ ): Promise<{ pass: boolean; message: () => string }> {
75
+ const matcherName = 'toThrowType';
76
+ const options = {
77
+ isNot: this.isNot,
78
+ promise: this.promise,
79
+ };
80
+
81
+ let error: unknown;
82
+ let pass = false;
83
+
84
+ try {
85
+ if (this.promise) {
86
+ try {
87
+ await received;
88
+ pass = false;
89
+ } catch (e) {
90
+ error = e;
91
+ if (
92
+ error instanceof Error &&
93
+ (error instanceof errorType || error.constructor === errorType)
94
+ ) {
95
+ pass = true;
96
+ if (validator) {
97
+ await validator(error as E);
98
+ }
99
+ }
100
+ }
101
+ } else {
102
+ if (typeof received !== 'function') {
103
+ throw new Error(
104
+ this.utils.matcherHint(matcherName, undefined, undefined, options) +
105
+ '\n\n' +
106
+ 'Received value must be a function',
107
+ );
108
+ }
109
+ try {
110
+ await (received as () => unknown | Promise<unknown>)();
111
+ pass = false;
112
+ } catch (e) {
113
+ error = e;
114
+ if (
115
+ error instanceof Error &&
116
+ (error instanceof errorType || error.constructor === errorType)
117
+ ) {
118
+ pass = true;
119
+ if (validator) {
120
+ await validator(error as E);
121
+ }
122
+ }
123
+ }
124
+ }
125
+ } catch (validatorError) {
126
+ const error =
127
+ validatorError instanceof Error
128
+ ? validatorError
129
+ : new Error(String(validatorError));
130
+ const message = error.message;
131
+ const stack = error.stack || '';
132
+
133
+ let diffString: string;
134
+ if (isMatcherError(error) && error.matcherResult) {
135
+ diffString =
136
+ this.utils.diff(
137
+ error.matcherResult.expected,
138
+ error.matcherResult.received,
139
+ ) || '';
140
+ } else {
141
+ diffString = this.utils.diff('Error to match assertions', message) || '';
142
+ }
143
+
144
+ const { testHierarchy, location } = extractTestInfo(stack);
145
+
146
+ return {
147
+ pass: false,
148
+ message: () =>
149
+ `\n\n${this.utils.RECEIVED_COLOR(
150
+ `● ${testHierarchy.join(' › ')}${location ? ` ${location}` : ''}`,
151
+ )}\n\n` +
152
+ this.utils.matcherHint(matcherName, undefined, undefined, options) +
153
+ '\n\n' +
154
+ diffString +
155
+ '\n\n' +
156
+ (stack
157
+ ? this.utils.RECEIVED_COLOR(stack.split('\n').slice(1).join('\n'))
158
+ : ''),
159
+ };
160
+ }
161
+
162
+ const testHeader =
163
+ error instanceof Error && error.stack
164
+ ? (() => {
165
+ const { testHierarchy, location } = extractTestInfo(error.stack);
166
+ return `\n\n${this.utils.RECEIVED_COLOR(
167
+ `● ${testHierarchy.join(' › ')}${location}`,
168
+ )}\n\n`;
169
+ })()
170
+ : '\n';
171
+
172
+ return {
173
+ pass,
174
+ message: () =>
175
+ testHeader +
176
+ this.utils.matcherHint(matcherName, undefined, undefined, options) +
177
+ '\n\n' +
178
+ (pass
179
+ ? `Expected function not to throw ${this.utils.printExpected(
180
+ errorType.name,
181
+ )}`
182
+ : this.promise
183
+ ? this.utils.matcherErrorMessage(
184
+ this.utils.matcherHint(matcherName, undefined, undefined, options),
185
+ 'Expected promise to reject',
186
+ 'Promise resolved successfully',
187
+ )
188
+ : this.utils.matcherErrorMessage(
189
+ this.utils.matcherHint(matcherName, undefined, undefined, options),
190
+ `Expected function to throw ${this.utils.printExpected(
191
+ errorType.name,
192
+ )}`,
193
+ `Received: ${this.utils.printReceived(
194
+ error instanceof Error ? error.constructor.name : typeof error,
195
+ )}`,
196
+ )),
197
+ };
198
+ };
199
+
200
+ expect.extend({ toThrowType });
@@ -0,0 +1,31 @@
1
+ /// <reference types="jest" />
2
+
3
+ export {};
4
+
5
+ declare global {
6
+ namespace jest {
7
+ interface Matchers<R> {
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ toThrowType<E extends Error, T extends new (...args: any[]) => E>(
10
+ errorType: T,
11
+ validator?: (error: E) => void,
12
+ ): R;
13
+ }
14
+ }
15
+ }
16
+
17
+ declare module 'expect' {
18
+ interface ExpectExtendMap {
19
+ toThrowType: {
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ <E extends Error, T extends new (...args: any[]) => E>(
22
+ this: jest.MatcherContext,
23
+ received: () => unknown,
24
+ errorType: T,
25
+ validator?: (error: E) => void,
26
+ ): jest.CustomMatcherResult;
27
+ };
28
+ }
29
+ }
30
+
31
+