@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,78 @@
1
+ import { readCallOptions } from "./call-options.js";
2
+ import { formatValue } from "./format-value.js";
3
+ import { misuse } from "./misuse.js";
4
+ import { isThenable } from "./operation.js";
5
+ /** A dialog the parent handed to the test, which the test answers through the page that showed it. */
6
+ export class AppDialog {
7
+ type;
8
+ message;
9
+ defaultValue;
10
+ #run;
11
+ #app;
12
+ #id;
13
+ #acted;
14
+ #answered;
15
+ constructor(run, app, record, acted) {
16
+ this.type = record.type;
17
+ this.message = record.message;
18
+ this.defaultValue = record.defaultValue;
19
+ this.#run = run;
20
+ this.#app = app;
21
+ this.#id = record.id;
22
+ this.#acted = acted;
23
+ }
24
+ accept(promptText) {
25
+ const location = this.#run.location();
26
+ if (promptText !== undefined && typeof promptText !== 'string')
27
+ throw misuse(`accept() takes the text a prompt dialog answers with, received ${formatValue(promptText)}.`, this.#run);
28
+ if (promptText !== undefined && this.type !== 'prompt')
29
+ throw misuse(`accept() takes text only for a prompt dialog, and this is ${article(this.type)} ${this.type} dialog.`, this.#run);
30
+ return this.#answer({ accept: true, promptText, label: 'dialog.accept()', how: 'accepted', location });
31
+ }
32
+ dismiss() {
33
+ return this.#answer({ accept: false, promptText: undefined, label: 'dialog.dismiss()', how: 'dismissed', location: this.#run.location() });
34
+ }
35
+ #answer({ accept, promptText, label, how, location }) {
36
+ const answered = this.#answered;
37
+ if (answered !== undefined)
38
+ throw misuse(`This ${this.type} dialog was already ${answered.how} on ${this.#run.describeLine(answered.location)}. Answer each dialog once.`, this.#run);
39
+ this.#answered = { how, location };
40
+ return this.#run.answerDialog({ app: this.#app, dialog: this.#id, accept, promptText, label, location, acted: this.#acted });
41
+ }
42
+ }
43
+ /**
44
+ * Arms an app's page for its next dialog, runs `action`, and resolves with the dialog as soon as it opens, while the
45
+ * action still waits on it. `options` may shorten the wait, as `{ timeout }` does an action's.
46
+ *
47
+ * @example const dialog = await waitForDialog({ run, app: 'web' }, () => page.getByRole('button', { name: 'Delete' }).click())
48
+ */
49
+ export function waitForDialog({ run, app }, action, options) {
50
+ const location = run.location();
51
+ const read = readOpening('dialog', run, action, options);
52
+ return run.waitForDialog({ app, label: 'page.dialog()', location, timeoutMs: read.timeoutMs, action: read.action }, (record, acted) => new AppDialog(run, app, record, acted));
53
+ }
54
+ /**
55
+ * Reads the action and the options `page.dialog()` or `page.popup()` was given, as a JavaScript caller may have passed
56
+ * them any way it liked. The action must be a function, and what it returns must be a promise, the action's own.
57
+ *
58
+ * @example readOpening('popup', run, () => page.getByRole('link', { name: 'Terms' }).click(), undefined).timeoutMs // undefined
59
+ */
60
+ export function readOpening(kind, run, action, options) {
61
+ const call = `page.${kind}()`;
62
+ const example = kind === 'dialog' ? "() => page.getByRole('button', { name: 'Delete' }).click()" : "() => page.getByRole('link', { name: 'Terms' }).click()";
63
+ if (typeof action !== 'function')
64
+ throw misuse(`${call} takes a function that runs the action, such as ${example}, received ${formatValue(action)}.`, run);
65
+ const read = readCallOptions(kind, options);
66
+ if (!read.ok)
67
+ throw misuse(read.problem, run);
68
+ const returning = async () => {
69
+ const returned = action();
70
+ if (!isThenable(returned))
71
+ throw misuse(`${call} runs its function and waits for the promise it returns, and it returned ${formatValue(returned)}. Return the action, as in ${example}.`, run);
72
+ return returned;
73
+ };
74
+ return { action: returning, timeoutMs: read.timeoutMs };
75
+ }
76
+ function article(word) {
77
+ return /^[aeiou]/.test(word) ? 'an' : 'a';
78
+ }
@@ -0,0 +1,32 @@
1
+ import type { CallOptions } from './call-options.ts';
2
+ /**
3
+ * Options a drag takes: `steps`, how many positions the mouse moves through on its way to the target, 5 when absent and
4
+ * at most 100, and the `timeout` every action takes.
5
+ */
6
+ export type DragOptions = CallOptions & {
7
+ readonly steps?: number | undefined;
8
+ };
9
+ /**
10
+ * A drag's options as the command takes them: its steps when given, and the timeout as the caller passed it, which the
11
+ * action reads as every action reads its own.
12
+ */
13
+ export type ReadDragOptions = {
14
+ readonly ok: true;
15
+ readonly steps: {
16
+ steps?: number;
17
+ };
18
+ readonly call: {
19
+ readonly timeout: unknown;
20
+ } | undefined;
21
+ } | {
22
+ readonly ok: false;
23
+ readonly problem: string;
24
+ };
25
+ /**
26
+ * Reads the options a JavaScript caller may have passed `dragTo` any way it liked, and says what is wrong as a sentence.
27
+ * `timeout` is left for the action to read, as every action reads its own.
28
+ *
29
+ * @example readDragOptions({ steps: 10, timeout: 2000 }) // { ok: true, steps: { steps: 10 }, call: { timeout: 2000 } }
30
+ */
31
+ export declare function readDragOptions(options: unknown): ReadDragOptions;
32
+ //# sourceMappingURL=drag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drag.d.ts","sourceRoot":"","sources":["../../src/api/drag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAKpD;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG;IAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AAE/E;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEpM;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,CAWjE"}
@@ -0,0 +1,26 @@
1
+ import { dragProblem } from "../protocol/drag.js";
2
+ import { isPlainObject } from "../protocol/schema.js";
3
+ import { formatValue } from "./format-value.js";
4
+ /**
5
+ * Reads the options a JavaScript caller may have passed `dragTo` any way it liked, and says what is wrong as a sentence.
6
+ * `timeout` is left for the action to read, as every action reads its own.
7
+ *
8
+ * @example readDragOptions({ steps: 10, timeout: 2000 }) // { ok: true, steps: { steps: 10 }, call: { timeout: 2000 } }
9
+ */
10
+ export function readDragOptions(options) {
11
+ if (options === undefined)
12
+ return { ok: true, steps: {}, call: undefined };
13
+ if (!isPlainObject(options))
14
+ return { ok: false, problem: `dragTo() takes options such as { steps: 10, timeout: 2000 }, received ${formatValue(options)}.` };
15
+ const given = Object.entries(options).filter(([, value]) => value !== undefined);
16
+ const unknown = given.find(([key]) => key !== 'steps' && key !== 'timeout');
17
+ if (unknown !== undefined)
18
+ return { ok: false, problem: `Unknown dragTo() option ${JSON.stringify(unknown[0])}. Its options are steps and timeout.` };
19
+ const { steps, timeout } = Object.fromEntries(given);
20
+ if (steps !== undefined && typeof steps !== 'number')
21
+ return { ok: false, problem: `dragTo() takes steps as a whole number from 1 to 100, received ${formatValue(steps)}.` };
22
+ const problem = dragProblem({ steps });
23
+ if (problem !== undefined)
24
+ return { ok: false, problem: problem.message };
25
+ return { ok: true, steps: steps === undefined ? {} : { steps }, call: timeout === undefined ? undefined : { timeout } };
26
+ }
@@ -0,0 +1,49 @@
1
+ import type { InputFileRecord } from '../protocol/files.ts';
2
+ import type { LocatorRecipe } from '../protocol/locator.ts';
3
+ import type { Download, FileChooser } from './page.ts';
4
+ import type { TestRun } from './test-run.ts';
5
+ /** The test, app and recipe behind a locator, as giving files to it needs them. */
6
+ type InputTarget = {
7
+ readonly run: TestRun;
8
+ readonly app: string;
9
+ readonly recipe: LocatorRecipe;
10
+ };
11
+ /** The test and app behind a page. */
12
+ type PageTarget = {
13
+ readonly run: TestRun;
14
+ readonly app: string;
15
+ };
16
+ /**
17
+ * Gives the one file input a locator finds the files: the input that `setInputFiles` is called on.
18
+ *
19
+ * @example await setInputFiles(target, './report.pdf', undefined)
20
+ */
21
+ export declare function setInputFiles({ run, app, recipe }: InputTarget, files: unknown, options: unknown): Promise<void>;
22
+ /**
23
+ * Arms the app's page for the next file picker it opens, runs `action`, and gives the picker that opened. The wait for
24
+ * it starts before the action and lasts the action budget, or a shorter `{ timeout }`.
25
+ *
26
+ * @example const chooser = await fileChooser(page, () => button.click(), undefined)
27
+ */
28
+ export declare function fileChooser(page: PageTarget, action: unknown, options: unknown): Promise<FileChooser>;
29
+ /**
30
+ * Arms the app's page for the next download it begins, runs `action`, and gives the download that began. The wait for
31
+ * it starts before the action and lasts the action budget, or a shorter `{ timeout }`.
32
+ *
33
+ * @example const download = await download(page, () => link.click(), undefined)
34
+ */
35
+ export declare function download(page: PageTarget, action: unknown, options: unknown): Promise<Download>;
36
+ /** The files a test gave, as the parent takes them, and whether it gave a list. */
37
+ type ReadFiles = {
38
+ files: InputFileRecord[];
39
+ list: boolean;
40
+ };
41
+ /**
42
+ * Reads the files a JavaScript caller may have passed any way it liked, and says what is wrong as a sentence. A path is
43
+ * kept as the test wrote it, with the folder it is read against.
44
+ *
45
+ * @example readInputFiles('./report.pdf', '/work/tests', 'setInputFiles') // { files: [{ path: './report.pdf', folder: '/work/tests' }], list: false }
46
+ */
47
+ export declare function readInputFiles(value: unknown, folder: string, method: string): ReadFiles | string;
48
+ export {};
49
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/api/files.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAc,MAAM,WAAW,CAAA;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAU5C,mFAAmF;AACnF,KAAK,WAAW,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAAE,CAAA;AAElG,sCAAsC;AACtC,KAAK,UAAU,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AAkEjE;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhH;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,CAUrG;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAU/F;AAQD,mFAAmF;AACnF,KAAK,SAAS,GAAG;IAAE,KAAK,EAAE,eAAe,EAAE,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAA;AAE5D;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAejG"}
@@ -0,0 +1,168 @@
1
+ import { copyFile, mkdir } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { errorMessage, failure } from "../protocol/failures.js";
4
+ import { inputFilesProblem } from "../protocol/files.js";
5
+ import { isArray, isPlainObject } from "../protocol/schema.js";
6
+ import { readCallOptions } from "./call-options.js";
7
+ import { formatValue } from "./format-value.js";
8
+ import { misuse } from "./misuse.js";
9
+ /** A file picker the parent caught for the test, whose input the test gives files through the page that opened it. */
10
+ class AppFileChooser {
11
+ multiple;
12
+ #page;
13
+ #chooser;
14
+ constructor(page, chooser, multiple) {
15
+ this.#page = page;
16
+ this.#chooser = chooser;
17
+ this.multiple = multiple;
18
+ }
19
+ setFiles(files, options) {
20
+ const { run, app } = this.#page;
21
+ const location = run.location();
22
+ const read = readInputFiles(files, run.callerFolder(location), 'setFiles');
23
+ if (typeof read === 'string')
24
+ throw misuse(read, run);
25
+ return run.action(app, { kind: 'setChooserFiles', chooser: this.#chooser, files: read.files, ...(read.list ? { list: true } : {}) }, location, options);
26
+ }
27
+ }
28
+ /** A download the parent caught for the test, whose saved file the test asks the parent for. */
29
+ class AppDownload {
30
+ suggestedFilename;
31
+ #page;
32
+ #download;
33
+ constructor(page, download, suggestedFilename) {
34
+ this.#page = page;
35
+ this.#download = download;
36
+ this.suggestedFilename = suggestedFilename;
37
+ }
38
+ path(options) {
39
+ return this.#saved('download.path()', 'path', options, async (path) => path);
40
+ }
41
+ saveAs(path, options) {
42
+ const { run } = this.#page;
43
+ if (typeof path !== 'string' || path === '')
44
+ throw misuse(`download.saveAs() takes the path to copy the file to, received ${formatValue(path)}.`, run);
45
+ const target = resolve(run.callerFolder(run.location()), path);
46
+ return this.#saved('download.saveAs()', 'saveAs', options, async (saved) => {
47
+ try {
48
+ await mkdir(dirname(target), { recursive: true });
49
+ await copyFile(saved, target);
50
+ }
51
+ catch (error) {
52
+ throw run.fail(failure('usage', `download.saveAs() could not copy ${this.suggestedFilename} to ${target}: ${errorMessage(error)}`, run.location()));
53
+ }
54
+ });
55
+ }
56
+ #saved(label, method, options, then) {
57
+ const { run } = this.#page;
58
+ const location = run.location();
59
+ const read = readCallOptions(method, options);
60
+ if (!read.ok)
61
+ throw misuse(read.problem, run);
62
+ const timeoutMs = Math.min(read.timeoutMs ?? run.timeouts.navigation, run.timeouts.navigation);
63
+ return run.awaitFiles({ label, request: { kind: 'saved', download: this.#download }, location, timeoutMs }, (answer) => {
64
+ if (answer.kind !== 'saved')
65
+ throw new Error(`Retest answered a wait for a saved download with ${answer.kind}`);
66
+ return then(answer.path);
67
+ });
68
+ }
69
+ }
70
+ /**
71
+ * Gives the one file input a locator finds the files: the input that `setInputFiles` is called on.
72
+ *
73
+ * @example await setInputFiles(target, './report.pdf', undefined)
74
+ */
75
+ export function setInputFiles({ run, app, recipe }, files, options) {
76
+ const location = run.location();
77
+ const read = readInputFiles(files, run.callerFolder(location), 'setInputFiles');
78
+ if (typeof read === 'string')
79
+ throw misuse(read, run);
80
+ return run.action(app, { kind: 'setInputFiles', locator: recipe, files: read.files, ...(read.list ? { list: true } : {}) }, location, options);
81
+ }
82
+ /**
83
+ * Arms the app's page for the next file picker it opens, runs `action`, and gives the picker that opened. The wait for
84
+ * it starts before the action and lasts the action budget, or a shorter `{ timeout }`.
85
+ *
86
+ * @example const chooser = await fileChooser(page, () => button.click(), undefined)
87
+ */
88
+ export function fileChooser(page, action, options) {
89
+ const { run } = page;
90
+ const location = run.location();
91
+ if (typeof action !== 'function')
92
+ throw misuse(`page.fileChooser() takes the action that opens the picker, such as () => upload.click(), received ${formatValue(action)}.`, run);
93
+ const timeoutMs = actionBudget(run, 'fileChooser', options);
94
+ const request = { kind: 'chooser', app: page.app };
95
+ return run.awaitFiles({ label: 'page.fileChooser()', request, location, timeoutMs }, (answer) => {
96
+ if (answer.kind !== 'chooser')
97
+ throw new Error(`Retest answered a wait for a file picker with ${answer.kind}`);
98
+ return new AppFileChooser(page, answer.chooser, answer.multiple);
99
+ }, () => action());
100
+ }
101
+ /**
102
+ * Arms the app's page for the next download it begins, runs `action`, and gives the download that began. The wait for
103
+ * it starts before the action and lasts the action budget, or a shorter `{ timeout }`.
104
+ *
105
+ * @example const download = await download(page, () => link.click(), undefined)
106
+ */
107
+ export function download(page, action, options) {
108
+ const { run } = page;
109
+ const location = run.location();
110
+ if (typeof action !== 'function')
111
+ throw misuse(`page.download() takes the action that starts the download, such as () => link.click(), received ${formatValue(action)}.`, run);
112
+ const timeoutMs = actionBudget(run, 'download', options);
113
+ const request = { kind: 'download', app: page.app };
114
+ return run.awaitFiles({ label: 'page.download()', request, location, timeoutMs }, (answer) => {
115
+ if (answer.kind !== 'download')
116
+ throw new Error(`Retest answered a wait for a download with ${answer.kind}`);
117
+ return new AppDownload(page, answer.download, answer.suggestedFilename);
118
+ }, () => action());
119
+ }
120
+ function actionBudget(run, method, options) {
121
+ const read = readCallOptions(method, options);
122
+ if (!read.ok)
123
+ throw misuse(read.problem, run);
124
+ return Math.min(read.timeoutMs ?? run.timeouts.action, run.timeouts.action);
125
+ }
126
+ /**
127
+ * Reads the files a JavaScript caller may have passed any way it liked, and says what is wrong as a sentence. A path is
128
+ * kept as the test wrote it, with the folder it is read against.
129
+ *
130
+ * @example readInputFiles('./report.pdf', '/work/tests', 'setInputFiles') // { files: [{ path: './report.pdf', folder: '/work/tests' }], list: false }
131
+ */
132
+ export function readInputFiles(value, folder, method) {
133
+ const list = isArray(value);
134
+ const given = isArray(value) ? value : [value];
135
+ const files = [];
136
+ for (const item of given) {
137
+ const file = readInputFile(item, folder);
138
+ if (file === undefined) {
139
+ const shown = list ? `a list holding ${formatValue(item)}` : formatValue(value);
140
+ return `${method}() takes a path, a file such as { name: 'notes.txt', content: 'hello' }, or a list of them, received ${shown}.`;
141
+ }
142
+ if (typeof file === 'string')
143
+ return `${method}() ${file}`;
144
+ files.push(file);
145
+ }
146
+ const problem = inputFilesProblem(files, method);
147
+ return problem === undefined ? { files, list } : problem.message;
148
+ }
149
+ // One file, or undefined for something that is not one, or what is wrong with a file by content.
150
+ function readInputFile(item, folder) {
151
+ if (typeof item === 'string')
152
+ return { path: item, folder };
153
+ if (!isPlainObject(item))
154
+ return undefined;
155
+ const given = Object.entries(item).filter(([, entry]) => entry !== undefined);
156
+ const unknown = given.find(([key]) => key !== 'name' && key !== 'mimeType' && key !== 'content');
157
+ if (unknown !== undefined)
158
+ return `has no file option ${JSON.stringify(unknown[0])}. A file by content takes name, mimeType and content.`;
159
+ const { name, mimeType, content } = Object.fromEntries(given);
160
+ if (typeof name !== 'string')
161
+ return `takes a file's name as text, received ${formatValue(name)}.`;
162
+ if (mimeType !== undefined && typeof mimeType !== 'string')
163
+ return `takes mimeType as text, such as 'text/plain', received ${formatValue(mimeType)}.`;
164
+ const bytes = typeof content === 'string' ? Buffer.from(content, 'utf8') : content instanceof Uint8Array ? Buffer.from(content.buffer, content.byteOffset, content.byteLength) : undefined;
165
+ if (bytes === undefined)
166
+ return `takes a file's content as text or bytes, received ${formatValue(content)}.`;
167
+ return { name, ...(mimeType === undefined ? {} : { mimeType }), base64: bytes.toString('base64') };
168
+ }
@@ -0,0 +1,97 @@
1
+ import type { InboxName } from '../config/register.ts';
2
+ import type { InboxCodeRef, InboxLinkRef, ReceivedMessageRecord } from '../protocol/inbox.ts';
3
+ import { Secret } from './secret.ts';
4
+ /**
5
+ * How `latest()` reads: `subject` picks the newest message whose subject holds this text, in any case, or matches this
6
+ * `RegExp`, as the test reads the subject, with its codes and links replaced by placeholders. `timeout` is how long it
7
+ * looks, in milliseconds, the assertion budget by default, never longer than the test has left.
8
+ */
9
+ export type InboxReadOptions = {
10
+ readonly subject?: string | RegExp | undefined;
11
+ readonly timeout?: number | undefined;
12
+ };
13
+ /** Which link `link()` takes: its number among the message's links, from 1, or the first whose path is `path` or lies under it. */
14
+ export type LinkChoice = number | {
15
+ readonly path: string;
16
+ };
17
+ /**
18
+ * A link in a received message: the text it was shown under, its origin, and its path with each token in it replaced
19
+ * by its placeholder. `token` says whether it carries a token. Its query and fragment are never in the test's process.
20
+ */
21
+ export type ReceivedLink = {
22
+ readonly text: string;
23
+ readonly origin: string;
24
+ readonly path: string;
25
+ readonly token: boolean;
26
+ };
27
+ /**
28
+ * A one-time code from a message, named but never held: the process that runs Retest types it, bound to origins as a
29
+ * secret is. It is a secret, so it goes only to `fill()`, and however it is printed it reads `{{code}}`.
30
+ */
31
+ export declare class InboxCode extends Secret {
32
+ #private;
33
+ constructor(reference: InboxCodeRef);
34
+ }
35
+ /**
36
+ * A link from a message, named but never held: the process that runs Retest opens it, when its origin is one the test
37
+ * may open. It goes only to `goto()`, and however it is printed it reads `{{link}}`, or `{{link:2}}` when it was chosen
38
+ * by number or path.
39
+ */
40
+ export declare class InboxLink {
41
+ #private;
42
+ constructor(reference: InboxLinkRef);
43
+ toString(): string;
44
+ toJSON(): string;
45
+ }
46
+ /**
47
+ * A message an inbox gave the test. `subject`, `from` and `text` have each code written `{{code}}` and each link that
48
+ * carries a token written `{{link:1}}`, `{{link:2}}` and so on, by its number. `code` is its first code, for `fill()`,
49
+ * and `link()` one of its links, for `goto()`; the test's process never holds either value.
50
+ */
51
+ export declare class ReceivedMessage {
52
+ #private;
53
+ readonly subject: string;
54
+ readonly from: string;
55
+ readonly text: string;
56
+ /** When the mailbox received it. */
57
+ readonly receivedAt: Date;
58
+ readonly links: readonly ReceivedLink[];
59
+ /** The message's first code. A fill of it fails when the message holds none. */
60
+ readonly code: InboxCode;
61
+ constructor(record: ReceivedMessageRecord);
62
+ /**
63
+ * One of the message's links, for `goto()`. With no argument, the first link that carries a token, or else the first
64
+ * link. A number takes that link, from 1; `{ path }` the first link whose path is `path` or lies under it.
65
+ *
66
+ * @example await page.goto(message.link({ path: '/verify' }))
67
+ */
68
+ link(choice?: LinkChoice): InboxLink;
69
+ }
70
+ /** A test mailbox from the config, named for the reads a test makes of it. */
71
+ export declare class Inbox {
72
+ #private;
73
+ constructor(name: string);
74
+ /** The inbox's name in the config. */
75
+ get name(): string;
76
+ /**
77
+ * Waits for the newest message the mailbox received since the test started, or since the last message this test was
78
+ * given from it, and gives it to the test. It looks again, as a matcher does, until one arrives or its time runs out.
79
+ *
80
+ * @example const message = await inbox('owner').latest({ subject: 'Your sign-in code' })
81
+ */
82
+ latest(options?: InboxReadOptions): Promise<ReceivedMessage>;
83
+ }
84
+ /**
85
+ * Names an inbox from the config, whose messages a test reads with `latest()`. The test's process never holds a code or
86
+ * a link that carries a token.
87
+ *
88
+ * @example
89
+ * const message = await inbox('owner').latest({ subject: 'Your sign-in code' })
90
+ * await page.getByLabel('Code').fill(message.code)
91
+ */
92
+ export declare function inbox(name: InboxName): Inbox;
93
+ /** The code a value names, when it is a code from an inbox. */
94
+ export declare function inboxCodeReference(value: unknown): InboxCodeRef | undefined;
95
+ /** The link a value names, when it is a link from an inbox. */
96
+ export declare function inboxLinkReference(value: unknown): InboxLinkRef | undefined;
97
+ //# sourceMappingURL=inbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../src/api/inbox.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAgB,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAS3G,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AAExH,mIAAmI;AACnI,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAE3D;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAA;AAK7H;;;GAGG;AACH,qBAAa,SAAU,SAAQ,MAAM;;gBAOvB,SAAS,EAAE,YAAY;CAIpC;AAED;;;;GAIG;AACH,qBAAa,SAAS;;gBAaR,SAAS,EAAE,YAAY;IAInC,QAAQ,IAAI,MAAM;IAIlB,MAAM,IAAI,MAAM;CAGjB;AAED;;;;GAIG;AACH,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,oCAAoC;IACpC,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAA;IACvC,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;gBAGZ,MAAM,EAAE,qBAAqB;IAUzC;;;;;OAKG;IACH,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS;CA+BrC;AAED,8EAA8E;AAC9E,qBAAa,KAAK;;gBAGJ,IAAI,EAAE,MAAM;IAIxB,sCAAsC;IACtC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;CAS7D;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,CAK5C;AAED,+DAA+D;AAC/D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAE3E;AAED,+DAA+D;AAC/D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAE3E"}
@@ -0,0 +1,191 @@
1
+ import { inspect } from 'node:util';
2
+ import { describeInboxReference } from "../protocol/inbox.js";
3
+ import { patternProblem, textPatternOf } from "../protocol/locator.js";
4
+ import { isPlainObject } from "../protocol/schema.js";
5
+ import { maxTimeout } from "../protocol/timeouts.js";
6
+ import { requireScope } from "./context.js";
7
+ import { formatValue } from "./format-value.js";
8
+ import { misuse } from "./misuse.js";
9
+ import { Secret } from "./secret.js";
10
+ let codeReference;
11
+ let linkReference;
12
+ /**
13
+ * A one-time code from a message, named but never held: the process that runs Retest types it, bound to origins as a
14
+ * secret is. It is a secret, so it goes only to `fill()`, and however it is printed it reads `{{code}}`.
15
+ */
16
+ export class InboxCode extends Secret {
17
+ #reference;
18
+ static {
19
+ codeReference = (code) => code.#reference;
20
+ }
21
+ constructor(reference) {
22
+ super('code');
23
+ this.#reference = reference;
24
+ }
25
+ }
26
+ /**
27
+ * A link from a message, named but never held: the process that runs Retest opens it, when its origin is one the test
28
+ * may open. It goes only to `goto()`, and however it is printed it reads `{{link}}`, or `{{link:2}}` when it was chosen
29
+ * by number or path.
30
+ */
31
+ export class InboxLink {
32
+ #reference;
33
+ static {
34
+ linkReference = (link) => link.#reference;
35
+ // Node's inspect hook sits on the prototype, off the declared type, so the public type needs no Node types.
36
+ Object.defineProperty(InboxLink.prototype, inspect.custom, {
37
+ value() {
38
+ return this.toString();
39
+ },
40
+ });
41
+ }
42
+ constructor(reference) {
43
+ this.#reference = reference;
44
+ }
45
+ toString() {
46
+ return describeInboxReference(this.#reference);
47
+ }
48
+ toJSON() {
49
+ return this.toString();
50
+ }
51
+ }
52
+ /**
53
+ * A message an inbox gave the test. `subject`, `from` and `text` have each code written `{{code}}` and each link that
54
+ * carries a token written `{{link:1}}`, `{{link:2}}` and so on, by its number. `code` is its first code, for `fill()`,
55
+ * and `link()` one of its links, for `goto()`; the test's process never holds either value.
56
+ */
57
+ export class ReceivedMessage {
58
+ subject;
59
+ from;
60
+ text;
61
+ /** When the mailbox received it. */
62
+ receivedAt;
63
+ links;
64
+ /** The message's first code. A fill of it fails when the message holds none. */
65
+ code;
66
+ #record;
67
+ constructor(record) {
68
+ this.#record = record;
69
+ this.subject = record.subject;
70
+ this.from = record.from;
71
+ this.text = record.text;
72
+ this.receivedAt = new Date(record.receivedAt);
73
+ this.links = Object.freeze(record.links.map((link) => Object.freeze({ ...link })));
74
+ this.code = new InboxCode({ inbox: record.inbox, message: record.id, code: 1 });
75
+ }
76
+ /**
77
+ * One of the message's links, for `goto()`. With no argument, the first link that carries a token, or else the first
78
+ * link. A number takes that link, from 1; `{ path }` the first link whose path is `path` or lies under it.
79
+ *
80
+ * @example await page.goto(message.link({ path: '/verify' }))
81
+ */
82
+ link(choice) {
83
+ const { inbox, id } = this.#record;
84
+ const chosen = this.#choose(choice);
85
+ if (typeof chosen === 'string')
86
+ throw misuse(chosen);
87
+ return new InboxLink({ inbox, message: id, link: chosen, ...(choice === undefined ? { default: true } : {}) });
88
+ }
89
+ // A link's number from 1, or why there is none to take.
90
+ #choose(choice) {
91
+ const { links } = this;
92
+ const subject = JSON.stringify(this.subject);
93
+ if (choice === undefined) {
94
+ if (links.length === 0)
95
+ return `link() found no link in the message ${subject}.`;
96
+ const carrying = links.findIndex((link) => link.token);
97
+ return (carrying === -1 ? 0 : carrying) + 1;
98
+ }
99
+ if (typeof choice === 'number') {
100
+ if (Number.isInteger(choice) && choice >= 1 && choice <= links.length)
101
+ return choice;
102
+ return `link(${formatValue(choice)}) takes a link's number from 1 to ${links.length}, the links of the message ${subject}.`;
103
+ }
104
+ if (isPlainObject(choice) && Object.keys(choice).length === 1 && typeof choice['path'] === 'string') {
105
+ const path = choice['path'];
106
+ const under = path.endsWith('/') ? path : `${path}/`;
107
+ const found = links.findIndex((link) => link.path === path || link.path.startsWith(under));
108
+ if (found !== -1)
109
+ return found + 1;
110
+ const paths = links.map((link) => link.path).filter((each) => each !== '');
111
+ const listed = paths.length === 0 ? 'Its links have no web address.' : `Its links go to ${paths.join(', ')}.`;
112
+ return `link({ path: ${JSON.stringify(path)} }) matches no link in the message ${subject}. ${listed}`;
113
+ }
114
+ return `link() takes a link's number from 1, such as link(2), or its path, such as link({ path: '/verify' }), received ${formatValue(choice)}.`;
115
+ }
116
+ }
117
+ /** A test mailbox from the config, named for the reads a test makes of it. */
118
+ export class Inbox {
119
+ #name;
120
+ constructor(name) {
121
+ this.#name = name;
122
+ }
123
+ /** The inbox's name in the config. */
124
+ get name() {
125
+ return this.#name;
126
+ }
127
+ /**
128
+ * Waits for the newest message the mailbox received since the test started, or since the last message this test was
129
+ * given from it, and gives it to the test. It looks again, as a matcher does, until one arrives or its time runs out.
130
+ *
131
+ * @example const message = await inbox('owner').latest({ subject: 'Your sign-in code' })
132
+ */
133
+ latest(options) {
134
+ const call = `inbox(${JSON.stringify(this.#name)}).latest()`;
135
+ const { run } = requireScope(call);
136
+ const location = run.location();
137
+ const read = readOptions(options);
138
+ if (typeof read === 'string')
139
+ throw misuse(`${call} ${read}`, run);
140
+ const request = { inbox: this.#name, ...(read.subject === undefined ? {} : { subject: read.subject }) };
141
+ return run.readInbox({ label: call, request, location, ...(read.timeoutMs === undefined ? {} : { timeoutMs: read.timeoutMs }) }, (record) => new ReceivedMessage(record));
142
+ }
143
+ }
144
+ /**
145
+ * Names an inbox from the config, whose messages a test reads with `latest()`. The test's process never holds a code or
146
+ * a link that carries a token.
147
+ *
148
+ * @example
149
+ * const message = await inbox('owner').latest({ subject: 'Your sign-in code' })
150
+ * await page.getByLabel('Code').fill(message.code)
151
+ */
152
+ export function inbox(name) {
153
+ // Read as JavaScript passed it: a registered config narrows the type, not what arrives.
154
+ const given = name;
155
+ if (typeof given !== 'string' || given === '')
156
+ throw misuse(`inbox() takes the name of an inbox from the config, received ${formatValue(given)}.`);
157
+ return new Inbox(given);
158
+ }
159
+ /** The code a value names, when it is a code from an inbox. */
160
+ export function inboxCodeReference(value) {
161
+ return value instanceof InboxCode ? codeReference?.(value) : undefined;
162
+ }
163
+ /** The link a value names, when it is a link from an inbox. */
164
+ export function inboxLinkReference(value) {
165
+ return value instanceof InboxLink ? linkReference?.(value) : undefined;
166
+ }
167
+ // Every option is checked, because JavaScript callers have no types. A key set to undefined counts as absent.
168
+ function readOptions(options) {
169
+ if (options === undefined)
170
+ return {};
171
+ if (!isPlainObject(options))
172
+ return `takes options such as { subject: 'Your code', timeout: 10_000 }, received ${formatValue(options)}.`;
173
+ const given = Object.entries(options).filter(([, value]) => value !== undefined);
174
+ const unknown = given.find(([key]) => key !== 'subject' && key !== 'timeout');
175
+ if (unknown !== undefined)
176
+ return `has no option ${JSON.stringify(unknown[0])}. It takes subject and timeout.`;
177
+ const { subject, timeout } = Object.fromEntries(given);
178
+ if (timeout !== undefined && !(typeof timeout === 'number' && Number.isInteger(timeout) && timeout >= 1 && timeout <= maxTimeout)) {
179
+ return `takes timeout as a whole number of milliseconds from 1 to ${maxTimeout}, received ${formatValue(timeout)}.`;
180
+ }
181
+ const timed = timeout === undefined ? {} : { timeoutMs: timeout };
182
+ if (subject === undefined)
183
+ return timed;
184
+ if (typeof subject === 'string')
185
+ return subject === '' ? 'takes subject as text to find in the subject, received an empty string.' : { subject, ...timed };
186
+ if (!(subject instanceof RegExp))
187
+ return `takes subject as text or a RegExp, received ${formatValue(subject)}.`;
188
+ const pattern = textPatternOf(subject);
189
+ const problem = patternProblem(pattern);
190
+ return problem === undefined ? { subject: pattern, ...timed } : `received a subject RegExp Retest cannot read: ${problem}.`;
191
+ }