@agent-native/creative-context 0.5.12 → 0.6.2

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.
@@ -0,0 +1,380 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const isBlockedExtensionUrlWithDns = vi.hoisted(() => vi.fn(async () => false));
4
+ const ssrfSafeFetch = vi.hoisted(() => vi.fn());
5
+
6
+ vi.mock("@agent-native/core/extensions/url-safety", () => ({
7
+ isBlockedExtensionUrlWithDns,
8
+ ssrfSafeFetch,
9
+ }));
10
+
11
+ const { renderWithPlaywright } = await import("./rendered-page.js");
12
+
13
+ describe("renderWithPlaywright lifecycle", () => {
14
+ afterEach(() => {
15
+ vi.useRealTimers();
16
+ ssrfSafeFetch.mockReset();
17
+ });
18
+
19
+ it("closes isolated contexts and reports bounded stabilization failures", async () => {
20
+ vi.useFakeTimers();
21
+ const events: string[] = [];
22
+ let evaluateCalls = 0;
23
+ const page = {
24
+ async route() {},
25
+ async goto() {},
26
+ async waitForLoadState(state: string) {
27
+ throw new Error(`${state} timed out`);
28
+ },
29
+ async title() {
30
+ return "Example";
31
+ },
32
+ url() {
33
+ return "https://example.com/";
34
+ },
35
+ locator() {
36
+ return { innerText: async () => "Example content" };
37
+ },
38
+ async setViewportSize() {},
39
+ async screenshot() {
40
+ return new Uint8Array([1]);
41
+ },
42
+ async evaluate() {
43
+ evaluateCalls += 1;
44
+ if (evaluateCalls === 1) return new Promise(() => {});
45
+ if (evaluateCalls === 2) return undefined;
46
+ throw new Error("computed styles unavailable");
47
+ },
48
+ };
49
+ const context = {
50
+ pages: () => [],
51
+ newPage: async () => page,
52
+ async close() {
53
+ events.push("context.close");
54
+ },
55
+ };
56
+ const browser = {
57
+ contexts: () => [],
58
+ async newContext() {
59
+ events.push("context.new");
60
+ return context;
61
+ },
62
+ async close() {
63
+ events.push("browser.close");
64
+ },
65
+ };
66
+ const renderPromise = renderWithPlaywright(
67
+ {
68
+ chromium: {
69
+ async launch() {
70
+ return browser;
71
+ },
72
+ async connectOverCDP() {
73
+ return browser;
74
+ },
75
+ },
76
+ } as never,
77
+ { url: "https://example.com/", timeoutMs: 1_000 },
78
+ [],
79
+ "local-playwright",
80
+ );
81
+
82
+ await vi.advanceTimersByTimeAsync(1_200);
83
+ const result = await renderPromise;
84
+
85
+ expect(events).toEqual(["context.new", "context.close", "browser.close"]);
86
+ expect(result.warnings).toEqual(
87
+ expect.arrayContaining([
88
+ "Browser load stabilization unavailable: load timed out",
89
+ "Browser network-idle stabilization unavailable: networkidle timed out",
90
+ "Browser font readiness unavailable: font readiness timed out after 1000ms",
91
+ "Browser style extraction unavailable: computed styles unavailable",
92
+ ]),
93
+ );
94
+ expect(result.diagnostics).toEqual(expect.arrayContaining(result.warnings));
95
+ });
96
+
97
+ it("hydrates through the SSRF-safe network proxy without forwarding cookies", async () => {
98
+ ssrfSafeFetch
99
+ .mockResolvedValueOnce(
100
+ new Response(
101
+ "<!doctype html><html><head><title>Proxy example</title></head><body>Hydrated CTA</body></html>",
102
+ { status: 200, headers: { "content-type": "text/html" } },
103
+ ),
104
+ )
105
+ .mockResolvedValueOnce(
106
+ new Response(".cta{color:rgb(13 20 27)}", {
107
+ status: 200,
108
+ headers: { "content-type": "text/css" },
109
+ }),
110
+ );
111
+
112
+ let routeHandler:
113
+ | ((route: {
114
+ request: () => {
115
+ url: () => string;
116
+ isNavigationRequest: () => boolean;
117
+ resourceType: () => string;
118
+ method: () => string;
119
+ headers: () => Record<string, string>;
120
+ };
121
+ continue: () => Promise<void>;
122
+ abort: () => Promise<void>;
123
+ fulfill: (options: {
124
+ status: number;
125
+ headers: Record<string, string>;
126
+ body: Uint8Array;
127
+ }) => Promise<void>;
128
+ }) => Promise<void>)
129
+ | undefined;
130
+ const fulfilled: Array<{
131
+ status: number;
132
+ headers: Record<string, string>;
133
+ body: Uint8Array;
134
+ }> = [];
135
+
136
+ const invokeRoute = async (
137
+ url: string,
138
+ navigation: boolean,
139
+ type: string,
140
+ ) => {
141
+ if (!routeHandler) throw new Error("route handler was not installed");
142
+ await routeHandler({
143
+ request: () => ({
144
+ url: () => url,
145
+ isNavigationRequest: () => navigation,
146
+ resourceType: () => type,
147
+ method: () => "GET",
148
+ headers: () => ({
149
+ accept: "text/html",
150
+ cookie: "session=must-not-forward",
151
+ "user-agent": "fixture-browser",
152
+ }),
153
+ }),
154
+ continue: async () => undefined,
155
+ abort: async () => undefined,
156
+ fulfill: async (options) => {
157
+ fulfilled.push(options);
158
+ },
159
+ });
160
+ };
161
+
162
+ let evaluateCalls = 0;
163
+ const page = {
164
+ async route(_pattern: string, handler: (route: never) => Promise<void>) {
165
+ routeHandler = handler as typeof routeHandler;
166
+ },
167
+ async goto() {
168
+ await invokeRoute("https://example.com/", true, "document");
169
+ await invokeRoute(
170
+ "https://example.com/styles.css",
171
+ false,
172
+ "stylesheet",
173
+ );
174
+ },
175
+ async waitForLoadState() {},
176
+ async title() {
177
+ return "Proxy example";
178
+ },
179
+ url() {
180
+ return "https://example.com/";
181
+ },
182
+ locator() {
183
+ return { innerText: async () => "Hydrated CTA" };
184
+ },
185
+ async setViewportSize() {},
186
+ async screenshot() {
187
+ return new Uint8Array([1]);
188
+ },
189
+ async evaluate<T>() {
190
+ evaluateCalls += 1;
191
+ if (evaluateCalls === 3) {
192
+ return {
193
+ title: "Proxy example",
194
+ text: "Hydrated CTA",
195
+ assets: [],
196
+ internalLinks: [],
197
+ designTokens: {
198
+ colors: ["rgb(13 20 27)"],
199
+ typography: [],
200
+ spacing: [],
201
+ radii: [],
202
+ cssVariables: {},
203
+ },
204
+ } as T;
205
+ }
206
+ return undefined as T;
207
+ },
208
+ };
209
+ const context = {
210
+ async newPage() {
211
+ return page;
212
+ },
213
+ async close() {},
214
+ };
215
+ const browser = {
216
+ contexts: () => [],
217
+ async newContext() {
218
+ return context;
219
+ },
220
+ async close() {},
221
+ };
222
+
223
+ const result = await renderWithPlaywright(
224
+ {
225
+ chromium: {
226
+ async launch() {
227
+ return browser;
228
+ },
229
+ async connectOverCDP() {
230
+ return browser;
231
+ },
232
+ },
233
+ } as never,
234
+ { url: "https://example.com/", timeoutMs: 5_000 },
235
+ [],
236
+ "local-playwright",
237
+ );
238
+
239
+ expect(result).toMatchObject({
240
+ rendered: true,
241
+ method: "local-playwright",
242
+ text: "Hydrated CTA",
243
+ });
244
+ expect(fulfilled).toHaveLength(2);
245
+ expect(new TextDecoder().decode(fulfilled[0].body)).toContain(
246
+ "Proxy example",
247
+ );
248
+ expect(ssrfSafeFetch).toHaveBeenCalledTimes(2);
249
+ expect(ssrfSafeFetch.mock.calls[0][1].headers).toEqual({
250
+ accept: "text/html",
251
+ "user-agent": "fixture-browser",
252
+ });
253
+ });
254
+
255
+ it("reserves browser resource slots before overlapping proxy fetches", async () => {
256
+ let releaseFetch!: () => void;
257
+ const fetchGate = new Promise<void>((resolve) => {
258
+ releaseFetch = resolve;
259
+ });
260
+ let fetchCount = 0;
261
+ ssrfSafeFetch.mockImplementation(async () => {
262
+ fetchCount += 1;
263
+ await fetchGate;
264
+ return new Response("resource", { status: 200 });
265
+ });
266
+
267
+ let routeHandler:
268
+ | ((route: {
269
+ request: () => {
270
+ url: () => string;
271
+ isNavigationRequest: () => boolean;
272
+ resourceType: () => string;
273
+ method: () => string;
274
+ headers: () => Record<string, string>;
275
+ };
276
+ continue: () => Promise<void>;
277
+ abort: (reason?: string) => Promise<void>;
278
+ fulfill: (options: {
279
+ status: number;
280
+ headers: Record<string, string>;
281
+ body: Uint8Array;
282
+ }) => Promise<void>;
283
+ }) => Promise<void>)
284
+ | undefined;
285
+ let abortCount = 0;
286
+
287
+ const page = {
288
+ async route(_pattern: string, handler: (route: never) => Promise<void>) {
289
+ routeHandler = handler as typeof routeHandler;
290
+ },
291
+ async goto() {
292
+ if (!routeHandler) throw new Error("route handler was not installed");
293
+ const requests = Array.from({ length: 401 }, (_, index) =>
294
+ routeHandler!({
295
+ request: () => ({
296
+ url: () => `https://example.com/resource-${index}`,
297
+ isNavigationRequest: () => index === 0,
298
+ resourceType: () => "script",
299
+ method: () => "GET",
300
+ headers: () => ({}),
301
+ }),
302
+ continue: async () => undefined,
303
+ abort: async () => {
304
+ abortCount += 1;
305
+ },
306
+ fulfill: async () => undefined,
307
+ }),
308
+ );
309
+ while (fetchCount < 400) await Promise.resolve();
310
+ releaseFetch();
311
+ await Promise.all(requests);
312
+ },
313
+ async waitForLoadState() {},
314
+ async title() {
315
+ return "Example";
316
+ },
317
+ url() {
318
+ return "https://example.com/";
319
+ },
320
+ locator() {
321
+ return { innerText: async () => "Example" };
322
+ },
323
+ async setViewportSize() {},
324
+ async screenshot() {
325
+ return new Uint8Array([1]);
326
+ },
327
+ async evaluate<T>() {
328
+ return {
329
+ title: "Example",
330
+ text: "Example",
331
+ assets: [],
332
+ internalLinks: [],
333
+ designTokens: {
334
+ colors: [],
335
+ typography: [],
336
+ spacing: [],
337
+ radii: [],
338
+ cssVariables: {},
339
+ },
340
+ } as T;
341
+ },
342
+ };
343
+ const context = {
344
+ pages: () => [],
345
+ async newPage() {
346
+ return page;
347
+ },
348
+ async close() {},
349
+ };
350
+ const browser = {
351
+ contexts: () => [],
352
+ async newContext() {
353
+ return context;
354
+ },
355
+ async close() {},
356
+ };
357
+
358
+ const result = await renderWithPlaywright(
359
+ {
360
+ chromium: {
361
+ async launch() {
362
+ return browser;
363
+ },
364
+ async connectOverCDP() {
365
+ return browser;
366
+ },
367
+ },
368
+ } as never,
369
+ { url: "https://example.com/", timeoutMs: 5_000 },
370
+ [],
371
+ "local-playwright",
372
+ );
373
+
374
+ expect(fetchCount).toBe(400);
375
+ expect(abortCount).toBe(1);
376
+ expect(result.warnings).toContain(
377
+ "Browser resource budget reached (400 requests).",
378
+ );
379
+ });
380
+ });