@clipboard-health/playwright-toolkit 1.4.1 → 1.4.3
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/README.md +10 -2
- package/package.json +2 -2
- package/src/lib/jsonResponse.d.ts +5 -3
- package/src/lib/jsonResponse.js +106 -15
- package/src/lib/jsonResponse.js.map +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,8 @@ Playwright response bodies can become unreadable after a document navigation. Us
|
|
|
94
94
|
`waitForParsedJsonResponse` when a page action both waits for a JSON response and crosses a
|
|
95
95
|
navigation boundary. The helper parses each candidate inside the `waitForResponse` predicate and
|
|
96
96
|
returns only the parsed value, so callers cannot accidentally retain a lazy `Response` handle.
|
|
97
|
+
Predicate-local parsing narrows the response-lifetime window but cannot eliminate it: Chromium can
|
|
98
|
+
discard the underlying CDP resource while `response.json()` is still pending.
|
|
97
99
|
|
|
98
100
|
```typescript
|
|
99
101
|
const workerResponsePromise = waitForParsedJsonResponse({
|
|
@@ -110,8 +112,14 @@ const workerResponse = await workerResponsePromise;
|
|
|
110
112
|
```
|
|
111
113
|
|
|
112
114
|
Return `undefined` from `parseCandidate` when the URL-level candidate is valid but its body is not
|
|
113
|
-
the response the test needs.
|
|
114
|
-
|
|
115
|
+
the response the test needs. If a candidate body read fails with the exact Chromium
|
|
116
|
+
`Network.getResponseBody` / `No resource with given identifier found` signature, the helper rejects
|
|
117
|
+
only that candidate and continues inside the original finite waiter. It does not issue another
|
|
118
|
+
request or navigation, add a retry or sleep, or widen the timeout. Invalid JSON, parsing and schema
|
|
119
|
+
errors, near-match protocol errors, page or target closure, and cancellation propagate immediately.
|
|
120
|
+
If the outer waiter times out after classified losses, the error preserves the original timeout as
|
|
121
|
+
its cause and adds only bounded diagnostics with the loss count, method, status, and a redacted path
|
|
122
|
+
template. Keep repository-specific URL, method, resource-type, and schema matching in the consumer.
|
|
115
123
|
|
|
116
124
|
## Per-test traceparent fixture
|
|
117
125
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/playwright-toolkit",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "Shared anti-flake primitives for Clipboard Health Playwright suites.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cognito",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@clipboard-health/util-ts": "5.17.
|
|
29
|
+
"@clipboard-health/util-ts": "5.17.2",
|
|
30
30
|
"tslib": "2.8.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Page, type Response } from "@playwright/test";
|
|
2
2
|
export interface WaitForParsedJsonResponseParams<T> {
|
|
3
3
|
isCandidate: (params: {
|
|
4
4
|
response: Response;
|
|
@@ -12,7 +12,9 @@ export interface WaitForParsedJsonResponseParams<T> {
|
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
14
|
* Waits for a matching JSON response and parses its body while Playwright's
|
|
15
|
-
* document-scoped response resource is still readable.
|
|
16
|
-
*
|
|
15
|
+
* document-scoped response resource is still readable. If Chromium discards
|
|
16
|
+
* only that candidate's response body during the read, the unchanged waiter
|
|
17
|
+
* continues. Return `undefined` from `parseCandidate` to keep waiting for
|
|
18
|
+
* another candidate response.
|
|
17
19
|
*/
|
|
18
20
|
export declare function waitForParsedJsonResponse<T>(params: WaitForParsedJsonResponseParams<T>): Promise<T>;
|
package/src/lib/jsonResponse.js
CHANGED
|
@@ -2,33 +2,124 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.waitForParsedJsonResponse = waitForParsedJsonResponse;
|
|
4
4
|
const util_ts_1 = require("@clipboard-health/util-ts");
|
|
5
|
+
const test_1 = require("@playwright/test");
|
|
6
|
+
const MAX_RESPONSE_BODY_LOSS_DIAGNOSTICS = 3;
|
|
7
|
+
const RESPONSE_BODY_METHOD_SIGNATURE = "Protocol error (Network.getResponseBody):";
|
|
8
|
+
const RESPONSE_BODY_LOSS_MESSAGE = "No resource with given identifier found";
|
|
5
9
|
/**
|
|
6
10
|
* Waits for a matching JSON response and parses its body while Playwright's
|
|
7
|
-
* document-scoped response resource is still readable.
|
|
8
|
-
*
|
|
11
|
+
* document-scoped response resource is still readable. If Chromium discards
|
|
12
|
+
* only that candidate's response body during the read, the unchanged waiter
|
|
13
|
+
* continues. Return `undefined` from `parseCandidate` to keep waiting for
|
|
14
|
+
* another candidate response.
|
|
9
15
|
*/
|
|
10
16
|
async function waitForParsedJsonResponse(params) {
|
|
11
17
|
const { isCandidate, page, parseCandidate, timeoutMs } = params;
|
|
18
|
+
const candidateTimeoutErrors = new WeakSet();
|
|
12
19
|
const parsedCandidates = new WeakMap();
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
const responseBodyLossDiagnostics = new Set();
|
|
21
|
+
let responseBodyLossCount = 0;
|
|
22
|
+
let response;
|
|
23
|
+
try {
|
|
24
|
+
response = await page.waitForResponse(async (candidateResponse) => {
|
|
25
|
+
try {
|
|
26
|
+
if (!isCandidate({ response: candidateResponse })) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
let body;
|
|
30
|
+
try {
|
|
31
|
+
body = await candidateResponse.json();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (!isResponseBodyLoss({ error })) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
responseBodyLossCount += 1;
|
|
38
|
+
if (responseBodyLossDiagnostics.size < MAX_RESPONSE_BODY_LOSS_DIAGNOSTICS) {
|
|
39
|
+
responseBodyLossDiagnostics.add(formatResponseBodyLossDiagnostic({ response: candidateResponse }));
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const parsedCandidate = parseCandidate({
|
|
44
|
+
body,
|
|
45
|
+
response: candidateResponse,
|
|
46
|
+
});
|
|
47
|
+
// JSON null is a valid parsed value; only undefined means "keep waiting".
|
|
48
|
+
if (parsedCandidate === undefined) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
parsedCandidates.set(candidateResponse, { value: parsedCandidate });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof test_1.errors.TimeoutError) {
|
|
56
|
+
candidateTimeoutErrors.add(error);
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}, { timeout: timeoutMs });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (!(error instanceof test_1.errors.TimeoutError) ||
|
|
64
|
+
candidateTimeoutErrors.has(error) ||
|
|
65
|
+
responseBodyLossCount === 0) {
|
|
66
|
+
throw error;
|
|
16
67
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
68
|
+
throw createResponseBodyLossTimeoutError({
|
|
69
|
+
diagnostics: [...responseBodyLossDiagnostics],
|
|
70
|
+
responseBodyLossCount,
|
|
71
|
+
timeoutError: error,
|
|
20
72
|
});
|
|
21
|
-
|
|
22
|
-
if (parsedCandidate === undefined) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
parsedCandidates.set(candidateResponse, { value: parsedCandidate });
|
|
26
|
-
return true;
|
|
27
|
-
}, { timeout: timeoutMs });
|
|
73
|
+
}
|
|
28
74
|
const parsedResponse = parsedCandidates.get(response);
|
|
29
75
|
if (!(0, util_ts_1.isDefined)(parsedResponse)) {
|
|
30
76
|
throw new Error("Expected the matched JSON response body to be captured while it was readable.");
|
|
31
77
|
}
|
|
32
78
|
return parsedResponse.value;
|
|
33
79
|
}
|
|
80
|
+
function createResponseBodyLossTimeoutError(params) {
|
|
81
|
+
const { diagnostics, responseBodyLossCount, timeoutError } = params;
|
|
82
|
+
return new Error(`${timeoutError.message}\nResponse body loss diagnostics: count=${responseBodyLossCount}; candidates=${diagnostics.join(", ")}`, { cause: timeoutError });
|
|
83
|
+
}
|
|
84
|
+
function formatResponseBodyLossDiagnostic(params) {
|
|
85
|
+
const { response } = params;
|
|
86
|
+
return [
|
|
87
|
+
`method=${getResponseMethod({ response })}`,
|
|
88
|
+
`status=${getResponseStatus({ response })}`,
|
|
89
|
+
`path=${getResponsePathTemplate({ response })}`,
|
|
90
|
+
].join(" ");
|
|
91
|
+
}
|
|
92
|
+
function getResponseMethod(params) {
|
|
93
|
+
try {
|
|
94
|
+
const method = params.response.request().method();
|
|
95
|
+
return /^[A-Za-z]{1,16}$/.test(method) ? method.toUpperCase() : "[redacted]";
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return "[unavailable]";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function getResponsePathTemplate(params) {
|
|
102
|
+
try {
|
|
103
|
+
return new URL(params.response.url()).pathname === "/" ? "/" : "/[redacted-path]";
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return "/[redacted-path]";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function getResponseStatus(params) {
|
|
110
|
+
try {
|
|
111
|
+
const status = params.response.status();
|
|
112
|
+
return Number.isInteger(status) && status >= 100 && status <= 599
|
|
113
|
+
? String(status)
|
|
114
|
+
: "[unavailable]";
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return "[unavailable]";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function isResponseBodyLoss(params) {
|
|
121
|
+
const { error } = params;
|
|
122
|
+
const message = (0, util_ts_1.toError)(error).message;
|
|
123
|
+
return (message.includes(RESPONSE_BODY_METHOD_SIGNATURE) && message.includes(RESPONSE_BODY_LOSS_MESSAGE));
|
|
124
|
+
}
|
|
34
125
|
//# sourceMappingURL=jsonResponse.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonResponse.js","sourceRoot":"","sources":["../../../../../packages/playwright-toolkit/src/lib/jsonResponse.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"jsonResponse.js","sourceRoot":"","sources":["../../../../../packages/playwright-toolkit/src/lib/jsonResponse.ts"],"names":[],"mappings":"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"}
|