testaro 78.0.8 → 78.6.0

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 (357) hide show
  1. package/.gitattributes +94 -0
  2. package/.github/workflows/ci.yml +58 -0
  3. package/.github/workflows/publish.yml +50 -0
  4. package/.github/workflows/typescript.yml +35 -0
  5. package/AGENTS.md +2 -2
  6. package/CLAUDE.md +24 -18
  7. package/CONTAINERS.md +9 -6
  8. package/CONTRIBUTING.md +29 -3
  9. package/Dockerfile +1 -1
  10. package/README.md +143 -19
  11. package/UPGRADES.md +4 -0
  12. package/actSpecs-doc.md +13 -1
  13. package/actSpecs.js +29 -5
  14. package/call.js +6 -5
  15. package/dist/nameComputation.js +1 -0
  16. package/docker-compose.yml +8 -2
  17. package/docs/checkpoint-scanning.md +199 -0
  18. package/docs/standard-result-outcome.md +165 -0
  19. package/env.example +49 -10
  20. package/eslint.config.mjs +101 -0
  21. package/netWatch.js +372 -222
  22. package/package.json +46 -31
  23. package/pour/README.md +47 -0
  24. package/pour/pour.min.js +8 -0
  25. package/procs/actDo.js +808 -0
  26. package/procs/catalog.d.ts +17 -0
  27. package/procs/catalog.js +312 -221
  28. package/procs/catalog.ts +398 -0
  29. package/procs/checkpoint.js +110 -0
  30. package/procs/config.d.ts +41 -0
  31. package/procs/config.js +98 -0
  32. package/procs/dateTime.js +2 -1
  33. package/procs/doActs.js +442 -849
  34. package/procs/doTestAct.js +27 -121
  35. package/procs/flow.js +221 -0
  36. package/procs/generateRuleRegistry.js +94 -0
  37. package/procs/getSource.d.ts +26 -0
  38. package/procs/job.js +115 -1
  39. package/procs/launch.d.ts +40 -0
  40. package/procs/launch.js +243 -108
  41. package/procs/nu.d.ts +48 -0
  42. package/procs/scope.js +196 -0
  43. package/procs/shoot.d.ts +33 -0
  44. package/procs/standard.d.ts +18 -0
  45. package/procs/standard.js +83 -0
  46. package/procs/standard.ts +126 -0
  47. package/procs/testAct.js +145 -0
  48. package/procs/testaro.d.ts +20 -0
  49. package/procs/testaro.js +236 -189
  50. package/procs/testaro.ts +321 -0
  51. package/procs/userPath.js +126 -0
  52. package/procs/xPath.d.ts +4 -0
  53. package/procs/xPath.js +81 -69
  54. package/procs/xPath.ts +106 -0
  55. package/procs/xPathScript.d.ts +6 -0
  56. package/procs/xPathScript.js +70 -0
  57. package/run.js +3 -2
  58. package/surea11y/README.md +44 -0
  59. package/surea11y/surea11y.browser.js +14 -0
  60. package/testaro/adbID.d.ts +3 -0
  61. package/testaro/adbID.js +38 -41
  62. package/testaro/adbID.ts +66 -0
  63. package/testaro/allCapStyle.d.ts +3 -0
  64. package/testaro/allCapStyle.js +31 -34
  65. package/testaro/allCapStyle.ts +55 -0
  66. package/testaro/allCaps.d.ts +16 -0
  67. package/testaro/allCaps.js +179 -151
  68. package/testaro/allCaps.ts +228 -0
  69. package/testaro/allHidden.d.ts +13 -0
  70. package/testaro/allHidden.js +29 -30
  71. package/testaro/allHidden.ts +50 -0
  72. package/testaro/allSlanted.d.ts +3 -0
  73. package/testaro/allSlanted.js +30 -33
  74. package/testaro/allSlanted.ts +54 -0
  75. package/testaro/altScheme.d.ts +3 -0
  76. package/testaro/altScheme.js +27 -30
  77. package/testaro/altScheme.ts +50 -0
  78. package/testaro/attVal.d.ts +3 -0
  79. package/testaro/attVal.js +20 -21
  80. package/testaro/attVal.ts +52 -0
  81. package/testaro/autocomplete.d.ts +3 -0
  82. package/testaro/autocomplete.js +59 -75
  83. package/testaro/autocomplete.ts +101 -0
  84. package/testaro/bulk.d.ts +13 -0
  85. package/testaro/bulk.js +32 -33
  86. package/testaro/bulk.ts +55 -0
  87. package/testaro/buttonMenu.d.ts +9 -0
  88. package/testaro/buttonMenu.js +317 -319
  89. package/testaro/buttonMenu.ts +391 -0
  90. package/testaro/captionLoc.d.ts +3 -0
  91. package/testaro/captionLoc.js +17 -20
  92. package/testaro/captionLoc.ts +40 -0
  93. package/testaro/datalistRef.d.ts +3 -0
  94. package/testaro/datalistRef.js +33 -36
  95. package/testaro/datalistRef.ts +55 -0
  96. package/testaro/distortion.d.ts +3 -0
  97. package/testaro/distortion.js +57 -26
  98. package/testaro/distortion.ts +81 -0
  99. package/testaro/docType.d.ts +15 -0
  100. package/testaro/docType.js +25 -25
  101. package/testaro/docType.ts +45 -0
  102. package/testaro/dupAtt.d.ts +16 -0
  103. package/testaro/dupAtt.js +113 -104
  104. package/testaro/dupAtt.ts +144 -0
  105. package/testaro/elements.d.ts +6 -0
  106. package/testaro/elements.js +153 -153
  107. package/testaro/elements.ts +215 -0
  108. package/testaro/embAc.d.ts +3 -0
  109. package/testaro/embAc.js +19 -20
  110. package/testaro/embAc.ts +40 -0
  111. package/testaro/focAll.d.ts +13 -0
  112. package/testaro/focAll.js +185 -191
  113. package/testaro/focAll.ts +217 -0
  114. package/testaro/focAndOp.d.ts +3 -0
  115. package/testaro/focAndOp.js +100 -104
  116. package/testaro/focAndOp.ts +128 -0
  117. package/testaro/focInd.d.ts +3 -0
  118. package/testaro/focInd.js +64 -66
  119. package/testaro/focInd.ts +96 -0
  120. package/testaro/focVis.d.ts +3 -0
  121. package/testaro/focVis.js +28 -30
  122. package/testaro/focVis.ts +52 -0
  123. package/testaro/headEl.d.ts +10 -0
  124. package/testaro/headEl.js +61 -62
  125. package/testaro/headEl.ts +82 -0
  126. package/testaro/headingAmb.d.ts +3 -0
  127. package/testaro/headingAmb.js +54 -63
  128. package/testaro/headingAmb.ts +72 -0
  129. package/testaro/hovInd.d.ts +12 -0
  130. package/testaro/hovInd.js +163 -130
  131. package/testaro/hovInd.ts +199 -0
  132. package/testaro/hover.d.ts +3 -0
  133. package/testaro/hover.js +155 -126
  134. package/testaro/hover.ts +154 -0
  135. package/testaro/hr.d.ts +3 -0
  136. package/testaro/hr.js +14 -17
  137. package/testaro/hr.ts +36 -0
  138. package/testaro/imageLink.d.ts +3 -0
  139. package/testaro/imageLink.js +17 -20
  140. package/testaro/imageLink.ts +42 -0
  141. package/testaro/labClash.d.ts +3 -0
  142. package/testaro/labClash.js +31 -33
  143. package/testaro/labClash.ts +54 -0
  144. package/testaro/legendLoc.d.ts +3 -0
  145. package/testaro/legendLoc.js +17 -20
  146. package/testaro/legendLoc.ts +42 -0
  147. package/testaro/lineHeight.d.ts +3 -0
  148. package/testaro/lineHeight.js +43 -48
  149. package/testaro/lineHeight.ts +70 -0
  150. package/testaro/linkAmb.d.ts +7 -0
  151. package/testaro/linkAmb.js +79 -80
  152. package/testaro/linkAmb.ts +105 -0
  153. package/testaro/linkExt.d.ts +3 -0
  154. package/testaro/linkExt.js +13 -16
  155. package/testaro/linkExt.ts +35 -0
  156. package/testaro/linkOldAtt.d.ts +3 -0
  157. package/testaro/linkOldAtt.js +25 -28
  158. package/testaro/linkOldAtt.ts +48 -0
  159. package/testaro/linkTo.d.ts +3 -0
  160. package/testaro/linkTo.js +22 -22
  161. package/testaro/linkTo.ts +43 -0
  162. package/testaro/linkUl.d.ts +3 -0
  163. package/testaro/linkUl.js +30 -34
  164. package/testaro/linkUl.ts +54 -0
  165. package/testaro/miniText.d.ts +3 -0
  166. package/testaro/miniText.js +41 -44
  167. package/testaro/miniText.ts +68 -0
  168. package/testaro/motion.d.ts +10 -0
  169. package/testaro/motion.js +92 -96
  170. package/testaro/motion.ts +125 -0
  171. package/testaro/nonTable.d.ts +3 -0
  172. package/testaro/nonTable.js +39 -45
  173. package/testaro/nonTable.ts +66 -0
  174. package/testaro/optRoleSel.d.ts +3 -0
  175. package/testaro/optRoleSel.js +16 -19
  176. package/testaro/optRoleSel.ts +41 -0
  177. package/testaro/phOnly.d.ts +3 -0
  178. package/testaro/phOnly.js +18 -21
  179. package/testaro/phOnly.ts +43 -0
  180. package/testaro/pseudoP.d.ts +3 -0
  181. package/testaro/pseudoP.js +36 -38
  182. package/testaro/pseudoP.ts +59 -0
  183. package/testaro/radioSet.d.ts +3 -0
  184. package/testaro/radioSet.js +57 -59
  185. package/testaro/radioSet.ts +79 -0
  186. package/testaro/registry.d.ts +63 -0
  187. package/testaro/registry.js +67 -0
  188. package/testaro/registry.ts +141 -0
  189. package/testaro/role.d.ts +3 -0
  190. package/testaro/role.js +27 -29
  191. package/testaro/role.ts +53 -0
  192. package/testaro/secHeading.d.ts +3 -0
  193. package/testaro/secHeading.js +30 -33
  194. package/testaro/secHeading.ts +53 -0
  195. package/testaro/styleDiff.d.ts +25 -0
  196. package/testaro/styleDiff.js +248 -252
  197. package/testaro/styleDiff.ts +303 -0
  198. package/testaro/tabNav.d.ts +33 -0
  199. package/testaro/tabNav.js +272 -342
  200. package/testaro/tabNav.ts +454 -0
  201. package/testaro/targetsNear.d.ts +9 -0
  202. package/testaro/targetsNear.js +130 -132
  203. package/testaro/targetsNear.ts +160 -0
  204. package/testaro/textNodes.d.ts +6 -0
  205. package/testaro/textNodes.js +139 -135
  206. package/testaro/textNodes.ts +185 -0
  207. package/testaro/textSem.d.ts +3 -0
  208. package/testaro/textSem.js +25 -28
  209. package/testaro/textSem.ts +47 -0
  210. package/testaro/title.d.ts +9 -0
  211. package/testaro/title.js +16 -13
  212. package/testaro/title.ts +31 -0
  213. package/testaro/titledEl.d.ts +3 -0
  214. package/testaro/titledEl.js +15 -18
  215. package/testaro/titledEl.ts +38 -0
  216. package/testaro/zIndex.d.ts +3 -0
  217. package/testaro/zIndex.js +19 -22
  218. package/testaro/zIndex.ts +42 -0
  219. package/tests/alfa.d.ts +45 -0
  220. package/tests/alfa.js +138 -141
  221. package/tests/alfa.ts +214 -0
  222. package/tests/aslint.d.ts +33 -0
  223. package/tests/aslint.js +272 -249
  224. package/tests/aslint.ts +301 -0
  225. package/tests/axe.d.ts +27 -0
  226. package/tests/axe.js +199 -200
  227. package/tests/axe.ts +277 -0
  228. package/tests/ed11y.d.ts +28 -0
  229. package/tests/ed11y.js +141 -99
  230. package/tests/ed11y.ts +178 -0
  231. package/tests/htmlcs.d.ts +21 -0
  232. package/tests/htmlcs.js +174 -140
  233. package/tests/htmlcs.ts +181 -0
  234. package/tests/ibm.d.ts +57 -0
  235. package/tests/ibm.js +182 -175
  236. package/tests/ibm.ts +287 -0
  237. package/tests/nuVal.d.ts +13 -0
  238. package/tests/nuVal.js +107 -112
  239. package/tests/nuVal.ts +145 -0
  240. package/tests/nuVnu.d.ts +15 -0
  241. package/tests/nuVnu.js +141 -111
  242. package/tests/nuVnu.ts +144 -0
  243. package/tests/pour.d.ts +31 -0
  244. package/tests/pour.js +242 -0
  245. package/tests/pour.ts +273 -0
  246. package/tests/qualWeb.d.ts +39 -0
  247. package/tests/qualWeb.js +262 -273
  248. package/tests/qualWeb.ts +344 -0
  249. package/tests/surea11y.d.ts +33 -0
  250. package/tests/surea11y.js +288 -0
  251. package/tests/surea11y.ts +334 -0
  252. package/tests/testaro.d.ts +25 -0
  253. package/tests/testaro.js +745 -652
  254. package/tests/testaro.ts +862 -0
  255. package/tests/wave.d.ts +46 -0
  256. package/tests/wave.js +166 -177
  257. package/tests/wave.ts +252 -0
  258. package/tsconfig.json +17 -0
  259. package/types.d.ts +243 -0
  260. package/types.js +10 -0
  261. package/types.ts +376 -0
  262. package/validation/act/README.md +46 -0
  263. package/validation/act/capture.js +877 -0
  264. package/validation/act/chromium-issue-draft.md +66 -0
  265. package/validation/act/fp-triage-2026-08-22.md +86 -0
  266. package/validation/act/isolation-notes.md +159 -0
  267. package/validation/act/playwright-issue-draft.md +73 -0
  268. package/validation/act/propose-mappings.js +0 -0
  269. package/validation/act/repro-cdp-raw.js +105 -0
  270. package/validation/act/repro-metarefresh.js +65 -0
  271. package/validation/act/score.js +184 -0
  272. package/validation/act/stage3a-stress-report.md +69 -0
  273. package/validation/act/stage3b-mapping-proposals.md +94 -0
  274. package/validation/act/stage3b-triage-draft.md +138 -0
  275. package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
  276. package/validation/executors/netWatch.js +180 -90
  277. package/validation/executors/test.js +17 -2
  278. package/validation/executors/tests.js +118 -10
  279. package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
  280. package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
  281. package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
  282. package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
  283. package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
  284. package/validation/knownFailures.json +1 -0
  285. package/validation/tests/jobProperties/adbID.json +27 -2
  286. package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
  287. package/validation/tests/jobProperties/allCaps.json +25 -0
  288. package/validation/tests/jobProperties/allHidden.json +133 -13
  289. package/validation/tests/jobProperties/allSlanted.json +3 -3
  290. package/validation/tests/jobProperties/altScheme.json +23 -8
  291. package/validation/tests/jobProperties/attVal.json +57 -57
  292. package/validation/tests/jobProperties/autocomplete.json +11 -1
  293. package/validation/tests/jobProperties/bulk.json +6 -1
  294. package/validation/tests/jobProperties/buttonMenu.json +63 -42
  295. package/validation/tests/jobProperties/captionLoc.json +1 -6
  296. package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
  297. package/validation/tests/jobProperties/checkpoint-page.json +397 -0
  298. package/validation/tests/jobProperties/checkpoint.json +395 -0
  299. package/validation/tests/jobProperties/datalistRef.json +20 -5
  300. package/validation/tests/jobProperties/distortion.json +28 -3
  301. package/validation/tests/jobProperties/docType.json +2 -2
  302. package/validation/tests/jobProperties/dupAtt.json +48 -33
  303. package/validation/tests/jobProperties/elements.json +28 -28
  304. package/validation/tests/jobProperties/embAc.json +36 -31
  305. package/validation/tests/jobProperties/focAndOp.json +284 -0
  306. package/validation/tests/jobProperties/focInd.json +34 -34
  307. package/validation/tests/jobProperties/focVis.json +1 -1
  308. package/validation/tests/jobProperties/hover.json +39 -37
  309. package/validation/tests/jobProperties/hr.json +13 -3
  310. package/validation/tests/jobProperties/imageLink.json +0 -5
  311. package/validation/tests/jobProperties/labClash.json +68 -33
  312. package/validation/tests/jobProperties/legendLoc.json +1 -6
  313. package/validation/tests/jobProperties/lineHeight.json +2 -2
  314. package/validation/tests/jobProperties/linkAmb.json +13 -18
  315. package/validation/tests/jobProperties/linkExt.json +1 -1
  316. package/validation/tests/jobProperties/linkOldAtt.json +12 -2
  317. package/validation/tests/jobProperties/linkTo.json +1 -1
  318. package/validation/tests/jobProperties/linkUl.json +65 -65
  319. package/validation/tests/jobProperties/miniText.json +7 -2
  320. package/validation/tests/jobProperties/motion.json +4 -50
  321. package/validation/tests/jobProperties/nonTable.json +48 -3
  322. package/validation/tests/jobProperties/optRoleSel.json +12 -2
  323. package/validation/tests/jobProperties/phOnly.json +6 -16
  324. package/validation/tests/jobProperties/pseudoP.json +17 -2
  325. package/validation/tests/jobProperties/radioSet.json +30 -30
  326. package/validation/tests/jobProperties/role.json +25 -5
  327. package/validation/tests/jobProperties/secHeading.json +26 -21
  328. package/validation/tests/jobProperties/styleDiff.json +35 -5
  329. package/validation/tests/jobProperties/tabNav.json +3 -1
  330. package/validation/tests/jobProperties/targetsNear.json +237 -0
  331. package/validation/tests/jobProperties/textNodes.json +29 -29
  332. package/validation/tests/jobProperties/textSem.json +26 -1
  333. package/validation/tests/jobProperties/title.json +12 -2
  334. package/validation/tests/jobProperties/titledEl.json +39 -9
  335. package/validation/tests/jobProperties/userPath.json +317 -0
  336. package/validation/tests/jobProperties/zIndex.json +40 -35
  337. package/validation/tests/targets/allCapStyle/index.html +26 -0
  338. package/validation/tests/targets/checkpoint/index.html +38 -0
  339. package/validation/tests/targets/datalistRef/index.html +1 -1
  340. package/validation/tests/targets/focAndOp/bad.html +29 -0
  341. package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
  342. package/validation/tests/targets/focInd/bad.html +2 -1
  343. package/validation/tests/targets/headEl/index.html +10 -1
  344. package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
  345. package/validation/tests/targets/userPath/index.html +42 -0
  346. package/validation/validateTest.js +61 -9
  347. package/.claude/settings.local.json +0 -11
  348. package/.eslintrc.json +0 -41
  349. package/htmlcs/.eslintrc.json +0 -67
  350. package/validation/tests/jobProperties/linkTitle.json +0 -127
  351. package/validation/tests/jobProperties/opFoc.json +0 -164
  352. package/validation/tests/jobProperties/targetSmall.json +0 -152
  353. package/validation/tests/jobProperties/targetTiny.json +0 -142
  354. package/validation/tests/targets/focOp/bad.html +0 -25
  355. package/validation/tests/targets/linkTitle/index.html +0 -24
  356. package/validation/tests/targets/opFoc/bad.html +0 -26
  357. package/validation/tests/targets/opFoc/good.html +0 -23
@@ -0,0 +1,66 @@
1
+ # Chromium bug report — Target.closeTarget acks success without closing during navigation commit
2
+
3
+ **NOT FILED as a new issue — already tracked upstream.** Pre-filing search (2026-08-22) found
4
+ <https://issues.chromium.org/issues/536385539> (filed 2026-07-19 by a Google engineer, P2/S2,
5
+ pending code change 8251379): `Target.closeTarget` is lost when a navigation commit swaps the
6
+ main RenderFrameHost right after `ClosePage` — the pending `ClosePage` callback dies with the
7
+ old RFH and is never re-issued. Same defect; their trigger was back-to-back failing navigations,
8
+ ours (meta-refresh 0) is an additional ordinary-page trigger of the same race.
9
+
10
+ **Actions taken instead:** corroborating comment posted as comment #2 on 536385539 (meta-refresh
11
+ trigger, raw-CDP hit rates, workaround matrix, link to
12
+ <https://github.com/microsoft/playwright/issues/42366>); issue upvoted; cross-link comment added
13
+ on the Playwright issue. Repro retained here: `repro-cdp-raw.js`.
14
+
15
+ The draft below is preserved for reference (its evidence went into the comment).
16
+
17
+ ---
18
+
19
+ **Title:** Target.closeTarget returns success:true without closing the target when the close
20
+ races a client-side (meta refresh) navigation commit; no Target.targetDestroyed is ever emitted
21
+
22
+ ## Environment
23
+
24
+ - Chromium 151.0.7922.34, `--headless=new`, Linux
25
+ - Raw CDP over WebSocket (no client library — reproduces identically through Playwright 1.62.1,
26
+ where it strands `page.close()` forever)
27
+ - Not reproducible in Firefox or WebKit equivalents
28
+
29
+ ## Steps
30
+
31
+ 1. Create a target and navigate it to a page carrying
32
+ `<meta http-equiv="refresh" content="0; URL='/target.html'">` (immediate client-side
33
+ redirect).
34
+ 2. Leave a `Runtime.evaluate` (`awaitPromise: true`) in flight for the refresh navigation to
35
+ destroy.
36
+ 3. ~45–60 ms after the navigate (during the refresh navigation's commit window), send
37
+ `Target.closeTarget {targetId}` on the browser session.
38
+
39
+ Reproduction rate ~3/10 rounds with the attached script (a timing race; the script sweeps the
40
+ window).
41
+
42
+ ## Expected
43
+
44
+ Either the target closes — `Target.targetDestroyed` fires and the target disappears from
45
+ `Target.getTargets` — or `closeTarget` reports `success: false`/an error.
46
+
47
+ ## Actual
48
+
49
+ `closeTarget` returns `{"result":{"success":true}}`, but:
50
+
51
+ - no `Target.targetDestroyed` is ever emitted for the target;
52
+ - the target remains listed in `Target.getTargets`;
53
+ - the target proceeds to fully load the redirect destination (frameNavigated,
54
+ DOMContentLoaded, load, paint lifecycle events all arrive *after* the successful-looking
55
+ close acknowledgment).
56
+
57
+ A second `Target.closeTarget` for the same target afterwards does close it, as does closing its
58
+ browser context — the target is not stuck, the close was simply dropped.
59
+
60
+ ## Impact
61
+
62
+ Any CDP client that treats `success: true` as "the target will be destroyed" waits forever.
63
+ Playwright's `page.close()` does exactly this: its promise resolves on target destruction, so
64
+ automation against pages using meta-refresh (common in the wild; the W3C ACT-Rules bc659a
65
+ "passed" fixtures are examples) hangs nondeterministically with no error. Observed at ~70–80%
66
+ per attempt via Playwright's timing on real fixture pages.
@@ -0,0 +1,86 @@
1
+ # Pour FP triage — 2026-08-22 capture (440-testcase sample, asserted band)
2
+
3
+ Manual read of every pour false-positive fixture from `results/merged-2026-08-22.jsonl`.
4
+ Verdicts: **real** (pour asserts a failure ACT deems passed/inapplicable — upstream candidate) or
5
+ **artifact** (correct finding counted against the wrong ACT rule by the criterion-level scoring
6
+ policy — not a pour defect).
7
+
8
+ ## Real — upstream filing candidates
9
+
10
+ 1. **Contrast asserted on ACT-exempt text** (3 FPs, `afw4f7`/`09o5cg`). Symbol-only text
11
+ (`09o5cg` Passed Ex 6: a paragraph of `----====++++…` at 3.66:1) and letter-as-icon glyphs
12
+ (`afw4f7` Passed Ex 7 / `09o5cg` Passed Ex 6: `<button aria-label="Close">X</button>`) are
13
+ expected *passed* by the ACT contrast rules; pour's `color-contrast` /
14
+ `color-contrast-enhanced` assert failures. Axe returns no finding on all three (TN). Given
15
+ pour's "report what it cannot prove" ethos, asserting on ACT-consensus-exempt text is worth an
16
+ upstream issue.
17
+ 2. **`button-name` on `<button role="none" disabled>`** (1 FP, `97a4e1` Inapplicable Ex 5). With
18
+ the element disabled (not focusable), `role="none"` holds and the element is out of scope for
19
+ the button-name rule; pour flags it.
20
+ 3. **`link-name` misses descendant `aria-labelledby`** (1 FP, `c487ae` Passed Ex 8):
21
+ `<a href><img aria-labelledby="id1"></a>` — the link's name-from-content includes the image's
22
+ aria-labelledby name, so the link is correctly named; pour flags it empty. Accessible-name
23
+ computation gap.
24
+ 4. **Language rules skip ACT applicability conditions** (4 FPs, `de46e4`/`off6ek`/`ucwvc8`).
25
+ `valid-lang-parts` flags invalid `lang` values on elements whose text is overridden by a
26
+ descendant `lang` or entirely `display:none` (ACT: passed/inapplicable — the SC concerns text
27
+ that *uses* the language); `html-lang` flags a completely empty `<html></html>` page (ACT:
28
+ inapplicable without text). Defensible strictness, but a divergence from ACT consensus worth
29
+ raising upstream.
30
+
31
+ ## Artifact — scoring policy, not pour
32
+
33
+ - **`c487ae` Inapplicable Ex 4** (`<a aria-hidden="true" href>…</a>`): pour's `aria-hidden-focus`
34
+ finding is a *correct* 4.1.2 issue (focusable content hidden from AT); it counts as an FP only
35
+ because the fixture tests a different ACT rule on the same criterion. This is the documented
36
+ generosity of criterion-level matching; an exact engine-rule → ACT-rule map would reclassify it.
37
+
38
+ ## Tally
39
+
40
+ Of 15 criterion-level FPs in the sample, ~9 fixtures inspected covering all hotspots: 9 real
41
+ across 4 defect classes, 1 artifact. The 4 classes are small, well-defined applicability/name-
42
+ computation gaps — exactly the kind of finding the candidacy pipeline is meant to hand upstream
43
+ (engine-candidacy-pipeline.md, Stage 5 / benchmarks §6 stewardship).
44
+
45
+ ## Recheck 2026-09-01 (pour 1.37.0, surea11y 1.7.0)
46
+
47
+ Re-ran every fixture of the eight ACT rules above with the current bundles
48
+ (`--engines pour,surea11y --rules afw4f7,09o5cg,97a4e1,c487ae,de46e4,off6ek,ucwvc8,4b1c6c`).
49
+
50
+ - Pour classes 1–3 persist unchanged in 1.37.0 (contrast on `afw4f7`/`09o5cg` Passed Example 7,
51
+ `button-name` on `97a4e1` Inapplicable Example 5, `link-name` on `c487ae` Passed Example 8).
52
+ Contrast also asserts on `09o5cg` Inapplicable Examples 6 and 7 — label text of a disabled
53
+ control, which ACT's "disabled label" applicability exemption excludes — a third sub-case of
54
+ class 1. Class 4 narrows to `valid-lang-parts` (`de46e4` Passed Example 4, Inapplicable 3–5,
55
+ `off6ek` Inapplicable 5): the `html-lang` hits on empty documents (`ucwvc8` Inapplicable 2–3,
56
+ `97a4e1` Passed 6, `c487ae` Passed 9) are correct under ACT `b5c3f8`, whose applicability has
57
+ no content condition, and count as FPs only through criterion-level scoring. Same for
58
+ surea11y's `html-lang-attr-present` on those fixtures.
59
+ - **surea11y `4b1c6c` (11 FP)** is `iframe-title-unique`, not the 1.7.0 rule
60
+ `identical-iframes-same-purpose` (which already reports differing resources as `cantTell`):
61
+ it fails any two frames sharing a title attribute, including Passed Examples 1, 4–9 where the
62
+ frames embed the same or equivalent resource, and Inapplicable Example 5.
63
+ - Upstream filings prepared from this recheck: pour — presentational-role conflict ignoring
64
+ `disabled`, name-from-content ignoring descendant `aria-labelledby`, contrast on
65
+ non-human-language text / icon glyphs / disabled labels, `valid-lang-parts` applicability;
66
+ surea11y — `iframe-title-unique` vs ACT 4b1c6c.
67
+
68
+ ### Filed upstream 2026-09-01 (pour-engine)
69
+
70
+ | Class | Issue | Pull request |
71
+ | --- | --- | --- |
72
+ | button-name on `<button role="none" disabled>` (presentational-role conflict ignores `:disabled`) | [#3](https://github.com/pourdev/pour-engine/issues/3) | [#7](https://github.com/pourdev/pour-engine/pull/7) |
73
+ | link-name: name from content ignores a descendant's `aria-labelledby` | [#4](https://github.com/pourdev/pour-engine/issues/4) | [#8](https://github.com/pourdev/pour-engine/pull/8) |
74
+ | contrast on symbol-only text, single-letter icons, labels of disabled widgets | [#5](https://github.com/pourdev/pour-engine/issues/5) | [#9](https://github.com/pourdev/pour-engine/pull/9) |
75
+ | valid-lang-parts on `lang` attributes that govern no text | [#6](https://github.com/pourdev/pour-engine/issues/6) | [#10](https://github.com/pourdev/pour-engine/pull/10) |
76
+
77
+ ### Filed upstream 2026-09-01 (surea11y core)
78
+
79
+ | Class | Issue | Pull request |
80
+ | --- | --- | --- |
81
+ | `iframe-title-unique` fails frames sharing a title that embed the same resource (ACT 4b1c6c) | [#16](https://github.com/SureA11y/core/issues/16) | [#17](https://github.com/SureA11y/core/pull/17) |
82
+
83
+ The maintainers document the strict behaviour as deliberate (`docs/ACT_RULE_MAPPING.md`), so the
84
+ issue asks them to choose between aligning the rule with 4b1c6c (the PR) and deprecating it in
85
+ favour of `identical-iframes-same-purpose`. Fixture branches live on the forks
86
+ `wittjeff/pour-engine` and `wittjeff/core`.
@@ -0,0 +1,159 @@
1
+ # Long-lived-browser injection failure — evidence log (2026-08-22)
2
+
3
+ Motivation beyond this harness: production testaro launches a fresh browser per test act because
4
+ of recurring spurious test-isolation bugs, at a large throughput cost. Anything learned here about
5
+ what long-lived-browser state actually breaks — and what level of recycling (context vs browser)
6
+ resets it — bears on whether that cost can be reduced.
7
+
8
+ ## The incident
9
+
10
+ Full ACT capture (1,213 testcases × pour + axe, one Chromium browser + one context for the whole
11
+ run, fresh page per testcase × engine, pages closed after use). From testcase 274 — exactly the
12
+ first fixture of rule `7d6734`, immediately after the last `c487ae` fixture — **every** pour row
13
+ for the remainder of the run (940 rows) failed the same way: script element inserted, no
14
+ exception, `pourEngine` global never defined. Axe, whose adapter executes via `page.evaluate`
15
+ instead of a `<script>` tag, kept detecting instances to the final row. No recovery for ~940
16
+ consecutive pages over ~13 minutes.
17
+
18
+ ## Experiments
19
+
20
+ | # | Setup | Result |
21
+ | --- | --- | --- |
22
+ | 1 | Fresh process, the boundary fixtures (`7d6734`, `afw4f7`) | All pass — fixtures innocent in isolation |
23
+ | 2 | 700 cycles, local benign fixtures, one browser, axe-style page interleaved (data-xpath stamping + axe evaluate), no recycling | 0 failures, flat RSS — page count + interleave alone insufficient |
24
+ | 3 | Full-feed pour re-capture, browser recycled every 100 testcases, no axe interleave | Clean through the entire former cliff region (34/34 on `7d6734`/`307n5z`/`a25f45`) |
25
+ | 4 | Cliff-neighborhood replay (`c487ae`→`a25f45`, 69 testcases), pour + axe interleaved, **no recycling**, fresh process | 69/69 clean — the fixture region + interleave is not a deterministic trigger |
26
+ | 5 | Full-feed pour-only recapture, hardened harness, solo machine, recycling every 100 | **Cliff recurred**: 41 consecutive rows dead starting mid-`1a02b0` (video-transcript fixtures) — in a browser only ~36 pages old. Rules out cumulative-pages-per-browser; the concurrent-load theory weakens too (run was solo). |
27
+ | 6 | Meta-refresh(0) fixtures, local repro (`repro-metarefresh.js`): goto → evaluate (interrupted by the refresh navigation) → `page.close()` | **`page.close()` hangs indefinitely** — never resolves, never rejects — on ~half the rounds, on both a single-redirect page and an a→b→a redirect loop. Playwright 1.62.1, Chromium headless. The context stays usable for *new* pages afterward. Independently, both full captures wedged at exactly the first `bc659a` ("Meta element has no refresh delay") fixture — its passed examples are `content="0; URL=…"` immediate redirects — frozen >35 min in an untimeouted `await page.close()`. |
28
+ | 7 | Replay of the recurrence region (`1a02b0`,`2ee8b8`,`59br37`, 61 testcases), no recycling, fresh process | 61/61 clean — the video region is not a deterministic trigger either. |
29
+ | 8 | Wedged-browser aftermath (from the bc659a incident): `newPage` on the wedged browser | Throws `Protocol error (Target.createTarget): Not supported`, then `Target page, context or browser has been closed` once the process dies — a recognizable signature usable as a replace trigger. A deadline-only trigger let 73 rows (pour) / 886 rows (qualWeb) fail before the next scheduled recycle. |
30
+
31
+ ## Conclusion: probabilistic wedge events, answered by canary-triggered recycling
32
+
33
+ The injection cliff is **nondeterministic**: it recurred at a different position in a nearly-new
34
+ browser on a quiet machine, and every regional replay of a cliff neighborhood comes back clean.
35
+ Best model: races between fixture-initiated behavior (media load, client-side navigation) and
36
+ harness operations occasionally wedge browser-wide `<script>`-element execution — same family as
37
+ the reproducible close() hang, at roughly 1–2 events per ~1,200 real-page cycles. Chasing a
38
+ deterministic trigger further has diminishing value; the effective countermeasures, all now in
39
+ `capture.js`, are:
40
+
41
+ 1. **Deadline-race every browser-touching await** (nothing can hang the loop);
42
+ 2. **Canary assertions** (expected tool global present on text/html pages);
43
+ 3. **Replace the browser on any wedge signature**: tripped deadline, `Protocol error
44
+ (Target.createTarget)`, "has been closed", or a failed canary.
45
+
46
+ ## Upstream status (2026-08-22)
47
+
48
+ - Playwright: filed as <https://github.com/microsoft/playwright/issues/42366> (likely duplicate
49
+ of #42068, which the team milestoned v1.63); **fix PR submitted:**
50
+ <https://github.com/microsoft/playwright/pull/42367> (bounded re-issue of `Target.closeTarget`
51
+ in `CRBrowser._closePage` + stale-fixme removal; their close-related suites pass 41/41).
52
+ Note: Playwright's rolled Chromium 152.0.7977.54 narrows the race window sharply through
53
+ Playwright's call timing (their reload-trigger test passes 6/6 unpatched), but raw CDP still
54
+ reproduces the contract violation on 152 (~2/10) — so channel builds (Chrome/Edge stable,
55
+ 151-class, 70–80% hit rate) remain the population the guard protects.
56
+ - testaro: `browserClose` hardened with a 10 s settle-deadline per close (try/catch cannot catch
57
+ non-settlement) — PR <https://github.com/YRA-Tech/testaro/pull/92>. (Discovered en route:
58
+ jrpool/testaro now redirects to YRA-Tech/testaro — the repo was transferred, so #92 IS the
59
+ upstream PR.) The same patch is applied to the working tree used by the harness.
60
+ - Chromium: already tracked as <https://issues.chromium.org/issues/536385539> (filed 2026-07-19
61
+ by a Google engineer; P2/S2; **pending code change 8251379**). Their C++ root cause matches our
62
+ protocol-level one exactly: `WebContentsImpl::ClosePage` arms the close on the current main
63
+ RenderFrameHost; a racing navigation commit swaps the RFH; the pending `ClosePage` callback
64
+ dies with the old RFH and is never re-issued. Our meta-refresh(0) trigger, hit rates, and
65
+ workaround matrix posted there as comment #2; both issues cross-linked. Practical upshot for
66
+ testaro: a fix is in flight upstream, but the deadline+canary+context-recycle defenses remain
67
+ necessary until it ships in a released Chromium — and for the injection cliff, which has no
68
+ upstream issue yet.
69
+
70
+ ## Discovery #2 — root-caused (2026-08-22 evening)
71
+
72
+ A `DEBUG=pw:protocol` trace of a hanging round shows the full mechanism: Playwright sends
73
+ `Target.closeTarget`, Chromium replies `{"result":{"success":true}}`, but the target — mid-commit
74
+ in the meta-refresh navigation — never closes; the same session then emits the redirect
75
+ destination's entire load lifecycle after the "successful" close, `Target.targetDestroyed` never
76
+ fires, and `page.close()` waits for it forever. Chromium acknowledges a close it does not
77
+ perform; Playwright trusts the acknowledgment with no timeout.
78
+
79
+ Matrix (10 rounds/cell): **Chromium-only** (Firefox 153 and WebKit: 0/10 everywhere), and
80
+ **default `close()` only** — `close({runBeforeUnload: true})` 0/10, `context.close()` 0/10.
81
+ After a hang, `context.close()` still settles and destroys the page (**8/8 recoveries**).
82
+
83
+ The recovery result upgrades the production recommendation: for this wedge class,
84
+ **context-level teardown is a sufficient and reliable recovery** — no browser relaunch needed
85
+ (~10 ms vs ~300 ms). A pooled-browser testaro could run context-per-act with deadline-raced
86
+ closes, escalating to browser replacement only if the context teardown itself trips its
87
+ deadline. (The injection cliff's recovery level remains untested — it is not reproducible on
88
+ demand — so the escalation path should stay.)
89
+
90
+ ## Discovery #2 (original observation): page.close() can hang forever after a meta-refresh navigation
91
+
92
+ This is a second, *reproducible-at-will* failure mode, distinct from the injection cliff but in
93
+ the same family (navigation racing teardown corrupts page state):
94
+
95
+ - Signature: `close()` neither resolves nor rejects, so `await page.close().catch(...)` blocks
96
+ forever — `.catch` guards rejection, not non-settlement. A loop with no deadline around close
97
+ freezes permanently, silently.
98
+ - Nondeterministic (~50% of rounds locally): it is a race with the refresh-triggered navigation,
99
+ which also explains why one run passes a fixture another run wedges on.
100
+ - No matching upstream issue found (2026-08-22 search); nearest family member is
101
+ [playwright#33806](https://github.com/microsoft/playwright/issues/33806) (hangs waiting for
102
+ pending navigations). **Fileable** with `repro-metarefresh.js` (~40 lines, local pages, no
103
+ external deps beyond playwright + any large script to inject — the script is incidental).
104
+ - Harness guard shipped in `capture.js`: every browser-touching await is raced against a
105
+ deadline (`withDeadline`), a tripped deadline presumes a wedged browser and replaces it
106
+ (`replaceBrowser`, itself deadline-guarded), and captures resume by skipping rows already in
107
+ the out file. Verified against the live `bc659a` fixtures: one real close-hang tripped the 5 s
108
+ deadline, the browser was replaced, and all 15 fixtures completed.
109
+
110
+ ### Production translation (testaro)
111
+
112
+ Pages that navigate during scanning (meta refresh is common on real sites) can hang `close()`
113
+ forever. In a browser-per-act world the leaked hang is bounded by the act's child process exit;
114
+ in any pooled/long-lived design it is fatal without deadlines. Concretely: (1) every
115
+ `page.close()`/`browserClose()` should be deadline-raced, (2) a tripped deadline should recycle
116
+ the browser, not just the page, and (3) the meta-refresh fixture class belongs in any isolation
117
+ test suite as the canonical wedge-inducer.
118
+
119
+ The overall answer to "could testaro keep a browser across acts?": the observed isolation
120
+ failures here were **rare, probabilistic wedge events with recognizable signatures**, not
121
+ gradual deterministic leakage. That favors a middle path over browser-per-act: pool the browser,
122
+ assert a cheap per-act canary (script-injection round-trip + expected globals), and recycle the
123
+ browser only on a wedge signature or every N acts — the three-countermeasure pattern above,
124
+ already exercised at ~1,200-page scale by this harness. The remaining unknown for production is
125
+ whether *cross-page contamination* (state leaking between scanned pages, distinct from wedges)
126
+ also occurs; this harness's same-tool-different-page design cannot see that class, so a pooled
127
+ design should keep context-per-act (cheap) even if the browser is shared.
128
+
129
+ ## Explained failure classes (distinct from the cliff)
130
+
131
+ - `contentType application/xml` fixtures: `document.createElement('script')` in an XML document
132
+ creates a null-namespace element that never executes — expected, benign, now visible via the
133
+ adapter's diagnostic error (content type, script chars, ready state).
134
+ - "Resulting promise was garbage collected": fixtures that navigate (meta refresh) during the
135
+ in-page evaluate. All observed on `inapplicable` testcases.
136
+
137
+ ## Current best theory
138
+
139
+ Not page count, not the axe interleave, not any single fixture so far: either (a) cumulative
140
+ state that only the full 273-testcase real-page history builds up, or (b) a nondeterministic
141
+ in-browser event (renderer/helper-process crash or OOM kill — the host was carrying ~31 GB of
142
+ desktop Chrome RSS during the incident) that wedges `<script>`-element execution while leaving
143
+ CDP `evaluate` execution intact. All fixtures being same-origin (`www.w3.org`) means Chromium packs
144
+ them into shared renderer processes, which is consistent with one wedged process poisoning every
145
+ subsequent same-origin page.
146
+
147
+ ## Production relevance (testaro browser-per-act)
148
+
149
+ - The failure mode was **silent** — no exception anywhere; only an output-level check (expected
150
+ global absent) caught it. Any move away from browser-per-act needs a per-act canary assertion
151
+ of this kind, not just error handling.
152
+ - Recycling every N acts (experiment 3) is so far sufficient as a guard and is the cheap middle
153
+ ground between browser-per-act and browser-forever; whether *context*-per-act (much cheaper
154
+ than browser launch) also resets the poisoned state is untested — worth adding
155
+ `--recycle-context` bisection if experiment 5 reproduces the cliff.
156
+ - If experiment 5 comes back clean, the honest conclusion is: one observed environmental
157
+ incident in ~5,000 page-cycles, mitigated by periodic recycling — suggestive that testaro's
158
+ isolation bugs may likewise be rare wedge events rather than deterministic leakage, which would
159
+ argue for recycle-on-anomaly (canary-triggered) rather than always-fresh browsers.
@@ -0,0 +1,73 @@
1
+ # Playwright bug report — page.close() never settles after meta-refresh navigation
2
+
3
+ **FILED 2026-08-22: <https://github.com/microsoft/playwright/issues/42366>**
4
+ Minimal repro: `repro-metarefresh.js` in this directory (self-contained, local HTTP server).
5
+ Chromium side: already tracked as <https://issues.chromium.org/issues/536385539> (pending code
6
+ change 8251379); cross-linked in both directions 2026-08-22 — see `chromium-issue-draft.md`.
7
+
8
+ ---
9
+
10
+ **Title:** [Bug]: page.close() hangs forever — Chromium's Target.closeTarget returns success:true
11
+ without closing a target that is mid-commit in a client-side (meta refresh) navigation
12
+
13
+ ## System info
14
+
15
+ - Playwright Version: 1.62.1 (latest at time of report)
16
+ - Operating System: Linux (7.0.0-30-generic)
17
+ - Browser: Chromium (bundled, HeadlessChrome/151.0.7922.34) — **Chromium only**; Firefox and
18
+ WebKit are unaffected (0/10 each in the same harness)
19
+ - Other info: Node 24.14.1
20
+
21
+ ## Repro
22
+
23
+ 1. Serve a page with `<meta http-equiv="refresh" content="0; URL='/target.html'">` (immediate
24
+ client-side redirect; the W3C ACT-Rules bc659a "passed" fixtures are real-world examples).
25
+ 2. `page.goto(url, {waitUntil: 'load'})` — resolves.
26
+ 3. `page.evaluate(...)` — rejects with "Execution context was destroyed" (expected; the refresh
27
+ navigation fired mid-evaluate).
28
+ 4. `await page.close()` — **hangs on ~70–80% of rounds** (8/10, 7/10 across runs): the promise
29
+ never resolves and never rejects. `.catch()` never fires; a loop awaiting it freezes forever.
30
+
31
+ ## Root cause (from a `DEBUG=pw:protocol` trace of a hanging round)
32
+
33
+ - Playwright sends `Target.closeTarget {targetId}`.
34
+ - The browser replies `{"result":{"success":true}}`.
35
+ - The target does not close: the meta-refresh navigation was mid-commit, and the same session
36
+ then emits the full lifecycle of the redirect destination — `Page.frameNavigated`,
37
+ `DOMContentLoaded`, `load`, paint events, `networkIdle` — all *after* the successful-looking
38
+ `closeTarget` response.
39
+ - `Target.targetDestroyed` is never emitted; `page.close()` waits for it indefinitely.
40
+
41
+ So Chromium acknowledges the close without performing it when the close races a navigation
42
+ commit, and Playwright trusts the acknowledgment with no timeout or retry.
43
+
44
+ The browser-side half is confirmed **without Playwright**: a raw-CDP repro (plain WebSocket, no
45
+ client library) shows `Target.closeTarget` → `success:true` with no `Target.targetDestroyed`
46
+ ever emitted and the target still listed in `Target.getTargets`, ~3/10 rounds. A Chromium bug is
47
+ being filed in parallel (link when available). Playwright still has its own half: `page.close()`
48
+ trusts the acknowledgment indefinitely — a deadline/retry (or routing default close through the
49
+ `runBeforeUnload` path, which is immune) would make clients robust to the browser defect.
50
+
51
+ ## Matrix (10 rounds per cell, same repro)
52
+
53
+ | | `close()` | `close({runBeforeUnload:true})` | `context.close()` |
54
+ | --- | --- | --- | --- |
55
+ | Chromium | **8/10 hang** | 0/10 | 0/10 |
56
+ | Firefox | 0/10 | 0/10 | 0/10 |
57
+ | WebKit | 0/10 | 0/10 | 0/10 |
58
+
59
+ After a hang, `context.close()` still settles and destroys the page (**8/8 recoveries**) — the
60
+ context teardown path is unaffected.
61
+
62
+ ## Expected behavior
63
+
64
+ `page.close()` always settles — resolves once the page is gone, or rejects.
65
+
66
+ **Workarounds** (verified): `page.close({runBeforeUnload: true})`; or race `close()` against a
67
+ deadline and call `context.close()` on expiry (recovers reliably). Without a guard, a wedged
68
+ browser's subsequent `newPage` can then fail with `Protocol error (Target.createTarget): Not
69
+ supported` followed by "Target page, context or browser has been closed".
70
+
71
+ Possibly related: #33806 (hangs while waiting for pending navigations) — different in that no
72
+ dialog is involved and the acknowledged-but-not-performed `Target.closeTarget` is visible in the
73
+ protocol trace.
@@ -0,0 +1,105 @@
1
+ // Playwright-free reproduction attempt: raw CDP over WebSocket (Node's
2
+ // native WebSocket client). Question: does Chromium's Target.closeTarget
3
+ // return {success:true} WITHOUT closing (no Target.targetDestroyed, target
4
+ // still listed) when the close races a meta-refresh navigation commit?
5
+ // If yes → browser-side contract violation, independent of Playwright.
6
+ const http = require('http');
7
+ const {spawn} = require('child_process');
8
+ // Any Chromium binary works; defaults to Playwright's bundled one.
9
+ const {chromium} = require('playwright');
10
+ const CHROME = process.env.CHROME_PATH || chromium.executablePath();
11
+
12
+ const pages = {
13
+ '/a.html': `<!DOCTYPE html><html lang="en"><head><title>a</title>
14
+ <meta http-equiv="refresh" content="0; URL='/target.html'"></head>
15
+ <body><p>redirecting</p></body></html>`,
16
+ '/target.html': `<!DOCTYPE html><html lang="en"><head><title>t</title></head>
17
+ <body><p>arrived</p></body></html>`
18
+ };
19
+
20
+ (async () => {
21
+ const server = http.createServer((req, res) => {
22
+ res.setHeader('content-type', 'text/html');
23
+ res.end(pages[req.url] || pages['/target.html']);
24
+ });
25
+ await new Promise(resolve => server.listen(0, resolve));
26
+ const base = `http://127.0.0.1:${server.address().port}`;
27
+
28
+ // Launch the same Chromium binary Playwright uses, but bare.
29
+ const chrome = spawn(CHROME, [
30
+ '--headless=new', '--remote-debugging-port=0', '--no-first-run', '--no-default-browser-check',
31
+ '--user-data-dir=/tmp/cdp-repro-profile'
32
+ ]);
33
+ const wsURL = await new Promise((resolve, reject) => {
34
+ let stderr = '';
35
+ chrome.stderr.on('data', chunk => {
36
+ stderr += chunk;
37
+ const match = stderr.match(/DevTools listening on (ws:\/\/\S+)/);
38
+ if (match) resolve(match[1]);
39
+ });
40
+ setTimeout(() => reject(new Error('no DevTools ws URL')), 15000);
41
+ });
42
+
43
+ const ws = new WebSocket(wsURL);
44
+ await new Promise(resolve => ws.onopen = resolve);
45
+ let nextId = 1;
46
+ const pending = new Map();
47
+ const events = [];
48
+ ws.onmessage = messageEvent => {
49
+ const message = JSON.parse(messageEvent.data);
50
+ if (message.id && pending.has(message.id)) {
51
+ pending.get(message.id)(message);
52
+ pending.delete(message.id);
53
+ }
54
+ else if (message.method) {
55
+ events.push(message);
56
+ }
57
+ };
58
+ const send = (method, params = {}, sessionId) => new Promise(resolve => {
59
+ const id = nextId++;
60
+ pending.set(id, resolve);
61
+ ws.send(JSON.stringify({id, method, params, ...(sessionId ? {sessionId} : {})}));
62
+ });
63
+ const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
64
+
65
+ await send('Target.setDiscoverTargets', {discover: true});
66
+ let contractViolations = 0;
67
+ const ROUNDS = 10;
68
+ for (let round = 0; round < ROUNDS; round++) {
69
+ const {result: {targetId}} = await send('Target.createTarget', {url: 'about:blank'});
70
+ const {result: {sessionId}} = await send('Target.attachToTarget', {targetId, flatten: true});
71
+ await send('Page.enable', {}, sessionId);
72
+ await send('Runtime.enable', {}, sessionId);
73
+ await send('Page.navigate', {url: `${base}/a.html`}, sessionId);
74
+ // Leave an evaluate in flight for the refresh navigation to destroy
75
+ // (mirrors the real-world trigger), then close during the commit window.
76
+ send('Runtime.evaluate', {
77
+ expression: 'new Promise(r => setTimeout(r, 100))',
78
+ awaitPromise: true
79
+ }, sessionId);
80
+ await wait(30 + (round % 5) * 15);
81
+ events.length = 0;
82
+ const closeReply = await send('Target.closeTarget', {targetId});
83
+ const success = closeReply.result && closeReply.result.success;
84
+ await wait(3000);
85
+ const destroyed = events.some(
86
+ e => e.method === 'Target.targetDestroyed' && e.params.targetId === targetId
87
+ );
88
+ const {result: {targetInfos}} = await send('Target.getTargets');
89
+ const stillListed = targetInfos.some(t => t.targetId === targetId);
90
+ const verdict = success && ! destroyed && stillListed ? 'CONTRACT VIOLATION'
91
+ : success && destroyed ? 'closed properly'
92
+ : `other (success=${success} destroyed=${destroyed} listed=${stillListed})`;
93
+ console.log(`round ${round}: closeTarget success=${success}, targetDestroyed=${destroyed}, stillListed=${stillListed} → ${verdict}`);
94
+ if (verdict === 'CONTRACT VIOLATION') {
95
+ contractViolations++;
96
+ // Clean up the orphan so rounds stay independent.
97
+ await send('Target.closeTarget', {targetId});
98
+ await wait(500);
99
+ }
100
+ }
101
+ console.log(`${contractViolations}/${ROUNDS} rounds: success:true with target neither destroyed nor removed`);
102
+ chrome.kill();
103
+ server.close();
104
+ process.exit(0);
105
+ })();
@@ -0,0 +1,65 @@
1
+ /*
2
+ © 2026 Jeff Witt.
3
+
4
+ Licensed under the MIT License. See LICENSE file at the project root or
5
+ https://opensource.org/license/mit/ for details.
6
+
7
+ SPDX-License-Identifier: MIT
8
+ */
9
+
10
+ /*
11
+ repro-metarefresh.js
12
+ Minimal reproduction: page.close() intermittently NEVER SETTLES (neither
13
+ resolves nor rejects) when the page performed a meta-refresh(0) navigation
14
+ that interrupted an in-flight evaluate. See playwright-issue-draft.md.
15
+ Observed on playwright 1.62.1, Chromium headless: ~50% of rounds hang.
16
+ Run from the repository root: node validation/act/repro-metarefresh.js
17
+ */
18
+
19
+ const http = require('http');
20
+ const {chromium} = require('playwright');
21
+
22
+ const pages = {
23
+ '/a.html': `<!DOCTYPE html><html lang="en"><head><title>a</title>
24
+ <meta http-equiv="refresh" content="0; URL='/target.html'"></head>
25
+ <body><p>redirecting</p></body></html>`,
26
+ '/target.html': `<!DOCTYPE html><html lang="en"><head><title>t</title></head>
27
+ <body><p>arrived</p></body></html>`
28
+ };
29
+
30
+ // Reports whether a promise settles within ms.
31
+ const settles = (promise, ms) => Promise.race([
32
+ promise.then(() => 'resolved').catch(() => 'rejected'),
33
+ new Promise(resolve => setTimeout(() => resolve('NEVER SETTLED'), ms))
34
+ ]);
35
+
36
+ (async () => {
37
+ const server = http.createServer((req, res) => {
38
+ res.setHeader('content-type', 'text/html');
39
+ res.end(pages[req.url] || pages['/target.html']);
40
+ });
41
+ await new Promise(resolve => server.listen(0, resolve));
42
+ const base = `http://127.0.0.1:${server.address().port}`;
43
+ const browser = await chromium.launch();
44
+ const context = await browser.newContext();
45
+ let hangs = 0;
46
+ const ROUNDS = 10;
47
+ for (let round = 0; round < ROUNDS; round++) {
48
+ const page = await context.newPage();
49
+ await page.goto(`${base}/a.html`, {waitUntil: 'load'});
50
+ // An evaluate for the meta refresh to interrupt (it rejects with
51
+ // "Execution context was destroyed" — expected and irrelevant).
52
+ await page.evaluate(
53
+ () => new Promise(resolve => setTimeout(resolve, 100))
54
+ ).catch(() => {});
55
+ const outcome = await settles(page.close(), 10000);
56
+ console.log(`round ${round}: page.close() ${outcome}`);
57
+ if (outcome === 'NEVER SETTLED') {
58
+ hangs++;
59
+ }
60
+ }
61
+ console.log(`${hangs}/${ROUNDS} rounds hung`);
62
+ await Promise.race([browser.close(), new Promise(r => setTimeout(r, 5000))]);
63
+ server.close();
64
+ process.exit(hangs ? 1 : 0);
65
+ })();