@page-component-object/msw 0.1.0

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) 2026 David Vega
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.
@@ -0,0 +1,45 @@
1
+ import * as _semantic_matchers_core from '@semantic-matchers/core';
2
+ import { MockFn } from '@page-component-object/core';
3
+ import { PathParams, HttpHandler } from 'msw';
4
+ import { SetupServer } from 'msw/node';
5
+
6
+ type JsonBodyType = Record<string, unknown> | unknown[];
7
+ type DefaultBodyType = JsonBodyType | string | null | void;
8
+ type HttpHandlerInfo = {
9
+ request: Request;
10
+ params: PathParams;
11
+ cookies: Record<string, string>;
12
+ };
13
+ type FlexibleRestHandler<TRequest extends DefaultBodyType = DefaultBodyType, TResponse extends DefaultBodyType = DefaultBodyType> = (info: HttpHandlerInfo & {
14
+ body?: TRequest;
15
+ }) => Response | TResponse | Promise<Response> | Promise<TResponse>;
16
+ type FlexibleRequest<TRequest extends DefaultBodyType = DefaultBodyType, TResponse extends DefaultBodyType = DefaultBodyType> = FlexibleRestHandler<TRequest, TResponse> | Promise<Response | TResponse> | Response | TResponse;
17
+ type ResponseTrigger<TRequest extends DefaultBodyType = DefaultBodyType, TResponse extends DefaultBodyType = DefaultBodyType> = (value: FlexibleRequest<TRequest, TResponse>) => Promise<void>;
18
+ declare class ApiTestObject {
19
+ static server: SetupServer | null;
20
+ static handlers: HttpHandler[];
21
+ static apiBaseUrl: string;
22
+ private static restHandlersByKey;
23
+ private static restHandlerKey;
24
+ private replaceRestHandler;
25
+ registerHandler(handler: HttpHandler): void;
26
+ registerRestHandler<TRequest extends DefaultBodyType = DefaultBodyType, TResponse extends DefaultBodyType = DefaultBodyType>(method: 'get' | 'post' | 'put' | 'delete' | 'patch', path: string, handler: FlexibleRequest<TRequest, TResponse>): MockFn;
27
+ registerTriggeredRestHandler<TRequest extends DefaultBodyType = DefaultBodyType, TResponse extends DefaultBodyType = DefaultBodyType>(method: 'get' | 'post' | 'put' | 'delete' | 'patch', path: string): readonly [MockFn<(_: HttpHandlerInfo & {
28
+ body?: TRequest;
29
+ }) => Promise<void>>, ResponseTrigger<TRequest, TResponse>];
30
+ static setupServer(): void;
31
+ static startServer(): void;
32
+ static stopServer(): void;
33
+ static resetHandlers(): void;
34
+ static clear(): void;
35
+ }
36
+ declare function resolveApiUrl(path: string): string;
37
+
38
+ /** Global matcher for MSW API spies registered via `ApiTestObject`. */
39
+ declare const toHaveBeenLastCalledWithUrl: _semantic_matchers_core.SemanticMatcherFn<MockFn<(...args: never[]) => unknown>, [string | RegExp]>;
40
+ /** Register with `semanticExpect.extendGlobal(apiMockGlobalMatchers)` in Jest/Vitest setup. */
41
+ declare const apiMockGlobalMatchers: {
42
+ toHaveBeenLastCalledWithUrl: _semantic_matchers_core.SemanticMatcherFn<MockFn<(...args: never[]) => unknown>, [string | RegExp]>;
43
+ };
44
+
45
+ export { ApiTestObject as A, type FlexibleRequest as F, type HttpHandlerInfo as H, type ResponseTrigger as R, type FlexibleRestHandler as a, apiMockGlobalMatchers as b, resolveApiUrl as r, toHaveBeenLastCalledWithUrl as t };
@@ -0,0 +1,5 @@
1
+ import '@semantic-matchers/core';
2
+ import '@page-component-object/core';
3
+ export { H as HttpHandlerInfo, b as apiMockGlobalMatchers, t as toHaveBeenLastCalledWithUrl } from './apiMatchers-DVLvnm4q.js';
4
+ import 'msw';
5
+ import 'msw/node';
@@ -0,0 +1,9 @@
1
+ import {
2
+ apiMockGlobalMatchers,
3
+ toHaveBeenLastCalledWithUrl
4
+ } from "./chunk-FOADFUEL.js";
5
+ export {
6
+ apiMockGlobalMatchers,
7
+ toHaveBeenLastCalledWithUrl
8
+ };
9
+ //# sourceMappingURL=apiMatchers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,46 @@
1
+ // src/apiMatchers.ts
2
+ import { defineMatcher } from "@semantic-matchers/core";
3
+ function isMockFn(value) {
4
+ return typeof value === "function" && typeof value.mock === "object" && Array.isArray(value.mock.calls);
5
+ }
6
+ var toHaveBeenLastCalledWithUrl = defineMatcher(
7
+ function toHaveBeenLastCalledWithUrl2(received, expected) {
8
+ if (!isMockFn(received)) {
9
+ return {
10
+ pass: false,
11
+ message: () => "Expected an API mock function registered via ApiTestObject."
12
+ };
13
+ }
14
+ if (received.mock.calls.length === 0) {
15
+ return {
16
+ pass: false,
17
+ message: () => "Expected mock function to have been called, but it was not called."
18
+ };
19
+ }
20
+ const lastCall = received.mock.calls[received.mock.calls.length - 1];
21
+ const info = lastCall[0];
22
+ const actualUrl = info.request.url;
23
+ const host = this.host ?? {};
24
+ const equals = host.equals ?? Object.is;
25
+ const pass = expected instanceof RegExp ? expected.test(actualUrl) : equals(actualUrl, expected);
26
+ return {
27
+ pass,
28
+ message: () => `Expected request URL to match:
29
+ ${String(expected)}
30
+
31
+ Received:
32
+ ${actualUrl}`,
33
+ actual: actualUrl,
34
+ expected
35
+ };
36
+ }
37
+ );
38
+ var apiMockGlobalMatchers = {
39
+ toHaveBeenLastCalledWithUrl
40
+ };
41
+
42
+ export {
43
+ toHaveBeenLastCalledWithUrl,
44
+ apiMockGlobalMatchers
45
+ };
46
+ //# sourceMappingURL=chunk-FOADFUEL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/apiMatchers.ts"],"sourcesContent":["import { defineMatcher } from '@semantic-matchers/core';\n\nimport type { MockFn } from '@page-component-object/core';\n\nimport type { HttpHandlerInfo } from './ApiTestObject';\n\nfunction isMockFn(value: unknown): value is MockFn {\n return (\n typeof value === 'function' &&\n typeof (value as MockFn).mock === 'object' &&\n Array.isArray((value as MockFn).mock.calls)\n );\n}\n\n/** Global matcher for MSW API spies registered via `ApiTestObject`. */\nexport const toHaveBeenLastCalledWithUrl = defineMatcher<MockFn, [string | RegExp]>(\n function toHaveBeenLastCalledWithUrl(received, expected) {\n if (!isMockFn(received)) {\n return {\n pass: false,\n message: () => 'Expected an API mock function registered via ApiTestObject.',\n };\n }\n\n if (received.mock.calls.length === 0) {\n return {\n pass: false,\n message: () => 'Expected mock function to have been called, but it was not called.',\n };\n }\n\n const lastCall = received.mock.calls[received.mock.calls.length - 1];\n const info = lastCall[0] as HttpHandlerInfo;\n const actualUrl = info.request.url;\n\n const host = (this.host ?? {}) as { equals?: (a: unknown, b: unknown) => boolean };\n const equals = host.equals ?? Object.is;\n const pass =\n expected instanceof RegExp ? expected.test(actualUrl) : equals(actualUrl, expected);\n\n return {\n pass,\n message: () =>\n `Expected request URL to match:\\n ${String(expected)}\\n\\nReceived:\\n ${actualUrl}`,\n actual: actualUrl,\n expected,\n };\n },\n);\n\n/** Register with `semanticExpect.extendGlobal(apiMockGlobalMatchers)` in Jest/Vitest setup. */\nexport const apiMockGlobalMatchers = {\n toHaveBeenLastCalledWithUrl,\n};\n\nexport type { HttpHandlerInfo } from './ApiTestObject';\n"],"mappings":";AAAA,SAAS,qBAAqB;AAM9B,SAAS,SAAS,OAAiC;AACjD,SACE,OAAO,UAAU,cACjB,OAAQ,MAAiB,SAAS,YAClC,MAAM,QAAS,MAAiB,KAAK,KAAK;AAE9C;AAGO,IAAM,8BAA8B;AAAA,EACzC,SAASA,6BAA4B,UAAU,UAAU;AACvD,QAAI,CAAC,SAAS,QAAQ,GAAG;AACvB,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS,MAAM;AAAA,MACjB;AAAA,IACF;AAEA,QAAI,SAAS,KAAK,MAAM,WAAW,GAAG;AACpC,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS,MAAM;AAAA,MACjB;AAAA,IACF;AAEA,UAAM,WAAW,SAAS,KAAK,MAAM,SAAS,KAAK,MAAM,SAAS,CAAC;AACnE,UAAM,OAAO,SAAS,CAAC;AACvB,UAAM,YAAY,KAAK,QAAQ;AAE/B,UAAM,OAAQ,KAAK,QAAQ,CAAC;AAC5B,UAAM,SAAS,KAAK,UAAU,OAAO;AACrC,UAAM,OACJ,oBAAoB,SAAS,SAAS,KAAK,SAAS,IAAI,OAAO,WAAW,QAAQ;AAEpF,WAAO;AAAA,MACL;AAAA,MACA,SAAS,MACP;AAAA,IAAqC,OAAO,QAAQ,CAAC;AAAA;AAAA;AAAA,IAAoB,SAAS;AAAA,MACpF,QAAQ;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;AAGO,IAAM,wBAAwB;AAAA,EACnC;AACF;","names":["toHaveBeenLastCalledWithUrl"]}
@@ -0,0 +1,32 @@
1
+ export { A as ApiTestObject, F as FlexibleRequest, a as FlexibleRestHandler, H as HttpHandlerInfo, R as ResponseTrigger, b as apiMockGlobalMatchers, r as resolveApiUrl, t as toHaveBeenLastCalledWithUrl } from './apiMatchers-DVLvnm4q.js';
2
+ import { HttpHandler } from 'msw';
3
+ export { HttpHandler } from 'msw';
4
+ import '@semantic-matchers/core';
5
+ import '@page-component-object/core';
6
+ import 'msw/node';
7
+
8
+ type MockDataSource = {
9
+ setupMockData(): unknown;
10
+ };
11
+ type MockSession<T extends MockDataSource> = {
12
+ handlers: HttpHandler[];
13
+ instance: T;
14
+ mocks: ReturnType<T['setupMockData']>;
15
+ };
16
+ /** Copy of handlers registered since last reset — safe to pass to Storybook or node MSW. */
17
+ declare function snapshotHandlers(): HttpHandler[];
18
+ /**
19
+ * Register mocks in an isolated session without starting the node MSW server.
20
+ * Returns the same handler instances (and embedded spies) for Vitest/Jest and Storybook.
21
+ *
22
+ * `configure` runs after `setupMockData()` so stories can override view fields or
23
+ * register additional handlers before handlers are snapshotted.
24
+ */
25
+ declare function createMockSession<T extends MockDataSource>(factory: () => T, configure?: (instance: T, mocks: ReturnType<T['setupMockData']>) => void): MockSession<T>;
26
+ declare function buildMswParameters(handlers: HttpHandler[]): {
27
+ msw: {
28
+ handlers: HttpHandler[];
29
+ };
30
+ };
31
+
32
+ export { type MockDataSource, type MockSession, buildMswParameters, createMockSession, snapshotHandlers };
package/dist/index.js ADDED
@@ -0,0 +1,157 @@
1
+ import {
2
+ apiMockGlobalMatchers,
3
+ toHaveBeenLastCalledWithUrl
4
+ } from "./chunk-FOADFUEL.js";
5
+
6
+ // src/ApiTestObject.ts
7
+ import { http, HttpResponse } from "msw";
8
+ import { setupServer } from "msw/node";
9
+ import { getRuntime } from "@page-component-object/core";
10
+ async function flexibleRequestToResult(request, info) {
11
+ const result = typeof request === "function" ? await request(info) : request;
12
+ if (result instanceof Response) {
13
+ return result;
14
+ }
15
+ if (typeof result === "object" && result !== null) {
16
+ return HttpResponse.json(result);
17
+ }
18
+ return new HttpResponse(String(result));
19
+ }
20
+ async function safeParseBody(request) {
21
+ try {
22
+ const cloned = request.clone();
23
+ const ct = cloned.headers.get("content-type") ?? "";
24
+ if (ct.includes("application/json")) {
25
+ return await cloned.json();
26
+ }
27
+ const text = await cloned.text();
28
+ if (!text) return void 0;
29
+ try {
30
+ return JSON.parse(text);
31
+ } catch {
32
+ return text;
33
+ }
34
+ } catch {
35
+ return void 0;
36
+ }
37
+ }
38
+ function flexRequestToHttpHandler(requestHandler) {
39
+ const { spyFactory } = getRuntime();
40
+ const mocked = spyFactory.fn(
41
+ async (enriched) => {
42
+ return flexibleRequestToResult(requestHandler, enriched);
43
+ }
44
+ );
45
+ const mswResolver = async (info) => {
46
+ const body = await safeParseBody(info.request);
47
+ const enriched = { ...info, body };
48
+ return mocked(enriched);
49
+ };
50
+ return { mswResolver, mocked };
51
+ }
52
+ var ApiTestObject = class _ApiTestObject {
53
+ static server = null;
54
+ static handlers = [];
55
+ static apiBaseUrl = "";
56
+ static restHandlersByKey = /* @__PURE__ */ new Map();
57
+ static restHandlerKey(method, path) {
58
+ return `${method.toUpperCase()}:${path}`;
59
+ }
60
+ replaceRestHandler(method, path, httpHandler) {
61
+ const key = _ApiTestObject.restHandlerKey(method, path);
62
+ const previous = _ApiTestObject.restHandlersByKey.get(key);
63
+ if (previous) {
64
+ _ApiTestObject.handlers = _ApiTestObject.handlers.filter((handler) => handler !== previous);
65
+ }
66
+ _ApiTestObject.restHandlersByKey.set(key, httpHandler);
67
+ }
68
+ registerHandler(handler) {
69
+ _ApiTestObject.handlers.push(handler);
70
+ }
71
+ registerRestHandler(method, path, handler) {
72
+ const { mswResolver, mocked } = flexRequestToHttpHandler(handler);
73
+ const httpHandler = http[method](path, mswResolver);
74
+ this.replaceRestHandler(method, path, httpHandler);
75
+ this.registerHandler(httpHandler);
76
+ _ApiTestObject.server?.use(httpHandler);
77
+ return mocked;
78
+ }
79
+ registerTriggeredRestHandler(method, path) {
80
+ const { spyFactory } = getRuntime();
81
+ let trigger = null;
82
+ const mockedHandler = spyFactory.fn(async (_) => {
83
+ });
84
+ const httpHandler = http[method](path, async (info) => {
85
+ const body = await safeParseBody(info.request);
86
+ const enriched = { ...info, body };
87
+ await mockedHandler(enriched);
88
+ return new Promise((resolve) => {
89
+ trigger = async (value) => {
90
+ resolve(await flexibleRequestToResult(value, enriched));
91
+ };
92
+ });
93
+ });
94
+ this.replaceRestHandler(method, path, httpHandler);
95
+ this.registerHandler(httpHandler);
96
+ _ApiTestObject.server?.use(httpHandler);
97
+ const triggerShell = async (value) => {
98
+ if (!trigger) throw new Error("Handler has never been called");
99
+ await trigger(value);
100
+ };
101
+ return [mockedHandler, triggerShell];
102
+ }
103
+ static setupServer() {
104
+ const refreshTokenHandler = http.post(
105
+ "*/refresh-token",
106
+ () => HttpResponse.json({ message: "Refresh token endpoint not mocked in test" }, { status: 500 })
107
+ );
108
+ this.server = setupServer(refreshTokenHandler, ..._ApiTestObject.handlers.reverse());
109
+ }
110
+ static startServer() {
111
+ this.server?.listen({ onUnhandledRequest: "warn" });
112
+ }
113
+ static stopServer() {
114
+ this.server?.close();
115
+ this.server = null;
116
+ }
117
+ static resetHandlers() {
118
+ this.handlers = [];
119
+ this.restHandlersByKey.clear();
120
+ }
121
+ static clear() {
122
+ this.stopServer();
123
+ this.resetHandlers();
124
+ }
125
+ };
126
+ function resolveApiUrl(path) {
127
+ const base = ApiTestObject.apiBaseUrl.replace(/\/$/, "");
128
+ const normalized = path.startsWith("/") ? path : `/${path}`;
129
+ return `${base}${normalized}`;
130
+ }
131
+
132
+ // src/mockSession.ts
133
+ function snapshotHandlers() {
134
+ return [...ApiTestObject.handlers];
135
+ }
136
+ function createMockSession(factory, configure) {
137
+ ApiTestObject.resetHandlers();
138
+ const instance = factory();
139
+ const mocks = instance.setupMockData();
140
+ configure?.(instance, mocks);
141
+ const handlers = snapshotHandlers();
142
+ ApiTestObject.resetHandlers();
143
+ return { handlers, instance, mocks };
144
+ }
145
+ function buildMswParameters(handlers) {
146
+ return { msw: { handlers } };
147
+ }
148
+ export {
149
+ ApiTestObject,
150
+ apiMockGlobalMatchers,
151
+ buildMswParameters,
152
+ createMockSession,
153
+ resolveApiUrl,
154
+ snapshotHandlers,
155
+ toHaveBeenLastCalledWithUrl
156
+ };
157
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ApiTestObject.ts","../src/mockSession.ts"],"sourcesContent":["import { http, HttpResponse, type HttpHandler, type PathParams } from 'msw';\nimport { setupServer, type SetupServer } from 'msw/node';\n\nimport { getRuntime, type MockFn } from '@page-component-object/core';\n\ntype JsonBodyType = Record<string, unknown> | unknown[];\ntype DefaultBodyType = JsonBodyType | string | null | void;\n\nexport type HttpHandlerInfo = {\n request: Request;\n params: PathParams;\n cookies: Record<string, string>;\n};\n\nexport type FlexibleRestHandler<\n TRequest extends DefaultBodyType = DefaultBodyType,\n TResponse extends DefaultBodyType = DefaultBodyType,\n> = (\n info: HttpHandlerInfo & { body?: TRequest },\n) => Response | TResponse | Promise<Response> | Promise<TResponse>;\n\nexport type FlexibleRequest<\n TRequest extends DefaultBodyType = DefaultBodyType,\n TResponse extends DefaultBodyType = DefaultBodyType,\n> =\n | FlexibleRestHandler<TRequest, TResponse>\n | Promise<Response | TResponse>\n | Response\n | TResponse;\n\nexport type ResponseTrigger<\n TRequest extends DefaultBodyType = DefaultBodyType,\n TResponse extends DefaultBodyType = DefaultBodyType,\n> = (value: FlexibleRequest<TRequest, TResponse>) => Promise<void>;\n\nasync function flexibleRequestToResult<\n TRequest extends DefaultBodyType,\n TResponse extends DefaultBodyType,\n>(\n request: FlexibleRequest<TRequest, TResponse>,\n info: HttpHandlerInfo & { body?: TRequest },\n): Promise<Response> {\n const result = typeof request === 'function' ? await request(info) : request;\n\n if (result instanceof Response) {\n return result;\n }\n\n if (typeof result === 'object' && result !== null) {\n return HttpResponse.json(result);\n }\n\n return new HttpResponse(String(result));\n}\n\nasync function safeParseBody<T>(request: Request): Promise<T | undefined> {\n try {\n const cloned = request.clone();\n const ct = cloned.headers.get('content-type') ?? '';\n if (ct.includes('application/json')) {\n return (await cloned.json()) as T;\n }\n const text = await cloned.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text) as T;\n } catch {\n return text as unknown as T;\n }\n } catch {\n return undefined;\n }\n}\n\nfunction flexRequestToHttpHandler<\n TRequest extends DefaultBodyType = DefaultBodyType,\n TResponse extends DefaultBodyType = DefaultBodyType,\n>(requestHandler: FlexibleRequest<TRequest, TResponse>) {\n const { spyFactory } = getRuntime();\n const mocked = spyFactory.fn(\n async (enriched: HttpHandlerInfo & { body?: TRequest }): Promise<Response> => {\n return flexibleRequestToResult(requestHandler, enriched);\n },\n );\n\n const mswResolver = async (info: HttpHandlerInfo): Promise<Response> => {\n const body = await safeParseBody<TRequest>(info.request);\n const enriched = { ...info, body } as HttpHandlerInfo & { body?: TRequest };\n return mocked(enriched);\n };\n\n return { mswResolver, mocked };\n}\n\nexport interface ApiMockConfig {\n apiBaseUrl?: string;\n}\n\nexport default class ApiTestObject {\n static server: SetupServer | null = null;\n static handlers: HttpHandler[] = [];\n static apiBaseUrl = '';\n private static restHandlersByKey = new Map<string, HttpHandler>();\n\n private static restHandlerKey(method: string, path: string): string {\n return `${method.toUpperCase()}:${path}`;\n }\n\n private replaceRestHandler(method: string, path: string, httpHandler: HttpHandler): void {\n const key = ApiTestObject.restHandlerKey(method, path);\n const previous = ApiTestObject.restHandlersByKey.get(key);\n if (previous) {\n ApiTestObject.handlers = ApiTestObject.handlers.filter((handler) => handler !== previous);\n }\n ApiTestObject.restHandlersByKey.set(key, httpHandler);\n }\n\n registerHandler(handler: HttpHandler): void {\n ApiTestObject.handlers.push(handler);\n }\n\n registerRestHandler<\n TRequest extends DefaultBodyType = DefaultBodyType,\n TResponse extends DefaultBodyType = DefaultBodyType,\n >(\n method: 'get' | 'post' | 'put' | 'delete' | 'patch',\n path: string,\n handler: FlexibleRequest<TRequest, TResponse>,\n ): MockFn {\n const { mswResolver, mocked } = flexRequestToHttpHandler<TRequest, TResponse>(handler);\n const httpHandler = http[method](path, mswResolver);\n this.replaceRestHandler(method, path, httpHandler);\n this.registerHandler(httpHandler);\n ApiTestObject.server?.use(httpHandler);\n return mocked;\n }\n\n registerTriggeredRestHandler<\n TRequest extends DefaultBodyType = DefaultBodyType,\n TResponse extends DefaultBodyType = DefaultBodyType,\n >(method: 'get' | 'post' | 'put' | 'delete' | 'patch', path: string) {\n const { spyFactory } = getRuntime();\n let trigger: ResponseTrigger<TRequest, TResponse> | null = null;\n const mockedHandler = spyFactory.fn(async (_: HttpHandlerInfo & { body?: TRequest }) => {});\n\n const httpHandler = http[method](path, async (info: HttpHandlerInfo): Promise<Response> => {\n const body = await safeParseBody<TRequest>(info.request);\n const enriched = { ...info, body } as HttpHandlerInfo & { body?: TRequest };\n await mockedHandler(enriched);\n\n return new Promise<Response>((resolve) => {\n trigger = async (value: FlexibleRequest<TRequest, TResponse>) => {\n resolve(await flexibleRequestToResult(value, enriched));\n };\n });\n });\n\n this.replaceRestHandler(method, path, httpHandler);\n this.registerHandler(httpHandler);\n ApiTestObject.server?.use(httpHandler);\n\n const triggerShell: ResponseTrigger<TRequest, TResponse> = async (value) => {\n if (!trigger) throw new Error('Handler has never been called');\n await trigger(value);\n };\n\n return [mockedHandler, triggerShell] as const;\n }\n\n static setupServer(): void {\n const refreshTokenHandler = http.post('*/refresh-token', () =>\n HttpResponse.json({ message: 'Refresh token endpoint not mocked in test' }, { status: 500 }),\n );\n this.server = setupServer(refreshTokenHandler, ...ApiTestObject.handlers.reverse());\n }\n\n static startServer(): void {\n this.server?.listen({ onUnhandledRequest: 'warn' });\n }\n\n static stopServer(): void {\n this.server?.close();\n this.server = null;\n }\n\n static resetHandlers(): void {\n this.handlers = [];\n this.restHandlersByKey.clear();\n }\n\n static clear(): void {\n this.stopServer();\n this.resetHandlers();\n }\n}\n\nexport function resolveApiUrl(path: string): string {\n const base = ApiTestObject.apiBaseUrl.replace(/\\/$/, '');\n const normalized = path.startsWith('/') ? path : `/${path}`;\n return `${base}${normalized}`;\n}\n","import type { HttpHandler } from 'msw';\r\n\r\nimport ApiTestObject from './ApiTestObject';\r\n\r\nexport type MockDataSource = {\r\n setupMockData(): unknown;\r\n};\r\n\r\nexport type MockSession<T extends MockDataSource> = {\r\n handlers: HttpHandler[];\r\n instance: T;\r\n mocks: ReturnType<T['setupMockData']>;\r\n};\r\n\r\n/** Copy of handlers registered since last reset — safe to pass to Storybook or node MSW. */\r\nexport function snapshotHandlers(): HttpHandler[] {\r\n return [...ApiTestObject.handlers];\r\n}\r\n\r\n/**\r\n * Register mocks in an isolated session without starting the node MSW server.\r\n * Returns the same handler instances (and embedded spies) for Vitest/Jest and Storybook.\r\n *\r\n * `configure` runs after `setupMockData()` so stories can override view fields or\r\n * register additional handlers before handlers are snapshotted.\r\n */\r\nexport function createMockSession<T extends MockDataSource>(\r\n factory: () => T,\r\n configure?: (instance: T, mocks: ReturnType<T['setupMockData']>) => void,\r\n): MockSession<T> {\r\n ApiTestObject.resetHandlers();\r\n const instance = factory();\r\n const mocks = instance.setupMockData() as ReturnType<T['setupMockData']>;\r\n configure?.(instance, mocks);\r\n const handlers = snapshotHandlers();\r\n ApiTestObject.resetHandlers();\r\n return { handlers, instance, mocks };\r\n}\r\n\r\nexport function buildMswParameters(handlers: HttpHandler[]): { msw: { handlers: HttpHandler[] } } {\r\n return { msw: { handlers } };\r\n}\r\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@page-component-object/msw",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js"
9
+ },
10
+ "./matchers": {
11
+ "types": "./dist/apiMatchers.d.ts",
12
+ "import": "./dist/apiMatchers.js"
13
+ }
14
+ },
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "dependencies": {
21
+ "@semantic-matchers/core": "^0.1.1",
22
+ "@page-component-object/core": "0.1.0"
23
+ },
24
+ "peerDependencies": {
25
+ "msw": "^2.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "msw": "^2.7.0",
29
+ "rimraf": "^6.0.1",
30
+ "tsup": "^8.3.5",
31
+ "typescript": "^5.7.2",
32
+ "vitest": "^2.1.8"
33
+ },
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/dvegap95/page-component-test-object.git",
38
+ "directory": "packages/msw"
39
+ },
40
+ "homepage": "https://github.com/dvegap95/page-component-test-object#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/dvegap95/page-component-test-object/issues"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup",
49
+ "test": "vitest run",
50
+ "typecheck": "tsc --noEmit",
51
+ "clean": "rimraf dist"
52
+ }
53
+ }