@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
|
@@ -6,9 +6,11 @@ import { isThenable } from "../api/operation.js";
|
|
|
6
6
|
import { Secret } from "../api/secret.js";
|
|
7
7
|
import { describeLocator, textPatternOf } from "../protocol/locator.js";
|
|
8
8
|
import { maxTimeout } from "../protocol/timeouts.js";
|
|
9
|
+
import { pollLayout } from "./poll-layout.js";
|
|
9
10
|
import { pollLocator } from "./poll-locator.js";
|
|
10
11
|
import { pollPage } from "./poll-page.js";
|
|
11
12
|
import { pollValue } from "./poll-value.js";
|
|
13
|
+
import { pollScreenshot, readScreenshotCall } from "./screenshot.js";
|
|
12
14
|
import { assertValue } from "./value.js";
|
|
13
15
|
import { containCheck, equalCheck, matchCheck, sameCheck } from "./value-checks.js";
|
|
14
16
|
// The type checker and JavaScript callers get the same sentence for a matcher used on the wrong kind of thing.
|
|
@@ -29,6 +31,14 @@ const misplaced = {
|
|
|
29
31
|
toHaveValue: 'toHaveValue is for locators. Use toBe on a value.',
|
|
30
32
|
toHaveURL: 'toHaveURL is for a page. Use toBe on a value.',
|
|
31
33
|
toHaveTitle: 'toHaveTitle is for a page. Use toBe on a value.',
|
|
34
|
+
toOverlap: 'toOverlap is for locators. Use toBe on a value.',
|
|
35
|
+
toOverflowViewport: 'toOverflowViewport is for locators. Use toBe on a value.',
|
|
36
|
+
toHaveClippedText: 'toHaveClippedText is for locators. Use toBe on a value.',
|
|
37
|
+
toScrollHorizontally: 'toScrollHorizontally is for a page. Use toBe on a value.',
|
|
38
|
+
layout: 'Layout matchers measure a web page. A native app has no page layout to measure.',
|
|
39
|
+
toHaveScreenshot: 'toHaveScreenshot is for locators and pages. Use toEqual on a value.',
|
|
40
|
+
screenshotNot: 'toHaveScreenshot has no .not. Save a baseline of the picture you expect instead.',
|
|
41
|
+
screenshotNative: 'toHaveScreenshot compares web pages. Retest compares no picture of a native app yet.',
|
|
32
42
|
not: '.not is for locator and page matchers. Write the value matcher that says what you expect.',
|
|
33
43
|
twice: '.not is written once.',
|
|
34
44
|
};
|
|
@@ -121,6 +131,40 @@ class LocatorExpectation {
|
|
|
121
131
|
return this.#assert({ matcher: 'toHaveValue', pattern: textPatternOf(value) }, options);
|
|
122
132
|
throw misuse(`toHaveValue() takes the expected value as a string or a RegExp, received ${formatValue(value)}.`, this.#scope.run);
|
|
123
133
|
}
|
|
134
|
+
toOverlap(other, options) {
|
|
135
|
+
const { run } = this.#scope;
|
|
136
|
+
const found = locatorTarget(other);
|
|
137
|
+
if (found === undefined)
|
|
138
|
+
throw misuse(`toOverlap() takes the other element's locator, such as page.getByTestId('help'), received ${formatValue(other)}.`, run);
|
|
139
|
+
if (found.run !== run)
|
|
140
|
+
throw misuse('The other locator belongs to another test. Find it again with its page.', run);
|
|
141
|
+
if (found.app !== this.#target.app) {
|
|
142
|
+
const own = describeLocator(this.#target.recipe);
|
|
143
|
+
throw misuse(`toOverlap() compares two elements of one page: ${own} is on ${this.#target.app} and ${describeLocator(found.recipe)} on ${found.app}.`, run);
|
|
144
|
+
}
|
|
145
|
+
return this.#measure({ matcher: 'toOverlap', other: found.recipe }, options);
|
|
146
|
+
}
|
|
147
|
+
toOverflowViewport(options) {
|
|
148
|
+
return this.#measure({ matcher: 'toOverflowViewport' }, options);
|
|
149
|
+
}
|
|
150
|
+
toHaveClippedText(options) {
|
|
151
|
+
return this.#measure({ matcher: 'toHaveClippedText' }, options);
|
|
152
|
+
}
|
|
153
|
+
toHaveScreenshot(name, options) {
|
|
154
|
+
const { run, stepId } = this.#scope;
|
|
155
|
+
const { app, recipe } = this.#target;
|
|
156
|
+
const { soft, negated } = this.#sense;
|
|
157
|
+
const location = run.location();
|
|
158
|
+
if (negated)
|
|
159
|
+
throw misuse(misplaced.screenshotNot, run);
|
|
160
|
+
if (this.#target.run !== run)
|
|
161
|
+
throw misuse('This locator belongs to another test. Find it again with its page.', run);
|
|
162
|
+
const call = readScreenshotCall(name, options);
|
|
163
|
+
if (typeof call === 'string')
|
|
164
|
+
throw misuse(call, run);
|
|
165
|
+
const label = `${soft ? 'expect.soft' : 'expect'}(${describeLocator(recipe)}).toHaveScreenshot()`;
|
|
166
|
+
return run.assertion(label, location, () => pollScreenshot({ run, stepId, app, recipe, record: call.record, location, soft, timeoutMs: call.timeoutMs }), app);
|
|
167
|
+
}
|
|
124
168
|
toBe() {
|
|
125
169
|
throw misuse(misplaced.toBe, this.#scope.run);
|
|
126
170
|
}
|
|
@@ -145,6 +189,18 @@ class LocatorExpectation {
|
|
|
145
189
|
const label = `${soft ? 'expect.soft' : 'expect'}(${describeLocator(recipe)})${negated ? '.not' : ''}.${matched.matcher}()`;
|
|
146
190
|
return run.assertion(label, location, () => pollLocator({ run, stepId, app, recipe, record, location, soft, timeoutMs }), app);
|
|
147
191
|
}
|
|
192
|
+
#measure(matched, options) {
|
|
193
|
+
const { run, stepId } = this.#scope;
|
|
194
|
+
const { app, recipe } = this.#target;
|
|
195
|
+
const { soft, negated } = this.#sense;
|
|
196
|
+
const location = run.location();
|
|
197
|
+
if (this.#target.run !== run)
|
|
198
|
+
throw misuse('This locator belongs to another test. Find it again with its page.', run);
|
|
199
|
+
const timeoutMs = readAssertionOptions(matched.matcher, options, this.#scope);
|
|
200
|
+
const record = negated ? { ...matched, not: true } : matched;
|
|
201
|
+
const label = `${soft ? 'expect.soft' : 'expect'}(${describeLocator(recipe)})${negated ? '.not' : ''}.${matched.matcher}()`;
|
|
202
|
+
return run.assertion(label, location, () => pollLayout({ run, stepId, app, recipe, record, location, soft, timeoutMs }), app);
|
|
203
|
+
}
|
|
148
204
|
}
|
|
149
205
|
class PageExpectation {
|
|
150
206
|
#scope;
|
|
@@ -175,6 +231,33 @@ class PageExpectation {
|
|
|
175
231
|
return this.#assert({ matcher: 'toHaveTitle', pattern: textPatternOf(title) }, options);
|
|
176
232
|
throw misuse(`toHaveTitle() takes the title as a string or a RegExp, received ${formatValue(title)}.`, this.#scope.run);
|
|
177
233
|
}
|
|
234
|
+
toScrollHorizontally(options) {
|
|
235
|
+
const { run, stepId } = this.#scope;
|
|
236
|
+
const { app } = this.#target;
|
|
237
|
+
const { soft, negated } = this.#sense;
|
|
238
|
+
const location = run.location();
|
|
239
|
+
if (this.#target.run !== run)
|
|
240
|
+
throw misuse('This page belongs to another test. Use the page this test was given.', run);
|
|
241
|
+
const timeoutMs = readAssertionOptions('toScrollHorizontally', options, this.#scope);
|
|
242
|
+
const record = negated ? { matcher: 'toScrollHorizontally', not: true } : { matcher: 'toScrollHorizontally' };
|
|
243
|
+
const label = `${soft ? 'expect.soft' : 'expect'}(page)${negated ? '.not' : ''}.toScrollHorizontally()`;
|
|
244
|
+
return run.assertion(label, location, () => pollLayout({ run, stepId, app, recipe: undefined, record, location, soft, timeoutMs }), app);
|
|
245
|
+
}
|
|
246
|
+
toHaveScreenshot(name, options) {
|
|
247
|
+
const { run, stepId } = this.#scope;
|
|
248
|
+
const { app } = this.#target;
|
|
249
|
+
const { soft, negated } = this.#sense;
|
|
250
|
+
const location = run.location();
|
|
251
|
+
if (negated)
|
|
252
|
+
throw misuse(misplaced.screenshotNot, run);
|
|
253
|
+
if (this.#target.run !== run)
|
|
254
|
+
throw misuse('This page belongs to another test. Use the page this test was given.', run);
|
|
255
|
+
const call = readScreenshotCall(name, options);
|
|
256
|
+
if (typeof call === 'string')
|
|
257
|
+
throw misuse(call, run);
|
|
258
|
+
const label = `${soft ? 'expect.soft' : 'expect'}(page).toHaveScreenshot()`;
|
|
259
|
+
return run.assertion(label, location, () => pollScreenshot({ run, stepId, app, recipe: undefined, record: call.record, location, soft, timeoutMs: call.timeoutMs }), app);
|
|
260
|
+
}
|
|
178
261
|
toBe() {
|
|
179
262
|
throw misuse(misplaced.toBe, this.#scope.run);
|
|
180
263
|
}
|
|
@@ -260,6 +343,21 @@ class ValueExpectation {
|
|
|
260
343
|
toHaveTitle() {
|
|
261
344
|
throw misuse(misplaced.toHaveTitle, this.#scope.run);
|
|
262
345
|
}
|
|
346
|
+
toOverlap() {
|
|
347
|
+
throw misuse(misplaced.toOverlap, this.#scope.run);
|
|
348
|
+
}
|
|
349
|
+
toOverflowViewport() {
|
|
350
|
+
throw misuse(misplaced.toOverflowViewport, this.#scope.run);
|
|
351
|
+
}
|
|
352
|
+
toHaveClippedText() {
|
|
353
|
+
throw misuse(misplaced.toHaveClippedText, this.#scope.run);
|
|
354
|
+
}
|
|
355
|
+
toScrollHorizontally() {
|
|
356
|
+
throw misuse(misplaced.toScrollHorizontally, this.#scope.run);
|
|
357
|
+
}
|
|
358
|
+
toHaveScreenshot() {
|
|
359
|
+
throw misuse(misplaced.toHaveScreenshot, this.#scope.run);
|
|
360
|
+
}
|
|
263
361
|
get not() {
|
|
264
362
|
throw misuse(misplaced.not, this.#scope.run);
|
|
265
363
|
}
|
|
@@ -19,6 +19,11 @@ export type LookOptions<Observed> = {
|
|
|
19
19
|
ok: true;
|
|
20
20
|
}>): Look<Observed> | undefined;
|
|
21
21
|
passes(observation: Observed): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The waits before each look in milliseconds, counting from 0 for the first, the last repeating, for a check whose
|
|
24
|
+
* looks must be spaced, such as a screenshot's. Absent: `pollDelay`'s.
|
|
25
|
+
*/
|
|
26
|
+
delays?: readonly number[];
|
|
22
27
|
};
|
|
23
28
|
/** How the looks went: the last, how many, when they began, and the failure that stopped them, if one did. */
|
|
24
29
|
export type Looked<Observed> = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"look-until.d.ts","sourceRoot":"","sources":["../../src/assertions/look-until.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAItE,mHAAmH;AACnH,MAAM,MAAM,IAAI,CAAC,QAAQ,IAAI;IAC3B,WAAW,EAAE,QAAQ,CAAA;IACrB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,WAAW,CAAC,QAAQ,IAAI;IAClC,GAAG,EAAE,OAAO,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,cAAc,GAAG,SAAS,CAAA;IACpC,+FAA+F;IAC/F,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;IAChF,4EAA4E;IAC5E,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,EAAE;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAA;IAC9E,MAAM,CAAC,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"look-until.d.ts","sourceRoot":"","sources":["../../src/assertions/look-until.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAItE,mHAAmH;AACnH,MAAM,MAAM,IAAI,CAAC,QAAQ,IAAI;IAC3B,WAAW,EAAE,QAAQ,CAAA;IACrB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,WAAW,CAAC,QAAQ,IAAI;IAClC,GAAG,EAAE,OAAO,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,cAAc,GAAG,SAAS,CAAA;IACpC,+FAA+F;IAC/F,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;IAChF,4EAA4E;IAC5E,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,EAAE;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAA;IAC9E,MAAM,CAAC,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAA;IACtC;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC3B,CAAA;AAED,8GAA8G;AAC9G,MAAM,MAAM,MAAM,CAAC,QAAQ,IAAI;IAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,GAAG,SAAS,CAAA;CAAE,CAAA;AAKtI,kHAAkH;AAClH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,+HAA+H;AAC/H,eAAO,MAAM,WAAW,EAAE,MAAW,CAAA;AAErC;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,WAAW,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CA0CnG"}
|
|
@@ -28,7 +28,7 @@ export async function lookUntil(options) {
|
|
|
28
28
|
for (;;) {
|
|
29
29
|
// Rounded up, so a wait cut to the deadline ends at it; a timer that fires early leaves time, and the loop waits again.
|
|
30
30
|
const waitToEndMs = deadline.waitToEndMs;
|
|
31
|
-
const delay = smallestBudget(pollDelay(attempts), waitToEndMs);
|
|
31
|
+
const delay = smallestBudget(options.delays === undefined ? pollDelay(attempts) : (options.delays[Math.min(attempts, options.delays.length - 1)] ?? 0), waitToEndMs);
|
|
32
32
|
let after;
|
|
33
33
|
const changes = last?.changes;
|
|
34
34
|
if (changes === undefined) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TestRun } from '../api/test-run.ts';
|
|
2
|
+
import type { SourceLocation } from '../protocol/failures.ts';
|
|
3
|
+
import type { LayoutCheckRecord } from '../protocol/layout-checks.ts';
|
|
4
|
+
import type { LocatorRecipe } from '../protocol/locator.ts';
|
|
5
|
+
export type LayoutPollOptions = {
|
|
6
|
+
run: TestRun;
|
|
7
|
+
stepId: string | undefined;
|
|
8
|
+
app: string;
|
|
9
|
+
/** The element the matcher was called on; absent for a page matcher. */
|
|
10
|
+
recipe: LocatorRecipe | undefined;
|
|
11
|
+
/** The matcher and its arguments, which the parent judges the look the event names with. */
|
|
12
|
+
record: LayoutCheckRecord;
|
|
13
|
+
location: SourceLocation | undefined;
|
|
14
|
+
soft: boolean;
|
|
15
|
+
/** The matcher's own `timeout`, which may shorten the assertion budget and never lengthens it. */
|
|
16
|
+
timeoutMs?: number | undefined;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Measures an app's page until the layout check passes or the assertion's time runs out, as a locator assertion looks:
|
|
20
|
+
* it only reads, and never repeats the action before it. The event names the look its verdict rested on, carries the
|
|
21
|
+
* check whole, and records what that look measured, so the parent can judge it again.
|
|
22
|
+
*/
|
|
23
|
+
export declare function pollLayout(options: LayoutPollOptions): Promise<void>;
|
|
24
|
+
//# sourceMappingURL=poll-layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poll-layout.d.ts","sourceRoot":"","sources":["../../src/assertions/poll-layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAW,cAAc,EAAiB,MAAM,yBAAyB,CAAA;AAErF,OAAO,KAAK,EAAe,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAClF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAQ3D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,OAAO,CAAA;IACZ,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,wEAAwE;IACxE,MAAM,EAAE,aAAa,GAAG,SAAS,CAAA;IACjC,4FAA4F;IAC5F,MAAM,EAAE,iBAAiB,CAAA;IACzB,QAAQ,EAAE,cAAc,GAAG,SAAS,CAAA;IACpC,IAAI,EAAE,OAAO,CAAA;IACb,kGAAkG;IAClG,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC/B,CAAA;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsC1E"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { elapsedMs } from "../protocol/deadline.js";
|
|
2
|
+
import { failure, truncateText } from "../protocol/failures.js";
|
|
3
|
+
import { layoutCheck, layoutRequest } from "../protocol/layout-checks.js";
|
|
4
|
+
import { lookUntil } from "./look-until.js";
|
|
5
|
+
import { assertionTime, lookOf, lookReference } from "./poll-locator.js";
|
|
6
|
+
import { reportAssertion } from "./report.js";
|
|
7
|
+
/**
|
|
8
|
+
* Measures an app's page until the layout check passes or the assertion's time runs out, as a locator assertion looks:
|
|
9
|
+
* it only reads, and never repeats the action before it. The event names the look its verdict rested on, carries the
|
|
10
|
+
* check whole, and records what that look measured, so the parent can judge it again.
|
|
11
|
+
*/
|
|
12
|
+
export async function pollLayout(options) {
|
|
13
|
+
const { run, app, recipe, record, location } = options;
|
|
14
|
+
const check = layoutCheck(record);
|
|
15
|
+
const request = layoutRequest(record, recipe);
|
|
16
|
+
const timeoutMs = assertionTime(run, options.timeoutMs);
|
|
17
|
+
const looked = await lookUntil({
|
|
18
|
+
run,
|
|
19
|
+
timeoutMs,
|
|
20
|
+
location,
|
|
21
|
+
look: (lookMs, after) => run.observeLayout(app, request, lookMs, location, after),
|
|
22
|
+
read: (result) => (result.kind === 'observeLayout' ? lookOf(result.observation, result) : undefined),
|
|
23
|
+
passes: check.passes,
|
|
24
|
+
});
|
|
25
|
+
const { stopped, attempts } = looked;
|
|
26
|
+
const last = looked.last?.observation;
|
|
27
|
+
const passed = stopped === undefined && last !== undefined && check.passes(last);
|
|
28
|
+
const fields = {
|
|
29
|
+
testId: run.testId,
|
|
30
|
+
attemptId: run.attemptId,
|
|
31
|
+
...(options.stepId === undefined ? {} : { stepId: options.stepId }),
|
|
32
|
+
session: app,
|
|
33
|
+
matcher: check.matcher,
|
|
34
|
+
...(recipe === undefined ? {} : { locator: recipe }),
|
|
35
|
+
expected: truncateText(check.expected),
|
|
36
|
+
actual: last === undefined ? null : nullableText(check.actual(last)),
|
|
37
|
+
comparison: check.comparison,
|
|
38
|
+
attempts,
|
|
39
|
+
timeoutMs,
|
|
40
|
+
durationMs: elapsedMs(looked.startedAt, run.time.now),
|
|
41
|
+
...(location === undefined ? {} : { location }),
|
|
42
|
+
...lookReference(looked.last),
|
|
43
|
+
...(last === undefined ? {} : { layout: check.measured(last) }),
|
|
44
|
+
check: record,
|
|
45
|
+
};
|
|
46
|
+
if (passed)
|
|
47
|
+
return reportAssertion(run, fields, undefined, false);
|
|
48
|
+
// Only the check's own verdict is softened; a page that could not be read stops the test as usual.
|
|
49
|
+
if (stopped !== undefined)
|
|
50
|
+
return reportAssertion(run, fields, stopped, false);
|
|
51
|
+
reportAssertion(run, fields, unmet({ check, recipe, last, attempts, timeoutMs, location }), options.soft);
|
|
52
|
+
}
|
|
53
|
+
function unmet({ check, recipe, last, attempts, timeoutMs, location }) {
|
|
54
|
+
if (last === undefined)
|
|
55
|
+
return failure('timeout', `The page answered no look within ${timeoutMs} ms.`, location);
|
|
56
|
+
const looked = `Looked ${attempts} ${attempts === 1 ? 'time' : 'times'} in ${timeoutMs} ms.`;
|
|
57
|
+
const details = { expected: truncateText(check.expected), received: nullableText(check.actual(last)), attempts, timeoutMs, comparison: check.comparison };
|
|
58
|
+
const reason = check.unmet(last, recipe);
|
|
59
|
+
return { ...failure(reason.class, `${reason.message} ${looked}`, location), details };
|
|
60
|
+
}
|
|
61
|
+
function nullableText(text) {
|
|
62
|
+
return text === null ? null : truncateText(text);
|
|
63
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TestRun } from '../api/test-run.ts';
|
|
2
|
+
import type { SourceLocation } from '../protocol/failures.ts';
|
|
3
|
+
import type { LocatorRecipe } from '../protocol/locator.ts';
|
|
4
|
+
import type { ScreenshotCheckRecord } from '../protocol/screenshot.ts';
|
|
5
|
+
/** A screenshot check as the test called it: its record, and its own `timeout`, when it gave one. */
|
|
6
|
+
export type ScreenshotCall = {
|
|
7
|
+
record: ScreenshotCheckRecord;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Reads what a test passed `toHaveScreenshot`: the baseline's name, with a `.png` it ends in left off, and the options.
|
|
12
|
+
* Answers with a sentence for anything else, which the caller throws as misuse.
|
|
13
|
+
*
|
|
14
|
+
* @example readScreenshotCall('dashboard.png', { maxDiffPixels: 50 }) // { record: { matcher: 'toHaveScreenshot', name: 'dashboard', maxDiffPixels: 50 } }
|
|
15
|
+
*/
|
|
16
|
+
export declare function readScreenshotCall(name: unknown, options: unknown): ScreenshotCall | string;
|
|
17
|
+
export type ScreenshotPollOptions = {
|
|
18
|
+
run: TestRun;
|
|
19
|
+
stepId: string | undefined;
|
|
20
|
+
app: string;
|
|
21
|
+
/** The element the matcher was called on; absent for the page. */
|
|
22
|
+
recipe: LocatorRecipe | undefined;
|
|
23
|
+
record: ScreenshotCheckRecord;
|
|
24
|
+
location: SourceLocation | undefined;
|
|
25
|
+
soft: boolean;
|
|
26
|
+
timeoutMs?: number | undefined;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Takes pictures of an app's page, or of one element, at the waits `pictureWaits` lists, until a picture has settled
|
|
30
|
+
* and passes against its baseline, the baseline is missing or cannot be read, or the assertion's time runs out, then
|
|
31
|
+
* judges the last. It only reads, and never repeats the action before it, so a page still loading passes once it
|
|
32
|
+
* shows its baseline, and a page that never stops moving fails as one that did not settle. The event names the look
|
|
33
|
+
* its verdict rested on and carries the check whole; the parent records what the picture showed.
|
|
34
|
+
*/
|
|
35
|
+
export declare function pollScreenshot(options: ScreenshotPollOptions): Promise<void>;
|
|
36
|
+
//# sourceMappingURL=screenshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/assertions/screenshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAW,cAAc,EAAiB,MAAM,yBAAyB,CAAA;AACrF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAmB,qBAAqB,EAAkB,MAAM,2BAA2B,CAAA;AAUvG,qGAAqG;AACrG,MAAM,MAAM,cAAc,GAAG;IAAE,MAAM,EAAE,qBAAqB,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAIlF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,cAAc,GAAG,MAAM,CA2B3F;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,OAAO,CAAA;IACZ,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,kEAAkE;IAClE,MAAM,EAAE,aAAa,GAAG,SAAS,CAAA;IACjC,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE,cAAc,GAAG,SAAS,CAAA;IACpC,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC/B,CAAA;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2ClF"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { formatValue } from "../api/format-value.js";
|
|
2
|
+
import { elapsedMs } from "../protocol/deadline.js";
|
|
3
|
+
import { failure, truncateText } from "../protocol/failures.js";
|
|
4
|
+
import { pictureWaits, screenshotCheck, screenshotNameRefusal, screenshotRequest, thresholdRefusal } from "../protocol/screenshot.js";
|
|
5
|
+
import { maxTimeout } from "../protocol/timeouts.js";
|
|
6
|
+
import { lookUntil } from "./look-until.js";
|
|
7
|
+
import { assertionTime, lookOf, lookReference } from "./poll-locator.js";
|
|
8
|
+
import { reportAssertion } from "./report.js";
|
|
9
|
+
const optionNames = ['threshold', 'maxDiffPixels', 'maxDiffPixelRatio', 'timeout'];
|
|
10
|
+
/**
|
|
11
|
+
* Reads what a test passed `toHaveScreenshot`: the baseline's name, with a `.png` it ends in left off, and the options.
|
|
12
|
+
* Answers with a sentence for anything else, which the caller throws as misuse.
|
|
13
|
+
*
|
|
14
|
+
* @example readScreenshotCall('dashboard.png', { maxDiffPixels: 50 }) // { record: { matcher: 'toHaveScreenshot', name: 'dashboard', maxDiffPixels: 50 } }
|
|
15
|
+
*/
|
|
16
|
+
export function readScreenshotCall(name, options) {
|
|
17
|
+
if (typeof name !== 'string')
|
|
18
|
+
return `toHaveScreenshot() takes the name of its baseline, such as 'dashboard', received ${formatValue(name)}.`;
|
|
19
|
+
const named = name.endsWith('.png') ? name.slice(0, -'.png'.length) : name;
|
|
20
|
+
const refused = screenshotNameRefusal(named);
|
|
21
|
+
if (refused !== undefined)
|
|
22
|
+
return name === named ? refused : `${refused} A .png at the end of ${formatValue(name)} is left off.`;
|
|
23
|
+
if (options === undefined)
|
|
24
|
+
return { record: { matcher: 'toHaveScreenshot', name: named } };
|
|
25
|
+
const shape = `toHaveScreenshot() takes options such as { maxDiffPixels: 50 }, received ${formatValue(options)}.`;
|
|
26
|
+
if (typeof options !== 'object' || options === null || Array.isArray(options))
|
|
27
|
+
return shape;
|
|
28
|
+
const given = Object.entries(options).filter(([, value]) => value !== undefined);
|
|
29
|
+
if (given.some(([key]) => !optionNames.includes(key)))
|
|
30
|
+
return `${shape} Its options are threshold, maxDiffPixels, maxDiffPixelRatio and timeout.`;
|
|
31
|
+
const read = (key) => given.find(([option]) => option === key)?.[1];
|
|
32
|
+
const threshold = read('threshold');
|
|
33
|
+
const maxDiffPixels = read('maxDiffPixels');
|
|
34
|
+
const maxDiffPixelRatio = read('maxDiffPixelRatio');
|
|
35
|
+
const timeout = read('timeout');
|
|
36
|
+
if (threshold !== undefined && !isFraction(threshold))
|
|
37
|
+
return thresholdRefusal(formatValue(threshold));
|
|
38
|
+
if (maxDiffPixels !== undefined && !isCount(maxDiffPixels))
|
|
39
|
+
return `The maxDiffPixels option of toHaveScreenshot() is a whole number of pixels from 0, received ${formatValue(maxDiffPixels)}.`;
|
|
40
|
+
if (maxDiffPixelRatio !== undefined && !isFraction(maxDiffPixelRatio))
|
|
41
|
+
return `The maxDiffPixelRatio option of toHaveScreenshot() is a part of the pixels from 0 to 1, received ${formatValue(maxDiffPixelRatio)}.`;
|
|
42
|
+
if (timeout !== undefined && !(isCount(timeout) && timeout >= 1 && timeout <= maxTimeout))
|
|
43
|
+
return `The timeout option of toHaveScreenshot() must be a whole number of milliseconds from 1 to ${maxTimeout}, received ${formatValue(timeout)}.`;
|
|
44
|
+
const record = {
|
|
45
|
+
matcher: 'toHaveScreenshot',
|
|
46
|
+
name: named,
|
|
47
|
+
...(threshold === undefined ? {} : { threshold }),
|
|
48
|
+
...(maxDiffPixels === undefined ? {} : { maxDiffPixels }),
|
|
49
|
+
...(maxDiffPixelRatio === undefined ? {} : { maxDiffPixelRatio }),
|
|
50
|
+
};
|
|
51
|
+
return timeout === undefined ? { record } : { record, timeoutMs: timeout };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Takes pictures of an app's page, or of one element, at the waits `pictureWaits` lists, until a picture has settled
|
|
55
|
+
* and passes against its baseline, the baseline is missing or cannot be read, or the assertion's time runs out, then
|
|
56
|
+
* judges the last. It only reads, and never repeats the action before it, so a page still loading passes once it
|
|
57
|
+
* shows its baseline, and a page that never stops moving fails as one that did not settle. The event names the look
|
|
58
|
+
* its verdict rested on and carries the check whole; the parent records what the picture showed.
|
|
59
|
+
*/
|
|
60
|
+
export async function pollScreenshot(options) {
|
|
61
|
+
const { run, app, recipe, record, location } = options;
|
|
62
|
+
const check = screenshotCheck(record);
|
|
63
|
+
const request = screenshotRequest(record, recipe);
|
|
64
|
+
const timeoutMs = assertionTime(run, options.timeoutMs);
|
|
65
|
+
let previous;
|
|
66
|
+
const looked = await lookUntil({
|
|
67
|
+
run,
|
|
68
|
+
timeoutMs,
|
|
69
|
+
location,
|
|
70
|
+
look: (lookMs) => run.observeScreenshot(app, request, previous, lookMs, location),
|
|
71
|
+
read: (result) => {
|
|
72
|
+
if (result.kind !== 'observeScreenshot')
|
|
73
|
+
return undefined;
|
|
74
|
+
previous = result.observationId;
|
|
75
|
+
return lookOf(result.observation, result);
|
|
76
|
+
},
|
|
77
|
+
passes: check.done,
|
|
78
|
+
delays: pictureWaits,
|
|
79
|
+
});
|
|
80
|
+
const { stopped, attempts } = looked;
|
|
81
|
+
const last = looked.last?.observation;
|
|
82
|
+
const passed = stopped === undefined && last !== undefined && check.passes(last);
|
|
83
|
+
const fields = {
|
|
84
|
+
testId: run.testId,
|
|
85
|
+
attemptId: run.attemptId,
|
|
86
|
+
...(options.stepId === undefined ? {} : { stepId: options.stepId }),
|
|
87
|
+
session: app,
|
|
88
|
+
matcher: check.matcher,
|
|
89
|
+
...(recipe === undefined ? {} : { locator: recipe }),
|
|
90
|
+
expected: truncateText(check.expected),
|
|
91
|
+
actual: last === undefined ? null : nullableText(check.actual(last)),
|
|
92
|
+
comparison: check.comparison,
|
|
93
|
+
attempts,
|
|
94
|
+
timeoutMs,
|
|
95
|
+
durationMs: elapsedMs(looked.startedAt, run.time.now),
|
|
96
|
+
...(location === undefined ? {} : { location }),
|
|
97
|
+
...lookReference(looked.last),
|
|
98
|
+
check: record,
|
|
99
|
+
};
|
|
100
|
+
if (passed)
|
|
101
|
+
return reportAssertion(run, fields, undefined, false);
|
|
102
|
+
// Only the check's own verdict is softened; a page that could not be pictured stops the test as usual.
|
|
103
|
+
if (stopped !== undefined)
|
|
104
|
+
return reportAssertion(run, fields, stopped, false);
|
|
105
|
+
reportAssertion(run, fields, unmet({ check, recipe, last, attempts, timeoutMs, location }), options.soft);
|
|
106
|
+
}
|
|
107
|
+
function unmet({ check, recipe, last, attempts, timeoutMs, location }) {
|
|
108
|
+
if (last === undefined)
|
|
109
|
+
return failure('timeout', `The page answered no look within ${timeoutMs} ms.`, location);
|
|
110
|
+
const looked = `Looked ${attempts} ${attempts === 1 ? 'time' : 'times'} in ${timeoutMs} ms.`;
|
|
111
|
+
const details = { expected: truncateText(check.expected), received: nullableText(check.actual(last)), attempts, timeoutMs, comparison: check.comparison };
|
|
112
|
+
const reason = check.unmet(last, recipe);
|
|
113
|
+
// A picture or a baseline that cannot be read says all there is, as the parent's own judgement says it.
|
|
114
|
+
const message = reason.class === 'setup_failed' ? reason.message : `${reason.message} ${looked}`;
|
|
115
|
+
return { ...failure(reason.class, message, location), details };
|
|
116
|
+
}
|
|
117
|
+
function nullableText(text) {
|
|
118
|
+
return text === null ? null : truncateText(text);
|
|
119
|
+
}
|
|
120
|
+
function isFraction(value) {
|
|
121
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
122
|
+
}
|
|
123
|
+
function isCount(value) {
|
|
124
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
125
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accessibility.d.ts","sourceRoot":"","sources":["../../src/browser/accessibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACpE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAMvD,6HAA6H;AAC7H,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAA;AAEtF,oDAAoD;AACpD,eAAO,MAAM,aAAa,EAAE,SAAS,QAAQ,EAU5C,CAAA;
|
|
1
|
+
{"version":3,"file":"accessibility.d.ts","sourceRoot":"","sources":["../../src/browser/accessibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACpE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAMvD,6HAA6H;AAC7H,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAA;AAEtF,oDAAoD;AACpD,eAAO,MAAM,aAAa,EAAE,SAAS,QAAQ,EAU5C,CAAA;AA0BD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,MAAM,EAAE,CAEhE;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAiBtH"}
|
|
@@ -29,6 +29,9 @@ const treeSchema = s.object({
|
|
|
29
29
|
nodes: s.array(s.object({ ignored: s.boolean(), name: nameSchema, backendDOMNodeId: s.optional(s.number({ integer: true })) })),
|
|
30
30
|
});
|
|
31
31
|
const resolvedSchema = s.object({ object: s.object({ objectId: s.optional(s.string()) }) });
|
|
32
|
+
const fullTreeSchema = s.object({
|
|
33
|
+
nodes: s.array(s.object({ ignored: s.boolean(), role: s.optional(s.object({ value: s.optional(s.string()) })), name: nameSchema, backendDOMNodeId: s.optional(s.number({ integer: true })) })),
|
|
34
|
+
});
|
|
32
35
|
/**
|
|
33
36
|
* The names to ask Chrome's tree for a WAI-ARIA role by.
|
|
34
37
|
*
|
|
@@ -43,6 +46,8 @@ export function chromeRolesFor(role) {
|
|
|
43
46
|
* as elements under `aria-hidden="true"`; elements that are not rendered or are inert are not in its tree at all.
|
|
44
47
|
*/
|
|
45
48
|
export async function elementsByRole(scope, query, deadline) {
|
|
49
|
+
if (scope.frame !== undefined)
|
|
50
|
+
return frameElementsByRole(scope, scope.frame, query, deadline);
|
|
46
51
|
const { session, context, objectGroup } = scope;
|
|
47
52
|
const evaluate = { expression: 'document', contextId: context, objectGroup };
|
|
48
53
|
const document = await request(session, 'Runtime.evaluate', evaluate, documentSchema, sendOptions(deadline));
|
|
@@ -57,7 +62,23 @@ export async function elementsByRole(scope, query, deadline) {
|
|
|
57
62
|
if (query.name === undefined || matchesText(node.name?.value ?? '', query.name, query.exact))
|
|
58
63
|
found.add(node.backendDOMNodeId);
|
|
59
64
|
}
|
|
60
|
-
|
|
65
|
+
return resolveAll(scope, [...found], deadline);
|
|
66
|
+
}
|
|
67
|
+
// A frame of another site that is out of view is never drawn, and Chrome does not answer a query of its tree that waits
|
|
68
|
+
// for it to be (`Accessibility.queryAXTree`). So a frame's whole tree is read at once, as Chrome computes it then, and the
|
|
69
|
+
// query is answered from it by the same roles and names.
|
|
70
|
+
async function frameElementsByRole(scope, frameId, query, deadline) {
|
|
71
|
+
const { nodes } = await request(scope.session, 'Accessibility.getFullAXTree', { frameId }, fullTreeSchema, sendOptions(deadline));
|
|
72
|
+
const found = new Set();
|
|
73
|
+
for (const node of nodes) {
|
|
74
|
+
if (node.ignored || node.backendDOMNodeId === undefined || !query.roles.includes(node.role?.value ?? ''))
|
|
75
|
+
continue;
|
|
76
|
+
if (query.name === undefined || matchesText(node.name?.value ?? '', query.name, query.exact))
|
|
77
|
+
found.add(node.backendDOMNodeId);
|
|
78
|
+
}
|
|
79
|
+
return resolveAll(scope, [...found], deadline);
|
|
80
|
+
}
|
|
81
|
+
async function resolveAll(scope, ids, deadline) {
|
|
61
82
|
const elements = [];
|
|
62
83
|
for (let start = 0; start < ids.length; start += resolveBatch) {
|
|
63
84
|
const objectIds = await Promise.all(ids.slice(start, start + resolveBatch).map((id) => resolve(scope, id, deadline)));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ActionIntent, SelectPlan } from './element-queries.ts';
|
|
2
2
|
import type { Point } from './input.ts';
|
|
3
|
-
import type {
|
|
3
|
+
import type { Guard } from './input-guard.ts';
|
|
4
|
+
import type { PageWorld } from './isolated-world.ts';
|
|
4
5
|
import type { Deadline } from '../protocol/deadline.ts';
|
|
5
6
|
import type { Failure } from '../protocol/failures.ts';
|
|
6
7
|
import type { LocatorRecipe } from '../protocol/locator.ts';
|
|
@@ -9,12 +10,14 @@ import type { PageFacts } from '../protocol/page-facts.ts';
|
|
|
9
10
|
* An element, or the document, that passed every check. `point` is where to act, or null for a key, which goes to
|
|
10
11
|
* the focus. `context` is the document the look ran in, which holds the guard the look armed with `token`, or none
|
|
11
12
|
* for a `select`'s look. `via` is `'label'` when a hidden control is acted on through its label, `scale` the visual
|
|
12
|
-
* viewport's, `page` the page the look read, and `plan` the keys that make a select's choice.
|
|
13
|
+
* viewport's, `page` the page the look read, and `plan` the keys that make a select's choice. An element in a frame
|
|
14
|
+
* has, in `around`, the guards its look armed in the documents around the frame.
|
|
13
15
|
*/
|
|
14
16
|
export type ReadyTarget = {
|
|
15
17
|
point: Point | null;
|
|
16
18
|
context: number;
|
|
17
19
|
token: number | null;
|
|
20
|
+
around?: readonly Guard[];
|
|
18
21
|
via: 'label' | undefined;
|
|
19
22
|
scale: number;
|
|
20
23
|
page: PageFacts;
|
|
@@ -41,7 +44,7 @@ export type PendingNavigation = {
|
|
|
41
44
|
url: string;
|
|
42
45
|
};
|
|
43
46
|
export type ActionabilityOptions = {
|
|
44
|
-
world:
|
|
47
|
+
world: PageWorld;
|
|
45
48
|
/** The element to act on, or undefined for input the document takes, which only waits out a navigation. */
|
|
46
49
|
locator: LocatorRecipe | undefined;
|
|
47
50
|
intent: ActionIntent;
|
|
@@ -70,10 +73,10 @@ export declare function invalidSelector({ step, message }: {
|
|
|
70
73
|
message: string;
|
|
71
74
|
}, locator: LocatorRecipe, intent: ActionIntent | undefined): Failure;
|
|
72
75
|
/**
|
|
73
|
-
* The failure for a
|
|
74
|
-
*
|
|
76
|
+
* The failure for a role or label lookup whose search holds an open shadow root, on an engine whose accessibility tree,
|
|
77
|
+
* as Retest reads it, does not look inside one. Chrome's does, so the lookup fails at once rather than find less.
|
|
75
78
|
*
|
|
76
|
-
* @example shadowRefused('<todo-list>', { by: 'role', role: 'button'
|
|
79
|
+
* @example shadowRefused('<todo-list>', { by: 'role', role: 'button' }, undefined).class // 'unsupported'
|
|
77
80
|
*/
|
|
78
81
|
export declare function shadowRefused(host: string, locator: LocatorRecipe, intent: ActionIntent | undefined): Failure;
|
|
79
82
|
//# sourceMappingURL=actionability.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actionability.d.ts","sourceRoot":"","sources":["../../src/browser/actionability.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"actionability.d.ts","sourceRoot":"","sources":["../../src/browser/actionability.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAA8C,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAChH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AAY1D;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,SAAS,KAAK,EAAE,CAAA;IACzB,GAAG,EAAE,OAAO,GAAG,SAAS,CAAA;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,SAAS,CAAA;IACf,IAAI,EAAE,UAAU,GAAG,IAAI,CAAA;CACxB,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GACpB,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG,WAAW,CAAC,GAC3C;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAA;AAEnC,mHAAmH;AACnH,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AAE/C,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,SAAS,CAAA;IAChB,2GAA2G;IAC3G,OAAO,EAAE,aAAa,GAAG,SAAS,CAAA;IAClC,MAAM,EAAE,YAAY,CAAA;IACpB,QAAQ,EAAE,QAAQ,CAAA;IAClB,+FAA+F;IAC/F,iBAAiB,EAAE,MAAM,iBAAiB,GAAG,SAAS,CAAA;CACvD,CAAA;AA0BD;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAW9F;AAoFD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,CAKvJ;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,CAQ7G"}
|
|
@@ -5,6 +5,7 @@ import { secretPlaceholder } from "../protocol/secret.js";
|
|
|
5
5
|
import { CdpTimeoutError } from "./cdp/errors.js";
|
|
6
6
|
import { pageFactsOf } from "./document-facts.js";
|
|
7
7
|
import { armDocument, describeAction, prepare } from "./element-queries.js";
|
|
8
|
+
import { engineShortOfShadowRoots } from "./locate.js";
|
|
8
9
|
import { originRefusal } from "./origin-refusal.js";
|
|
9
10
|
import { originAndPath } from "./page-url.js";
|
|
10
11
|
const firstPauseMs = 20;
|
|
@@ -61,10 +62,10 @@ async function look({ world, locator, intent, deadline, pendingNavigation }, las
|
|
|
61
62
|
}
|
|
62
63
|
return lookedAt(seen, locator, intent);
|
|
63
64
|
}
|
|
64
|
-
function lookedAt({ value: readiness, context }, locator, intent) {
|
|
65
|
+
function lookedAt({ value: readiness, context, around }, locator, intent) {
|
|
65
66
|
switch (readiness.status) {
|
|
66
67
|
case 'ready':
|
|
67
|
-
return { kind: 'settled', target: readyTarget(readiness, context) };
|
|
68
|
+
return { kind: 'settled', target: readyTarget(readiness, context, around) };
|
|
68
69
|
case 'unchanged':
|
|
69
70
|
return { kind: 'settled', target: { ok: true, kind: 'unchanged', page: pageFactsOf(readiness.page), context } };
|
|
70
71
|
case 'missing':
|
|
@@ -102,9 +103,10 @@ function settledFailure(readiness, locator, intent) {
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
|
-
function readyTarget(readiness, context) {
|
|
106
|
+
function readyTarget(readiness, context, around) {
|
|
106
107
|
const { point, token, via, scale, page, plan } = readiness;
|
|
107
|
-
|
|
108
|
+
const through = around === undefined || around.length === 0 ? {} : { around };
|
|
109
|
+
return { ok: true, kind: 'ready', point, context, token, ...through, via: via ?? undefined, scale, page: pageFactsOf(page), plan };
|
|
108
110
|
}
|
|
109
111
|
// Only an action on an element can meet an element that does not suit it.
|
|
110
112
|
function elementOf(locator) {
|
|
@@ -133,16 +135,17 @@ export function invalidSelector({ step, message }, locator, intent) {
|
|
|
133
135
|
return { class: 'usage', message: `Could not ${action}: the page cannot read the CSS selector in ${selector}. ${message}`, details: { selector } };
|
|
134
136
|
}
|
|
135
137
|
/**
|
|
136
|
-
* The failure for a
|
|
137
|
-
*
|
|
138
|
+
* The failure for a role or label lookup whose search holds an open shadow root, on an engine whose accessibility tree,
|
|
139
|
+
* as Retest reads it, does not look inside one. Chrome's does, so the lookup fails at once rather than find less.
|
|
138
140
|
*
|
|
139
|
-
* @example shadowRefused('<todo-list>', { by: 'role', role: 'button'
|
|
141
|
+
* @example shadowRefused('<todo-list>', { by: 'role', role: 'button' }, undefined).class // 'unsupported'
|
|
140
142
|
*/
|
|
141
143
|
export function shadowRefused(host, locator, intent) {
|
|
142
144
|
const action = intent === undefined ? `read ${describeLocator(locator)}` : describeAction(intent, locator);
|
|
145
|
+
const engine = engineShortOfShadowRoots() ?? 'this browser';
|
|
143
146
|
return {
|
|
144
147
|
class: 'unsupported',
|
|
145
|
-
message: `Could not ${action}:
|
|
148
|
+
message: `Could not ${action}: on ${engine}, Retest finds by role or label outside shadow roots only, and the open shadow root of ${host} could hold a match. Find the element by test id, text or CSS.`,
|
|
146
149
|
details: { host },
|
|
147
150
|
};
|
|
148
151
|
}
|
|
@@ -214,6 +217,12 @@ function unsupported(readiness, locator, intent) {
|
|
|
214
217
|
message: `Could not ${action}: it is ${element}, which takes one option, and select() was given a list. Pass one option, not a list.`,
|
|
215
218
|
details: { element },
|
|
216
219
|
};
|
|
220
|
+
case 'toggle': {
|
|
221
|
+
// Only an intent that carries the engine's reason asks the page to refuse a toggle.
|
|
222
|
+
if (intent.action !== 'select' || intent.toggleRefusal === undefined)
|
|
223
|
+
throw new Error("Retest's page script refused a toggle its engine sends");
|
|
224
|
+
return { class: 'unsupported', message: `Could not ${action}: ${intent.toggleRefusal} Retest sent no input.`, details: { inputSent: false } };
|
|
225
|
+
}
|
|
217
226
|
}
|
|
218
227
|
}
|
|
219
228
|
function unready(last, locator, intent, deadline) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/browser/browser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/browser/browser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAiB7G,4HAA4H;AAC5H,MAAM,MAAM,cAAc,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvG,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,eAAe,CAAA;IACxB,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,aAAa,CAAA;IACzB,OAAO,EAAE,cAAc,CAAA;IACvB,+FAA+F;IAC/F,eAAe,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAC1C,CAAA;AAKD,oEAAoE;AACpE,qBAAa,eAAgB,YAAW,UAAU;;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,wFAAwF;IACxF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;gBAYhB,OAAO,EAAE,cAAc;IA8BnC,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,QAAQ,IAAI,kBAAkB,CAEjC;IAIK,OAAO,CAAC,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IA2C9E,YAAY,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAU5D,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAmDxC;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,CAIzH"}
|