@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
@@ -0,0 +1,584 @@
1
+ import { waitBeforeRead } from "../assertions/wait-before-read.js";
2
+ import { Deadline } from "../protocol/deadline.js";
3
+ import { defaultDragSteps, dragProblem } from "../protocol/drag.js";
4
+ import { describeEmptyStep, describeLocator, describeStep, locatorSteps } from "../protocol/locator.js";
5
+ import { s } from "../protocol/schema.js";
6
+ import { CdpAbortedError, CdpTimeoutError } from "./cdp/errors.js";
7
+ import { readProtocol, request, sendOptions } from "./cdp-results.js";
8
+ import { documentFactsSchema, pageFactsOf } from "./document-facts.js";
9
+ import { prepare } from "./element-queries.js";
10
+ import { disarmGuard, guardInput } from "./input-guard.js";
11
+ import { isGoneContext } from "./isolated-world.js";
12
+ import { locatorArguments } from "./locate.js";
13
+ import { originAndPath } from "./page-url.js";
14
+ import { locatorHelpers, pageFactsFunction } from "./page-scripts.js";
15
+ const dragDataSchema = s.object({
16
+ items: s.array(s.object({ mimeType: s.string(), data: s.string(), title: s.optional(s.string()), baseURL: s.optional(s.string()) })),
17
+ files: s.optional(s.array(s.string())),
18
+ dragOperationsMask: s.number({ integer: true }),
19
+ });
20
+ const interceptedSchema = s.object({ data: dragDataSchema });
21
+ const spotSchema = s.discriminatedUnion('status', [
22
+ s.object({ status: s.literal('ready'), point: s.object({ x: s.number(), y: s.number() }), covered: s.nullable(s.string()) }),
23
+ s.object({ status: s.literal('missing') }),
24
+ s.object({ status: s.literal('ambiguous'), count: s.number({ integer: true, min: 2 }) }),
25
+ s.object({ status: s.literal('hidden') }),
26
+ s.object({ status: s.literal('outside') }),
27
+ ]);
28
+ // The target's visible centre in the viewport, as a press's look finds an element's, and what the hit test finds there,
29
+ // through open shadow roots. It arms no guard, so the source's arming stays the one the press is checked against.
30
+ const spotFunction = `function dragSpot(query, ...elements) {
31
+ ${locatorHelpers}
32
+ // A block of its own, so no name here meets one of the helpers'.
33
+ {
34
+ const resolved = resolve(query, elements)
35
+ if (resolved.invalid !== null || resolved.found.length === 0) return { status: 'missing' }
36
+ if (resolved.found.length > 1) return { status: 'ambiguous', count: resolved.found.length }
37
+ const element = resolved.found[0]
38
+ if (!element.isConnected || !isVisible(element)) return { status: 'hidden' }
39
+ const box = element.getBoundingClientRect()
40
+ const { offsetLeft, offsetTop, width, height } = visualViewport
41
+ const left = Math.max(box.left, offsetLeft)
42
+ const right = Math.min(box.right, offsetLeft + width)
43
+ const top = Math.max(box.top, offsetTop)
44
+ const bottom = Math.min(box.bottom, offsetTop + height)
45
+ if (right <= left || bottom <= top) return { status: 'outside' }
46
+ const point = { x: (left + right) / 2, y: (top + bottom) / 2 }
47
+ let hit = document.elementFromPoint(point.x, point.y)
48
+ while (hit !== null && hit.shadowRoot !== null) {
49
+ const inner = hit.shadowRoot.elementFromPoint(point.x, point.y)
50
+ if (inner === null || inner === hit) break
51
+ hit = inner
52
+ }
53
+ let inside = false
54
+ for (let node = hit; node !== null && node !== undefined; node = node.parentNode instanceof ShadowRoot ? node.parentNode.host : node.parentNode) {
55
+ if (node === element) inside = true
56
+ }
57
+ return { status: 'ready', point, covered: inside ? null : hit === null ? 'nothing' : describe(hit) }
58
+ }
59
+ }`;
60
+ // Retest's world in every document hears each dragstart and drop the page hears, before any listener of the page's own:
61
+ // it is added as the document starts, so a page that stops a dragstart's propagation, or a drop's, still lets Retest
62
+ // hear it. It notes only while a drag of Retest's is armed, and keeps the first of each: the dragstart, which Retest reads
63
+ // once the page's own listeners ran to learn whether the page let the drag begin, and the element a drop reached.
64
+ const dragListener = String.raw `
65
+ if (globalThis.retestDrags === undefined) {
66
+ const drags = { armed: null }
67
+ const heard = (event) => {
68
+ const armed = drags.armed
69
+ if (armed === null || !event.isTrusted) return
70
+ if (event.type === 'dragstart') armed.start ??= event
71
+ else armed.drop ??= event.composedPath()[0] ?? event.target
72
+ }
73
+ for (const type of ['dragstart', 'drop']) addEventListener(type, heard, { capture: true })
74
+ globalThis.retestDrags = drags
75
+ }
76
+ `;
77
+ /** Installs Retest's drag listener in each new document, before the page's own scripts run. */
78
+ export const dragListenerScript = `(() => {
79
+ ${dragListener}
80
+ })()`;
81
+ // Arms the listener for one drag. A document that began before Retest added the listener, as an app's first window
82
+ // does, gets it now, after the page's own.
83
+ const armFunction = `function armDrags() {
84
+ ${dragListener}
85
+ globalThis.retestDrags.armed = { start: null, drop: null }
86
+ return true
87
+ }`;
88
+ const startedFunction = `function dragStarted() {
89
+ const event = globalThis.retestDrags?.armed?.start ?? null
90
+ return event === null ? 'none' : event.defaultPrevented ? 'prevented' : 'started'
91
+ }`;
92
+ // Where the drop went: nowhere the page heard, the target or an element inside it, through open shadow roots, or another
93
+ // element, described.
94
+ const droppedFunction = `function dropped(query, ...elements) {
95
+ ${locatorHelpers}
96
+ {
97
+ const reached = globalThis.retestDrags?.armed?.drop ?? null
98
+ if (reached === null) return { status: 'none' }
99
+ const resolved = resolve(query, elements)
100
+ const target = resolved.invalid === null && resolved.found.length === 1 ? resolved.found[0] : null
101
+ for (let node = reached; node !== null && node !== undefined; node = node.parentNode instanceof ShadowRoot ? node.parentNode.host : node.parentNode) {
102
+ if (node === target) return { status: 'target' }
103
+ }
104
+ return { status: 'elsewhere', by: reached instanceof Element ? describe(reached) : 'the page' }
105
+ }
106
+ }`;
107
+ const disarmFunction = `function disarmDrags() {
108
+ if (globalThis.retestDrags !== undefined) globalThis.retestDrags.armed = null
109
+ return true
110
+ }`;
111
+ // Whether the guard armed with `token` stopped the press because another element took it. Read while the guard is still
112
+ // armed, so the release that frees the button is stopped too, with the click it would make. The guard's own state is
113
+ // read here because no answer of its verdict comes before it is disarmed.
114
+ const pressTakenFunction = `function pressTaken(token) {
115
+ const armed = globalThis.retestGuard?.armed ?? null
116
+ return armed !== null && armed.token === token && armed.intercepted !== null
117
+ }`;
118
+ const startedSchema = s.enum(['none', 'prevented', 'started']);
119
+ const droppedSchema = s.discriminatedUnion('status', [s.object({ status: s.literal('none') }), s.object({ status: s.literal('target') }), s.object({ status: s.literal('elsewhere'), by: s.string() })]);
120
+ const firstPauseMs = 20;
121
+ const maxPauseMs = 200;
122
+ // How far apart, in CSS pixels, the target's centre at the drop may be from where the moves went without another move.
123
+ const dropTolerance = 1;
124
+ const hover = { action: 'hover', multiline: false };
125
+ const click = { action: 'click', multiline: false };
126
+ /**
127
+ * Drags the element `command.locator` finds onto the element `command.target` finds. Nothing is sent until both are
128
+ * ready: the source as for a click, visible, stable, enabled and not covered, and the target visible, stable, in view
129
+ * and not covered at its centre, both in view together. The press is guarded as a click's is; then the mouse moves in
130
+ * `steps` from the source's centre to the target's, and is released there. A press and a move that begin an HTML drag
131
+ * carry it on through drag events, and the drop ends it, with no release, as a person's drop ends one. A target gone
132
+ * by the time of the drop fails `not_actionable`, after the drag is cancelled, or the mouse released where the moves
133
+ * ended, and says so; so does an HTML drop another element covers or takes. Every other failure before the press sends
134
+ * nothing, and one after it leaves no drag held and no button down.
135
+ *
136
+ * @example const result = await dragTo(page, { kind: 'dragTo', locator: card, target: column }, deadline, dispatch)
137
+ */
138
+ export async function dragTo(page, command, deadline, dispatch) {
139
+ const problem = dragProblem(command) ?? framed(command) ?? (page.touch ? touchRefused(command) : undefined);
140
+ if (problem !== undefined)
141
+ return { ok: false, failure: problem };
142
+ for (;;) {
143
+ const ready = await bothReady(page, command, deadline);
144
+ if (ready.kind === 'failed')
145
+ return { ok: false, failure: ready.failure };
146
+ // A navigation the browser began after the looks would take the press into the document it opens, and a document
147
+ // that has gone takes the look with it: the elements are looked for again in the one that came.
148
+ if (page.pendingNavigation() !== undefined || !(await watching(page, ready, deadline))) {
149
+ await disarmGuard(page.world, ready.guard, deadline);
150
+ continue;
151
+ }
152
+ return drag(page, command, ready, deadline, dispatch);
153
+ }
154
+ }
155
+ async function bothReady(page, command, deadline) {
156
+ let last;
157
+ for (let attempt = 0;; attempt += 1) {
158
+ const finalRead = deadline.reached;
159
+ const looked = await look(page, command, deadline, last);
160
+ if (looked.kind !== 'unready')
161
+ return looked;
162
+ last = looked.unready;
163
+ if (last.status === 'timeout' && finalRead)
164
+ throw last.error;
165
+ if (finalRead)
166
+ return { kind: 'failed', failure: unreadyFailure(last, command, deadline) };
167
+ await waitBeforeRead(deadline, Math.min(firstPauseMs * 2 ** attempt, maxPauseMs));
168
+ }
169
+ }
170
+ // The target is looked at first, and its guard let go of at once; then the source, whose guard stays armed for the
171
+ // press; then the target again, without arming, since bringing the source into view may have moved it.
172
+ async function look(page, command, deadline, last) {
173
+ const pending = page.pendingNavigation();
174
+ if (pending !== undefined)
175
+ return { kind: 'unready', unready: { status: 'navigating', url: pending.url } };
176
+ const { world } = page;
177
+ try {
178
+ const target = await prepare(world, command.target, hover, deadline);
179
+ const targetLook = judged(target.value, 'target', command);
180
+ if (targetLook.kind !== 'ready')
181
+ return targetLook;
182
+ await disarmGuard(world, { context: target.context, token: targetLook.token }, deadline);
183
+ const source = await prepare(world, command.locator, click, deadline);
184
+ const sourceLook = judged(source.value, 'source', command);
185
+ if (sourceLook.kind !== 'ready')
186
+ return sourceLook;
187
+ const guard = { context: source.context, token: sourceLook.token };
188
+ const spot = await readSpot(world, command.target, deadline);
189
+ if (spot.status === 'ready' && spot.covered === null)
190
+ return { kind: 'ready', from: sourceLook.point, to: spot.point, guard, page: sourceLook.page };
191
+ await disarmGuard(world, guard, deadline);
192
+ return { kind: 'unready', unready: spotUnready(spot) };
193
+ }
194
+ catch (error) {
195
+ // A look whose time ran out keeps the reason before it, and the drag tries again until its own time is up.
196
+ if (error instanceof CdpTimeoutError)
197
+ return { kind: 'unready', unready: last ?? { status: 'timeout', error } };
198
+ throw error;
199
+ }
200
+ }
201
+ function judged(readiness, role, command) {
202
+ const locator = role === 'source' ? command.locator : command.target;
203
+ switch (readiness.status) {
204
+ case 'ready': {
205
+ const { point, token, page } = readiness;
206
+ if (point === null || token === null)
207
+ throw new Error("Retest's page script readied a drag's element without a point or a guard");
208
+ return { kind: 'ready', point, token, page: pageFactsOf(page) };
209
+ }
210
+ case 'missing':
211
+ return { kind: 'unready', unready: { status: 'missing', role, empty: readiness.empty } };
212
+ case 'blocked':
213
+ return { kind: 'unready', unready: { status: 'blocked', role, check: readiness.check, covering: readiness.check === 'hit-target' ? readiness.detail : null } };
214
+ case 'ambiguous':
215
+ return failedWith({ class: 'ambiguous', message: `${couldNot(command)}: ${describeLocator(locator)} matches ${readiness.count} elements, and a locator must match exactly one. Retest did not drag.`, details: { count: readiness.count, element: role } });
216
+ case 'invalid': {
217
+ const step = locatorSteps(locator)[readiness.step];
218
+ const selector = step === undefined ? describeLocator(locator) : describeStep(step, locator.dialect);
219
+ return failedWith({ class: 'usage', message: `${couldNot(command)}: the page cannot read the CSS selector in ${selector}. ${readiness.message}`, details: { selector } });
220
+ }
221
+ case 'shadow':
222
+ return failedWith({
223
+ class: 'unsupported',
224
+ message: `${couldNot(command)}: the page holds an open shadow root, in ${readiness.host}. Playwright looks inside shadow roots and Retest does not, so Retest refuses the lookup rather than find less than Playwright would.`,
225
+ details: { host: readiness.host },
226
+ });
227
+ case 'moved':
228
+ return failedWith({ class: 'not_actionable', message: `${couldNot(command)}: ${describeLocator(locator)} finds another element than the one Retest was asked to drag, and Retest sent nothing.`, details: { refused: 'moved', inputSent: false } });
229
+ default:
230
+ throw new Error(`Retest's page script answered ${readiness.status} for a drag's ${role}`);
231
+ }
232
+ }
233
+ function failedWith(failure) {
234
+ return { kind: 'failed', failure };
235
+ }
236
+ // Read in the document the page holds now, which is the one the press went to unless the drag opened another.
237
+ async function readSpot(world, target, deadline) {
238
+ try {
239
+ return await world.call(spotFunction, locatorArguments(target, []), spotSchema, deadline);
240
+ }
241
+ catch (error) {
242
+ if (isGoneContext(error))
243
+ return { status: 'missing' };
244
+ throw error;
245
+ }
246
+ }
247
+ function spotUnready(spot) {
248
+ switch (spot.status) {
249
+ case 'ready':
250
+ return { status: 'blocked', role: 'target', check: 'hit-target', covering: spot.covered === 'nothing' ? null : spot.covered };
251
+ case 'missing':
252
+ case 'ambiguous':
253
+ return { status: 'missing', role: 'target', empty: null };
254
+ case 'hidden':
255
+ return { status: 'blocked', role: 'target', check: 'visible', covering: null };
256
+ case 'outside':
257
+ return { status: 'apart' };
258
+ }
259
+ }
260
+ // How long the end of a drag that failed may take to cancel the drag Chrome holds or release the button, past the
261
+ // action's own time, which may be spent: a page busy in its own handlers answers late.
262
+ const settleMs = 3000;
263
+ // Every input of the drag goes once both elements are ready. The press is the guarded part; the moves and the drop
264
+ // belong to the page, which may draw anything under the mouse while it drags. Whatever ends the drag, a drag Chrome
265
+ // still holds is cancelled and a button still down is released, so the page takes input again.
266
+ async function drag(page, command, ready, deadline, dispatch) {
267
+ const { session } = page;
268
+ const state = { held: undefined, ended: false, pressed: false, released: false, at: ready.from };
269
+ const handed = Promise.withResolvers();
270
+ const stopListening = session.on('Input.dragIntercepted', (params) => {
271
+ const { data } = readProtocol(interceptedSchema, params, { method: 'Input.dragIntercepted', sessionId: session.id });
272
+ state.held ??= data;
273
+ handed.resolve(data);
274
+ });
275
+ try {
276
+ await request(session, 'Input.setInterceptDrags', { enabled: true }, s.object({}), sendOptions(deadline));
277
+ return await page.delivering(async () => {
278
+ const pressed = await press(page, ready, state, deadline, dispatch);
279
+ const refused = pressFailure(pressed, command, ready);
280
+ if (refused !== undefined) {
281
+ if (refused.release)
282
+ await release(session, state, deadline, dispatch);
283
+ return { ok: false, failure: refused.failure };
284
+ }
285
+ return carry({ page, command, ready, state, deadline, dispatch, handed: handed.promise });
286
+ });
287
+ }
288
+ finally {
289
+ stopListening();
290
+ await settle(session, state, dispatch);
291
+ await letGo(page, ready.guard.context);
292
+ }
293
+ }
294
+ // The page's dragstart and drop are listened for in the document the press goes to, before anything is sent there.
295
+ async function watching({ world }, ready, deadline) {
296
+ try {
297
+ return await world.callIn(ready.guard.context, armFunction, [], s.boolean(), deadline);
298
+ }
299
+ catch (error) {
300
+ if (isGoneContext(error))
301
+ return false;
302
+ throw error;
303
+ }
304
+ }
305
+ async function press(page, ready, state, deadline, dispatch) {
306
+ const { session, world } = page;
307
+ return guardInput(world, ready.guard, deadline, async () => {
308
+ await mouse(session, 'mouseMoved', ready.from, deadline, dispatch, 0);
309
+ state.pressed = true;
310
+ await mouse(session, 'mousePressed', ready.from, deadline, dispatch);
311
+ const taken = await world.callIn(ready.guard.context, pressTakenFunction, [ready.guard.token], s.boolean(), deadline);
312
+ if (taken)
313
+ await release(session, state, deadline, dispatch);
314
+ });
315
+ }
316
+ function pressFailure(verdict, command, ready) {
317
+ const source = describeLocator(command.locator);
318
+ if (verdict.kind === 'unregistered') {
319
+ return { failure: { class: 'not_actionable', message: `${couldNot(command)}: the page moved to a new document before Retest sent input.`, details: { reason: 'the page moved to a new document', inputSent: false } }, release: false };
320
+ }
321
+ if (verdict.kind !== 'seen') {
322
+ const message = `The page moved to a new document while Retest pressed ${source} to drag it, so Retest cannot tell what the press did. Retest released the mouse button there.`;
323
+ return { failure: { class: 'outcome_unknown', message, details: { reason: 'the page moved to a new document' } }, release: true };
324
+ }
325
+ const { reached, intercepted, landed } = verdict;
326
+ if (intercepted !== null) {
327
+ const message = `${couldNot(command)}: another element, ${intercepted.by}, was on top of ${source} when Retest pressed. Retest stopped the press and its release before the page received them.`;
328
+ return { failure: { class: 'not_actionable', message, details: { check: 'hit-target', interceptedBy: intercepted.by, event: intercepted.event } }, release: false };
329
+ }
330
+ if (reached.includes('pointerdown'))
331
+ return undefined;
332
+ const at = `(${Math.round(ready.from.x)}, ${Math.round(ready.from.y)})`;
333
+ const message = `Retest pressed the centre of ${source} at ${at} to drag it, but the press never reached the element's document, and ${landed ?? 'no element'} is at that point. Retest released the mouse button there and cannot tell what received the press.`;
334
+ return { failure: { class: 'outcome_unknown', message, details: { landed } }, release: true };
335
+ }
336
+ // The moves go to the target's centre as it stands once the press is in, since a page may lay itself out again when an
337
+ // item is lifted. A drag is the browser's once Chrome hands it over, and from then on every position is a drag event.
338
+ // Chrome tells of it after the move that began it, sometimes after answering that move, so after each move the page is
339
+ // asked whether it heard a dragstart it let go on, and then Retest waits for the drag Chrome holds.
340
+ async function carry({ page, command, ready, state, deadline, dispatch, handed }) {
341
+ const { session, world } = page;
342
+ const context = ready.guard.context;
343
+ const afterPress = await readSpot(world, command.target, deadline);
344
+ const to = afterPress.status === 'ready' ? afterPress.point : ready.to;
345
+ const steps = command.steps ?? defaultDragSteps;
346
+ let entered = false;
347
+ let listening = true;
348
+ for (let step = 1; step <= steps; step += 1) {
349
+ state.at = between(ready.from, to, step / steps);
350
+ if (entered && state.held !== undefined) {
351
+ await dragEvent(session, 'dragOver', state.at, state.held, deadline, dispatch);
352
+ continue;
353
+ }
354
+ await mouse(session, 'mouseMoved', state.at, deadline, dispatch);
355
+ if (state.held === undefined && listening) {
356
+ const started = await world.callIn(context, startedFunction, [], startedSchema, deadline);
357
+ if (started === 'prevented')
358
+ listening = false;
359
+ if (started === 'started')
360
+ await handedWithin(handed, deadline);
361
+ }
362
+ if (state.held === undefined)
363
+ continue;
364
+ await dragEvent(session, 'dragEnter', state.at, state.held, deadline, dispatch);
365
+ entered = true;
366
+ }
367
+ // A drag Chrome handed over after the last move is carried on from where the mouse is.
368
+ if (!entered && state.held !== undefined)
369
+ await dragEvent(session, 'dragEnter', state.at, state.held, deadline, dispatch);
370
+ return drop({ page, command, state, deadline, dispatch });
371
+ }
372
+ // Chrome hands over the drag a dragstart began within the move that began it, or soon after it answered that move.
373
+ async function handedWithin(handed, deadline) {
374
+ const command = { method: 'Input.dragIntercepted', sessionId: undefined };
375
+ const waited = Promise.withResolvers();
376
+ const timer = setTimeout(() => waited.reject(new CdpTimeoutError(command, { timeoutMs: deadline.budgetMs, written: true })), deadline.remainingMs);
377
+ const stop = () => waited.reject(new CdpAbortedError(command, { written: true }));
378
+ deadline.signal?.addEventListener('abort', stop, { once: true });
379
+ handed.then(waited.resolve, waited.reject);
380
+ try {
381
+ return await waited.promise;
382
+ }
383
+ finally {
384
+ clearTimeout(timer);
385
+ deadline.signal?.removeEventListener('abort', stop);
386
+ }
387
+ }
388
+ // The target is read once more before the drop: one the page moved meanwhile gets one more move to its centre, and one
389
+ // that left the page gets no drop at all. Drag events go to the element at the point, so a drag whose target another
390
+ // element covers by then is cancelled, and a drop the page heard on another element fails. A mouse drag is released
391
+ // whatever is at the point, since a page that follows the mouse often draws what it drags there.
392
+ async function drop({ page, command, state, deadline, dispatch }) {
393
+ const { session, world } = page;
394
+ const spot = await readSpot(world, command.target, deadline);
395
+ const held = state.held;
396
+ const through = held === undefined ? 'mouse' : 'drag-events';
397
+ if (spot.status !== 'ready') {
398
+ if (held !== undefined)
399
+ await cancel(session, state, deadline, dispatch);
400
+ else
401
+ await release(session, state, deadline, dispatch);
402
+ return { ok: false, failure: targetLeft(command, through, state.at, spot) };
403
+ }
404
+ if (held !== undefined && spot.covered !== null) {
405
+ await cancel(session, state, deadline, dispatch);
406
+ return { ok: false, failure: dropCovered(command, spot.covered) };
407
+ }
408
+ const point = distance(spot.point, state.at) > dropTolerance ? spot.point : state.at;
409
+ if (point !== state.at) {
410
+ state.at = point;
411
+ if (held !== undefined)
412
+ await dragEvent(session, 'dragOver', point, held, deadline, dispatch);
413
+ else
414
+ await mouse(session, 'mouseMoved', point, deadline, dispatch);
415
+ }
416
+ if (held === undefined) {
417
+ await release(session, state, deadline, dispatch);
418
+ }
419
+ else {
420
+ await dragEvent(session, 'drop', point, held, deadline, dispatch);
421
+ state.ended = true;
422
+ const reached = await world.call(droppedFunction, locatorArguments(command.target, []), droppedSchema, deadline);
423
+ if (reached.status === 'elsewhere')
424
+ return { ok: false, failure: droppedElsewhere(command, reached.by) };
425
+ }
426
+ const facts = await pageFacts(world, deadline);
427
+ return { ok: true, kind: 'dragTo', through, ...(facts === undefined ? {} : { page: facts }) };
428
+ }
429
+ async function release(session, state, deadline, dispatch) {
430
+ state.released = true;
431
+ await mouse(session, 'mouseReleased', state.at, deadline, dispatch);
432
+ }
433
+ async function cancel(session, state, deadline, dispatch) {
434
+ if (state.held === undefined)
435
+ return;
436
+ state.ended = true;
437
+ await dragEvent(session, 'dragCancel', state.at, state.held, deadline, dispatch);
438
+ }
439
+ // A drag that failed part way, its time spent or an answer lost, leaves Chrome holding the drag, which takes every later
440
+ // mouse event of the page, or the button down: the drag is cancelled, as when a person presses Escape, or the button
441
+ // released where the mouse is, within a moment of its own. A drop that went is not undone.
442
+ async function settle(session, state, dispatch) {
443
+ const moment = new Deadline(settleMs);
444
+ try {
445
+ if (state.held !== undefined && !state.ended)
446
+ await cancel(session, state, moment, dispatch);
447
+ else if (state.held === undefined && state.pressed && !state.released)
448
+ await release(session, state, moment, dispatch);
449
+ }
450
+ catch {
451
+ // The page has gone, or answers nothing: there is no input of its left to end.
452
+ }
453
+ }
454
+ // The page the drop left, as every action names the page it went to; a page too busy to answer keeps none.
455
+ async function pageFacts(world, deadline) {
456
+ try {
457
+ return pageFactsOf(await world.call(pageFactsFunction, [], documentFactsSchema, deadline));
458
+ }
459
+ catch (error) {
460
+ if (error instanceof CdpTimeoutError || isGoneContext(error))
461
+ return undefined;
462
+ throw error;
463
+ }
464
+ }
465
+ // Nothing of this is input: the page's listener and Chrome's hand-over end with the drag, however it ended. They are
466
+ // let go of within a moment of their own, since the drag's time may be spent.
467
+ async function letGo(page, context) {
468
+ const moment = new Deadline(1000);
469
+ await request(page.session, 'Input.setInterceptDrags', { enabled: false }, s.object({}), sendOptions(moment)).catch(() => undefined);
470
+ await page.world.callIn(context, disarmFunction, [], s.boolean(), moment).catch(() => undefined);
471
+ }
472
+ async function mouse(session, type, point, deadline, dispatch, buttons = type === 'mouseReleased' ? 0 : 1) {
473
+ const pressing = type === 'mouseMoved' ? {} : { clickCount: 1 };
474
+ const button = type === 'mouseMoved' && buttons === 0 ? 'none' : 'left';
475
+ await dispatch.send(session, 'Input.dispatchMouseEvent', { type, x: point.x, y: point.y, button, buttons, ...pressing }, deadline);
476
+ }
477
+ async function dragEvent(session, type, point, data, deadline, dispatch) {
478
+ await dispatch.send(session, 'Input.dispatchDragEvent', { type, x: point.x, y: point.y, data }, deadline);
479
+ }
480
+ /**
481
+ * The position `fraction` of the way from `from` to `to`, on the straight line between them.
482
+ *
483
+ * @example between({ x: 0, y: 0 }, { x: 100, y: 50 }, 0.2) // { x: 20, y: 10 }
484
+ */
485
+ export function between(from, to, fraction) {
486
+ return { x: from.x + (to.x - from.x) * fraction, y: from.y + (to.y - from.y) * fraction };
487
+ }
488
+ function distance(first, second) {
489
+ return Math.hypot(first.x - second.x, first.y - second.y);
490
+ }
491
+ function couldNot(command) {
492
+ return `Could not drag ${describeLocator(command.locator)} to ${describeLocator(command.target)}`;
493
+ }
494
+ const checkWords = {
495
+ attached: 'was removed from the page while Retest checked it',
496
+ visible: 'is not visible',
497
+ enabled: 'is disabled',
498
+ editable: 'is read-only',
499
+ stable: 'kept moving',
500
+ 'in-view': 'stays outside the viewport after scrolling',
501
+ 'hit-target': 'has no element at its centre',
502
+ focused: 'did not keep the keyboard focus',
503
+ };
504
+ function unreadyFailure(last, command, deadline) {
505
+ const waitedMs = deadline.budgetMs;
506
+ switch (last.status) {
507
+ case 'timeout':
508
+ throw last.error;
509
+ case 'missing': {
510
+ const locator = last.role === 'source' ? command.locator : command.target;
511
+ const step = last.empty === null ? undefined : describeEmptyStep(locator, last.empty);
512
+ const message = `${couldNot(command)}: no element matched ${describeLocator(locator)} within ${waitedMs} ms.${step === undefined ? '' : ` ${step}`}`;
513
+ return { class: 'not_found', message, details: { element: last.role, waitedMs } };
514
+ }
515
+ case 'blocked': {
516
+ const locator = describeLocator(last.role === 'source' ? command.locator : command.target);
517
+ const reason = last.covering === null ? `${locator} ${checkWords[last.check]}` : `another element, ${last.covering}, covers the centre of ${locator}`;
518
+ return { class: 'not_actionable', message: `${couldNot(command)} within ${waitedMs} ms: ${reason}.`, details: { element: last.role, check: last.check, covering: last.covering, waitedMs } };
519
+ }
520
+ case 'apart':
521
+ return {
522
+ class: 'not_actionable',
523
+ message: `${couldNot(command)} within ${waitedMs} ms: the two do not fit in the viewport together, and Retest does not scroll while it drags.`,
524
+ details: { element: 'target', check: 'in-view', waitedMs },
525
+ };
526
+ case 'navigating': {
527
+ const parsed = URL.parse(last.url);
528
+ const opening = parsed === null ? last.url : originAndPath(parsed);
529
+ return {
530
+ class: 'not_actionable',
531
+ message: `${couldNot(command)} within ${waitedMs} ms: the page was still opening ${opening}, and Retest does not drag in a document about to be replaced.`,
532
+ details: { check: 'navigation', url: opening, waitedMs },
533
+ };
534
+ }
535
+ }
536
+ }
537
+ function targetLeft(command, through, at, spot) {
538
+ const target = describeLocator(command.target);
539
+ const gone = spot.status === 'hidden'
540
+ ? `${target} was hidden`
541
+ : spot.status === 'outside'
542
+ ? `${target} moved out of the viewport`
543
+ : spot.status === 'ambiguous'
544
+ ? `${target} came to match ${spot.count} elements`
545
+ : `${target} left the page`;
546
+ const ending = through === 'drag-events'
547
+ ? 'Retest cancelled the drag, so nothing was dropped.'
548
+ : `Retest released the mouse where the moves ended, at (${Math.round(at.x)}, ${Math.round(at.y)}), and the page may have taken that as a drop.`;
549
+ return { class: 'not_actionable', message: `${couldNot(command)}: ${gone} while Retest dragged. ${ending}`, details: { element: 'target', check: 'attached', through, inputSent: true } };
550
+ }
551
+ function dropCovered(command, covered) {
552
+ const target = describeLocator(command.target);
553
+ const what = covered === 'nothing' ? `no element was at the centre of ${target}` : `another element, ${covered}, covered the centre of ${target}`;
554
+ return {
555
+ class: 'not_actionable',
556
+ message: `${couldNot(command)}: ${what} when Retest was to drop. Retest cancelled the drag, so nothing was dropped.`,
557
+ details: { element: 'target', check: 'hit-target', covering: covered === 'nothing' ? null : covered, through: 'drag-events', inputSent: true },
558
+ };
559
+ }
560
+ function droppedElsewhere(command, by) {
561
+ return {
562
+ class: 'not_actionable',
563
+ message: `${couldNot(command)}: the drop went to another element, ${by}, which the page heard in its place.`,
564
+ details: { element: 'target', check: 'hit-target', droppedOn: by, through: 'drag-events', inputSent: true },
565
+ };
566
+ }
567
+ // A frame's element is reached through a world of its own, and a drag moves through the page's own document only.
568
+ function framed(command) {
569
+ const inFrame = [command.locator, command.target].find((locator) => locator.frame !== undefined && locator.frame.length > 0);
570
+ if (inFrame === undefined)
571
+ return undefined;
572
+ return { class: 'unsupported', message: `${couldNot(command)}: ${describeLocator(inFrame)} is inside a frame, and dragTo() drags between elements of the page itself.`, details: { inputSent: false } };
573
+ }
574
+ function touchRefused(command) {
575
+ return { class: 'unsupported', message: `${couldNot(command)}: the page emulates a touch screen, and dragTo() drags with the mouse.`, details: { inputSent: false } };
576
+ }
577
+ /**
578
+ * The failure an engine without drag and drop gives `dragTo()`, naming the engine. Nothing is sent.
579
+ *
580
+ * @example dragRefused('Firefox').message // 'dragTo() is not available on Firefox yet. Retest drags on Chrome, Chromium, Edge and Electron.'
581
+ */
582
+ export function dragRefused(engine) {
583
+ return { class: 'unsupported', message: `dragTo() is not available on ${engine} yet. Retest drags on Chrome, Chromium, Edge and Electron.`, details: { inputSent: false } };
584
+ }
@@ -0,0 +1,32 @@
1
+ import type { CdpConnection } from './cdp/connection.ts';
2
+ import type { CdpSession } from './cdp/session.ts';
3
+ import type { UnwaitedDialog } from './dialogs.ts';
4
+ import type { Deadline } from '../protocol/deadline.ts';
5
+ /** The app's first window as the watch attached it: its session, its target and its browser context. */
6
+ export type AttachedWindow = {
7
+ readonly session: CdpSession;
8
+ readonly targetId: string;
9
+ readonly browserContextId: string;
10
+ };
11
+ /**
12
+ * Attaches an Electron app's first window before its document can show a dialog Retest would not hear, and answers
13
+ * the dialogs it shows until its page takes them over with `handOver`. Every other page the app opens is let run at
14
+ * once and let go, as a test reaches only the first window.
15
+ */
16
+ export declare class ElectronWindowWatch {
17
+ #private;
18
+ constructor(connection: CdpConnection);
19
+ /**
20
+ * Asks the app to attach every page it has and hold every new one. Call it before any other command, so it is the
21
+ * first the app reads; it settles once the app answered, within the deadline.
22
+ */
23
+ start(deadline: Deadline): Promise<void>;
24
+ /** The first window, once attached; undefined when the deadline passes or the app goes first. */
25
+ first(deadline: Deadline): Promise<AttachedWindow | undefined>;
26
+ /**
27
+ * Stops answering the first window's dialogs, and gives every dialog answered so far, in order. The page that takes
28
+ * the window over answers and tells every dialog from then on.
29
+ */
30
+ handOver(): readonly UnwaitedDialog[];
31
+ }
32
+ //# sourceMappingURL=electron-window-watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"electron-window-watch.d.ts","sourceRoot":"","sources":["../../src/browser/electron-window-watch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAevD,wGAAwG;AACxG,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAAA;AAa3H;;;;GAIG;AACH,qBAAa,mBAAmB;;gBAOlB,UAAU,EAAE,aAAa;IAKrC;;;OAGG;IACH,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAKxC,iGAAiG;IAC3F,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAcpE;;;OAGG;IACH,QAAQ,IAAI,SAAS,cAAc,EAAE;CAsCtC"}