@rehearsal-labs/retest 0.1.0 → 0.2.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 (315) hide show
  1. package/README.md +6 -0
  2. package/dist/agent/session.d.ts +21 -2
  3. package/dist/agent/session.d.ts.map +1 -1
  4. package/dist/agent/session.js +73 -11
  5. package/dist/api/app-page.d.ts +39 -3
  6. package/dist/api/app-page.d.ts.map +1 -1
  7. package/dist/api/app-page.js +108 -0
  8. package/dist/api/dialog.d.ts +38 -0
  9. package/dist/api/dialog.d.ts.map +1 -0
  10. package/dist/api/dialog.js +78 -0
  11. package/dist/api/drag.d.ts +32 -0
  12. package/dist/api/drag.d.ts.map +1 -0
  13. package/dist/api/drag.js +26 -0
  14. package/dist/api/files.d.ts +49 -0
  15. package/dist/api/files.d.ts.map +1 -0
  16. package/dist/api/files.js +168 -0
  17. package/dist/api/inbox.d.ts +97 -0
  18. package/dist/api/inbox.d.ts.map +1 -0
  19. package/dist/api/inbox.js +191 -0
  20. package/dist/api/locator-recipes.d.ts +1 -1
  21. package/dist/api/locator-recipes.d.ts.map +1 -1
  22. package/dist/api/locator-recipes.js +4 -3
  23. package/dist/api/page.d.ts +214 -4
  24. package/dist/api/page.d.ts.map +1 -1
  25. package/dist/api/route.d.ts +21 -0
  26. package/dist/api/route.d.ts.map +1 -0
  27. package/dist/api/route.js +99 -0
  28. package/dist/api/test-run.d.ts +127 -1
  29. package/dist/api/test-run.d.ts.map +1 -1
  30. package/dist/api/test-run.js +293 -0
  31. package/dist/assertions/expect.d.ts +91 -2
  32. package/dist/assertions/expect.d.ts.map +1 -1
  33. package/dist/assertions/expect.js +98 -0
  34. package/dist/assertions/look-until.d.ts +5 -0
  35. package/dist/assertions/look-until.d.ts.map +1 -1
  36. package/dist/assertions/look-until.js +1 -1
  37. package/dist/assertions/poll-layout.d.ts +24 -0
  38. package/dist/assertions/poll-layout.d.ts.map +1 -0
  39. package/dist/assertions/poll-layout.js +63 -0
  40. package/dist/assertions/screenshot.d.ts +36 -0
  41. package/dist/assertions/screenshot.d.ts.map +1 -0
  42. package/dist/assertions/screenshot.js +125 -0
  43. package/dist/browser/accessibility.d.ts.map +1 -1
  44. package/dist/browser/accessibility.js +22 -1
  45. package/dist/browser/actionability.d.ts +9 -6
  46. package/dist/browser/actionability.d.ts.map +1 -1
  47. package/dist/browser/actionability.js +17 -8
  48. package/dist/browser/browser.d.ts.map +1 -1
  49. package/dist/browser/browser.js +15 -4
  50. package/dist/browser/cdp/connection.d.ts +6 -0
  51. package/dist/browser/cdp/connection.d.ts.map +1 -1
  52. package/dist/browser/cdp/connection.js +8 -0
  53. package/dist/browser/checked-state.d.ts +2 -2
  54. package/dist/browser/checked-state.d.ts.map +1 -1
  55. package/dist/browser/chromium-process.d.ts +9 -2
  56. package/dist/browser/chromium-process.d.ts.map +1 -1
  57. package/dist/browser/chromium-process.js +17 -4
  58. package/dist/browser/contract.d.ts +119 -5
  59. package/dist/browser/contract.d.ts.map +1 -1
  60. package/dist/browser/dialogs.d.ts +157 -0
  61. package/dist/browser/dialogs.d.ts.map +1 -0
  62. package/dist/browser/dialogs.js +271 -0
  63. package/dist/browser/downloads.d.ts +71 -0
  64. package/dist/browser/downloads.d.ts.map +1 -0
  65. package/dist/browser/downloads.js +253 -0
  66. package/dist/browser/drag.d.ts +50 -0
  67. package/dist/browser/drag.d.ts.map +1 -0
  68. package/dist/browser/drag.js +584 -0
  69. package/dist/browser/electron-window-watch.d.ts +32 -0
  70. package/dist/browser/electron-window-watch.d.ts.map +1 -0
  71. package/dist/browser/electron-window-watch.js +93 -0
  72. package/dist/browser/electron.d.ts +13 -2
  73. package/dist/browser/electron.d.ts.map +1 -1
  74. package/dist/browser/electron.js +53 -8
  75. package/dist/browser/element-queries.d.ts +51 -15
  76. package/dist/browser/element-queries.d.ts.map +1 -1
  77. package/dist/browser/element-queries.js +92 -13
  78. package/dist/browser/emulation.d.ts.map +1 -1
  79. package/dist/browser/emulation.js +7 -5
  80. package/dist/browser/file-chooser.d.ts +75 -0
  81. package/dist/browser/file-chooser.d.ts.map +1 -0
  82. package/dist/browser/file-chooser.js +449 -0
  83. package/dist/browser/firefox/input.d.ts +5 -0
  84. package/dist/browser/firefox/input.d.ts.map +1 -1
  85. package/dist/browser/firefox/input.js +6 -3
  86. package/dist/browser/firefox/page.d.ts +11 -1
  87. package/dist/browser/firefox/page.d.ts.map +1 -1
  88. package/dist/browser/firefox/page.js +66 -7
  89. package/dist/browser/frames.d.ts +205 -0
  90. package/dist/browser/frames.d.ts.map +1 -0
  91. package/dist/browser/frames.js +657 -0
  92. package/dist/browser/input-guard.d.ts +18 -5
  93. package/dist/browser/input-guard.d.ts.map +1 -1
  94. package/dist/browser/input-guard.js +65 -6
  95. package/dist/browser/isolated-world.d.ts +8 -1
  96. package/dist/browser/isolated-world.d.ts.map +1 -1
  97. package/dist/browser/layout.d.ts +120 -0
  98. package/dist/browser/layout.d.ts.map +1 -0
  99. package/dist/browser/layout.js +213 -0
  100. package/dist/browser/locate.d.ts +10 -0
  101. package/dist/browser/locate.d.ts.map +1 -1
  102. package/dist/browser/locate.js +24 -6
  103. package/dist/browser/origin-document.d.ts +4 -0
  104. package/dist/browser/origin-document.d.ts.map +1 -1
  105. package/dist/browser/origin-document.js +9 -1
  106. package/dist/browser/page-scripts.d.ts +48 -0
  107. package/dist/browser/page-scripts.d.ts.map +1 -1
  108. package/dist/browser/page-scripts.js +515 -39
  109. package/dist/browser/page.d.ts +60 -1
  110. package/dist/browser/page.d.ts.map +1 -1
  111. package/dist/browser/page.js +275 -32
  112. package/dist/browser/popups.d.ts +166 -0
  113. package/dist/browser/popups.d.ts.map +1 -0
  114. package/dist/browser/popups.js +317 -0
  115. package/dist/browser/routes.d.ts +97 -0
  116. package/dist/browser/routes.d.ts.map +1 -0
  117. package/dist/browser/routes.js +226 -0
  118. package/dist/browser/screenshot.d.ts +44 -0
  119. package/dist/browser/screenshot.d.ts.map +1 -0
  120. package/dist/browser/screenshot.js +51 -0
  121. package/dist/browser/shadow-candidates.d.ts +20 -0
  122. package/dist/browser/shadow-candidates.d.ts.map +1 -0
  123. package/dist/browser/shadow-candidates.js +77 -0
  124. package/dist/browser/webkit/page.d.ts +11 -1
  125. package/dist/browser/webkit/page.d.ts.map +1 -1
  126. package/dist/browser/webkit/page.js +53 -2
  127. package/dist/browser/webkit/relay.d.ts.map +1 -1
  128. package/dist/browser/webkit/relay.js +5 -1
  129. package/dist/cli/commands/run.d.ts.map +1 -1
  130. package/dist/cli/commands/run.js +2 -0
  131. package/dist/cli/inspect/test-timeline.d.ts.map +1 -1
  132. package/dist/cli/inspect/test-timeline.js +38 -4
  133. package/dist/config/loaded.d.ts +12 -2
  134. package/dist/config/loaded.d.ts.map +1 -1
  135. package/dist/config/read-inboxes.d.ts +10 -0
  136. package/dist/config/read-inboxes.d.ts.map +1 -0
  137. package/dist/config/read-inboxes.js +78 -0
  138. package/dist/config/register.d.ts +5 -1
  139. package/dist/config/register.d.ts.map +1 -1
  140. package/dist/config/types.d.ts +36 -0
  141. package/dist/config/types.d.ts.map +1 -1
  142. package/dist/config/validate.d.ts.map +1 -1
  143. package/dist/config/validate.js +4 -1
  144. package/dist/diagnostics/artifact.d.ts.map +1 -1
  145. package/dist/diagnostics/artifact.js +10 -2
  146. package/dist/diagnostics/attempt.d.ts +5 -0
  147. package/dist/diagnostics/attempt.d.ts.map +1 -1
  148. package/dist/diagnostics/attempt.js +10 -0
  149. package/dist/diagnostics/chromium-collector.d.ts +9 -1
  150. package/dist/diagnostics/chromium-collector.d.ts.map +1 -1
  151. package/dist/diagnostics/chromium-collector.js +19 -4
  152. package/dist/diagnostics/observations.d.ts +8 -0
  153. package/dist/diagnostics/observations.d.ts.map +1 -1
  154. package/dist/diagnostics/session-capture.d.ts.map +1 -1
  155. package/dist/diagnostics/session-capture.js +8 -0
  156. package/dist/index.d.ts +9 -4
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +1 -0
  159. package/dist/media/policy.d.ts +2 -2
  160. package/dist/media/policy.d.ts.map +1 -1
  161. package/dist/media/policy.js +1 -1
  162. package/dist/playwright/not-yet.d.ts.map +1 -1
  163. package/dist/playwright/not-yet.js +5 -2
  164. package/dist/protocol/commands.d.ts +122 -13
  165. package/dist/protocol/commands.d.ts.map +1 -1
  166. package/dist/protocol/commands.js +85 -6
  167. package/dist/protocol/diagnostics.d.ts +10 -0
  168. package/dist/protocol/diagnostics.d.ts.map +1 -1
  169. package/dist/protocol/diagnostics.js +5 -0
  170. package/dist/protocol/dialogs.d.ts +104 -0
  171. package/dist/protocol/dialogs.d.ts.map +1 -0
  172. package/dist/protocol/dialogs.js +101 -0
  173. package/dist/protocol/drag.d.ts +45 -0
  174. package/dist/protocol/drag.d.ts.map +1 -0
  175. package/dist/protocol/drag.js +31 -0
  176. package/dist/protocol/events.d.ts +153 -1
  177. package/dist/protocol/events.d.ts.map +1 -1
  178. package/dist/protocol/events.js +97 -0
  179. package/dist/protocol/files.d.ts +83 -0
  180. package/dist/protocol/files.d.ts.map +1 -0
  181. package/dist/protocol/files.js +68 -0
  182. package/dist/protocol/inbox.d.ts +87 -0
  183. package/dist/protocol/inbox.d.ts.map +1 -0
  184. package/dist/protocol/inbox.js +51 -0
  185. package/dist/protocol/index.d.ts +4 -1
  186. package/dist/protocol/index.d.ts.map +1 -1
  187. package/dist/protocol/layout-checks.d.ts +83 -0
  188. package/dist/protocol/layout-checks.d.ts.map +1 -0
  189. package/dist/protocol/layout-checks.js +214 -0
  190. package/dist/protocol/layout.d.ts +135 -0
  191. package/dist/protocol/layout.d.ts.map +1 -0
  192. package/dist/protocol/layout.js +124 -0
  193. package/dist/protocol/locator-checks.d.ts +19 -2
  194. package/dist/protocol/locator-checks.d.ts.map +1 -1
  195. package/dist/protocol/locator-checks.js +23 -1
  196. package/dist/protocol/locator.d.ts +55 -7
  197. package/dist/protocol/locator.d.ts.map +1 -1
  198. package/dist/protocol/locator.js +99 -18
  199. package/dist/protocol/messages.d.ts +95 -0
  200. package/dist/protocol/messages.d.ts.map +1 -1
  201. package/dist/protocol/messages.js +63 -0
  202. package/dist/protocol/popups.d.ts +43 -0
  203. package/dist/protocol/popups.d.ts.map +1 -0
  204. package/dist/protocol/popups.js +27 -0
  205. package/dist/protocol/result.d.ts +5 -0
  206. package/dist/protocol/result.d.ts.map +1 -1
  207. package/dist/protocol/result.js +1 -0
  208. package/dist/protocol/routes.d.ts +136 -0
  209. package/dist/protocol/routes.d.ts.map +1 -0
  210. package/dist/protocol/routes.js +191 -0
  211. package/dist/protocol/screenshot.d.ts +219 -0
  212. package/dist/protocol/screenshot.d.ts.map +1 -0
  213. package/dist/protocol/screenshot.js +335 -0
  214. package/dist/reporters/actions.d.ts +6 -4
  215. package/dist/reporters/actions.d.ts.map +1 -1
  216. package/dist/reporters/actions.js +24 -6
  217. package/dist/reporters/agent.d.ts.map +1 -1
  218. package/dist/reporters/agent.js +4 -2
  219. package/dist/reporters/dialogs-popups.d.ts +22 -0
  220. package/dist/reporters/dialogs-popups.d.ts.map +1 -0
  221. package/dist/reporters/dialogs-popups.js +30 -0
  222. package/dist/reporters/failure-card.d.ts +10 -1
  223. package/dist/reporters/failure-card.d.ts.map +1 -1
  224. package/dist/reporters/failure-card.js +5 -1
  225. package/dist/reporters/format.d.ts +33 -0
  226. package/dist/reporters/format.d.ts.map +1 -1
  227. package/dist/reporters/format.js +42 -0
  228. package/dist/reporters/html/comparison-view.d.ts +12 -0
  229. package/dist/reporters/html/comparison-view.d.ts.map +1 -0
  230. package/dist/reporters/html/comparison-view.js +21 -0
  231. package/dist/reporters/html/diagnostics-view.js +8 -0
  232. package/dist/reporters/html/downloads-view.d.ts +10 -0
  233. package/dist/reporters/html/downloads-view.d.ts.map +1 -0
  234. package/dist/reporters/html/downloads-view.js +29 -0
  235. package/dist/reporters/html/style.d.ts.map +1 -1
  236. package/dist/reporters/html/style.js +3 -0
  237. package/dist/reporters/html/summary-view.d.ts.map +1 -1
  238. package/dist/reporters/html/summary-view.js +3 -2
  239. package/dist/reporters/html/test-view.d.ts.map +1 -1
  240. package/dist/reporters/html/test-view.js +6 -3
  241. package/dist/reporters/html/timeline-view.d.ts.map +1 -1
  242. package/dist/reporters/html/timeline-view.js +26 -2
  243. package/dist/reporters/human-card.js +10 -0
  244. package/dist/reporters/human.d.ts.map +1 -1
  245. package/dist/reporters/human.js +3 -1
  246. package/dist/reporters/routes.d.ts +9 -0
  247. package/dist/reporters/routes.d.ts.map +1 -0
  248. package/dist/reporters/routes.js +17 -0
  249. package/dist/runner/child-program.js +10 -0
  250. package/dist/runner/contract.d.ts +5 -0
  251. package/dist/runner/contract.d.ts.map +1 -1
  252. package/dist/runner/dialogs.d.ts +71 -0
  253. package/dist/runner/dialogs.d.ts.map +1 -0
  254. package/dist/runner/dialogs.js +189 -0
  255. package/dist/runner/files.d.ts +79 -0
  256. package/dist/runner/files.d.ts.map +1 -0
  257. package/dist/runner/files.js +312 -0
  258. package/dist/runner/inbox-links.d.ts +19 -0
  259. package/dist/runner/inbox-links.d.ts.map +1 -0
  260. package/dist/runner/inbox-links.js +63 -0
  261. package/dist/runner/inbox.d.ts +87 -0
  262. package/dist/runner/inbox.d.ts.map +1 -0
  263. package/dist/runner/inbox.js +340 -0
  264. package/dist/runner/index.d.ts +2 -1
  265. package/dist/runner/index.d.ts.map +1 -1
  266. package/dist/runner/native-pool.d.ts.map +1 -1
  267. package/dist/runner/native-pool.js +2 -0
  268. package/dist/runner/observations.d.ts +15 -2
  269. package/dist/runner/observations.d.ts.map +1 -1
  270. package/dist/runner/observations.js +109 -6
  271. package/dist/runner/popups.d.ts +82 -0
  272. package/dist/runner/popups.d.ts.map +1 -0
  273. package/dist/runner/popups.js +236 -0
  274. package/dist/runner/redactor.d.ts.map +1 -1
  275. package/dist/runner/redactor.js +10 -6
  276. package/dist/runner/routes.d.ts +23 -0
  277. package/dist/runner/routes.d.ts.map +1 -0
  278. package/dist/runner/routes.js +128 -0
  279. package/dist/runner/run-media.d.ts +6 -0
  280. package/dist/runner/run-media.d.ts.map +1 -1
  281. package/dist/runner/run-media.js +21 -0
  282. package/dist/runner/run-session.d.ts.map +1 -1
  283. package/dist/runner/run-session.js +93 -8
  284. package/dist/runner/running-test.d.ts +35 -0
  285. package/dist/runner/running-test.d.ts.map +1 -1
  286. package/dist/runner/running-test.js +333 -22
  287. package/dist/runner/screenshot-baselines.d.ts +159 -0
  288. package/dist/runner/screenshot-baselines.d.ts.map +1 -0
  289. package/dist/runner/screenshot-baselines.js +357 -0
  290. package/dist/runner/secrets.d.ts +13 -0
  291. package/dist/runner/secrets.d.ts.map +1 -1
  292. package/dist/runner/secrets.js +6 -4
  293. package/dist/runner/test-pages.d.ts +3 -1
  294. package/dist/runner/test-pages.d.ts.map +1 -1
  295. package/dist/runner/test-pages.js +4 -3
  296. package/dist/schemas/event-v1.schema.json +118873 -6311
  297. package/dist/schemas/result-v1.schema.json +4 -0
  298. package/dist/shared/image-diff.d.ts +36 -0
  299. package/dist/shared/image-diff.d.ts.map +1 -0
  300. package/dist/shared/image-diff.js +242 -0
  301. package/dist/shared/metadata-process.d.ts +4 -1
  302. package/dist/shared/metadata-process.d.ts.map +1 -1
  303. package/dist/shared/metadata-process.js +51 -9
  304. package/dist/shared/png.d.ts +35 -0
  305. package/dist/shared/png.d.ts.map +1 -0
  306. package/dist/shared/png.js +251 -0
  307. package/dist/shared/process-ownership.d.ts +15 -4
  308. package/dist/shared/process-ownership.d.ts.map +1 -1
  309. package/dist/shared/process-ownership.js +123 -34
  310. package/dist/store/artifacts.d.ts +23 -2
  311. package/dist/store/artifacts.d.ts.map +1 -1
  312. package/dist/store/artifacts.js +40 -1
  313. package/dist/store/rebuild-result.d.ts.map +1 -1
  314. package/dist/store/rebuild-result.js +6 -0
  315. package/package.json +1 -1
@@ -757,6 +757,10 @@
757
757
  "attempts"
758
758
  ]
759
759
  },
760
+ "baselinesWritten": {
761
+ "type": "integer",
762
+ "minimum": 1
763
+ },
760
764
  "files": {
761
765
  "type": "array",
762
766
  "items": {
@@ -0,0 +1,36 @@
1
+ import type { RgbaImage } from './png.ts';
2
+ /** The colour distance, from 0 to 1, above which a pixel counts as different, as Playwright's `toHaveScreenshot` sets it. */
3
+ export declare const defaultThreshold = 0.2;
4
+ /** How two pictures of one size differ: the pixels counted as different, and the pixels left out as anti-aliasing. */
5
+ export type PixelDifference = {
6
+ readonly differentPixels: number;
7
+ readonly antiAliasedPixels: number;
8
+ };
9
+ /** The picture `differencePicture` draws, and the count it drew. */
10
+ export type DrawnDifference = PixelDifference & {
11
+ readonly picture: RgbaImage;
12
+ };
13
+ /**
14
+ * Whether two pictures are the same size and the same in every byte of every pixel.
15
+ *
16
+ * @example samePixels(first, second) // true
17
+ */
18
+ export declare function samePixels(first: RgbaImage, second: RgbaImage): boolean;
19
+ /**
20
+ * Counts the pixels of two pictures of one size whose colours are further apart than `threshold`, from 0 to 1, leaving
21
+ * out those that are anti-aliasing in either picture. Throws `RangeError` for pictures of different sizes or a
22
+ * threshold outside 0 to 1.
23
+ *
24
+ * @example comparePixels(baseline, picture, 0.2).differentPixels // 0
25
+ */
26
+ export declare function comparePixels(expected: RgbaImage, actual: RgbaImage, threshold?: number): PixelDifference;
27
+ /**
28
+ * Draws where two pictures differ, on a canvas as large as the larger of the two each way: the actual picture faded
29
+ * to grey, and each differing pixel in red. A pixel that only one picture covers is red. Anti-aliased pixels stay
30
+ * faded. The counts are those of `comparePixels` when the sizes match, and count the uncovered pixels as different
31
+ * when they do not.
32
+ *
33
+ * @example differencePicture(baseline, picture, 0.2).picture.width // 1280
34
+ */
35
+ export declare function differencePicture(expected: RgbaImage, actual: RgbaImage, threshold?: number): DrawnDifference;
36
+ //# sourceMappingURL=image-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-diff.d.ts","sourceRoot":"","sources":["../../src/shared/image-diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AA0BzC,6HAA6H;AAC7H,eAAO,MAAM,gBAAgB,MAAM,CAAA;AAEnC,sHAAsH;AACtH,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtG,oEAAoE;AACpE,MAAM,MAAM,eAAe,GAAG,eAAe,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAA;CAAE,CAAA;AAQ/E;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAGvE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAE,MAAyB,GAAG,eAAe,CAG3H;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAE,MAAyB,GAAG,eAAe,CAsB/H"}
@@ -0,0 +1,242 @@
1
+ // How two pictures differ, pixel by pixel, for a screenshot comparison. A pixel differs when the distance between its
2
+ // two colours, in the YIQ space that weighs brightness the way eyes do, is above a threshold; a pixel that differs only
3
+ // because an edge is anti-aliased a little differently is not counted.
4
+ //
5
+ // The colour distance and the anti-aliasing detector are adapted from pixelmatch 7.2.0 by Mapbox, read as published on
6
+ // npm, and match the version Playwright 1.63.0 vendors, whose `toHaveScreenshot` counts pixels this way, so a threshold
7
+ // means here what it means there. The distance follows Kotsarenko and Ramos, "Measuring perceived color difference using
8
+ // YIQ NTSC transmission color space in mobile applications" (2010); the detector follows Vysniauskas, "Anti-aliased
9
+ // pixel and intensity slope detector" (2009). Semi-transparent pixels are blended on white, as Playwright's copy does.
10
+ // pixelmatch's licence:
11
+ //
12
+ // ISC License
13
+ //
14
+ // Copyright (c) 2025, Mapbox
15
+ //
16
+ // Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby
17
+ // granted, provided that the above copyright notice and this permission notice appear in all copies.
18
+ //
19
+ // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING
20
+ // ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
21
+ // DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
22
+ // WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
23
+ // USE OR PERFORMANCE OF THIS SOFTWARE.
24
+ /** The colour distance, from 0 to 1, above which a pixel counts as different, as Playwright's `toHaveScreenshot` sets it. */
25
+ export const defaultThreshold = 0.2;
26
+ // The largest squared distance two colours can have in YIQ, so a threshold of 1 lets every colour through.
27
+ const maxSquaredDistance = 35215;
28
+ // How much of the actual picture's brightness shows in the difference picture: faded, so the red stands out.
29
+ const fade = 0.1;
30
+ const differenceColour = [255, 0, 0];
31
+ /**
32
+ * Whether two pictures are the same size and the same in every byte of every pixel.
33
+ *
34
+ * @example samePixels(first, second) // true
35
+ */
36
+ export function samePixels(first, second) {
37
+ if (first.width !== second.width || first.height !== second.height || first.data.length !== second.data.length)
38
+ return false;
39
+ return Buffer.from(first.data.buffer, first.data.byteOffset, first.data.byteLength).equals(Buffer.from(second.data.buffer, second.data.byteOffset, second.data.byteLength));
40
+ }
41
+ /**
42
+ * Counts the pixels of two pictures of one size whose colours are further apart than `threshold`, from 0 to 1, leaving
43
+ * out those that are anti-aliasing in either picture. Throws `RangeError` for pictures of different sizes or a
44
+ * threshold outside 0 to 1.
45
+ *
46
+ * @example comparePixels(baseline, picture, 0.2).differentPixels // 0
47
+ */
48
+ export function comparePixels(expected, actual, threshold = defaultThreshold) {
49
+ checkSizes(expected, actual);
50
+ return scan(expected, actual, threshold, undefined);
51
+ }
52
+ /**
53
+ * Draws where two pictures differ, on a canvas as large as the larger of the two each way: the actual picture faded
54
+ * to grey, and each differing pixel in red. A pixel that only one picture covers is red. Anti-aliased pixels stay
55
+ * faded. The counts are those of `comparePixels` when the sizes match, and count the uncovered pixels as different
56
+ * when they do not.
57
+ *
58
+ * @example differencePicture(baseline, picture, 0.2).picture.width // 1280
59
+ */
60
+ export function differencePicture(expected, actual, threshold = defaultThreshold) {
61
+ checkThreshold(threshold);
62
+ const width = Math.max(expected.width, actual.width);
63
+ const height = Math.max(expected.height, actual.height);
64
+ const data = new Uint8Array(width * height * 4);
65
+ const canvas = { width, height, data };
66
+ if (expected.width === actual.width && expected.height === actual.height) {
67
+ const counted = scan(expected, actual, threshold, (index, different) => (different ? paint(data, index * 4) : fadeInto(data, index * 4, actual.data, index * 4)));
68
+ return { ...counted, picture: canvas };
69
+ }
70
+ let differentPixels = 0;
71
+ for (let y = 0; y < height; y++) {
72
+ for (let x = 0; x < width; x++) {
73
+ const at = (y * width + x) * 4;
74
+ if (x < actual.width && y < actual.height && x < expected.width && y < expected.height)
75
+ fadeInto(data, at, actual.data, (y * actual.width + x) * 4);
76
+ else {
77
+ paint(data, at);
78
+ differentPixels++;
79
+ }
80
+ }
81
+ }
82
+ return { differentPixels, antiAliasedPixels: 0, picture: canvas };
83
+ }
84
+ function checkSizes(expected, actual) {
85
+ if (expected.width !== actual.width || expected.height !== actual.height) {
86
+ throw new RangeError(`Pixels compare only between pictures of one size, not ${expected.width} by ${expected.height} and ${actual.width} by ${actual.height}.`);
87
+ }
88
+ for (const image of [expected, actual])
89
+ if (image.data.length !== image.width * image.height * 4)
90
+ throw new RangeError(`${image.width} by ${image.height} RGBA pixels take ${image.width * image.height * 4} bytes, not ${image.data.length}.`);
91
+ }
92
+ function checkThreshold(threshold) {
93
+ if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1)
94
+ throw new RangeError(`A threshold is a colour distance from 0 to 1, not ${threshold}.`);
95
+ }
96
+ // Visits every pixel of two pictures of one size, counting those that differ, and tells `mark` of each, different or not.
97
+ function scan(expected, actual, threshold, mark) {
98
+ checkThreshold(threshold);
99
+ const { width, height } = expected;
100
+ const first = expected.data;
101
+ const second = actual.data;
102
+ const limit = maxSquaredDistance * threshold * threshold;
103
+ let differentPixels = 0;
104
+ let antiAliasedPixels = 0;
105
+ for (let y = 0; y < height; y++) {
106
+ for (let x = 0; x < width; x++) {
107
+ const index = y * width + x;
108
+ const delta = colourDelta(first, second, index * 4, index * 4, false);
109
+ if (Math.abs(delta) <= limit) {
110
+ mark?.(index, false);
111
+ continue;
112
+ }
113
+ if (antiAliased(first, second, x, y, width, height) || antiAliased(second, first, x, y, width, height)) {
114
+ antiAliasedPixels++;
115
+ mark?.(index, false);
116
+ continue;
117
+ }
118
+ differentPixels++;
119
+ mark?.(index, true);
120
+ }
121
+ }
122
+ return { differentPixels, antiAliasedPixels };
123
+ }
124
+ /**
125
+ * Whether the pixel at `x`, `y` of `picture` looks like part of an anti-aliased edge: among its eight neighbours it has
126
+ * no more than two of its own brightness, at least one darker and one brighter, and the darkest or the brightest of
127
+ * them sits in a flat run of at least three equal neighbours in both pictures.
128
+ */
129
+ function antiAliased(picture, other, x, y, width, height) {
130
+ const left = Math.max(x - 1, 0);
131
+ const top = Math.max(y - 1, 0);
132
+ const right = Math.min(x + 1, width - 1);
133
+ const bottom = Math.min(y + 1, height - 1);
134
+ const at = (y * width + x) * 4;
135
+ let equal = x === left || x === right || y === top || y === bottom ? 1 : 0;
136
+ let darkest = 0;
137
+ let brightest = 0;
138
+ let darkestAt;
139
+ let brightestAt;
140
+ for (let column = left; column <= right; column++) {
141
+ for (let row = top; row <= bottom; row++) {
142
+ if (column === x && row === y)
143
+ continue;
144
+ const delta = colourDelta(picture, picture, at, (row * width + column) * 4, true);
145
+ if (delta === 0) {
146
+ equal++;
147
+ if (equal > 2)
148
+ return false;
149
+ }
150
+ else if (delta < darkest) {
151
+ darkest = delta;
152
+ darkestAt = [column, row];
153
+ }
154
+ else if (delta > brightest) {
155
+ brightest = delta;
156
+ brightestAt = [column, row];
157
+ }
158
+ }
159
+ }
160
+ if (darkestAt === undefined || brightestAt === undefined)
161
+ return false;
162
+ const flat = ([column, row]) => hasManySiblings(picture, column, row, width, height) && hasManySiblings(other, column, row, width, height);
163
+ return flat(darkestAt) || flat(brightestAt);
164
+ }
165
+ // Whether a pixel has at least three neighbours exactly like it, a pixel on the picture's edge counting the edge as one.
166
+ function hasManySiblings(picture, x, y, width, height) {
167
+ const left = Math.max(x - 1, 0);
168
+ const top = Math.max(y - 1, 0);
169
+ const right = Math.min(x + 1, width - 1);
170
+ const bottom = Math.min(y + 1, height - 1);
171
+ const at = (y * width + x) * 4;
172
+ let equal = x === left || x === right || y === top || y === bottom ? 1 : 0;
173
+ for (let column = left; column <= right; column++) {
174
+ for (let row = top; row <= bottom; row++) {
175
+ if (column === x && row === y)
176
+ continue;
177
+ const other = (row * width + column) * 4;
178
+ if (picture[at] === picture[other] && picture[at + 1] === picture[other + 1] && picture[at + 2] === picture[other + 2] && picture[at + 3] === picture[other + 3])
179
+ equal++;
180
+ if (equal > 2)
181
+ return true;
182
+ }
183
+ }
184
+ return false;
185
+ }
186
+ // The squared YIQ distance between two pixels, negative when the first is the brighter; with `brightnessOnly`, the
187
+ // difference in brightness alone, first less second.
188
+ function colourDelta(first, second, at, other, brightnessOnly) {
189
+ const r1 = first[at] ?? 0;
190
+ const g1 = first[at + 1] ?? 0;
191
+ const b1 = first[at + 2] ?? 0;
192
+ const a1 = first[at + 3] ?? 0;
193
+ const r2 = second[other] ?? 0;
194
+ const g2 = second[other + 1] ?? 0;
195
+ const b2 = second[other + 2] ?? 0;
196
+ const a2 = second[other + 3] ?? 0;
197
+ if (r1 === r2 && g1 === g2 && b1 === b2 && a1 === a2)
198
+ return 0;
199
+ const [red1, green1, blue1] = onWhite(r1, g1, b1, a1);
200
+ const [red2, green2, blue2] = onWhite(r2, g2, b2, a2);
201
+ const brightness1 = luma(red1, green1, blue1);
202
+ const brightness2 = luma(red2, green2, blue2);
203
+ const y = brightness1 - brightness2;
204
+ if (brightnessOnly)
205
+ return y;
206
+ const i = inPhase(red1, green1, blue1) - inPhase(red2, green2, blue2);
207
+ const q = quadrature(red1, green1, blue1) - quadrature(red2, green2, blue2);
208
+ const delta = 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
209
+ return brightness1 > brightness2 ? -delta : delta;
210
+ }
211
+ function onWhite(red, green, blue, alpha) {
212
+ if (alpha === 255)
213
+ return [red, green, blue];
214
+ const opacity = alpha / 255;
215
+ return [blend(red, opacity), blend(green, opacity), blend(blue, opacity)];
216
+ }
217
+ function blend(channel, opacity) {
218
+ return 255 + (channel - 255) * opacity;
219
+ }
220
+ function luma(red, green, blue) {
221
+ return red * 0.29889531 + green * 0.58662247 + blue * 0.11448223;
222
+ }
223
+ function inPhase(red, green, blue) {
224
+ return red * 0.59597799 - green * 0.2741761 - blue * 0.32180189;
225
+ }
226
+ function quadrature(red, green, blue) {
227
+ return red * 0.21147017 - green * 0.52261711 + blue * 0.31114694;
228
+ }
229
+ function paint(data, at) {
230
+ data[at] = differenceColour[0];
231
+ data[at + 1] = differenceColour[1];
232
+ data[at + 2] = differenceColour[2];
233
+ data[at + 3] = 255;
234
+ }
235
+ function fadeInto(data, at, source, from) {
236
+ const opacity = (fade * (source[from + 3] ?? 0)) / 255;
237
+ const grey = blend(luma(source[from] ?? 0, source[from + 1] ?? 0, source[from + 2] ?? 0), opacity);
238
+ data[at] = grey;
239
+ data[at + 1] = grey;
240
+ data[at + 2] = grey;
241
+ data[at + 3] = 255;
242
+ }
@@ -36,13 +36,16 @@ export declare const metadataPending: number;
36
36
  export declare const metadataRunning: number;
37
37
  export declare const metadataComplete: number;
38
38
  export declare const metadataAbandoned: number;
39
+ /** The worker was ended for another query before it answered this one, which is asked again of a fresh worker. */
40
+ export declare const metadataWorkerEnded: number;
39
41
  export declare const metadataSuccess: number;
40
42
  export declare const metadataFailure: number;
41
43
  /**
42
44
  * A synchronous bridge for fresh ownership readings, including exit hooks. The worker uses an asynchronous child
43
45
  * and bounds its output and time itself, so Node's synchronous-child timeout never kills an unrecorded process.
44
46
  * A query the worker failed to answer fails alone: that worker is ended and the next query starts a fresh one, so one
45
- * stall under load never leaves a long-lived host without readings, and so without the right to signal anything.
47
+ * stall under load never leaves a long-lived host without readings, and so without the right to signal anything. A
48
+ * query the ended worker still held is asked again of the fresh one, within its own caller's deadline.
46
49
  */
47
50
  export declare function readMetadataProcess(options: MetadataProcessOptions): string;
48
51
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"metadata-process.d.ts","sourceRoot":"","sources":["../../src/shared/metadata-process.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAIvD,sFAAsF;AACtF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;IACxC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;IACnC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;CACnC,CAAA;AAED,eAAO,MAAM,mBAAmB,EAAE,MAAwB,CAAA;AAC1D;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAyB,CAAA;AAC/D,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,gBAAgB,EAAE,MAAU,CAAA;AACzC,eAAO,MAAM,iBAAiB,EAAE,MAAU,CAAA;AAC1C,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AAUxC;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAiB3E;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyB/F;AAqED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,IAAI,CAQ7D"}
1
+ {"version":3,"file":"metadata-process.d.ts","sourceRoot":"","sources":["../../src/shared/metadata-process.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAIvD,sFAAsF;AACtF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;IACxC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;IACnC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;CACnC,CAAA;AAED,eAAO,MAAM,mBAAmB,EAAE,MAAwB,CAAA;AAC1D;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAyB,CAAA;AAC/D,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,gBAAgB,EAAE,MAAU,CAAA;AACzC,eAAO,MAAM,iBAAiB,EAAE,MAAU,CAAA;AAC1C,kHAAkH;AAClH,eAAO,MAAM,mBAAmB,EAAE,MAAU,CAAA;AAC5C,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AAaxC;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAsB3E;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8B/F;AAuFD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,IAAI,CAQ7D"}
@@ -12,6 +12,8 @@ export const metadataPending = 0;
12
12
  export const metadataRunning = 1;
13
13
  export const metadataComplete = 2;
14
14
  export const metadataAbandoned = 3;
15
+ /** The worker was ended for another query before it answered this one, which is asked again of a fresh worker. */
16
+ export const metadataWorkerEnded = 4;
15
17
  export const metadataSuccess = 0;
16
18
  export const metadataFailure = 1;
17
19
  const queryLimitMs = 5000;
@@ -19,22 +21,31 @@ const workerAllowanceMs = 1000;
19
21
  // One spare of each production capacity. Only a completed query can return its buffer: an abandoned worker may
20
22
  // still be writing, and a concurrent query must hold a different buffer until its completion signal is consumed.
21
23
  const spareOutputs = new Map();
24
+ // Queries posted and not yet settled. A worker ended for one query fails the others it holds at once, rather than
25
+ // leaving each to wait out its whole bound for an answer that cannot come.
26
+ const inFlight = new Set();
22
27
  let bridge;
23
28
  let workerModule = new URL(import.meta.url.endsWith('.ts') ? './metadata-process-worker.ts' : './metadata-process-worker.js', import.meta.url);
24
29
  /**
25
30
  * A synchronous bridge for fresh ownership readings, including exit hooks. The worker uses an asynchronous child
26
31
  * and bounds its output and time itself, so Node's synchronous-child timeout never kills an unrecorded process.
27
32
  * A query the worker failed to answer fails alone: that worker is ended and the next query starts a fresh one, so one
28
- * stall under load never leaves a long-lived host without readings, and so without the right to signal anything.
33
+ * stall under load never leaves a long-lived host without readings, and so without the right to signal anything. A
34
+ * query the ended worker still held is asked again of the fresh one, within its own caller's deadline.
29
35
  */
30
36
  export function readMetadataProcess(options) {
31
- const query = submit(options);
37
+ let query = submit(options);
32
38
  try {
33
39
  for (;;) {
34
40
  checkCaller(query, options.deadline);
35
41
  const state = Atomics.load(query.control, 0);
36
42
  if (state === metadataComplete)
37
43
  return answer(query);
44
+ if (state === metadataWorkerEnded) {
45
+ settle(query);
46
+ query = submit(options);
47
+ continue;
48
+ }
38
49
  const remaining = query.deadline - performance.now();
39
50
  if (remaining <= 0) {
40
51
  if (abandoned(query))
@@ -45,7 +56,7 @@ export function readMetadataProcess(options) {
45
56
  }
46
57
  }
47
58
  finally {
48
- releaseOutput(query);
59
+ settle(query);
49
60
  }
50
61
  }
51
62
  /**
@@ -56,7 +67,7 @@ export function readMetadataProcess(options) {
56
67
  * @example const text = await readMetadataProcessAsync({ command: '/bin/ps', args: ['-axo', 'pid='], environment: { PATH: '/usr/bin:/bin' } })
57
68
  */
58
69
  export async function readMetadataProcessAsync(options) {
59
- const query = submit(options);
70
+ let query = submit(options);
60
71
  const wake = () => { Atomics.notify(query.control, 0); };
61
72
  // Neither the unreferenced worker nor an Atomics wait keeps Node alive. Keep this bounded query awaited even when
62
73
  // it is the host's last work, including when a failed worker can no longer notify it.
@@ -68,6 +79,11 @@ export async function readMetadataProcessAsync(options) {
68
79
  const state = Atomics.load(query.control, 0);
69
80
  if (state === metadataComplete)
70
81
  return answer(query);
82
+ if (state === metadataWorkerEnded) {
83
+ settle(query);
84
+ query = submit(options);
85
+ continue;
86
+ }
71
87
  const remaining = query.deadline - performance.now();
72
88
  if (remaining <= 0) {
73
89
  if (abandoned(query))
@@ -80,7 +96,7 @@ export async function readMetadataProcessAsync(options) {
80
96
  }
81
97
  }
82
98
  finally {
83
- releaseOutput(query);
99
+ settle(query);
84
100
  clearInterval(pending);
85
101
  options.deadline?.signal?.removeEventListener('abort', wake);
86
102
  }
@@ -118,7 +134,25 @@ function submit(options) {
118
134
  discardWorker(worker);
119
135
  throw new Error('The metadata worker could not receive the query.');
120
136
  }
121
- return { worker, control, output, deadline: Math.min(performance.now() + timeoutMs + workerAllowanceMs, request.deadline === undefined ? Infinity : request.deadline - performance.timeOrigin) };
137
+ const query = { worker, control, output, deadline: Math.min(performance.now() + timeoutMs + workerAllowanceMs, request.deadline === undefined ? Infinity : request.deadline - performance.timeOrigin) };
138
+ inFlight.add(query);
139
+ return query;
140
+ }
141
+ function settle(query) {
142
+ inFlight.delete(query);
143
+ releaseOutput(query);
144
+ }
145
+ // Fails a query its worker can no longer answer, unless the answer came first, and wakes whoever waits on it.
146
+ function endedWorker(query) {
147
+ for (;;) {
148
+ const latest = Atomics.load(query.control, 0);
149
+ if (latest === metadataComplete || latest === metadataAbandoned || latest === metadataWorkerEnded)
150
+ return;
151
+ if (Atomics.compareExchange(query.control, 0, latest, metadataWorkerEnded) !== latest)
152
+ continue;
153
+ Atomics.notify(query.control, 0);
154
+ return;
155
+ }
122
156
  }
123
157
  // A reply completing at the deadline may be consumed; an abandoned queued request must never start later. True once the
124
158
  // query is abandoned and its worker ended, false when the reply came first.
@@ -189,16 +223,24 @@ function metadataWorker() {
189
223
  throw new Error('The metadata worker could not start.');
190
224
  }
191
225
  worker.on('error', () => { discardWorker(worker); });
192
- worker.on('exit', () => { if (bridge === worker)
193
- bridge = undefined; });
226
+ worker.on('exit', () => {
227
+ if (bridge === worker)
228
+ bridge = undefined;
229
+ for (const query of inFlight)
230
+ if (query.worker === worker)
231
+ endedWorker(query);
232
+ });
194
233
  worker.unref();
195
234
  bridge = worker;
196
235
  return worker;
197
236
  }
198
- // The query that met the failure reports it; the next one starts a fresh worker.
237
+ // The query that met the failure reports it; the next one starts a fresh worker, and the others it held are asked again.
199
238
  function discardWorker(worker) {
200
239
  if (bridge === worker)
201
240
  bridge = undefined;
241
+ for (const query of inFlight)
242
+ if (query.worker === worker)
243
+ endedWorker(query);
202
244
  // Ending a failed worker thread closes its pipes; its detached child is never sent a process signal.
203
245
  void worker.terminate().catch(() => { });
204
246
  }
@@ -0,0 +1,35 @@
1
+ /** A picture as Retest compares it: `width` by `height` pixels, four bytes each, red, green, blue and alpha, row by row. */
2
+ export type RgbaImage = {
3
+ readonly width: number;
4
+ readonly height: number;
5
+ readonly data: Uint8Array;
6
+ };
7
+ /** A PNG as `readPng` reads it: its pixels as RGBA, and the text of its `tEXt` chunks by keyword. */
8
+ export type PngPicture = RgbaImage & {
9
+ readonly text: Readonly<Record<string, string>>;
10
+ };
11
+ /** What `writePng` adds to the pixels: `tEXt` chunks, by keyword, each keyword 1 to 79 Latin-1 letters. */
12
+ export type PngWriteOptions = {
13
+ readonly text?: Readonly<Record<string, string>>;
14
+ };
15
+ /** Thrown for bytes that are not a PNG Retest reads. The message says what the bytes were. */
16
+ export declare class PngError extends Error {
17
+ readonly name = "PngError";
18
+ }
19
+ /** The most bytes a decoded picture may take, filter bytes included, so a damaged file cannot ask for all memory. */
20
+ export declare const maxPictureBytes: number;
21
+ /**
22
+ * Decodes a PNG into RGBA pixels: 8-bit RGB or RGBA, not interlaced, its rows filtered with any of the five filters.
23
+ * Every chunk's checksum is checked. RGB pixels get an alpha of 255. Throws `PngError` for anything else.
24
+ *
25
+ * @example readPng(bytes).width // 1280
26
+ */
27
+ export declare function readPng(bytes: Uint8Array): PngPicture;
28
+ /**
29
+ * Encodes RGBA pixels as a PNG: RGB when every pixel is opaque, RGBA otherwise, each row with the filter that leaves the
30
+ * smallest sum, as libpng chooses, and any `tEXt` chunks the options name.
31
+ *
32
+ * @example writePng({ width: 1, height: 1, data: Uint8Array.of(255, 0, 0, 255) }, { text: { 'retest:scale': '2' } })
33
+ */
34
+ export declare function writePng(image: RgbaImage, options?: PngWriteOptions): Uint8Array;
35
+ //# sourceMappingURL=png.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"png.d.ts","sourceRoot":"","sources":["../../src/shared/png.ts"],"names":[],"mappings":"AAOA,4HAA4H;AAC5H,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAA;AAEtG,qGAAqG;AACrG,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,CAAA;AAExF,2GAA2G;AAC3G,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,CAAA;AAElF,8FAA8F;AAC9F,qBAAa,QAAS,SAAQ,KAAK;IACjC,SAAkB,IAAI,cAAa;CACpC;AAED,qHAAqH;AACrH,eAAO,MAAM,eAAe,EAAE,MAA0B,CAAA;AAQxD;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CA0CrD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,eAAoB,GAAG,UAAU,CAepF"}