@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 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. Parsing errors propagate immediately. Keep repository-specific URL,
114
- method, resource-type, and schema matching in the consumer.
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.1",
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.1",
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 { Page, Response } from "@playwright/test";
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. Return `undefined`
16
- * from `parseCandidate` to keep waiting for another candidate response.
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>;
@@ -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. Return `undefined`
8
- * from `parseCandidate` to keep waiting for another candidate response.
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 response = await page.waitForResponse(async (candidateResponse) => {
14
- if (!isCandidate({ response: candidateResponse })) {
15
- return false;
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
- const parsedCandidate = parseCandidate({
18
- body: await candidateResponse.json(),
19
- response: candidateResponse,
68
+ throw createResponseBodyLossTimeoutError({
69
+ diagnostics: [...responseBodyLossDiagnostics],
70
+ responseBodyLossCount,
71
+ timeoutError: error,
20
72
  });
21
- // JSON null is a valid parsed value; only undefined means "keep waiting".
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,uDAAsD;AActD;;;;GAIG;AACI,KAAK,oCACV,MAA0C;IAE1C,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,cAAc,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;IAChE,MAAM,gBAAgB,GAAG,IAAI,OAAO,EAAgC,CAAC;IAErE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,eAAe,CACzC,KAAK,EAAE,iBAAiB,EAAE,EAAE;QAC1B,IAAI,CAAC,WAAW,CAAC,EAAE,QAAQ,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;YAClD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,MAAM,eAAe,GAAG,cAAc,CAAC;YACrC,IAAI,EAAE,MAAM,iBAAiB,CAAC,IAAI,EAAE;YACpC,QAAQ,EAAE,iBAAiB;SAC5B,CAAC,CAAC;QACH,0EAA0E;QAC1E,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;YAClC,OAAO,KAAK,CAAC;QACf,CAAC;QAED,gBAAgB,CAAC,GAAG,CAAC,iBAAiB,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC;QACpE,OAAO,IAAI,CAAC;IACd,CAAC,EACD,EAAE,OAAO,EAAE,SAAS,EAAE,CACvB,CAAC;IACF,MAAM,cAAc,GAAG,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAEtD,IAAI,CAAC,IAAA,mBAAS,EAAC,cAAc,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,+EAA+E,CAChF,CAAC;IACJ,CAAC;IAED,OAAO,cAAc,CAAC,KAAK,CAAC;AAC9B,CAAC"}
1
+ {"version":3,"file":"jsonResponse.js","sourceRoot":"","sources":["../../../../../packages/playwright-toolkit/src/lib/jsonResponse.ts"],"names":[],"mappings":"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"}