@next/playwright 16.3.0-canary.74 → 16.3.0-canary.76
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/index.js +85 -45
- 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
|
-
|
|
29
|
-
|
|
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
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
/**
|