@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.
- package/README.md +6 -0
- package/dist/agent/session.d.ts +21 -2
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +73 -11
- package/dist/api/app-page.d.ts +39 -3
- package/dist/api/app-page.d.ts.map +1 -1
- package/dist/api/app-page.js +108 -0
- package/dist/api/dialog.d.ts +38 -0
- package/dist/api/dialog.d.ts.map +1 -0
- package/dist/api/dialog.js +78 -0
- package/dist/api/drag.d.ts +32 -0
- package/dist/api/drag.d.ts.map +1 -0
- package/dist/api/drag.js +26 -0
- package/dist/api/files.d.ts +49 -0
- package/dist/api/files.d.ts.map +1 -0
- package/dist/api/files.js +168 -0
- package/dist/api/inbox.d.ts +97 -0
- package/dist/api/inbox.d.ts.map +1 -0
- package/dist/api/inbox.js +191 -0
- package/dist/api/locator-recipes.d.ts +1 -1
- package/dist/api/locator-recipes.d.ts.map +1 -1
- package/dist/api/locator-recipes.js +4 -3
- package/dist/api/page.d.ts +214 -4
- package/dist/api/page.d.ts.map +1 -1
- package/dist/api/route.d.ts +21 -0
- package/dist/api/route.d.ts.map +1 -0
- package/dist/api/route.js +99 -0
- package/dist/api/test-run.d.ts +127 -1
- package/dist/api/test-run.d.ts.map +1 -1
- package/dist/api/test-run.js +293 -0
- package/dist/assertions/expect.d.ts +91 -2
- package/dist/assertions/expect.d.ts.map +1 -1
- package/dist/assertions/expect.js +98 -0
- package/dist/assertions/look-until.d.ts +5 -0
- package/dist/assertions/look-until.d.ts.map +1 -1
- package/dist/assertions/look-until.js +1 -1
- package/dist/assertions/poll-layout.d.ts +24 -0
- package/dist/assertions/poll-layout.d.ts.map +1 -0
- package/dist/assertions/poll-layout.js +63 -0
- package/dist/assertions/screenshot.d.ts +36 -0
- package/dist/assertions/screenshot.d.ts.map +1 -0
- package/dist/assertions/screenshot.js +125 -0
- package/dist/browser/accessibility.d.ts.map +1 -1
- package/dist/browser/accessibility.js +22 -1
- package/dist/browser/actionability.d.ts +9 -6
- package/dist/browser/actionability.d.ts.map +1 -1
- package/dist/browser/actionability.js +17 -8
- package/dist/browser/browser.d.ts.map +1 -1
- package/dist/browser/browser.js +15 -4
- package/dist/browser/cdp/connection.d.ts +6 -0
- package/dist/browser/cdp/connection.d.ts.map +1 -1
- package/dist/browser/cdp/connection.js +8 -0
- package/dist/browser/checked-state.d.ts +2 -2
- package/dist/browser/checked-state.d.ts.map +1 -1
- package/dist/browser/chromium-process.d.ts +9 -2
- package/dist/browser/chromium-process.d.ts.map +1 -1
- package/dist/browser/chromium-process.js +17 -4
- package/dist/browser/contract.d.ts +119 -5
- package/dist/browser/contract.d.ts.map +1 -1
- package/dist/browser/dialogs.d.ts +157 -0
- package/dist/browser/dialogs.d.ts.map +1 -0
- package/dist/browser/dialogs.js +271 -0
- package/dist/browser/downloads.d.ts +71 -0
- package/dist/browser/downloads.d.ts.map +1 -0
- package/dist/browser/downloads.js +253 -0
- package/dist/browser/drag.d.ts +50 -0
- package/dist/browser/drag.d.ts.map +1 -0
- package/dist/browser/drag.js +584 -0
- package/dist/browser/electron-window-watch.d.ts +32 -0
- package/dist/browser/electron-window-watch.d.ts.map +1 -0
- package/dist/browser/electron-window-watch.js +93 -0
- package/dist/browser/electron.d.ts +13 -2
- package/dist/browser/electron.d.ts.map +1 -1
- package/dist/browser/electron.js +53 -8
- package/dist/browser/element-queries.d.ts +51 -15
- package/dist/browser/element-queries.d.ts.map +1 -1
- package/dist/browser/element-queries.js +92 -13
- package/dist/browser/emulation.d.ts.map +1 -1
- package/dist/browser/emulation.js +7 -5
- package/dist/browser/file-chooser.d.ts +75 -0
- package/dist/browser/file-chooser.d.ts.map +1 -0
- package/dist/browser/file-chooser.js +449 -0
- package/dist/browser/firefox/input.d.ts +5 -0
- package/dist/browser/firefox/input.d.ts.map +1 -1
- package/dist/browser/firefox/input.js +6 -3
- package/dist/browser/firefox/page.d.ts +11 -1
- package/dist/browser/firefox/page.d.ts.map +1 -1
- package/dist/browser/firefox/page.js +66 -7
- package/dist/browser/frames.d.ts +205 -0
- package/dist/browser/frames.d.ts.map +1 -0
- package/dist/browser/frames.js +657 -0
- package/dist/browser/input-guard.d.ts +18 -5
- package/dist/browser/input-guard.d.ts.map +1 -1
- package/dist/browser/input-guard.js +65 -6
- package/dist/browser/isolated-world.d.ts +8 -1
- package/dist/browser/isolated-world.d.ts.map +1 -1
- package/dist/browser/layout.d.ts +120 -0
- package/dist/browser/layout.d.ts.map +1 -0
- package/dist/browser/layout.js +213 -0
- package/dist/browser/locate.d.ts +10 -0
- package/dist/browser/locate.d.ts.map +1 -1
- package/dist/browser/locate.js +24 -6
- package/dist/browser/origin-document.d.ts +4 -0
- package/dist/browser/origin-document.d.ts.map +1 -1
- package/dist/browser/origin-document.js +9 -1
- package/dist/browser/page-scripts.d.ts +48 -0
- package/dist/browser/page-scripts.d.ts.map +1 -1
- package/dist/browser/page-scripts.js +515 -39
- package/dist/browser/page.d.ts +60 -1
- package/dist/browser/page.d.ts.map +1 -1
- package/dist/browser/page.js +275 -32
- package/dist/browser/popups.d.ts +166 -0
- package/dist/browser/popups.d.ts.map +1 -0
- package/dist/browser/popups.js +317 -0
- package/dist/browser/routes.d.ts +97 -0
- package/dist/browser/routes.d.ts.map +1 -0
- package/dist/browser/routes.js +226 -0
- package/dist/browser/screenshot.d.ts +44 -0
- package/dist/browser/screenshot.d.ts.map +1 -0
- package/dist/browser/screenshot.js +51 -0
- package/dist/browser/shadow-candidates.d.ts +20 -0
- package/dist/browser/shadow-candidates.d.ts.map +1 -0
- package/dist/browser/shadow-candidates.js +77 -0
- package/dist/browser/webkit/page.d.ts +11 -1
- package/dist/browser/webkit/page.d.ts.map +1 -1
- package/dist/browser/webkit/page.js +53 -2
- package/dist/browser/webkit/relay.d.ts.map +1 -1
- package/dist/browser/webkit/relay.js +5 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +2 -0
- package/dist/cli/inspect/test-timeline.d.ts.map +1 -1
- package/dist/cli/inspect/test-timeline.js +38 -4
- package/dist/config/loaded.d.ts +12 -2
- package/dist/config/loaded.d.ts.map +1 -1
- package/dist/config/read-inboxes.d.ts +10 -0
- package/dist/config/read-inboxes.d.ts.map +1 -0
- package/dist/config/read-inboxes.js +78 -0
- package/dist/config/register.d.ts +5 -1
- package/dist/config/register.d.ts.map +1 -1
- package/dist/config/types.d.ts +36 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/validate.d.ts.map +1 -1
- package/dist/config/validate.js +4 -1
- package/dist/diagnostics/artifact.d.ts.map +1 -1
- package/dist/diagnostics/artifact.js +10 -2
- package/dist/diagnostics/attempt.d.ts +5 -0
- package/dist/diagnostics/attempt.d.ts.map +1 -1
- package/dist/diagnostics/attempt.js +10 -0
- package/dist/diagnostics/chromium-collector.d.ts +9 -1
- package/dist/diagnostics/chromium-collector.d.ts.map +1 -1
- package/dist/diagnostics/chromium-collector.js +19 -4
- package/dist/diagnostics/observations.d.ts +8 -0
- package/dist/diagnostics/observations.d.ts.map +1 -1
- package/dist/diagnostics/session-capture.d.ts.map +1 -1
- package/dist/diagnostics/session-capture.js +8 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/media/policy.d.ts +2 -2
- package/dist/media/policy.d.ts.map +1 -1
- package/dist/media/policy.js +1 -1
- package/dist/playwright/not-yet.d.ts.map +1 -1
- package/dist/playwright/not-yet.js +5 -2
- package/dist/protocol/commands.d.ts +122 -13
- package/dist/protocol/commands.d.ts.map +1 -1
- package/dist/protocol/commands.js +85 -6
- package/dist/protocol/diagnostics.d.ts +10 -0
- package/dist/protocol/diagnostics.d.ts.map +1 -1
- package/dist/protocol/diagnostics.js +5 -0
- package/dist/protocol/dialogs.d.ts +104 -0
- package/dist/protocol/dialogs.d.ts.map +1 -0
- package/dist/protocol/dialogs.js +101 -0
- package/dist/protocol/drag.d.ts +45 -0
- package/dist/protocol/drag.d.ts.map +1 -0
- package/dist/protocol/drag.js +31 -0
- package/dist/protocol/events.d.ts +153 -1
- package/dist/protocol/events.d.ts.map +1 -1
- package/dist/protocol/events.js +97 -0
- package/dist/protocol/files.d.ts +83 -0
- package/dist/protocol/files.d.ts.map +1 -0
- package/dist/protocol/files.js +68 -0
- package/dist/protocol/inbox.d.ts +87 -0
- package/dist/protocol/inbox.d.ts.map +1 -0
- package/dist/protocol/inbox.js +51 -0
- package/dist/protocol/index.d.ts +4 -1
- package/dist/protocol/index.d.ts.map +1 -1
- package/dist/protocol/layout-checks.d.ts +83 -0
- package/dist/protocol/layout-checks.d.ts.map +1 -0
- package/dist/protocol/layout-checks.js +214 -0
- package/dist/protocol/layout.d.ts +135 -0
- package/dist/protocol/layout.d.ts.map +1 -0
- package/dist/protocol/layout.js +124 -0
- package/dist/protocol/locator-checks.d.ts +19 -2
- package/dist/protocol/locator-checks.d.ts.map +1 -1
- package/dist/protocol/locator-checks.js +23 -1
- package/dist/protocol/locator.d.ts +55 -7
- package/dist/protocol/locator.d.ts.map +1 -1
- package/dist/protocol/locator.js +99 -18
- package/dist/protocol/messages.d.ts +95 -0
- package/dist/protocol/messages.d.ts.map +1 -1
- package/dist/protocol/messages.js +63 -0
- package/dist/protocol/popups.d.ts +43 -0
- package/dist/protocol/popups.d.ts.map +1 -0
- package/dist/protocol/popups.js +27 -0
- package/dist/protocol/result.d.ts +5 -0
- package/dist/protocol/result.d.ts.map +1 -1
- package/dist/protocol/result.js +1 -0
- package/dist/protocol/routes.d.ts +136 -0
- package/dist/protocol/routes.d.ts.map +1 -0
- package/dist/protocol/routes.js +191 -0
- package/dist/protocol/screenshot.d.ts +219 -0
- package/dist/protocol/screenshot.d.ts.map +1 -0
- package/dist/protocol/screenshot.js +335 -0
- package/dist/reporters/actions.d.ts +6 -4
- package/dist/reporters/actions.d.ts.map +1 -1
- package/dist/reporters/actions.js +24 -6
- package/dist/reporters/agent.d.ts.map +1 -1
- package/dist/reporters/agent.js +4 -2
- package/dist/reporters/dialogs-popups.d.ts +22 -0
- package/dist/reporters/dialogs-popups.d.ts.map +1 -0
- package/dist/reporters/dialogs-popups.js +30 -0
- package/dist/reporters/failure-card.d.ts +10 -1
- package/dist/reporters/failure-card.d.ts.map +1 -1
- package/dist/reporters/failure-card.js +5 -1
- package/dist/reporters/format.d.ts +33 -0
- package/dist/reporters/format.d.ts.map +1 -1
- package/dist/reporters/format.js +42 -0
- package/dist/reporters/html/comparison-view.d.ts +12 -0
- package/dist/reporters/html/comparison-view.d.ts.map +1 -0
- package/dist/reporters/html/comparison-view.js +21 -0
- package/dist/reporters/html/diagnostics-view.js +8 -0
- package/dist/reporters/html/downloads-view.d.ts +10 -0
- package/dist/reporters/html/downloads-view.d.ts.map +1 -0
- package/dist/reporters/html/downloads-view.js +29 -0
- package/dist/reporters/html/style.d.ts.map +1 -1
- package/dist/reporters/html/style.js +3 -0
- package/dist/reporters/html/summary-view.d.ts.map +1 -1
- package/dist/reporters/html/summary-view.js +3 -2
- package/dist/reporters/html/test-view.d.ts.map +1 -1
- package/dist/reporters/html/test-view.js +6 -3
- package/dist/reporters/html/timeline-view.d.ts.map +1 -1
- package/dist/reporters/html/timeline-view.js +26 -2
- package/dist/reporters/human-card.js +10 -0
- package/dist/reporters/human.d.ts.map +1 -1
- package/dist/reporters/human.js +3 -1
- package/dist/reporters/routes.d.ts +9 -0
- package/dist/reporters/routes.d.ts.map +1 -0
- package/dist/reporters/routes.js +17 -0
- package/dist/runner/child-program.js +10 -0
- package/dist/runner/contract.d.ts +5 -0
- package/dist/runner/contract.d.ts.map +1 -1
- package/dist/runner/dialogs.d.ts +71 -0
- package/dist/runner/dialogs.d.ts.map +1 -0
- package/dist/runner/dialogs.js +189 -0
- package/dist/runner/files.d.ts +79 -0
- package/dist/runner/files.d.ts.map +1 -0
- package/dist/runner/files.js +312 -0
- package/dist/runner/inbox-links.d.ts +19 -0
- package/dist/runner/inbox-links.d.ts.map +1 -0
- package/dist/runner/inbox-links.js +63 -0
- package/dist/runner/inbox.d.ts +87 -0
- package/dist/runner/inbox.d.ts.map +1 -0
- package/dist/runner/inbox.js +340 -0
- package/dist/runner/index.d.ts +2 -1
- package/dist/runner/index.d.ts.map +1 -1
- package/dist/runner/native-pool.d.ts.map +1 -1
- package/dist/runner/native-pool.js +2 -0
- package/dist/runner/observations.d.ts +15 -2
- package/dist/runner/observations.d.ts.map +1 -1
- package/dist/runner/observations.js +109 -6
- package/dist/runner/popups.d.ts +82 -0
- package/dist/runner/popups.d.ts.map +1 -0
- package/dist/runner/popups.js +236 -0
- package/dist/runner/redactor.d.ts.map +1 -1
- package/dist/runner/redactor.js +10 -6
- package/dist/runner/routes.d.ts +23 -0
- package/dist/runner/routes.d.ts.map +1 -0
- package/dist/runner/routes.js +128 -0
- package/dist/runner/run-media.d.ts +6 -0
- package/dist/runner/run-media.d.ts.map +1 -1
- package/dist/runner/run-media.js +21 -0
- package/dist/runner/run-session.d.ts.map +1 -1
- package/dist/runner/run-session.js +93 -8
- package/dist/runner/running-test.d.ts +35 -0
- package/dist/runner/running-test.d.ts.map +1 -1
- package/dist/runner/running-test.js +333 -22
- package/dist/runner/screenshot-baselines.d.ts +159 -0
- package/dist/runner/screenshot-baselines.d.ts.map +1 -0
- package/dist/runner/screenshot-baselines.js +357 -0
- package/dist/runner/secrets.d.ts +13 -0
- package/dist/runner/secrets.d.ts.map +1 -1
- package/dist/runner/secrets.js +6 -4
- package/dist/runner/test-pages.d.ts +3 -1
- package/dist/runner/test-pages.d.ts.map +1 -1
- package/dist/runner/test-pages.js +4 -3
- package/dist/schemas/event-v1.schema.json +118873 -6311
- package/dist/schemas/result-v1.schema.json +4 -0
- package/dist/shared/image-diff.d.ts +36 -0
- package/dist/shared/image-diff.d.ts.map +1 -0
- package/dist/shared/image-diff.js +242 -0
- package/dist/shared/metadata-process.d.ts +4 -1
- package/dist/shared/metadata-process.d.ts.map +1 -1
- package/dist/shared/metadata-process.js +51 -9
- package/dist/shared/png.d.ts +35 -0
- package/dist/shared/png.d.ts.map +1 -0
- package/dist/shared/png.js +251 -0
- package/dist/shared/process-ownership.d.ts +15 -4
- package/dist/shared/process-ownership.d.ts.map +1 -1
- package/dist/shared/process-ownership.js +123 -34
- package/dist/store/artifacts.d.ts +23 -2
- package/dist/store/artifacts.d.ts.map +1 -1
- package/dist/store/artifacts.js +40 -1
- package/dist/store/rebuild-result.d.ts.map +1 -1
- package/dist/store/rebuild-result.js +6 -0
- package/package.json +1 -1
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
import { unsupportedCheck } from "../native/assertions.js";
|
|
2
2
|
import { describeElement } from "../native/locators.js";
|
|
3
|
-
import { isPageCheck, locatorCheck } from "../protocol/locator-checks.js";
|
|
3
|
+
import { isLayoutCheck, isPageCheck, isScreenshotCheck, locatorCheck } from "../protocol/locator-checks.js";
|
|
4
4
|
import { NativePageAdapter } from "./native-pool.js";
|
|
5
5
|
import { refusedAnswer } from "../evaluation/attempt.js";
|
|
6
|
-
import { describeCommand, isNavigationKind } from "../protocol/commands.js";
|
|
6
|
+
import { commandLocators, describeCommand, isNavigationKind } from "../protocol/commands.js";
|
|
7
7
|
import { Deadline, elapsedMs, monotonicClock, smallestBudget } from "../protocol/deadline.js";
|
|
8
|
+
import { dragProblem } from "../protocol/drag.js";
|
|
8
9
|
import { formatSessionId } from "../protocol/evidence.js";
|
|
9
10
|
import { errorMessage, failure, withAlso, withLocation } from "../protocol/failures.js";
|
|
11
|
+
import { inputFilesProblem } from "../protocol/files.js";
|
|
12
|
+
import { withPickerAdvice } from "../browser/file-chooser.js";
|
|
13
|
+
import { noFramesInApp } from "../browser/frames.js";
|
|
10
14
|
import { parseKey } from "../protocol/keys.js";
|
|
15
|
+
import { codePlaceholderName, linkName } from "../protocol/inbox.js";
|
|
11
16
|
import { formatLine } from "../protocol/location.js";
|
|
12
17
|
import { locatorProblem, locatorSteps } from "../protocol/locator.js";
|
|
13
18
|
import { observedRecord } from "../protocol/observation-record.js";
|
|
14
19
|
import { selectCommandProblem } from "../protocol/option-choices.js";
|
|
20
|
+
import { screenshotRequestProblem } from "../protocol/screenshot.js";
|
|
15
21
|
import { scrollProblem } from "../protocol/scroll-delta.js";
|
|
16
22
|
import { describeExit } from "../shared/process-exit.js";
|
|
17
23
|
import { bounded } from "./bounded.js";
|
|
18
24
|
import { cleanedLook, pageFields, ServedObservations } from "./observations.js";
|
|
19
25
|
import { isOurs } from "./outcome.js";
|
|
20
26
|
import { PageNavigations } from "./page-navigations.js";
|
|
27
|
+
import { TestDialogs, withDialogText } from "./dialogs.js";
|
|
28
|
+
import { TestPopups } from "./popups.js";
|
|
29
|
+
import { TestRoutes } from "./routes.js";
|
|
21
30
|
/** How long a stopped test's process has to answer, and a page that lost its browser has to say so. */
|
|
22
31
|
export const abortGraceMs = 1000;
|
|
32
|
+
// What a test file can do about a file picker nobody waited for, which the picker's failure ends with.
|
|
33
|
+
const testPickerAdvice = 'To give a picker files, wrap the action that opens it in page.fileChooser(), or call setInputFiles() on the file input.';
|
|
34
|
+
const playwrightPickerAdvice = "Retest's Playwright compatibility cannot give a page files yet.";
|
|
23
35
|
// Commands that open a document, which the navigation budget times rather than the action budget.
|
|
24
36
|
const navigationKinds = new Set(['goto', 'reload', 'goBack', 'goForward']);
|
|
25
37
|
// The classes a poller gives a check whose time ran out on a look that fails it.
|
|
@@ -43,6 +55,19 @@ export class RunningTest {
|
|
|
43
55
|
#inFlight = new Map();
|
|
44
56
|
/** The AI checks running, by the id the test process gave each. */
|
|
45
57
|
#evaluating = new Map();
|
|
58
|
+
/** The inbox reads running, by the id the test process gave each, each stopped by its controller. */
|
|
59
|
+
#inboxReads = new Map();
|
|
60
|
+
/** The test's JavaScript dialogs, and the pages its pages opened, which it was given as pages of its own, by app. */
|
|
61
|
+
#dialogs;
|
|
62
|
+
#popups;
|
|
63
|
+
/** The test's changes to its pages' routes, which answer requests with the test's own responses. */
|
|
64
|
+
#routes;
|
|
65
|
+
#popupPages = new Map();
|
|
66
|
+
/** The waits for a file picker or a download running, by the id the test process gave each, and those it let go of. */
|
|
67
|
+
#filesRequests = new Map();
|
|
68
|
+
#releasedRequests = new Set();
|
|
69
|
+
/** The pictures screenshot looks took, by the id the test process gave each look, until the look is served. */
|
|
70
|
+
#pictures = new Map();
|
|
46
71
|
#finished = Promise.withResolvers();
|
|
47
72
|
#deadline;
|
|
48
73
|
#startedAt;
|
|
@@ -80,6 +105,34 @@ export class RunningTest {
|
|
|
80
105
|
constructor(options) {
|
|
81
106
|
this.#options = options;
|
|
82
107
|
this.#observations = new ServedObservations(options.redactor);
|
|
108
|
+
const host = {
|
|
109
|
+
testId: options.testId,
|
|
110
|
+
attemptId: options.attemptId,
|
|
111
|
+
timeouts: options.timeouts,
|
|
112
|
+
page: (app) => options.pages.get(app) ?? this.#popupPages.get(app),
|
|
113
|
+
refusal: () => (this.#report === undefined ? this.#revocation : failure('usage', 'No test is running, so Retest sent nothing to the page.')),
|
|
114
|
+
remainingMs: () => this.#deadline?.remainingMs,
|
|
115
|
+
answerable: () => this.#report === undefined && this.#revocation === undefined,
|
|
116
|
+
commandsSent: () => this.#sent.size > 0,
|
|
117
|
+
stepId: () => this.#stepId,
|
|
118
|
+
send: (message) => options.process.send(message),
|
|
119
|
+
emit: (body) => options.emit(body),
|
|
120
|
+
observe: (problem) => {
|
|
121
|
+
if (this.#report === undefined && this.#revocation === undefined)
|
|
122
|
+
this.#observed.push(problem);
|
|
123
|
+
},
|
|
124
|
+
violation: (problem) => this.#violation(problem),
|
|
125
|
+
redact: (text) => options.redactor?.redact(text) ?? text,
|
|
126
|
+
};
|
|
127
|
+
this.#dialogs = new TestDialogs(host);
|
|
128
|
+
// A popup is one of the test's pages from the moment it opens: its navigations are written, its dialogs heard.
|
|
129
|
+
const adopt = (app, page, follow) => {
|
|
130
|
+
this.#popupPages.set(app, page);
|
|
131
|
+
this.#stopNavigation.push(follow((navigation) => this.#navigated(app, navigation)), this.#dialogs.watch(app, page));
|
|
132
|
+
this.#stopNavigation.push(this.#routes.watch(app, page));
|
|
133
|
+
};
|
|
134
|
+
this.#popups = new TestPopups({ ...host, adopt }, options.popups);
|
|
135
|
+
this.#routes = new TestRoutes(host);
|
|
83
136
|
}
|
|
84
137
|
/** Starts the body in the child and resolves when it is over, however it ends. A process that has ended gets no body. */
|
|
85
138
|
run() {
|
|
@@ -89,6 +142,8 @@ export class RunningTest {
|
|
|
89
142
|
return this.#finished.promise;
|
|
90
143
|
}
|
|
91
144
|
this.#stopNavigation = [...pages].map(([app, page]) => page.onNavigation((navigation) => this.#navigated(app, navigation)));
|
|
145
|
+
this.#stopNavigation.push(...[...pages].map(([app, page]) => this.#dialogs.watch(app, page)));
|
|
146
|
+
this.#stopNavigation.push(...[...pages].map(([app, page]) => this.#routes.watch(app, page)));
|
|
92
147
|
process.listen((event) => this.#receive(event));
|
|
93
148
|
process.send({ type: 'run', testId, attemptId, timeouts, apps: [...pages.keys()], ...(variant === undefined ? {} : { variant }) });
|
|
94
149
|
this.#startedAt = monotonicClock();
|
|
@@ -112,6 +167,12 @@ export class RunningTest {
|
|
|
112
167
|
this.#observed.push(reason);
|
|
113
168
|
clearTimeout(this.#testTimer);
|
|
114
169
|
this.#options.evaluations?.cancel(reason);
|
|
170
|
+
for (const read of this.#inboxReads.values())
|
|
171
|
+
read.abort(reason);
|
|
172
|
+
for (const request of this.#filesRequests.values())
|
|
173
|
+
request.abort(reason);
|
|
174
|
+
this.#dialogs.stop(reason);
|
|
175
|
+
this.#popups.stop(reason);
|
|
115
176
|
for (const page of this.#options.pages.values())
|
|
116
177
|
if (page instanceof NativePageAdapter)
|
|
117
178
|
page.cancel(reason);
|
|
@@ -170,6 +231,8 @@ export class RunningTest {
|
|
|
170
231
|
*/
|
|
171
232
|
async settle(timeoutMs) {
|
|
172
233
|
const deadline = new Deadline(timeoutMs);
|
|
234
|
+
// A dialog the test left open holds its page and the action that opened it, so it is dismissed before anything waits.
|
|
235
|
+
await this.#dialogs.settle(deadline.remainingMs);
|
|
173
236
|
await bounded(Promise.all([...this.#inFlight.values()].map((entry) => entry.done.promise)), timeoutMs);
|
|
174
237
|
await this.#navigations.flush(deadline.remainingMs);
|
|
175
238
|
this.#completeAnswered();
|
|
@@ -197,6 +260,7 @@ export class RunningTest {
|
|
|
197
260
|
stop();
|
|
198
261
|
this.#navigations.writeWaiting();
|
|
199
262
|
this.#options.process.listen(undefined);
|
|
263
|
+
this.#options.files?.close();
|
|
200
264
|
}
|
|
201
265
|
#receive(event) {
|
|
202
266
|
if (event.kind === 'exit')
|
|
@@ -211,6 +275,16 @@ export class RunningTest {
|
|
|
211
275
|
return this.#childEvent(message.event);
|
|
212
276
|
case 'evaluate':
|
|
213
277
|
return this.#evaluate(message);
|
|
278
|
+
case 'inbox':
|
|
279
|
+
return this.#readInbox(message);
|
|
280
|
+
case 'dialog':
|
|
281
|
+
return this.#dialogs.receive(message);
|
|
282
|
+
case 'popup':
|
|
283
|
+
return this.#popups.receive(message);
|
|
284
|
+
case 'files':
|
|
285
|
+
return this.#filesRequest(message);
|
|
286
|
+
case 'route':
|
|
287
|
+
return this.#routes.receive(message);
|
|
214
288
|
case 'test-finished':
|
|
215
289
|
return this.#childFinished(message);
|
|
216
290
|
default:
|
|
@@ -225,10 +299,10 @@ export class RunningTest {
|
|
|
225
299
|
const refusal = this.#report === undefined ? this.#revocation : failure('usage', 'No test is running, so Retest sent nothing to the page.');
|
|
226
300
|
if (refusal !== undefined)
|
|
227
301
|
return this.#send(message.id, { ok: false, failure: withLocation(refusal, message.location) });
|
|
228
|
-
const page = this.#options.pages.get(message.app);
|
|
302
|
+
const page = this.#options.pages.get(message.app) ?? this.#popupPages.get(message.app);
|
|
229
303
|
if (page === undefined)
|
|
230
304
|
return this.#violation(`sent a command for the app ${JSON.stringify(message.app)}, which this test does not use`);
|
|
231
|
-
if (this.#inFlight.has(message.id) || this.#evaluating.has(message.id))
|
|
305
|
+
if (this.#inFlight.has(message.id) || this.#evaluating.has(message.id) || this.#inboxReads.has(message.id))
|
|
232
306
|
return this.#violation(`sent command ${message.id} again while the first was still running`);
|
|
233
307
|
const running = this.#busyWith(message.app, isLook(message.command));
|
|
234
308
|
if (running !== undefined)
|
|
@@ -309,6 +383,10 @@ export class RunningTest {
|
|
|
309
383
|
catch (error) {
|
|
310
384
|
result = thrownResult(command, error);
|
|
311
385
|
}
|
|
386
|
+
// A picker nobody waited for is answered by what this test file can call.
|
|
387
|
+
result = withPickerAdvice(result, this.#options.playwright === true ? playwrightPickerAdvice : testPickerAdvice);
|
|
388
|
+
// A dialog the page answered while the command ran is named in the failure, its text hidden as page text is.
|
|
389
|
+
result = this.#popups.settled(app, withDialogText(result, (text) => this.#options.redactor?.redact(text) ?? text));
|
|
312
390
|
entry.pageAnswer = result.ok ? result : { ok: false, failure: withLocation(result.failure, location) };
|
|
313
391
|
// A timeout spent the command's budget already. Optional titles cannot delay the page's answer further.
|
|
314
392
|
if (!result.ok && result.failure.class === 'timeout')
|
|
@@ -350,7 +428,7 @@ export class RunningTest {
|
|
|
350
428
|
// its own unredacted text.
|
|
351
429
|
async #run({ message, page, stop, commandToken }, timeoutMs) {
|
|
352
430
|
const { command } = message;
|
|
353
|
-
const problem = commandProblem(command) ?? secretLocatorProblem(command, this.#options.redactor) ?? (page instanceof NativePageAdapter ?
|
|
431
|
+
const problem = this.#popups.refusal(message.app, command) ?? commandProblem(command) ?? secretLocatorProblem(command, this.#options.redactor) ?? (page instanceof NativePageAdapter ? noFramesInApp(commandLocators(command)) : namedRowProblem(command));
|
|
354
432
|
if (problem !== undefined) {
|
|
355
433
|
// A look the parent refuses is refused every time, so no check on it can pass: the refusal is the parent's own
|
|
356
434
|
// failure, as an action's is through its event.
|
|
@@ -358,27 +436,63 @@ export class RunningTest {
|
|
|
358
436
|
this.#observed.push(withLocation(problem, message.location));
|
|
359
437
|
return { ok: false, failure: problem };
|
|
360
438
|
}
|
|
439
|
+
if (command.kind === 'close')
|
|
440
|
+
return this.#popups.close(message.app, message.location, () => page.execute(command, timeoutMs, stop.signal, commandToken));
|
|
441
|
+
if (command.kind === 'observeScreenshot')
|
|
442
|
+
return this.#takePicture(message, command, timeoutMs, stop.signal);
|
|
361
443
|
if (command.kind === 'swipe' || command.kind === 'nativeKeyboard' || command.kind === 'nativeAlert')
|
|
362
444
|
return page instanceof NativePageAdapter ? page.execute(command, timeoutMs, stop.signal) : { ok: false, failure: failure('unsupported', `${command.kind} needs a native app session.`) };
|
|
445
|
+
if (command.kind === 'goto') {
|
|
446
|
+
// A link from an inbox opens only once its origin is one it may open on; the page is given the address alone.
|
|
447
|
+
const opened = typeof command.url === 'string' ? { ok: true, url: command.url } : this.#openLink(command.url);
|
|
448
|
+
return opened.ok ? page.execute({ kind: 'goto', url: opened.url }, timeoutMs, stop.signal, commandToken) : { ok: false, failure: opened.failure };
|
|
449
|
+
}
|
|
450
|
+
if (command.kind === 'setInputFiles' || command.kind === 'setChooserFiles')
|
|
451
|
+
return this.#giveFiles(command, page, { timeoutMs, signal: stop.signal, commandToken });
|
|
363
452
|
if (command.kind !== 'fill')
|
|
364
453
|
return page.execute(command, timeoutMs, stop.signal, commandToken);
|
|
365
454
|
const { locator, value } = command;
|
|
366
455
|
if (typeof value === 'string')
|
|
367
456
|
return page.execute({ kind: 'fill', locator, value }, timeoutMs, stop.signal, commandToken);
|
|
368
457
|
const deadline = new Deadline(timeoutMs);
|
|
369
|
-
const { fillSecret } = this.#options;
|
|
370
458
|
// A native app has no address: the bundle id read from its installed app is where the secret would go. A page can be
|
|
371
459
|
// read again, so a fill that finds it on no address yet can wait for its first.
|
|
372
460
|
const destination = page instanceof NativePageAdapter ? { pageUrl: undefined, bundleId: page.bundleId } : { pageUrl: page.url, currentUrl: () => page.url };
|
|
373
461
|
const context = { app: message.app, ...destination, timeoutMs, signal: stop.signal };
|
|
374
|
-
const resolved =
|
|
462
|
+
const resolved = await this.#resolveFill(locator, value, context);
|
|
375
463
|
if (!resolved.ok)
|
|
376
464
|
return { ok: false, failure: resolved.failure };
|
|
377
|
-
const ended = this.#options.secretEntry?.(message.app, value.secret);
|
|
465
|
+
const ended = this.#options.secretEntry?.(message.app, 'secret' in value ? value.secret : codePlaceholderName);
|
|
378
466
|
const result = await page.execute(resolved.command, deadline.commandTimeoutMs, stop.signal, commandToken);
|
|
379
467
|
ended?.(result.ok ? 'sent' : result.failure.details?.['inputSent'] === false ? 'not_sent' : 'unknown');
|
|
380
468
|
return result;
|
|
381
469
|
}
|
|
470
|
+
// A screenshot look's picture stays in this process: the page takes it, it is kept until the look is served, and the
|
|
471
|
+
// test process is told only its size and how it stands against its baseline. A look the attempt's pictures refuse is
|
|
472
|
+
// refused every time, so no check on it can pass: the refusal is the parent's own failure.
|
|
473
|
+
async #takePicture(message, command, timeoutMs, signal) {
|
|
474
|
+
const { screenshots } = this.#options;
|
|
475
|
+
const taken = screenshots === undefined ? { ok: false, refused: true, failure: failure('unsupported', 'This run takes no pictures for screenshot checks.') } : await screenshots.take(message.app, command, timeoutMs, signal);
|
|
476
|
+
if (!taken.ok) {
|
|
477
|
+
if (taken.refused === true && this.#revocation === undefined)
|
|
478
|
+
this.#observed.push(withLocation(taken.failure, message.location));
|
|
479
|
+
return { ok: false, failure: taken.failure };
|
|
480
|
+
}
|
|
481
|
+
if (taken.picture !== undefined)
|
|
482
|
+
this.#pictures.set(message.id, taken.picture);
|
|
483
|
+
return { ok: true, kind: 'observeScreenshot', observation: taken.look, ...(taken.page === undefined ? {} : { page: taken.page }) };
|
|
484
|
+
}
|
|
485
|
+
// A secret is read by the run's secrets, and a code by the run's inboxes; either says why it may not be typed.
|
|
486
|
+
#resolveFill(locator, value, context) {
|
|
487
|
+
const { fillSecret, inbox } = this.#options;
|
|
488
|
+
if ('secret' in value)
|
|
489
|
+
return fillSecret === undefined ? Promise.resolve(noSecrets(value.secret)) : fillSecret({ kind: 'fill', locator, value }, context);
|
|
490
|
+
return inbox === undefined ? Promise.resolve({ ok: false, failure: noInboxes(value.inbox) }) : inbox.fillCode({ kind: 'fill', locator, value }, context);
|
|
491
|
+
}
|
|
492
|
+
#openLink(reference) {
|
|
493
|
+
const { inbox } = this.#options;
|
|
494
|
+
return inbox === undefined ? { ok: false, failure: noInboxes(reference.inbox) } : inbox.openLink(reference);
|
|
495
|
+
}
|
|
382
496
|
// An action is reported once: by the page's answer, or as unknown when the page gave none in time.
|
|
383
497
|
#reportAction(entry, result, page) {
|
|
384
498
|
const { command, callTimeoutMs } = entry.message;
|
|
@@ -438,6 +552,10 @@ export class RunningTest {
|
|
|
438
552
|
const { command, app, stepId } = entry.message;
|
|
439
553
|
if (result.ok && result.kind === 'observePage' && command.kind === 'observePage')
|
|
440
554
|
return this.#servePage(app, result);
|
|
555
|
+
if (result.ok && result.kind === 'observeLayout' && command.kind === 'observeLayout')
|
|
556
|
+
return this.#serveLayout(entry, command, result);
|
|
557
|
+
if (result.ok && result.kind === 'observeScreenshot' && command.kind === 'observeScreenshot')
|
|
558
|
+
return this.#servePicture(entry, command, result);
|
|
441
559
|
if (!result.ok || result.kind !== 'observe' || command.kind !== 'observe')
|
|
442
560
|
return result;
|
|
443
561
|
const { locator } = command;
|
|
@@ -464,6 +582,32 @@ export class RunningTest {
|
|
|
464
582
|
const observationId = this.#observations.serve({ app, sessionId, page: baseUrl === undefined ? observation : { ...observation, baseUrl } });
|
|
465
583
|
return { ...result, observation, observationId, sessionId };
|
|
466
584
|
}
|
|
585
|
+
// A layout look is kept for the assertion that names it, which records what it measured, with the elements and the
|
|
586
|
+
// words the look was asked to measure, so the parent can tell a look made for one check from a look made for another.
|
|
587
|
+
// Like a look at the page, it has no event of its own, since an observation event lists a locator's matches.
|
|
588
|
+
#serveLayout(entry, command, result) {
|
|
589
|
+
const { app } = entry.message;
|
|
590
|
+
const sessionId = formatSessionId(this.#options.attemptId, app);
|
|
591
|
+
const page = commandPage(entry, result, this.#documents.get(app));
|
|
592
|
+
const { locator, other, words } = command;
|
|
593
|
+
const request = { ...(locator === undefined ? {} : { locator }), ...(other === undefined ? {} : { other }), ...(words === undefined ? {} : { words }) };
|
|
594
|
+
const observationId = this.#observations.serve({ app, sessionId, request, layout: result.observation, ...page });
|
|
595
|
+
return { ...result, observationId, sessionId };
|
|
596
|
+
}
|
|
597
|
+
// A screenshot look is kept for the assertion that names it, as a layout look is, with the element, the name and the
|
|
598
|
+
// threshold it was taken for; its picture stays with the attempt's pictures. It has no event of its own.
|
|
599
|
+
#servePicture(entry, command, result) {
|
|
600
|
+
const { app, id } = entry.message;
|
|
601
|
+
const sessionId = formatSessionId(this.#options.attemptId, app);
|
|
602
|
+
const page = commandPage(entry, result, this.#documents.get(app));
|
|
603
|
+
const { locator, name, threshold } = command;
|
|
604
|
+
const request = { ...(locator === undefined ? {} : { locator }), name, ...(threshold === undefined ? {} : { threshold }) };
|
|
605
|
+
const observationId = this.#observations.serve({ app, sessionId, picture: request, screenshot: result.observation, ...page });
|
|
606
|
+
const picture = this.#pictures.get(id);
|
|
607
|
+
this.#pictures.delete(id);
|
|
608
|
+
this.#options.screenshots?.keep({ observationId, app, command, look: result.observation, picture });
|
|
609
|
+
return { ...result, observationId, sessionId };
|
|
610
|
+
}
|
|
467
611
|
// The page's own answer when it gave one. An action it never answered may have taken effect.
|
|
468
612
|
#lossFailure(reason) {
|
|
469
613
|
const unanswered = [...this.#inFlight.values()].find((entry) => !entry.message.command.kind.startsWith('observe'));
|
|
@@ -488,7 +632,7 @@ export class RunningTest {
|
|
|
488
632
|
return this.#options.emit(event, 'child');
|
|
489
633
|
// An assertion that names no app looks at the test's first one, as milestone 1's single page. One that names an app
|
|
490
634
|
// the test does not have names a session that never existed.
|
|
491
|
-
if (event.session !== undefined && !this.#options.pages.has(event.session)) {
|
|
635
|
+
if (event.session !== undefined && !this.#options.pages.has(event.session) && !this.#popupPages.has(event.session)) {
|
|
492
636
|
return this.#violation(`sent ${event.type} for the app ${JSON.stringify(event.session)}, which this test does not use`);
|
|
493
637
|
}
|
|
494
638
|
const [firstApp] = this.#options.pages.keys();
|
|
@@ -499,7 +643,7 @@ export class RunningTest {
|
|
|
499
643
|
// A native look is judged on the tree the parent read, which can also show the reference stale or the check one the
|
|
500
644
|
// platform cannot make; the parent's failure then stands in for any verdict the process sent, a pass included.
|
|
501
645
|
let nativeFailure;
|
|
502
|
-
if (native !== undefined && event.check !== undefined && !isPageCheck(event.check) && event.locator !== undefined) {
|
|
646
|
+
if (native !== undefined && event.check !== undefined && !isPageCheck(event.check) && !isLayoutCheck(event.check) && !isScreenshotCheck(event.check) && event.locator !== undefined) {
|
|
503
647
|
const rule = locatorCheck(event.check);
|
|
504
648
|
const unsupported = native.page.native.checkReference(native.look.reference) ?? unsupportedCheck(event.check, native.look.matches, event.locator, native.look.tree.platform);
|
|
505
649
|
if (unsupported !== undefined || !rule.passes(native.look.observation)) {
|
|
@@ -509,10 +653,12 @@ export class RunningTest {
|
|
|
509
653
|
assertion = event.type === 'assertion.failed' ? event : { ...event, type: 'assertion.failed', failure: nativeFailure };
|
|
510
654
|
}
|
|
511
655
|
}
|
|
512
|
-
|
|
656
|
+
// What a picture showed and the files a comparison left are the parent's to write, never the process's to claim.
|
|
657
|
+
const { screenshot: _claimedScreenshot, attachments: _claimedAttachments, ...sent } = assertion;
|
|
658
|
+
const judged = this.#observations.judge(sent, { app, ...looked });
|
|
513
659
|
if (!judged.ok)
|
|
514
660
|
return this.#violation(judged.problem);
|
|
515
|
-
const { event: recorded, origin, seen } = this.#parentVerdict(judged.event, { app, check: event.check, nativeFailure });
|
|
661
|
+
const { event: recorded, origin, seen } = this.#settlePicture(this.#parentVerdict(judged.event, { app, check: event.check, nativeFailure }), event.check);
|
|
516
662
|
// A class the parent did not see, and that would make the test ours rather than the app's, stays the process's claim.
|
|
517
663
|
if (recorded.type === 'assertion.failed' && (seen || !isOurs(recorded.failure)))
|
|
518
664
|
this.#observed.push(recorded.failure);
|
|
@@ -542,6 +688,26 @@ export class RunningTest {
|
|
|
542
688
|
return { event, origin: 'child', seen: true };
|
|
543
689
|
return { event: { ...event, failure: withAlso(withLocation(own, event.location), [event.failure]) }, origin: 'parent', seen: true };
|
|
544
690
|
}
|
|
691
|
+
// A judged screenshot assertion leaves its pictures in the run folder when it failed, or writes its baseline when it
|
|
692
|
+
// passed in a run that updates them. A baseline the run could not write fails the assertion with the parent's own
|
|
693
|
+
// failure, which leads whatever the process said.
|
|
694
|
+
#settlePicture(verdict, check) {
|
|
695
|
+
const { event } = verdict;
|
|
696
|
+
const { screenshots } = this.#options;
|
|
697
|
+
if (screenshots === undefined || check === undefined || !isScreenshotCheck(check) || event.observationId === undefined)
|
|
698
|
+
return verdict;
|
|
699
|
+
const step = event.stepId === undefined ? {} : { stepId: event.stepId };
|
|
700
|
+
const place = event.location === undefined ? {} : { location: event.location };
|
|
701
|
+
const settled = screenshots.settle({ passed: event.type === 'assertion.passed', check, observationId: event.observationId, ...step, ...place });
|
|
702
|
+
const attached = settled.attachments === undefined ? event : { ...event, attachments: settled.attachments };
|
|
703
|
+
if (settled.failure === undefined)
|
|
704
|
+
return { ...verdict, event: attached };
|
|
705
|
+
const failed = withLocation(settled.failure, event.location);
|
|
706
|
+
if (attached.type === 'assertion.failed')
|
|
707
|
+
return { event: { ...attached, failure: withAlso(failed, [attached.failure]) }, origin: 'parent', seen: true };
|
|
708
|
+
const { judgedBy: _judgedBy, type: _type, ...fields } = attached;
|
|
709
|
+
return { event: { ...fields, type: 'assertion.failed', failure: failed }, origin: 'parent', seen: true };
|
|
710
|
+
}
|
|
545
711
|
// A check that failed because the browser was lost while it looked is the parent's own record, since the parent saw
|
|
546
712
|
// the loss: the loss leads, at the check's place, and anything else the process says of it follows. Its matcher, its
|
|
547
713
|
// expected text and the look it rests on are what the parent judged, as for a check whose look answered.
|
|
@@ -594,6 +760,123 @@ export class RunningTest {
|
|
|
594
760
|
})
|
|
595
761
|
.finally(() => this.#evaluating.delete(message.id));
|
|
596
762
|
}
|
|
763
|
+
// The parent reads the mailbox itself; the test file's process only names the inbox and the subject, and its own time
|
|
764
|
+
// bounds the read, cut to what the test has left. A read touches no page, so no app is held. Every read the parent
|
|
765
|
+
// answers is written down, the one stopped with its test too. A read that failed is the parent's own failure, which
|
|
766
|
+
// fails the test whatever the process claims, unless the test was stopped already.
|
|
767
|
+
#readInbox(message) {
|
|
768
|
+
if (!this.#isOwn(message))
|
|
769
|
+
return this.#violation('sent an inbox read for an inactive test attempt');
|
|
770
|
+
if (this.#inboxReads.has(message.id) || this.#inFlight.has(message.id) || this.#evaluating.has(message.id))
|
|
771
|
+
return this.#violation(`sent inbox read ${message.id} again while the first was still running`);
|
|
772
|
+
if (this.#deadline?.reached === true)
|
|
773
|
+
this.#runOutOfTime();
|
|
774
|
+
const { inbox, process } = this.#options;
|
|
775
|
+
const refusal = this.#report === undefined ? this.#revocation : failure('usage', 'No test is running, so Retest read no inbox.');
|
|
776
|
+
if (refusal !== undefined)
|
|
777
|
+
return process.send({ type: 'inbox-result', id: message.id, answer: { ok: false, failure: withLocation(refusal, message.location) } });
|
|
778
|
+
if (inbox === undefined) {
|
|
779
|
+
const answer = { ok: false, failure: withLocation(noInboxes(message.request.inbox), message.location) };
|
|
780
|
+
this.#observed.push(answer.failure);
|
|
781
|
+
this.#writeInboxRead(message, { answer, messages: 0, looks: 0, waitedMs: 0 });
|
|
782
|
+
return process.send({ type: 'inbox-result', id: message.id, answer });
|
|
783
|
+
}
|
|
784
|
+
const stop = new AbortController();
|
|
785
|
+
this.#inboxReads.set(message.id, stop);
|
|
786
|
+
const timeoutMs = this.#deadline === undefined ? message.timeoutMs : smallestBudget(message.timeoutMs, this.#deadline.remainingMs);
|
|
787
|
+
const broken = (error) => {
|
|
788
|
+
const problem = failure('setup_failed', `Retest could not read the inbox ${JSON.stringify(message.request.inbox)}: ${this.#options.redactor?.redact(errorMessage(error)) ?? errorMessage(error)}`);
|
|
789
|
+
return { answer: { ok: false, failure: problem }, messages: 0, looks: 0, waitedMs: 0 };
|
|
790
|
+
};
|
|
791
|
+
void inbox.read(message.request, { timeoutMs, signal: stop.signal }).then(undefined, broken).then((read) => {
|
|
792
|
+
this.#inboxReads.delete(message.id);
|
|
793
|
+
const answer = read.answer.ok ? read.answer : { ok: false, failure: withLocation(read.answer.failure, message.location) };
|
|
794
|
+
if (!answer.ok && this.#revocation === undefined && this.#report === undefined)
|
|
795
|
+
this.#observed.push(answer.failure);
|
|
796
|
+
this.#writeInboxRead(message, { ...read, answer });
|
|
797
|
+
if (this.#report === undefined && this.#revocation === undefined)
|
|
798
|
+
process.send({ type: 'inbox-result', id: message.id, answer });
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
#writeInboxRead({ request, stepId, location }, { answer, messages, looks, waitedMs }) {
|
|
802
|
+
const { testId, attemptId } = this.#options;
|
|
803
|
+
const { subject } = request;
|
|
804
|
+
const wanted = subject === undefined ? {} : { wanted: typeof subject === 'string' ? subject : `/${subject.pattern}/${subject.flags}` };
|
|
805
|
+
const found = answer.ok
|
|
806
|
+
? { subject: answer.message.subject, receivedAt: answer.message.receivedAt, codes: answer.message.codes, links: answer.message.links.length }
|
|
807
|
+
: { failure: answer.failure };
|
|
808
|
+
const step = stepId === undefined ? {} : { stepId };
|
|
809
|
+
const place = location === undefined ? {} : { location };
|
|
810
|
+
this.#options.emit({ type: 'inbox.read', testId, attemptId, ...step, inbox: request.inbox, messages, looks, waitedMs, ...wanted, ...found, ...place });
|
|
811
|
+
}
|
|
812
|
+
// A file input takes paths on this machine: the parent checks each path the test gave, and writes each file given by
|
|
813
|
+
// content to the run's folder, before the page sees the command. A native app takes no files, and nothing is written.
|
|
814
|
+
#giveFiles(command, page, sending) {
|
|
815
|
+
const method = command.kind === 'setInputFiles' ? 'setInputFiles' : 'setFiles';
|
|
816
|
+
const refused = (problem) => Promise.resolve({ ok: false, failure: problem });
|
|
817
|
+
const problem = inputFilesProblem(command.files, method);
|
|
818
|
+
if (problem !== undefined)
|
|
819
|
+
return refused(problem);
|
|
820
|
+
if (page instanceof NativePageAdapter)
|
|
821
|
+
return refused(failure('unsupported', `${method}() gives files to a web page's file input, and a native app has none, so Retest gave none.`));
|
|
822
|
+
const { files } = this.#options;
|
|
823
|
+
if (files === undefined)
|
|
824
|
+
return refused(failure('unsupported', `${method}() needs a run that keeps files, and this one keeps none, so Retest gave none.`));
|
|
825
|
+
const staged = files.stage(command.files, method);
|
|
826
|
+
if (!staged.ok)
|
|
827
|
+
return refused(staged.failure);
|
|
828
|
+
const resolved = { ...command, paths: staged.paths, mimeTypes: staged.mimeTypes };
|
|
829
|
+
return page.execute(resolved, sending.timeoutMs, sending.signal, sending.commandToken);
|
|
830
|
+
}
|
|
831
|
+
// The parent arms the page and waits itself; the test file's process only names the app or the download, and its own
|
|
832
|
+
// time bounds the wait, cut to what the test has left. A wait holds no app: the action it waits on runs as an ordinary
|
|
833
|
+
// command. One that failed is the parent's own failure, which fails the test whatever the process claims, unless the
|
|
834
|
+
// test was stopped already or the process let go of it because its action failed.
|
|
835
|
+
#filesRequest(message) {
|
|
836
|
+
if (!this.#isOwn(message))
|
|
837
|
+
return this.#violation('sent a request about files for an inactive test attempt');
|
|
838
|
+
const { request, id, location } = message;
|
|
839
|
+
if (request.kind === 'release') {
|
|
840
|
+
// Only a wait still running is let go of; one already answered has nothing left to stop.
|
|
841
|
+
const waiting = this.#filesRequests.get(request.request);
|
|
842
|
+
if (waiting === undefined)
|
|
843
|
+
return;
|
|
844
|
+
this.#releasedRequests.add(request.request);
|
|
845
|
+
waiting.abort(failure('interrupted', 'The action the test waited on failed, so Retest stopped waiting.'));
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (this.#filesRequests.has(id) || this.#inFlight.has(id) || this.#evaluating.has(id) || this.#inboxReads.has(id))
|
|
849
|
+
return this.#violation(`sent request ${id} again while the first was still running`);
|
|
850
|
+
if (request.kind !== 'saved' && !this.#options.pages.has(request.app))
|
|
851
|
+
return this.#violation(`sent a request for the app ${JSON.stringify(request.app)}, which this test does not use`);
|
|
852
|
+
if (this.#deadline?.reached === true)
|
|
853
|
+
this.#runOutOfTime();
|
|
854
|
+
const { files, process } = this.#options;
|
|
855
|
+
const answer = (sent) => process.send({ type: 'files-result', id, answer: sent.ok ? sent : { ok: false, failure: this.#options.redactor?.redactFailure(sent.failure) ?? sent.failure } });
|
|
856
|
+
const refusal = this.#report === undefined ? this.#revocation : failure('usage', 'No test is running, so Retest waited for nothing.');
|
|
857
|
+
if (refusal !== undefined)
|
|
858
|
+
return answer({ ok: false, failure: withLocation(refusal, location) });
|
|
859
|
+
if (files === undefined) {
|
|
860
|
+
const unkept = withLocation(failure('unsupported', 'This run keeps no files, so Retest waited for nothing.'), location);
|
|
861
|
+
this.#observed.push(unkept);
|
|
862
|
+
return answer({ ok: false, failure: unkept });
|
|
863
|
+
}
|
|
864
|
+
const stop = new AbortController();
|
|
865
|
+
this.#filesRequests.set(id, stop);
|
|
866
|
+
const timeoutMs = this.#deadline === undefined ? message.timeoutMs : smallestBudget(message.timeoutMs, this.#deadline.remainingMs);
|
|
867
|
+
const budget = { timeoutMs, signal: stop.signal, stepId: message.stepId, location };
|
|
868
|
+
const waiting = request.kind === 'chooser' ? files.chooser(request.app, budget) : request.kind === 'download' ? files.download(request.app, budget) : files.saved(request.download, budget);
|
|
869
|
+
const broken = (error) => ({ ok: false, failure: failure('session_lost', `The browser call failed: ${errorMessage(error)}`) });
|
|
870
|
+
void waiting.then(undefined, broken).then((settled) => {
|
|
871
|
+
this.#filesRequests.delete(id);
|
|
872
|
+
const placed = settled.ok ? settled : { ok: false, failure: withLocation(settled.failure, location) };
|
|
873
|
+
const ignored = this.#releasedRequests.delete(id);
|
|
874
|
+
if (!placed.ok && !ignored && this.#revocation === undefined && this.#report === undefined)
|
|
875
|
+
this.#observed.push(placed.failure);
|
|
876
|
+
if (this.#report === undefined && this.#revocation === undefined && !ignored)
|
|
877
|
+
answer(placed);
|
|
878
|
+
});
|
|
879
|
+
}
|
|
597
880
|
#childFinished(message) {
|
|
598
881
|
if (!this.#isOwn(message))
|
|
599
882
|
return this.#violation('finished an inactive test attempt');
|
|
@@ -680,6 +963,13 @@ export class RunningTest {
|
|
|
680
963
|
this.#stepId = undefined;
|
|
681
964
|
clearTimeout(this.#testTimer);
|
|
682
965
|
this.#options.evaluations?.cancel(failure('evaluation_error', 'The test ended before the answer came.'));
|
|
966
|
+
for (const read of this.#inboxReads.values())
|
|
967
|
+
read.abort(failure('interrupted', 'The test ended before the inbox answered.'));
|
|
968
|
+
for (const request of this.#filesRequests.values())
|
|
969
|
+
request.abort(failure('interrupted', 'The test ended before the page opened a file picker or a download.'));
|
|
970
|
+
this.#dialogs.stop(failure('interrupted', 'The test ended before the page opened a dialog.'));
|
|
971
|
+
this.#popups.stop(failure('interrupted', 'The test ended before the page opened another page.'));
|
|
972
|
+
this.#releasedRequests.clear();
|
|
683
973
|
if (this.#timedOut)
|
|
684
974
|
this.#options.process.send({ type: 'close' });
|
|
685
975
|
else
|
|
@@ -715,7 +1005,7 @@ function openedDocument(result) {
|
|
|
715
1005
|
}
|
|
716
1006
|
// What the test process's own checks refuse, which the parent refuses again before the page sees the command.
|
|
717
1007
|
function commandProblem(command) {
|
|
718
|
-
const located =
|
|
1008
|
+
const located = commandLocators(command).map(locatorProblem).find((problem) => problem !== undefined);
|
|
719
1009
|
if (located !== undefined)
|
|
720
1010
|
return located;
|
|
721
1011
|
switch (command.kind) {
|
|
@@ -727,6 +1017,13 @@ function commandProblem(command) {
|
|
|
727
1017
|
return selectCommandProblem(command);
|
|
728
1018
|
case 'scroll':
|
|
729
1019
|
return scrollProblem(command);
|
|
1020
|
+
case 'dragTo':
|
|
1021
|
+
return dragProblem(command);
|
|
1022
|
+
case 'observeScreenshot': {
|
|
1023
|
+
// A name is a file name and a threshold a distance, so the parent refuses either before it reads any file.
|
|
1024
|
+
const problem = screenshotRequestProblem(command);
|
|
1025
|
+
return problem === undefined ? undefined : failure('usage', problem);
|
|
1026
|
+
}
|
|
730
1027
|
default:
|
|
731
1028
|
return undefined;
|
|
732
1029
|
}
|
|
@@ -738,9 +1035,9 @@ function commandProblem(command) {
|
|
|
738
1035
|
* Part of a value is not refused, since refusing it would itself say which texts are part of a secret.
|
|
739
1036
|
*/
|
|
740
1037
|
function secretLocatorProblem(command, redactor) {
|
|
741
|
-
if (redactor === undefined
|
|
1038
|
+
if (redactor === undefined)
|
|
742
1039
|
return undefined;
|
|
743
|
-
const held =
|
|
1040
|
+
const held = commandLocators(command).flatMap(matchedTexts).find((matched) => redactor.holdsValue(matched.text, matched.exact));
|
|
744
1041
|
if (held === undefined)
|
|
745
1042
|
return undefined;
|
|
746
1043
|
const message = `Retest did not send this command to the page: its locator's ${held.named} holds the value of a secret. Find the element by its test id, or by text that holds no secret.`;
|
|
@@ -753,9 +1050,7 @@ function secretLocatorProblem(command, redactor) {
|
|
|
753
1050
|
* every action and every check before the page is asked. A native app's tree names its rows, so it is not refused there.
|
|
754
1051
|
*/
|
|
755
1052
|
function namedRowProblem(command) {
|
|
756
|
-
|
|
757
|
-
return undefined;
|
|
758
|
-
const named = locatorSteps(command.locator).some((step) => step.by === 'role' && step.role === 'row' && step.name !== undefined);
|
|
1053
|
+
const named = commandLocators(command).some((locator) => locatorSteps(locator).some((step) => step.by === 'role' && step.role === 'row' && step.name !== undefined));
|
|
759
1054
|
if (!named)
|
|
760
1055
|
return undefined;
|
|
761
1056
|
const message = "getByRole('row', { name }) finds no row on a web page: the browser's accessibility tree gives a table row no name from its cells, so the lookup would match nothing and a check that the row is hidden would pass while it is on screen. Find the row by its test id, or find the cell by its own text, such as getByText('Grace Hopper') or getByRole('cell', { name: 'Grace Hopper' }).";
|
|
@@ -794,6 +1089,8 @@ function actionDetails(command, result, touch) {
|
|
|
794
1089
|
switch (command.kind) {
|
|
795
1090
|
case 'fill':
|
|
796
1091
|
return typedValue(command.value);
|
|
1092
|
+
case 'goto':
|
|
1093
|
+
return typeof command.url === 'string' ? {} : { secret: linkName(command.url.default === true ? undefined : command.url.link), inbox: command.url.inbox };
|
|
797
1094
|
case 'press':
|
|
798
1095
|
return { key: command.key };
|
|
799
1096
|
case 'select':
|
|
@@ -809,6 +1106,15 @@ function actionDetails(command, result, touch) {
|
|
|
809
1106
|
return { operation: command.operation };
|
|
810
1107
|
case 'nativeAlert':
|
|
811
1108
|
return { operation: command.operation, ...(command.button === undefined ? {} : { button: command.button }) };
|
|
1109
|
+
case 'setInputFiles':
|
|
1110
|
+
case 'setChooserFiles':
|
|
1111
|
+
if (!result.ok || !('files' in result) || result.files === undefined)
|
|
1112
|
+
return {};
|
|
1113
|
+
return result.kept === undefined ? { files: result.files } : { files: result.files, kept: result.kept };
|
|
1114
|
+
case 'dragTo': {
|
|
1115
|
+
const through = result.ok && result.kind === 'dragTo' ? { through: result.through } : {};
|
|
1116
|
+
return { drag: { target: command.target, ...(command.steps === undefined ? {} : { steps: command.steps }), ...through } };
|
|
1117
|
+
}
|
|
812
1118
|
default:
|
|
813
1119
|
return {};
|
|
814
1120
|
}
|
|
@@ -818,16 +1124,21 @@ function changedOf(result) {
|
|
|
818
1124
|
}
|
|
819
1125
|
// The page's answer says what it did; a failed click on a touch screen was a tap too.
|
|
820
1126
|
function recordedKind(sent, result, touch) {
|
|
821
|
-
if (result.ok && result.kind !== 'observe' && result.kind !== 'observePage')
|
|
1127
|
+
if (result.ok && result.kind !== 'observe' && result.kind !== 'observePage' && result.kind !== 'observeLayout' && result.kind !== 'observeScreenshot')
|
|
822
1128
|
return result.kind;
|
|
823
1129
|
return sent === 'click' && touch ? 'tap' : sent;
|
|
824
1130
|
}
|
|
825
1131
|
function noSecrets(name) {
|
|
826
1132
|
return { ok: false, failure: failure('usage', `secret(${JSON.stringify(name)}) needs a config that declares it. This run has no secrets.`) };
|
|
827
1133
|
}
|
|
828
|
-
// A fill records how much it typed, or which secret, and never the text itself.
|
|
1134
|
+
// A fill records how much it typed, or which secret, or that it typed an inbox's code, and never the text itself.
|
|
829
1135
|
function typedValue(value) {
|
|
830
|
-
|
|
1136
|
+
if (typeof value === 'string')
|
|
1137
|
+
return { valueLength: value.length };
|
|
1138
|
+
return 'secret' in value ? { secret: value.secret } : { secret: codePlaceholderName, inbox: value.inbox };
|
|
1139
|
+
}
|
|
1140
|
+
function noInboxes(name) {
|
|
1141
|
+
return failure('usage', `inbox(${JSON.stringify(name)}) needs a config that declares it. This run has no inboxes.`);
|
|
831
1142
|
}
|
|
832
1143
|
function thrownResult(command, error) {
|
|
833
1144
|
const message = `The browser call for ${describeCommand(command)} failed: ${errorMessage(error)}`;
|
|
@@ -850,7 +1161,7 @@ function concurrentFailure(asked, running, location, outcome) {
|
|
|
850
1161
|
}
|
|
851
1162
|
// A look reads its app's page and changes nothing there; every other command is an action.
|
|
852
1163
|
function isLook(command) {
|
|
853
|
-
return command.kind === 'observe' || command.kind === 'observePage';
|
|
1164
|
+
return command.kind === 'observe' || command.kind === 'observePage' || command.kind === 'observeLayout' || command.kind === 'observeScreenshot';
|
|
854
1165
|
}
|
|
855
1166
|
function lineOrNull(location) {
|
|
856
1167
|
return location === undefined ? null : formatLine(location);
|