@next/playwright 16.3.0-canary.74 → 16.3.0-canary.75

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.
Files changed (2) hide show
  1. package/dist/index.js +85 -45
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,6 +3,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.instant = instant;
4
4
  const step_1 = require("./step");
5
5
  const INSTANT_COOKIE = 'next-instant-navigation-testing';
6
+ // Browser contexts that currently have an instant() scope executing. The
7
+ // instant cookie is scoped to the browser context, so the context is the
8
+ // natural granularity for the scope: two concurrent instant() calls on the same
9
+ // context share one cookie and genuinely conflict, whereas calls on different
10
+ // contexts (or different browsers) are independent and must not. Keying on the
11
+ // context object preserves that isolation while giving a race-free nesting
12
+ // signal.
13
+ //
14
+ // We track this in-process rather than inferring nesting from the cookie's
15
+ // presence: a locked page asynchronously re-writes the instant cookie on every
16
+ // MPA load (see navigation-testing-lock.ts), and that write can land right
17
+ // after a prior scope's release deletes it, resurrecting the cookie once the
18
+ // scope has already ended. Treating such a leftover as an active scope would
19
+ // turn a benign residue into a cascading failure across every later test that
20
+ // shares the browser context.
21
+ const contextsWithActiveScope = new WeakSet();
6
22
  /**
7
23
  * Runs a function with instant navigation enabled. Within this scope,
8
24
  * navigations render the prefetched UI immediately and wait for the
@@ -25,59 +41,83 @@ const INSTANT_COOKIE = 'next-instant-navigation-testing';
25
41
  * as labeled steps in the Playwright UI.
26
42
  */
27
43
  async function instant(page, fn, options) {
28
- // Check for nested instant() calls. The cookie is scoped to the browser
29
- // context, so we can detect nesting by checking if it's already set.
30
- const existingCookies = await page.context().cookies();
31
- if (existingCookies.some((c) => c.name === INSTANT_COOKIE)) {
44
+ const context = page.context();
45
+ if (contextsWithActiveScope.has(context)) {
32
46
  throw new Error('An instant() scope is already active. Nesting instant() ' +
33
47
  'calls is not supported. Did you forget to await the ' +
34
48
  'previous instant() call?');
35
49
  }
36
- // Acquire the lock by setting the cookie via the browser context. This
37
- // ensures the cookie is present even on the very first navigation.
38
- // The cookie triggers the CookieStore change event in
39
- // navigation-testing-lock.ts, which acquires the in-memory navigation lock.
50
+ // Resolve the cookie's scope before touching any browser state, so misuse on
51
+ // a fresh page (no baseURL and no prior navigation) fails with the
52
+ // descriptive error from resolveURL rather than half-entering a scope.
40
53
  const { hostname } = new URL(resolveURL(page, options));
41
- await (0, step_1.step)('Acquire Instant Lock', () => page.context().addCookies([
42
- {
43
- name: INSTANT_COOKIE,
44
- value: JSON.stringify([0, `p${Math.random()}`]),
45
- domain: hostname,
46
- path: '/',
47
- },
48
- ]));
54
+ contextsWithActiveScope.add(context);
49
55
  try {
50
- return await fn();
56
+ // A completed prior scope on this context can leave the cookie behind (its
57
+ // client-side release races an in-flight captured-cookie write from a
58
+ // locked MPA page load; see the note above). No scope is active for this
59
+ // context, so a present cookie is always stale here — clear it before
60
+ // acquiring so a completed prior scope never blocks this one.
61
+ await releaseInstantCookie(context);
62
+ // Acquire the lock by setting the cookie via the browser context. This
63
+ // ensures the cookie is present even on the very first navigation. The
64
+ // cookie triggers the CookieStore change event in
65
+ // navigation-testing-lock.ts, which acquires the in-memory navigation lock.
66
+ await (0, step_1.step)('Acquire Instant Lock', () => context.addCookies([
67
+ {
68
+ name: INSTANT_COOKIE,
69
+ value: JSON.stringify([0, `p${Math.random()}`]),
70
+ domain: hostname,
71
+ path: '/',
72
+ },
73
+ ]));
74
+ try {
75
+ return await fn();
76
+ }
77
+ finally {
78
+ await (0, step_1.step)('Release Instant Lock', () => releaseInstantCookie(context));
79
+ }
51
80
  }
52
81
  finally {
53
- // Release the lock by expiring the instant cookie, leaving every other
54
- // cookie untouched.
55
- //
56
- // We must NOT use `context.clearCookies({ name: INSTANT_COOKIE })` here.
57
- // Playwright implements a filtered `clearCookies` by clearing the ENTIRE
58
- // cookie jar and then re-adding the cookies that don't match the filter.
59
- // That briefly removes the application's own cookies too. Next.js reacts
60
- // to the instant cookie's deletion by immediately re-rendering, and if
61
- // that render's request races the empty window it observes none of the
62
- // app's cookies (e.g. a navigated page renders as if no cookies were set).
63
- //
64
- // Instead we read the instant cookie's stored entries (Next.js may have
65
- // updated the value, e.g. from [0] to [1,null], but preserves the domain
66
- // and path) and re-add each with a past expiry, which deletes only those
67
- // entries without disturbing the rest of the jar.
68
- await (0, step_1.step)('Release Instant Lock', async () => {
69
- const instantCookies = (await page.context().cookies()).filter((cookie) => cookie.name === INSTANT_COOKIE);
70
- if (instantCookies.length > 0) {
71
- await page.context().addCookies(instantCookies.map((cookie) => ({
72
- name: cookie.name,
73
- value: cookie.value,
74
- domain: cookie.domain,
75
- path: cookie.path,
76
- // A past expiry (Unix epoch seconds) deletes the cookie.
77
- expires: 1,
78
- })));
79
- }
80
- });
82
+ contextsWithActiveScope.delete(context);
83
+ }
84
+ }
85
+ /**
86
+ * Deletes the instant cookie, leaving every other cookie untouched.
87
+ *
88
+ * We must NOT use `context.clearCookies({ name: INSTANT_COOKIE })` here.
89
+ * Playwright implements a filtered `clearCookies` by clearing the ENTIRE cookie
90
+ * jar and then re-adding the cookies that don't match the filter. That briefly
91
+ * removes the application's own cookies too. Next.js reacts to the instant
92
+ * cookie's deletion by immediately re-rendering, and if that render's request
93
+ * races the empty window it observes none of the app's cookies (e.g. a
94
+ * navigated page renders as if no cookies were set).
95
+ *
96
+ * Instead we read the instant cookie's stored entries (Next.js may have updated
97
+ * the value, e.g. from [0] to [1,null], but preserves the domain and path) and
98
+ * re-add each with a past expiry, which deletes only those entries without
99
+ * disturbing the rest of the jar.
100
+ *
101
+ * A locked MPA page load can asynchronously re-write (resurrect) the cookie
102
+ * just after we delete it: the client only stops writing once it observes the
103
+ * deletion, an event that races the pending write. We therefore re-read and
104
+ * re-delete until the cookie stays gone, bounded so a cookie that some other
105
+ * actor keeps re-setting can't loop forever.
106
+ */
107
+ async function releaseInstantCookie(context) {
108
+ for (let attempt = 0; attempt < 5; attempt++) {
109
+ const instantCookies = (await context.cookies()).filter((cookie) => cookie.name === INSTANT_COOKIE);
110
+ if (instantCookies.length === 0) {
111
+ return;
112
+ }
113
+ await context.addCookies(instantCookies.map((cookie) => ({
114
+ name: cookie.name,
115
+ value: cookie.value,
116
+ domain: cookie.domain,
117
+ path: cookie.path,
118
+ // A past expiry (Unix epoch seconds) deletes the cookie.
119
+ expires: 1,
120
+ })));
81
121
  }
82
122
  }
83
123
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/playwright",
3
- "version": "16.3.0-canary.74",
3
+ "version": "16.3.0-canary.75",
4
4
  "repository": {
5
5
  "url": "vercel/next.js",
6
6
  "directory": "packages/next-playwright"