@carlonicora/nextjs-jsonapi 4.2.2 → 5.0.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/dist/{BlockNoteEditor-FROUKBVK.mjs → BlockNoteEditor-7BKKGFH3.mjs} +2 -2
- package/dist/{BlockNoteEditor-SWO3X5CC.js → BlockNoteEditor-E6HJD4HB.js} +9 -9
- package/dist/{BlockNoteEditor-SWO3X5CC.js.map → BlockNoteEditor-E6HJD4HB.js.map} +1 -1
- package/dist/billing/index.js +371 -371
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-AICMLKWN.mjs → chunk-MELHUPVN.mjs} +34 -13
- package/dist/chunk-MELHUPVN.mjs.map +1 -0
- package/dist/{chunk-GYW7LLBQ.js → chunk-TPGXNPGY.js} +208 -187
- package/dist/chunk-TPGXNPGY.js.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/index.js +2 -2
- package/dist/components/index.mjs +1 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/features/analytics/index.js +88 -88
- package/dist/features/analytics/index.mjs +1 -1
- package/dist/features/help/index.js +31 -31
- package/dist/features/help/index.mjs +1 -1
- package/dist/features/tokenusage/index.js +75 -75
- package/dist/features/tokenusage/index.mjs +1 -1
- package/dist/next-config/index.d.mts +0 -1
- package/dist/next-config/index.d.ts +0 -1
- package/dist/next-config/index.js +0 -6
- package/dist/next-config/index.js.map +1 -1
- package/dist/next-config/index.mjs +0 -6
- package/dist/next-config/index.mjs.map +1 -1
- package/package.json +4 -2
- package/pwa/offline.template.html +138 -0
- package/pwa/sw.js +175 -0
- package/src/components/tables/ContentListTable.tsx +6 -0
- package/src/features/notification/components/notifications/PushNotificationProvider.tsx +5 -1
- package/src/features/notification/components/notifications/__tests__/PushNotificationProvider.spec.tsx +51 -0
- package/src/hooks/__tests__/usePushNotifications.spec.tsx +87 -34
- package/src/hooks/usePushNotifications.ts +42 -18
- package/src/next-config/__tests__/createNextConfig.spec.ts +7 -5
- package/src/next-config/index.ts +1 -8
- package/src/pwa/__tests__/offline.template.spec.ts +246 -0
- package/src/pwa/__tests__/sw.spec.ts +309 -0
- package/dist/chunk-AICMLKWN.mjs.map +0 -1
- package/dist/chunk-GYW7LLBQ.js.map +0 -1
- /package/dist/{BlockNoteEditor-FROUKBVK.mjs.map → BlockNoteEditor-7BKKGFH3.mjs.map} +0 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Guard 3: pwa/sw.js is a plain service-worker script, so it is evaluated in a
|
|
2
|
+
// node:vm context whose global is a stubbed worker `self`. Nothing here touches
|
|
3
|
+
// the test realm's globals, so no vi.stubGlobal is needed.
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import vm from "node:vm";
|
|
8
|
+
import { describe, expect, it, vi } from "vitest";
|
|
9
|
+
|
|
10
|
+
const SW_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../pwa/sw.js");
|
|
11
|
+
const SW_SOURCE = readFileSync(SW_PATH, "utf8");
|
|
12
|
+
|
|
13
|
+
const ORIGIN = "https://app.test";
|
|
14
|
+
|
|
15
|
+
type SwConfig = { appId: string; offlineRevision: string; notificationIcon: string };
|
|
16
|
+
type Listener = (event: any) => void;
|
|
17
|
+
|
|
18
|
+
// A worker resolves relative Request URLs against its own location; Node's
|
|
19
|
+
// Request has no base URL, so the sandbox gets a Request that adds one.
|
|
20
|
+
class WorkerRequest extends globalThis.Request {
|
|
21
|
+
constructor(input: RequestInfo | URL, init?: RequestInit) {
|
|
22
|
+
super(typeof input === "string" ? new URL(input, ORIGIN) : input, init);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function loadWorker(config: SwConfig = { appId: "test", offlineRevision: "abc12345", notificationIcon: "/icon.png" }) {
|
|
27
|
+
const listeners: Record<string, Listener> = {};
|
|
28
|
+
const sandbox: Record<string, any> = {
|
|
29
|
+
importScripts: vi.fn((_url: string) => {
|
|
30
|
+
sandbox.NJA_SW_CONFIG = config;
|
|
31
|
+
}),
|
|
32
|
+
addEventListener: vi.fn((type: string, listener: Listener) => {
|
|
33
|
+
listeners[type] = listener;
|
|
34
|
+
}),
|
|
35
|
+
skipWaiting: vi.fn(),
|
|
36
|
+
clients: { claim: vi.fn(), matchAll: vi.fn(), openWindow: vi.fn() },
|
|
37
|
+
registration: {
|
|
38
|
+
navigationPreload: { enable: vi.fn() },
|
|
39
|
+
showNotification: vi.fn(),
|
|
40
|
+
pushManager: { subscribe: vi.fn() },
|
|
41
|
+
},
|
|
42
|
+
caches: { open: vi.fn(), keys: vi.fn(), delete: vi.fn() },
|
|
43
|
+
fetch: vi.fn(),
|
|
44
|
+
Response: globalThis.Response,
|
|
45
|
+
Request: WorkerRequest,
|
|
46
|
+
location: { origin: ORIGIN },
|
|
47
|
+
console,
|
|
48
|
+
};
|
|
49
|
+
sandbox.self = sandbox;
|
|
50
|
+
|
|
51
|
+
vm.runInNewContext(SW_SOURCE, sandbox, { filename: SW_PATH });
|
|
52
|
+
|
|
53
|
+
function dispatch(type: string, init: Record<string, any> = {}) {
|
|
54
|
+
const listener = listeners[type];
|
|
55
|
+
if (!listener) throw new Error(`no ${type} listener registered`);
|
|
56
|
+
const event: Record<string, any> = {
|
|
57
|
+
...init,
|
|
58
|
+
waitUntilPromise: undefined as Promise<unknown> | undefined,
|
|
59
|
+
respondWithPromise: undefined as Promise<unknown> | undefined,
|
|
60
|
+
waitUntil: vi.fn((p: Promise<unknown>) => {
|
|
61
|
+
event.waitUntilPromise = p;
|
|
62
|
+
}),
|
|
63
|
+
respondWith: vi.fn((p: Promise<unknown>) => {
|
|
64
|
+
event.respondWithPromise = p;
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
listener(event);
|
|
68
|
+
return event;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { sandbox, listeners, dispatch };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function makeCache(overrides: Record<string, any> = {}) {
|
|
75
|
+
return {
|
|
76
|
+
add: vi.fn().mockResolvedValue(undefined),
|
|
77
|
+
put: vi.fn().mockResolvedValue(undefined),
|
|
78
|
+
addAll: vi.fn().mockResolvedValue(undefined),
|
|
79
|
+
match: vi.fn().mockResolvedValue(undefined),
|
|
80
|
+
...overrides,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function navigation(init: Record<string, any> = {}) {
|
|
85
|
+
return {
|
|
86
|
+
request: { mode: "navigate", destination: "document", url: `${ORIGIN}/campaigns` },
|
|
87
|
+
preloadResponse: Promise.resolve(undefined),
|
|
88
|
+
...init,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe("pwa/sw.js", () => {
|
|
93
|
+
it("imports /sw-config.js first", () => {
|
|
94
|
+
const { sandbox } = loadWorker();
|
|
95
|
+
|
|
96
|
+
expect(sandbox.importScripts).toHaveBeenCalledTimes(1);
|
|
97
|
+
expect(sandbox.importScripts).toHaveBeenCalledWith("/sw-config.js");
|
|
98
|
+
const importOrder = sandbox.importScripts.mock.invocationCallOrder[0];
|
|
99
|
+
const firstListenerOrder = Math.min(...sandbox.addEventListener.mock.invocationCallOrder);
|
|
100
|
+
expect(importOrder).toBeLessThan(firstListenerOrder);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("install caches only /offline.html with cache: reload", async () => {
|
|
104
|
+
const { sandbox, dispatch } = loadWorker();
|
|
105
|
+
const cache = makeCache();
|
|
106
|
+
sandbox.caches.open.mockResolvedValue(cache);
|
|
107
|
+
|
|
108
|
+
const event = dispatch("install");
|
|
109
|
+
await event.waitUntilPromise;
|
|
110
|
+
|
|
111
|
+
expect(sandbox.caches.open).toHaveBeenCalledTimes(1);
|
|
112
|
+
expect(sandbox.caches.open).toHaveBeenCalledWith("test-offline-abc12345");
|
|
113
|
+
expect(cache.add).toHaveBeenCalledTimes(1);
|
|
114
|
+
const request = cache.add.mock.calls[0][0];
|
|
115
|
+
expect(new URL(request.url).pathname).toBe("/offline.html");
|
|
116
|
+
expect(request.url.endsWith("/offline.html")).toBe(true);
|
|
117
|
+
expect(request.cache).toBe("reload");
|
|
118
|
+
expect(cache.put).not.toHaveBeenCalled();
|
|
119
|
+
expect(cache.addAll).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("install fails when the offline page cannot be fetched", async () => {
|
|
123
|
+
const { sandbox, dispatch } = loadWorker();
|
|
124
|
+
const cache = makeCache({ add: vi.fn().mockRejectedValue(new Error("Request failed")) });
|
|
125
|
+
sandbox.caches.open.mockResolvedValue(cache);
|
|
126
|
+
|
|
127
|
+
const event = dispatch("install");
|
|
128
|
+
|
|
129
|
+
await expect(event.waitUntilPromise).rejects.toThrow("Request failed");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("activate deletes every other cache, enables navigation preload, claims clients", async () => {
|
|
133
|
+
const { sandbox, dispatch } = loadWorker();
|
|
134
|
+
sandbox.caches.keys.mockResolvedValue(["serwist-precache", "only35-images-v4", "test-offline-abc12345"]);
|
|
135
|
+
sandbox.caches.delete.mockResolvedValue(true);
|
|
136
|
+
sandbox.registration.navigationPreload.enable.mockResolvedValue(undefined);
|
|
137
|
+
sandbox.clients.claim.mockResolvedValue(undefined);
|
|
138
|
+
|
|
139
|
+
const event = dispatch("activate");
|
|
140
|
+
await event.waitUntilPromise;
|
|
141
|
+
|
|
142
|
+
expect(sandbox.caches.delete).toHaveBeenCalledTimes(2);
|
|
143
|
+
expect(sandbox.caches.delete).toHaveBeenCalledWith("serwist-precache");
|
|
144
|
+
expect(sandbox.caches.delete).toHaveBeenCalledWith("only35-images-v4");
|
|
145
|
+
expect(sandbox.caches.delete).not.toHaveBeenCalledWith("test-offline-abc12345");
|
|
146
|
+
expect(sandbox.registration.navigationPreload.enable).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(sandbox.clients.claim).toHaveBeenCalledTimes(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("does not call respondWith for a non-navigation request", () => {
|
|
151
|
+
const { sandbox, dispatch } = loadWorker();
|
|
152
|
+
|
|
153
|
+
const event = dispatch("fetch", {
|
|
154
|
+
request: { mode: "no-cors", destination: "script", url: `${ORIGIN}/_next/static/chunk.js` },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(event.respondWith).not.toHaveBeenCalled();
|
|
158
|
+
expect(sandbox.fetch).not.toHaveBeenCalled();
|
|
159
|
+
expect(sandbox.caches.open).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("answers a navigation with the preload response when present", async () => {
|
|
163
|
+
const { sandbox, dispatch } = loadWorker();
|
|
164
|
+
const preload = new Response("preloaded", { status: 200 });
|
|
165
|
+
|
|
166
|
+
const event = dispatch("fetch", navigation({ preloadResponse: Promise.resolve(preload) }));
|
|
167
|
+
|
|
168
|
+
expect(event.respondWith).toHaveBeenCalledTimes(1);
|
|
169
|
+
expect(await event.respondWithPromise).toBe(preload);
|
|
170
|
+
expect(sandbox.fetch).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("answers a navigation with the network response when there is no preload", async () => {
|
|
174
|
+
const { sandbox, dispatch } = loadWorker();
|
|
175
|
+
const network = new Response("network", { status: 200 });
|
|
176
|
+
sandbox.fetch.mockResolvedValue(network);
|
|
177
|
+
const init = navigation();
|
|
178
|
+
|
|
179
|
+
const event = dispatch("fetch", init);
|
|
180
|
+
|
|
181
|
+
expect(await event.respondWithPromise).toBe(network);
|
|
182
|
+
expect(sandbox.fetch).toHaveBeenCalledTimes(1);
|
|
183
|
+
expect(sandbox.fetch).toHaveBeenCalledWith(init.request);
|
|
184
|
+
expect(sandbox.caches.open).not.toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("passes a 500 and a redirect through unchanged", async () => {
|
|
188
|
+
const { sandbox, dispatch } = loadWorker();
|
|
189
|
+
const serverError = new Response("boom", { status: 500 });
|
|
190
|
+
const redirect = Response.redirect(`${ORIGIN}/login`, 302);
|
|
191
|
+
sandbox.fetch.mockResolvedValueOnce(serverError).mockResolvedValueOnce(redirect);
|
|
192
|
+
|
|
193
|
+
const first = dispatch("fetch", navigation());
|
|
194
|
+
const firstResponse = await first.respondWithPromise;
|
|
195
|
+
const second = dispatch("fetch", navigation());
|
|
196
|
+
const secondResponse = await second.respondWithPromise;
|
|
197
|
+
|
|
198
|
+
expect(firstResponse).toBe(serverError);
|
|
199
|
+
expect((firstResponse as Response).status).toBe(500);
|
|
200
|
+
expect(secondResponse).toBe(redirect);
|
|
201
|
+
expect((secondResponse as Response).status).toBe(302);
|
|
202
|
+
expect(sandbox.caches.open).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("serves the cached offline page when the navigation fetch throws", async () => {
|
|
206
|
+
const { sandbox, dispatch } = loadWorker();
|
|
207
|
+
const offline = new Response("<!doctype html><title>Offline</title>", {
|
|
208
|
+
headers: { "content-type": "text/html" },
|
|
209
|
+
});
|
|
210
|
+
const cache = makeCache({ match: vi.fn().mockResolvedValue(offline) });
|
|
211
|
+
sandbox.caches.open.mockResolvedValue(cache);
|
|
212
|
+
sandbox.fetch.mockRejectedValue(new TypeError("Failed to fetch"));
|
|
213
|
+
|
|
214
|
+
const event = dispatch("fetch", navigation());
|
|
215
|
+
|
|
216
|
+
expect(await event.respondWithPromise).toBe(offline);
|
|
217
|
+
expect(sandbox.caches.open).toHaveBeenCalledWith("test-offline-abc12345");
|
|
218
|
+
expect(cache.match).toHaveBeenCalledWith("/offline.html");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("serves the inline fallback when the fetch throws and the cache is empty", async () => {
|
|
222
|
+
const { sandbox, dispatch } = loadWorker();
|
|
223
|
+
const cache = makeCache({ match: vi.fn().mockResolvedValue(undefined) });
|
|
224
|
+
sandbox.caches.open.mockResolvedValue(cache);
|
|
225
|
+
sandbox.fetch.mockRejectedValue(new TypeError("Failed to fetch"));
|
|
226
|
+
|
|
227
|
+
const event = dispatch("fetch", navigation());
|
|
228
|
+
const response = (await event.respondWithPromise) as Response;
|
|
229
|
+
|
|
230
|
+
expect(response).toBeTruthy();
|
|
231
|
+
expect(response.status).toBe(200);
|
|
232
|
+
expect((response.headers.get("content-type") ?? "").startsWith("text/html")).toBe(true);
|
|
233
|
+
const body = await response.text();
|
|
234
|
+
expect(body).toContain("<!doctype html>");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("calls skipWaiting on SKIP_WAITING and ignores other messages", () => {
|
|
238
|
+
const { sandbox, dispatch } = loadWorker();
|
|
239
|
+
|
|
240
|
+
dispatch("message", { data: { type: "SOMETHING_ELSE" } });
|
|
241
|
+
dispatch("message", { data: null });
|
|
242
|
+
dispatch("message", { data: "SKIP_WAITING" });
|
|
243
|
+
expect(sandbox.skipWaiting).not.toHaveBeenCalled();
|
|
244
|
+
|
|
245
|
+
dispatch("message", { data: { type: "SKIP_WAITING" } });
|
|
246
|
+
expect(sandbox.skipWaiting).toHaveBeenCalledTimes(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("shows a notification with the configured icon on push", async () => {
|
|
250
|
+
const { sandbox, dispatch } = loadWorker();
|
|
251
|
+
sandbox.registration.showNotification.mockResolvedValue(undefined);
|
|
252
|
+
|
|
253
|
+
const event = dispatch("push", {
|
|
254
|
+
data: { json: () => ({ title: "New session", message: "Friday at 8", url: "/sessions/1" }) },
|
|
255
|
+
});
|
|
256
|
+
await event.waitUntilPromise;
|
|
257
|
+
|
|
258
|
+
expect(sandbox.registration.showNotification).toHaveBeenCalledTimes(1);
|
|
259
|
+
const [title, options] = sandbox.registration.showNotification.mock.calls[0];
|
|
260
|
+
expect(title).toBe("New session");
|
|
261
|
+
expect(options.icon).toBe("/icon.png");
|
|
262
|
+
expect(options.body).toBe("Friday at 8");
|
|
263
|
+
expect(options.data).toBe("/sessions/1");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("posts PUSH_RESUBSCRIBED to one client only, preferring a visible one", async () => {
|
|
267
|
+
const { sandbox, dispatch } = loadWorker();
|
|
268
|
+
const keys = { p256dh: "p256dh-key", auth: "auth-key" };
|
|
269
|
+
sandbox.registration.pushManager.subscribe.mockResolvedValue({
|
|
270
|
+
endpoint: "https://push.example/new",
|
|
271
|
+
toJSON: () => ({ endpoint: "https://push.example/new", keys }),
|
|
272
|
+
});
|
|
273
|
+
const hidden = { visibilityState: "hidden", postMessage: vi.fn() };
|
|
274
|
+
const visible = { visibilityState: "visible", postMessage: vi.fn() };
|
|
275
|
+
sandbox.clients.matchAll.mockResolvedValue([hidden, visible]);
|
|
276
|
+
|
|
277
|
+
const event = dispatch("pushsubscriptionchange", {
|
|
278
|
+
oldSubscription: { options: { applicationServerKey: "server-key" } },
|
|
279
|
+
});
|
|
280
|
+
await event.waitUntilPromise;
|
|
281
|
+
|
|
282
|
+
expect(sandbox.registration.pushManager.subscribe).toHaveBeenCalledTimes(1);
|
|
283
|
+
const subscribeOptions = sandbox.registration.pushManager.subscribe.mock.calls[0][0];
|
|
284
|
+
expect(subscribeOptions.userVisibleOnly).toBe(true);
|
|
285
|
+
expect(subscribeOptions.applicationServerKey).toBe("server-key");
|
|
286
|
+
|
|
287
|
+
expect(hidden.postMessage).not.toHaveBeenCalled();
|
|
288
|
+
expect(visible.postMessage).toHaveBeenCalledTimes(1);
|
|
289
|
+
const message = visible.postMessage.mock.calls[0][0];
|
|
290
|
+
expect(message.type).toBe("PUSH_RESUBSCRIBED");
|
|
291
|
+
expect(message.subscription.endpoint).toBe("https://push.example/new");
|
|
292
|
+
expect(message.subscription.keys).toBe(keys);
|
|
293
|
+
expect(Object.keys(message).sort()).toEqual(["subscription", "type"]);
|
|
294
|
+
expect(Object.keys(message.subscription).sort()).toEqual(["endpoint", "keys"]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("does nothing on pushsubscriptionchange without an old subscription", async () => {
|
|
298
|
+
const { sandbox, dispatch } = loadWorker();
|
|
299
|
+
const client = { visibilityState: "visible", postMessage: vi.fn() };
|
|
300
|
+
sandbox.clients.matchAll.mockResolvedValue([client]);
|
|
301
|
+
|
|
302
|
+
const event = dispatch("pushsubscriptionchange", { oldSubscription: null });
|
|
303
|
+
await event.waitUntilPromise;
|
|
304
|
+
|
|
305
|
+
expect(sandbox.registration.pushManager.subscribe).not.toHaveBeenCalled();
|
|
306
|
+
expect(sandbox.clients.matchAll).not.toHaveBeenCalled();
|
|
307
|
+
expect(client.postMessage).not.toHaveBeenCalled();
|
|
308
|
+
});
|
|
309
|
+
});
|