@jterrazz/test 16.1.0 → 17.0.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 +45 -171
- package/dist/browser/index.js +2 -2867
- package/dist/browser/services.js +2 -0
- package/dist/browser/stubs.js +2 -0
- package/dist/checker.d.ts +6 -1
- package/dist/checker.js +4551 -2612
- package/dist/config.d.ts +40 -0
- package/dist/config.js +309 -0
- package/dist/in-place.global-setup.d.ts +11 -0
- package/dist/in-place.global-setup.js +21 -0
- package/dist/index.d.ts +1653 -3002
- package/dist/index.js +5226 -3845
- package/dist/mobile-session.global-setup.d.ts +14 -0
- package/dist/mobile-session.global-setup.js +31 -0
- package/dist/oxlint.cjs +6073 -5616
- package/dist/oxlint.d.cts +43 -7212
- package/dist/oxlint.d.ts +43 -7212
- package/dist/oxlint.js +6065 -5609
- package/dist/route-coverage.global-setup.d.ts +42 -0
- package/dist/route-coverage.global-setup.js +2 -0
- package/dist/services.d.ts +3 -0
- package/dist/services.global-setup.d.ts +43 -0
- package/dist/services.global-setup.js +2 -0
- package/dist/services.js +597 -0
- package/dist/shared-appium.adapter.js +428 -0
- package/dist/shared-artifacts.js +32 -0
- package/dist/shared-chapter.js +7 -0
- package/dist/shared-container.port.d.ts +31 -0
- package/dist/shared-derived-projects.js +58 -0
- package/dist/shared-device.port.d.ts +189 -0
- package/dist/shared-exchange.js +447 -0
- package/dist/shared-in-place.js +172 -0
- package/dist/shared-index.browser.js +3191 -0
- package/dist/shared-index.services.d.ts +346 -0
- package/dist/shared-index.stubs.d.ts +287 -0
- package/dist/shared-json-shape.js +17 -0
- package/dist/shared-landmarks.js +71 -0
- package/dist/shared-ledger.js +68 -0
- package/dist/shared-literate-plugin.js +183 -0
- package/dist/shared-manifest.js +1321 -0
- package/dist/shared-pause.js +11 -0
- package/dist/shared-peer.js +88 -0
- package/dist/shared-playwright.adapter.js +330 -0
- package/dist/shared-process.adapter.js +74 -0
- package/dist/shared-refusal.js +35 -0
- package/dist/shared-resolve.js +24 -0
- package/dist/shared-route-coverage.global-setup.js +187 -0
- package/dist/shared-scenario-failure.js +198 -0
- package/dist/shared-serve.js +365 -0
- package/dist/shared-service-images.js +10 -0
- package/dist/shared-services.global-setup.js +179 -0
- package/dist/shared-session-ledger.js +301 -0
- package/dist/shared-setup-entry.js +12 -0
- package/dist/shared-source-scan.js +133 -0
- package/dist/shared-spec-document.js +365 -0
- package/dist/shared-specifications.js +26 -0
- package/dist/shared-stream-body.js +130 -0
- package/dist/shared-stub-server.js +190 -0
- package/dist/shared-temp-root.global-setup.js +80 -0
- package/dist/shared-test-baseline.js +26 -0
- package/dist/shared-test-section.js +58 -0
- package/dist/shared-testcontainers.adapter.js +203 -0
- package/dist/shared-tokens.js +55 -0
- package/dist/shared-yaml-document.js +119 -0
- package/dist/stubs.d.ts +2 -0
- package/dist/stubs.js +657 -0
- package/dist/temp-root.global-setup.d.ts +7 -0
- package/dist/temp-root.global-setup.js +2 -0
- package/dist/vitest.d.ts +97 -163
- package/dist/vitest.js +374 -507
- package/docs/README.md +82 -36
- package/docs/concepts/architecture.md +118 -0
- package/docs/concepts/conventions.md +333 -0
- package/docs/guides/api-specs.md +374 -0
- package/docs/guides/cli-specs.md +564 -0
- package/docs/guides/component-specs.md +252 -0
- package/docs/guides/develop.md +94 -0
- package/docs/guides/integration-specs.md +181 -0
- package/docs/guides/job-specs.md +180 -0
- package/docs/guides/lint-specs.md +174 -0
- package/docs/guides/mobile-specs.md +319 -0
- package/docs/guides/module-tests.md +149 -0
- package/docs/guides/release.md +43 -0
- package/docs/guides/spec-documents.md +189 -0
- package/docs/guides/website-specs.md +413 -0
- package/docs/reference/assertions.md +345 -0
- package/docs/reference/glossary.md +20 -0
- package/docs/reference/linting.md +483 -0
- package/docs/reference/services.md +236 -0
- package/docs/reference/stubs.md +498 -0
- package/docs/reference/time-and-doubles.md +137 -0
- package/docs/reference/tokens.md +258 -0
- package/docs/reference/visitor.md +221 -0
- package/docs/start/getting-started.md +187 -0
- package/docs/start/set-up-a-project.md +214 -0
- package/package.json +50 -30
- package/schema/spec.schema.json +43 -17
- package/dist/appium.adapter.js +0 -489
- package/dist/artifacts.js +0 -32
- package/dist/fixtures.js +0 -110
- package/dist/literate.js +0 -1747
- package/dist/match.js +0 -804
- package/dist/playwright.adapter.js +0 -349
- package/dist/server.js +0 -160
- package/dist/stream.js +0 -321
- package/dist/substring-warning.js +0 -244
- package/docs/01-architecture.md +0 -95
- package/docs/02-developing.md +0 -351
- package/docs/03-testing.md +0 -495
- package/docs/04-operating.md +0 -101
- package/docs/05-module-tests.md +0 -152
- package/docs/06-integration.md +0 -180
- package/docs/07-component.md +0 -246
- package/docs/08-website.md +0 -404
- package/docs/09-mobile.md +0 -270
- package/docs/10-api.md +0 -264
- package/docs/11-jobs.md +0 -157
- package/docs/12-cli.md +0 -677
- package/docs/13-elements.md +0 -242
- package/docs/14-assertions.md +0 -314
- package/docs/15-tokens.md +0 -253
- package/docs/16-contracts.md +0 -438
- package/docs/17-services.md +0 -188
- package/docs/18-conventions.md +0 -305
- package/docs/19-linting.md +0 -510
- package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
- package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
- package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
- package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
- package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
- package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
- package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
- package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
- package/docs/decisions/_template.md +0 -22
- package/docs/reference/classes/Matcher.md +0 -120
- package/docs/reference/classes/ProcessHandle.md +0 -368
- package/docs/reference/functions/defineContract.md +0 -39
- package/docs/reference/functions/defineContracts.md +0 -36
- package/docs/reference/functions/disabled.md +0 -28
- package/docs/reference/functions/enabled.md +0 -20
- package/docs/reference/functions/focused.md +0 -26
- package/docs/reference/functions/mockOf.md +0 -79
- package/docs/reference/functions/postgres.md +0 -31
- package/docs/reference/functions/process.md +0 -33
- package/docs/reference/functions/redis.md +0 -28
- package/docs/reference/functions/required.md +0 -41
- package/docs/reference/functions/selected.md +0 -26
- package/docs/reference/functions/sqlite.md +0 -33
- package/docs/reference/functions/testId.md +0 -20
- package/docs/reference/functions/text.md +0 -40
- package/docs/reference/functions/valued.md +0 -28
- package/docs/reference/functions/waitUntil.md +0 -46
- package/docs/reference/functions/within.md +0 -29
- package/docs/reference/index.md +0 -196
- package/docs/reference/interfaces/BaseResult.md +0 -87
- package/docs/reference/interfaces/CallResult.md +0 -160
- package/docs/reference/interfaces/CaptureScope.md +0 -82
- package/docs/reference/interfaces/CliResult.md +0 -253
- package/docs/reference/interfaces/ContainerAccessor.md +0 -166
- package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
- package/docs/reference/interfaces/FetchResult.md +0 -180
- package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
- package/docs/reference/interfaces/HttpResult.md +0 -126
- package/docs/reference/interfaces/JsonAccessor.md +0 -46
- package/docs/reference/interfaces/PageResult.md +0 -356
- package/docs/reference/interfaces/RenderResult.md +0 -72
- package/docs/reference/interfaces/RenderedText.md +0 -110
- package/docs/reference/interfaces/ResponseAccessor.md +0 -47
- package/docs/reference/interfaces/ScreenResult.md +0 -128
- package/docs/reference/interfaces/TableAccessor.md +0 -26
- package/docs/reference/interfaces/TextAccessor.md +0 -96
- package/docs/reference/type-aliases/ApiHandle.md +0 -68
- package/docs/reference/type-aliases/ApiSpecification.md +0 -213
- package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
- package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
- package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
- package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
- package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
- package/docs/reference/type-aliases/BrowserPage.md +0 -132
- package/docs/reference/type-aliases/BrowserPort.md +0 -51
- package/docs/reference/type-aliases/CliEnv.md +0 -11
- package/docs/reference/type-aliases/CliHandle.md +0 -80
- package/docs/reference/type-aliases/CliInput.md +0 -36
- package/docs/reference/type-aliases/CliOutput.md +0 -46
- package/docs/reference/type-aliases/CliPort.md +0 -58
- package/docs/reference/type-aliases/CliSpecification.md +0 -144
- package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
- package/docs/reference/type-aliases/ComponentChain.md +0 -156
- package/docs/reference/type-aliases/ComponentScenario.md +0 -20
- package/docs/reference/type-aliases/ComponentUi.md +0 -47
- package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
- package/docs/reference/type-aliases/ContainerPort.md +0 -113
- package/docs/reference/type-aliases/Contract.md +0 -68
- package/docs/reference/type-aliases/ContractInput.md +0 -13
- package/docs/reference/type-aliases/ContractRequest.md +0 -95
- package/docs/reference/type-aliases/ContractResponder.md +0 -23
- package/docs/reference/type-aliases/ContractResponse.md +0 -78
- package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
- package/docs/reference/type-aliases/Contracts.md +0 -48
- package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
- package/docs/reference/type-aliases/DatabasePort.md +0 -72
- package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
- package/docs/reference/type-aliases/DevicePort.md +0 -51
- package/docs/reference/type-aliases/DeviceScreen.md +0 -32
- package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
- package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
- package/docs/reference/type-aliases/DomMount.md +0 -21
- package/docs/reference/type-aliases/ElementKind.md +0 -27
- package/docs/reference/type-aliases/ElementMatch.md +0 -74
- package/docs/reference/type-aliases/ElementOptions.md +0 -29
- package/docs/reference/type-aliases/ElementRef.md +0 -128
- package/docs/reference/type-aliases/ExecOptions.md +0 -37
- package/docs/reference/type-aliases/FileAccessor.md +0 -54
- package/docs/reference/type-aliases/HonoApp.md +0 -31
- package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
- package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
- package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
- package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
- package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
- package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
- package/docs/reference/type-aliases/Intercept.md +0 -19
- package/docs/reference/type-aliases/InterceptOptions.md +0 -28
- package/docs/reference/type-aliases/InterceptScope.md +0 -11
- package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
- package/docs/reference/type-aliases/JobHandle.md +0 -34
- package/docs/reference/type-aliases/JobsHandle.md +0 -57
- package/docs/reference/type-aliases/JobsResult.md +0 -16
- package/docs/reference/type-aliases/JobsSpecification.md +0 -94
- package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
- package/docs/reference/type-aliases/LandmarkKind.md +0 -20
- package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
- package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
- package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
- package/docs/reference/type-aliases/MatchableRequest.md +0 -61
- package/docs/reference/type-aliases/MatcherKind.md +0 -34
- package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
- package/docs/reference/type-aliases/MobileElementKind.md +0 -14
- package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
- package/docs/reference/type-aliases/MobileElementRef.md +0 -20
- package/docs/reference/type-aliases/MobileHandle.md +0 -65
- package/docs/reference/type-aliases/MobileScenario.md +0 -20
- package/docs/reference/type-aliases/MobileSpecification.md +0 -51
- package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
- package/docs/reference/type-aliases/MobileVisitor.md +0 -81
- package/docs/reference/type-aliases/MockOfOptions.md +0 -26
- package/docs/reference/type-aliases/MockPort.md +0 -10
- package/docs/reference/type-aliases/PinnedClock.md +0 -10
- package/docs/reference/type-aliases/PostgresOptions.md +0 -32
- package/docs/reference/type-aliases/ProcessOptions.md +0 -112
- package/docs/reference/type-aliases/RedisOptions.md +0 -20
- package/docs/reference/type-aliases/RenderSubject.md +0 -12
- package/docs/reference/type-aliases/ScreenNode.md +0 -67
- package/docs/reference/type-aliases/ServerPort.md +0 -36
- package/docs/reference/type-aliases/ServerResponse.md +0 -46
- package/docs/reference/type-aliases/ServerSpec.md +0 -21
- package/docs/reference/type-aliases/ServiceHandle.md +0 -216
- package/docs/reference/type-aliases/ServiceRecord.md +0 -12
- package/docs/reference/type-aliases/SpecDocument.md +0 -84
- package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
- package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
- package/docs/reference/type-aliases/SpecFixture.md +0 -30
- package/docs/reference/type-aliases/SpecKind.md +0 -8
- package/docs/reference/type-aliases/SpecRun.md +0 -108
- package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
- package/docs/reference/type-aliases/SpecStream.md +0 -32
- package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
- package/docs/reference/type-aliases/SqliteOptions.md +0 -36
- package/docs/reference/type-aliases/SseEvent.md +0 -58
- package/docs/reference/type-aliases/StreamBody.md +0 -61
- package/docs/reference/type-aliases/TextFilter.md +0 -22
- package/docs/reference/type-aliases/VisitScenario.md +0 -20
- package/docs/reference/type-aliases/Visitor.md +0 -215
- package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
- package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
- package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
- package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
- package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
- package/docs/reference/variables/anthropic.md +0 -19
- package/docs/reference/variables/banner.md +0 -10
- package/docs/reference/variables/button.md +0 -10
- package/docs/reference/variables/clock.md +0 -16
- package/docs/reference/variables/complementary.md +0 -10
- package/docs/reference/variables/component.md +0 -10
- package/docs/reference/variables/content.md +0 -10
- package/docs/reference/variables/contentinfo.md +0 -10
- package/docs/reference/variables/dialog.md +0 -10
- package/docs/reference/variables/field.md +0 -10
- package/docs/reference/variables/form.md +0 -10
- package/docs/reference/variables/heading.md +0 -10
- package/docs/reference/variables/http.md +0 -40
- package/docs/reference/variables/intercept.md +0 -11
- package/docs/reference/variables/link.md +0 -10
- package/docs/reference/variables/listitem.md +0 -10
- package/docs/reference/variables/main.md +0 -10
- package/docs/reference/variables/match.md +0 -8
- package/docs/reference/variables/navigation.md +0 -10
- package/docs/reference/variables/openai.md +0 -21
- package/docs/reference/variables/option.md +0 -17
- package/docs/reference/variables/region.md +0 -10
- package/docs/reference/variables/row.md +0 -10
- package/docs/reference/variables/search.md +0 -10
- package/docs/reference/variables/specification.md +0 -8
- package/docs/reference/variables/status.md +0 -10
- package/docs/reference/variables/table.md +0 -10
package/schema/spec.schema.json
CHANGED
|
@@ -10,8 +10,23 @@
|
|
|
10
10
|
"type": "string"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
|
+
"allOf": [
|
|
14
|
+
{
|
|
15
|
+
"anyOf": [
|
|
16
|
+
{
|
|
17
|
+
"required": [
|
|
18
|
+
"contains"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"required": [
|
|
23
|
+
"equals"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
],
|
|
13
29
|
"additionalProperties": false,
|
|
14
|
-
"minProperties": 1,
|
|
15
30
|
"properties": {
|
|
16
31
|
"contains": {
|
|
17
32
|
"oneOf": [
|
|
@@ -36,6 +51,20 @@
|
|
|
36
51
|
]
|
|
37
52
|
},
|
|
38
53
|
"run": {
|
|
54
|
+
"allOf": [
|
|
55
|
+
{
|
|
56
|
+
"not": {
|
|
57
|
+
"required": [
|
|
58
|
+
"stdin",
|
|
59
|
+
"waitFor"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"required": [
|
|
65
|
+
"command",
|
|
66
|
+
"exit"
|
|
67
|
+
],
|
|
39
68
|
"additionalProperties": false,
|
|
40
69
|
"properties": {
|
|
41
70
|
"command": {
|
|
@@ -74,10 +103,6 @@
|
|
|
74
103
|
"type": "string"
|
|
75
104
|
}
|
|
76
105
|
},
|
|
77
|
-
"required": [
|
|
78
|
-
"command",
|
|
79
|
-
"exit"
|
|
80
|
-
],
|
|
81
106
|
"type": "object"
|
|
82
107
|
},
|
|
83
108
|
"serveEntry": {
|
|
@@ -102,10 +127,10 @@
|
|
|
102
127
|
"$id": "https://jterrazz.com/schema/jterrazz-test/spec.schema.json",
|
|
103
128
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
104
129
|
"additionalProperties": false,
|
|
105
|
-
"description": "One
|
|
130
|
+
"description": "One case of the @jterrazz/test literate format, one file per test: the ground it stands on, what it does, and what came back.",
|
|
106
131
|
"properties": {
|
|
107
132
|
"description": {
|
|
108
|
-
"description": "The vitest test title
|
|
133
|
+
"description": "The vitest test title: one lowercase line, no trailing period.",
|
|
109
134
|
"type": "string"
|
|
110
135
|
},
|
|
111
136
|
"env": {
|
|
@@ -124,33 +149,34 @@
|
|
|
124
149
|
"description": "A bare word names an env set registered in code; KEY=value is inline. $WORKDIR expands."
|
|
125
150
|
},
|
|
126
151
|
"fixture": {
|
|
152
|
+
"description": "Fixture paths copied into the working directory, layering in order, each relative to this document (`./...`, `../...`) and in the two forms the chain's `file()` and `directory()` take: a trailing `/` names a directory whose contents are spread, without it one file is copied under its own name. The first ground folder a path passes through is __fixtures__/: ./__fixtures__/ beside the document, or specs/__fixtures__/ reached by the path that climbs to it.",
|
|
127
153
|
"oneOf": [
|
|
128
154
|
{
|
|
155
|
+
"pattern": "^\\.\\.?/(?:(?!(?:__expected__|__fixtures__|__requests__|__seeds__)/)[^/]+/)*?__fixtures__/.*$",
|
|
129
156
|
"type": "string"
|
|
130
157
|
},
|
|
131
158
|
{
|
|
132
159
|
"items": {
|
|
160
|
+
"pattern": "^\\.\\.?/(?:(?!(?:__expected__|__fixtures__|__requests__|__seeds__)/)[^/]+/)*?__fixtures__/.*$",
|
|
133
161
|
"type": "string"
|
|
134
162
|
},
|
|
135
163
|
"minItems": 1,
|
|
136
164
|
"type": "array"
|
|
137
165
|
}
|
|
138
|
-
]
|
|
139
|
-
"description": "Fixture paths copied into the working directory, layering in order. `$FIXTURES/<name>` is the shared pool at `<specs-root>/_fixtures/`; a bare path is the ground beside this document, at `_fixtures/<name>`."
|
|
140
|
-
},
|
|
141
|
-
"kind": {
|
|
142
|
-
"default": "cli",
|
|
143
|
-
"description": "The facet this document describes.",
|
|
144
|
-
"enum": [
|
|
145
|
-
"cli"
|
|
146
|
-
],
|
|
147
|
-
"type": "string"
|
|
166
|
+
]
|
|
148
167
|
},
|
|
149
168
|
"runs": {
|
|
169
|
+
"contains": {
|
|
170
|
+
"required": [
|
|
171
|
+
"waitFor"
|
|
172
|
+
]
|
|
173
|
+
},
|
|
150
174
|
"description": "The session: one or more runs, sequential, sharing one working directory.",
|
|
151
175
|
"items": {
|
|
152
176
|
"$ref": "#/$defs/run"
|
|
153
177
|
},
|
|
178
|
+
"maxContains": 1,
|
|
179
|
+
"minContains": 0,
|
|
154
180
|
"minItems": 1,
|
|
155
181
|
"type": "array"
|
|
156
182
|
},
|
package/dist/appium.adapter.js
DELETED
|
@@ -1,489 +0,0 @@
|
|
|
1
|
-
import { a as formatElement, r as AmbiguousElementError, t as warnSubstringOnly } from "./substring-warning.js";
|
|
2
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
//#region src/facets/mobile/ambiguity.ts
|
|
6
|
-
/**
|
|
7
|
-
* The ambiguity refusal, mobile edition — CONVENTIONS W3.
|
|
8
|
-
*
|
|
9
|
-
* A mobile descriptor must designate exactly one element. Acting on "the
|
|
10
|
-
* first match" is the failure mode this module exists to prevent: the spec
|
|
11
|
-
* keeps passing while the visitor taps something else, and nothing ever
|
|
12
|
-
* reports it. So when a descriptor matches several elements the framework
|
|
13
|
-
* refuses, and the refusal carries everything needed to fix it without
|
|
14
|
-
* opening the simulator — the descriptor in the caller's own vocabulary,
|
|
15
|
-
* every candidate, and the concrete rewrites that would resolve it.
|
|
16
|
-
*
|
|
17
|
-
* The element vocabulary is shared with the website facet, so the source
|
|
18
|
-
* rendering (`formatElement`) is too; only the evidence differs — XCUITest
|
|
19
|
-
* types, labels and accessibility identifiers instead of tags and landmarks.
|
|
20
|
-
* Pure string building: the appium integration stays a thin adapter.
|
|
21
|
-
*/
|
|
22
|
-
/** `1. Button "Bookmark" [testId: event-42]` — one evidence line per candidate. */
|
|
23
|
-
function formatMatch(match, index) {
|
|
24
|
-
const label = match.label === void 0 ? "" : ` ${JSON.stringify(match.label)}`;
|
|
25
|
-
const value = match.value === void 0 ? "" : ` value=${JSON.stringify(match.value)}`;
|
|
26
|
-
const identifier = match.identifier === void 0 ? "" : ` [testId: ${match.identifier}]`;
|
|
27
|
-
return ` ${index + 1}. ${match.type}${label}${value}${identifier}`;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* The rewrites worth offering, in the order they should be tried. Scoping is
|
|
31
|
-
* always available; `exact` only to a descriptor that opted OUT of it (exact
|
|
32
|
-
* is the default), and the count it would leave is stated so a
|
|
33
|
-
* still-ambiguous suggestion never reads as a fix; distinct identifiers make
|
|
34
|
-
* `testId()` a concrete rewrite rather than a guess.
|
|
35
|
-
*/
|
|
36
|
-
function formatFixes(element, matches) {
|
|
37
|
-
const fixes = [];
|
|
38
|
-
if (!element.scope) fixes.push(`scope it within(testId('…'), ${formatElement(element)}) — a screen has no landmarks; any descriptor works as the scope`);
|
|
39
|
-
if (element.exact === false && element.name !== void 0) {
|
|
40
|
-
const remaining = matches.filter((match) => match.label === element.name).length;
|
|
41
|
-
if (remaining > 0 && remaining < matches.length) fixes.push(`exact name ${formatElement({
|
|
42
|
-
exact: true,
|
|
43
|
-
kind: element.kind,
|
|
44
|
-
name: element.name
|
|
45
|
-
})} [leaves ${remaining} of ${matches.length}]`);
|
|
46
|
-
}
|
|
47
|
-
const identifiers = [...new Set(matches.map((match) => match.identifier).filter((identifier) => identifier !== void 0))];
|
|
48
|
-
if (identifiers.length > 1 && element.kind !== "testId") fixes.push(`test id testId(${JSON.stringify(identifiers[0])}) [also here: ${identifiers.slice(1).join(", ")}]`);
|
|
49
|
-
fixes.push("other element a button() or field() may name one thing where this does not");
|
|
50
|
-
return fixes;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Build the refusal thrown when a descriptor matches more than one element
|
|
54
|
-
* on the screen.
|
|
55
|
-
*
|
|
56
|
-
* @param options.element The descriptor as the caller wrote it.
|
|
57
|
-
* @param options.matches Every candidate, in screen order (already truncated).
|
|
58
|
-
*/
|
|
59
|
-
function describeMobileAmbiguity(options) {
|
|
60
|
-
const { element, matches } = options;
|
|
61
|
-
const fixes = formatFixes(element, matches).map((fix) => ` • ${fix}`);
|
|
62
|
-
return [
|
|
63
|
-
`Ambiguous element: ${formatElement(element)} matched ${matches.length} elements on the screen.`,
|
|
64
|
-
"",
|
|
65
|
-
"A spec must designate exactly one element. Acting on the first match would let",
|
|
66
|
-
"this test keep passing while the visitor taps something else.",
|
|
67
|
-
"",
|
|
68
|
-
"Matched:",
|
|
69
|
-
...matches.map((match, index) => formatMatch(match, index)),
|
|
70
|
-
"",
|
|
71
|
-
"Disambiguate with one of:",
|
|
72
|
-
...fixes,
|
|
73
|
-
"",
|
|
74
|
-
"Docs: docs/13-elements.md#designating-exactly-one-element (CONVENTIONS W3)"
|
|
75
|
-
].join("\n");
|
|
76
|
-
}
|
|
77
|
-
//#endregion
|
|
78
|
-
//#region src/facets/mobile/projection.ts
|
|
79
|
-
const TAG_PATTERN = /<(?<openTag>[A-Za-z][\w.]*)(?<rawAttributes>(?:\s+[\w:.-]+="[^"]*")*)\s*(?<selfClosing>\/?)>|<\/(?<closeTag>[\w.]*)\s*>/gu;
|
|
80
|
-
const ATTRIBUTE_PATTERN = /(?<key>[\w:.-]+)="(?<value>[^"]*)"/gu;
|
|
81
|
-
const NAMED_ENTITIES = {
|
|
82
|
-
"&": "&",
|
|
83
|
-
"'": "'",
|
|
84
|
-
">": ">",
|
|
85
|
-
"<": "<",
|
|
86
|
-
""": "\""
|
|
87
|
-
};
|
|
88
|
-
/** Decode the XML entities an attribute value can carry. */
|
|
89
|
-
function decodeEntities(value) {
|
|
90
|
-
return value.replaceAll(/&(?:#x(?<hex>[\dA-Fa-f]+)|#(?<decimal>\d+)|(?<named>amp|apos|gt|lt|quot));/gu, (entity, hex, decimal) => {
|
|
91
|
-
if (hex) return String.fromCodePoint(Number.parseInt(hex, 16));
|
|
92
|
-
if (decimal) return String.fromCodePoint(Number(decimal));
|
|
93
|
-
return NAMED_ENTITIES[entity] ?? entity;
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
/** Parse the page source into a raw element tree (strict, attribute-only XML). */
|
|
97
|
-
function parsePageSource(xml) {
|
|
98
|
-
const stack = [];
|
|
99
|
-
let root = null;
|
|
100
|
-
for (const match of xml.matchAll(TAG_PATTERN)) {
|
|
101
|
-
const { closeTag, openTag, rawAttributes, selfClosing } = match.groups ?? {};
|
|
102
|
-
if (closeTag !== void 0 || openTag === void 0) {
|
|
103
|
-
stack.pop();
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
const attributes = {};
|
|
107
|
-
for (const attribute of (rawAttributes ?? "").matchAll(ATTRIBUTE_PATTERN)) {
|
|
108
|
-
const { key, value } = attribute.groups ?? {};
|
|
109
|
-
if (key !== void 0 && value !== void 0) attributes[key] = decodeEntities(value);
|
|
110
|
-
}
|
|
111
|
-
const node = {
|
|
112
|
-
attributes,
|
|
113
|
-
children: [],
|
|
114
|
-
tag: openTag
|
|
115
|
-
};
|
|
116
|
-
const parent = stack.at(-1);
|
|
117
|
-
if (parent === void 0) root = node;
|
|
118
|
-
else parent.children.push(node);
|
|
119
|
-
if (!selfClosing) stack.push(node);
|
|
120
|
-
}
|
|
121
|
-
if (!root) throw new Error("projectScreen(): the page source contains no XML element");
|
|
122
|
-
return root;
|
|
123
|
-
}
|
|
124
|
-
/** The substance of a node — what survives projection, or nothing. */
|
|
125
|
-
function substance(node) {
|
|
126
|
-
const label = node.attributes.label || void 0;
|
|
127
|
-
const value = node.attributes.value || void 0;
|
|
128
|
-
const identifier = node.attributes.name || void 0;
|
|
129
|
-
return {
|
|
130
|
-
type: node.tag.replace(/^XCUIElementType/u, ""),
|
|
131
|
-
...label === void 0 ? {} : { label },
|
|
132
|
-
...value === void 0 || value === label ? {} : { value },
|
|
133
|
-
...identifier === void 0 || identifier === label ? {} : { identifier }
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
function hasSubstance(node) {
|
|
137
|
-
return node.label !== void 0 || node.value !== void 0 || node.identifier !== void 0;
|
|
138
|
-
}
|
|
139
|
-
/** A childless child that repeats its parent is the same accessibility element twice. */
|
|
140
|
-
function repeatsParent(child, parent) {
|
|
141
|
-
return child.children === void 0 && child.type === parent.type && child.label === parent.label && child.value === parent.value && child.identifier === parent.identifier;
|
|
142
|
-
}
|
|
143
|
-
/** Project one raw node: a kept node, or (hoisting) the projections of its children. */
|
|
144
|
-
function projectNode(node) {
|
|
145
|
-
const own = substance(node);
|
|
146
|
-
const children = node.children.flatMap((child) => projectNode(child)).filter((child) => !repeatsParent(child, own));
|
|
147
|
-
if (!hasSubstance(own)) return children;
|
|
148
|
-
return [{
|
|
149
|
-
...own,
|
|
150
|
-
...children.length > 0 ? { children } : {}
|
|
151
|
-
}];
|
|
152
|
-
}
|
|
153
|
-
/** Collect the visible texts in document order, collapsing consecutive repeats. */
|
|
154
|
-
function collectTexts(node, texts) {
|
|
155
|
-
if (node.attributes.visible === "true") {
|
|
156
|
-
const text = node.attributes.label || node.attributes.value || void 0;
|
|
157
|
-
if (text !== void 0 && texts.at(-1) !== text) texts.push(text);
|
|
158
|
-
}
|
|
159
|
-
for (const child of node.children) collectTexts(child, texts);
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Project a raw XCUITest page source into the captured screen: the collapsed
|
|
163
|
-
* tree (rooted at the application node, which is always kept) and the
|
|
164
|
-
* visible texts in document order.
|
|
165
|
-
*/
|
|
166
|
-
function projectScreen(xml) {
|
|
167
|
-
let root = parsePageSource(xml);
|
|
168
|
-
const [onlyChild] = root.children;
|
|
169
|
-
if (root.tag === "AppiumAUT" && root.children.length === 1 && onlyChild !== void 0) root = onlyChild;
|
|
170
|
-
const texts = [];
|
|
171
|
-
for (const child of root.children) collectTexts(child, texts);
|
|
172
|
-
const own = substance(root);
|
|
173
|
-
const children = root.children.flatMap((child) => projectNode(child)).filter((child) => !repeatsParent(child, own));
|
|
174
|
-
return {
|
|
175
|
-
texts,
|
|
176
|
-
tree: {
|
|
177
|
-
...own,
|
|
178
|
-
...children.length > 0 ? { children } : {}
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
//#endregion
|
|
183
|
-
//#region src/seams/appium/appium.adapter.ts
|
|
184
|
-
const DEFAULT_ACTION_TIMEOUT_MS = 3e4;
|
|
185
|
-
const POLL_INTERVAL_MS = 500;
|
|
186
|
-
/** How many candidates the ambiguity error enumerates before truncating. */
|
|
187
|
-
const MAX_REPORTED_MATCHES = 10;
|
|
188
|
-
/** WebDriverAgent builds once per simulator — the first session is the slow one. */
|
|
189
|
-
const DEFAULT_WDA_LAUNCH_TIMEOUT_MS = 24e4;
|
|
190
|
-
const delay = async (ms) => {
|
|
191
|
-
await new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
192
|
-
};
|
|
193
|
-
/** Escape a name for an NSPredicate single-quoted string literal. */
|
|
194
|
-
function escapePredicate(value) {
|
|
195
|
-
return value.replaceAll("\\", String.raw`\\`).replaceAll("'", String.raw`\'`);
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Translate a user-facing descriptor into an iOS predicate string. Landmarks
|
|
199
|
-
* have no XCUITest analog — the shared vocabulary is wider than a screen,
|
|
200
|
-
* and the boundary is named rather than silently approximated.
|
|
201
|
-
*/
|
|
202
|
-
function compilePredicate(element, options) {
|
|
203
|
-
const name = escapePredicate(element.name ?? "");
|
|
204
|
-
const contains = (attribute) => element.exact === false ? `${attribute} CONTAINS '${name}'` : `${attribute} == '${name}'`;
|
|
205
|
-
const visible = options?.anyVisibility ? "1 == 1" : "visible == 1";
|
|
206
|
-
switch (element.kind) {
|
|
207
|
-
case "button": return `type == 'XCUIElementTypeButton' AND ${contains("label")} AND ${visible}`;
|
|
208
|
-
case "field": return `type IN {'XCUIElementTypeTextField','XCUIElementTypeSecureTextField'} AND (${contains("label")} OR ${contains("value")}) AND ${visible}`;
|
|
209
|
-
case "testId": return `name == '${name}' AND ${visible}`;
|
|
210
|
-
case "text": return `(${contains("label")} OR ${contains("value")}) AND ${visible}`;
|
|
211
|
-
default: throw new Error(`${formatElement(element)}: landmarks are a website concept — an iOS screen has no '${element.kind}' region. Scope with within(testId('…'), …) instead.`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Every element a predicate designates under `scope`.
|
|
216
|
-
*
|
|
217
|
-
* ONE place awaits webdriverio's query: `$$` answers a chainable typed as the
|
|
218
|
-
* array it resolves to, so the await that makes the array real reads to the
|
|
219
|
-
* typechecker as an await of a non-promise.
|
|
220
|
-
*/
|
|
221
|
-
async function matching(scope, element, options) {
|
|
222
|
-
return await scope.$$(`-ios predicate string:${compilePredicate(element, options)}`);
|
|
223
|
-
}
|
|
224
|
-
/** The outside-in scope chain of a descriptor, ending on the target itself. */
|
|
225
|
-
function scopeChain(element) {
|
|
226
|
-
const chain = [];
|
|
227
|
-
for (let current = element; current; current = current.scope) chain.unshift(current);
|
|
228
|
-
return chain;
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Load webdriverio's session factory, naming the install when the optional
|
|
232
|
-
* peer dependency is absent. Lazy: the dependency is only required by a spec
|
|
233
|
-
* that actually opens the app.
|
|
234
|
-
*/
|
|
235
|
-
async function loadRemote() {
|
|
236
|
-
try {
|
|
237
|
-
const { remote: remoteFn } = await import("webdriverio");
|
|
238
|
-
return remoteFn;
|
|
239
|
-
} catch {
|
|
240
|
-
throw new Error(".open() requires webdriverio (optional peer dependency): npm install -D appium webdriverio && npx appium driver install xcuitest");
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
/** Capture one candidate's evidence for the ambiguity refusal. */
|
|
244
|
-
async function captureMatch(element) {
|
|
245
|
-
const [type, label, value, identifier] = await Promise.all([
|
|
246
|
-
element.getAttribute("type"),
|
|
247
|
-
element.getAttribute("label"),
|
|
248
|
-
element.getAttribute("value"),
|
|
249
|
-
element.getAttribute("name")
|
|
250
|
-
]);
|
|
251
|
-
const shortLabel = label ? label.replaceAll(/\s+/gu, " ").trim().slice(0, 80) : void 0;
|
|
252
|
-
return {
|
|
253
|
-
type: (type ?? "Unknown").replace(/^XCUIElementType/u, ""),
|
|
254
|
-
...shortLabel === void 0 ? {} : { label: shortLabel },
|
|
255
|
-
...value && value !== label ? { value } : {},
|
|
256
|
-
...identifier && identifier !== label ? { identifier } : {}
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Device adapter backed by appium's XCUITest driver over webdriverio.
|
|
261
|
-
*
|
|
262
|
-
* ONE driver session per adapter (= per runner = per vitest worker), created
|
|
263
|
-
* lazily on the first `open()` and reused — WebDriverAgent startup is the
|
|
264
|
-
* expensive part, not the app relaunch. Every `open()` terminates and
|
|
265
|
-
* relaunches the app, so each spec starts from a deterministic fresh state.
|
|
266
|
-
*
|
|
267
|
-
* Webdriverio is an optional peer dependency: it is only imported here, and
|
|
268
|
-
* this module is only loaded when a spec calls `.open()`.
|
|
269
|
-
*/
|
|
270
|
-
var AppiumAdapter = class {
|
|
271
|
-
/** The verb poll budget — the runner's `timeouts.action`, or the default. */
|
|
272
|
-
actionTimeoutMs;
|
|
273
|
-
driver = null;
|
|
274
|
-
/** The WebDriverAgent launch budget — the runner's `timeouts.launch`, or the default. */
|
|
275
|
-
launchTimeoutMs;
|
|
276
|
-
options;
|
|
277
|
-
/**
|
|
278
|
-
* The session factory. Absent in production — webdriverio is then loaded
|
|
279
|
-
* lazily on the first `open()`. Injected by this module's own unit test,
|
|
280
|
-
* which drives the waits over a stub driver with no simulator in reach
|
|
281
|
-
* (mocks are CODE here, CONVENTIONS I4).
|
|
282
|
-
*/
|
|
283
|
-
remote;
|
|
284
|
-
constructor(options) {
|
|
285
|
-
this.options = {
|
|
286
|
-
serverUrl: options.serverUrl,
|
|
287
|
-
udid: options.udid
|
|
288
|
-
};
|
|
289
|
-
this.remote = options.remote;
|
|
290
|
-
this.actionTimeoutMs = options.timeouts?.action ?? DEFAULT_ACTION_TIMEOUT_MS;
|
|
291
|
-
this.launchTimeoutMs = options.timeouts?.launch ?? DEFAULT_WDA_LAUNCH_TIMEOUT_MS;
|
|
292
|
-
}
|
|
293
|
-
async close() {
|
|
294
|
-
if (this.driver) {
|
|
295
|
-
await this.driver.deleteSession();
|
|
296
|
-
this.driver = null;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
async open(options) {
|
|
300
|
-
const driver = await this.session(options.bundleId);
|
|
301
|
-
try {
|
|
302
|
-
await driver.executeScript("mobile: terminateApp", [{ bundleId: options.bundleId }]);
|
|
303
|
-
} catch {}
|
|
304
|
-
await (options.deepLink === void 0 ? driver.executeScript("mobile: activateApp", [{ bundleId: options.bundleId }]) : driver.executeScript("mobile: deepLink", [{
|
|
305
|
-
bundleId: options.bundleId,
|
|
306
|
-
url: options.deepLink
|
|
307
|
-
}]));
|
|
308
|
-
if (options.scenario) try {
|
|
309
|
-
await options.scenario(this.createVisitor(driver));
|
|
310
|
-
} catch (error) {
|
|
311
|
-
const evidence = await this.captureEvidence(driver);
|
|
312
|
-
const suffix = evidence ? `\nEvidence: ${evidence}` : "";
|
|
313
|
-
if (error instanceof AmbiguousElementError) {
|
|
314
|
-
error.message += suffix;
|
|
315
|
-
throw error;
|
|
316
|
-
}
|
|
317
|
-
throw new Error(`open scenario failed: ${error instanceof Error ? error.message : String(error)}${suffix}`, { cause: error });
|
|
318
|
-
}
|
|
319
|
-
return projectScreen(await driver.getPageSource());
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* The visitor implementation. Acting verbs (tap/fill) poll until exactly
|
|
323
|
-
* one visible match exists (CONVENTIONS W3); `see()` acts on nothing, so
|
|
324
|
-
* ANY visible match satisfies it — XCUITest trees legitimately expose the
|
|
325
|
-
* same text more than once (container/child label duplication), and a
|
|
326
|
-
* synchronization primitive refusing on that would punish honest screens.
|
|
327
|
-
*/
|
|
328
|
-
createVisitor(driver) {
|
|
329
|
-
return {
|
|
330
|
-
fill: async (element, value) => {
|
|
331
|
-
await (await this.resolveOne(driver, element, "fill", "one")).setValue(value);
|
|
332
|
-
},
|
|
333
|
-
see: async (element) => {
|
|
334
|
-
await this.resolveOne(driver, element, "see", "any");
|
|
335
|
-
},
|
|
336
|
-
tap: async (element) => {
|
|
337
|
-
await (await this.resolveOne(driver, element, "tap", "one")).click();
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Resolve a descriptor: poll until at least one visible match exists,
|
|
343
|
-
* then enforce the verb's cardinality — `'one'` refuses ambiguity
|
|
344
|
-
* (CONVENTIONS W3), `'any'` settles for the first match. Scopes stay
|
|
345
|
-
* strict in BOTH modes: "within one of several containers" designates
|
|
346
|
-
* nothing, whatever the verb. The chain resolves outside-in, so an
|
|
347
|
-
* ambiguous scope names ITSELF as the fault rather than sending the
|
|
348
|
-
* author to fix the target.
|
|
349
|
-
*/
|
|
350
|
-
async resolveOne(driver, element, verb, cardinality) {
|
|
351
|
-
const chain = scopeChain(element);
|
|
352
|
-
const deadline = Date.now() + this.actionTimeoutMs;
|
|
353
|
-
let scrollAttempted = false;
|
|
354
|
-
let windowAsked = false;
|
|
355
|
-
let looking = chain;
|
|
356
|
-
for (;;) {
|
|
357
|
-
const resolved = await this.resolveChain(driver, looking, cardinality, { tryScroll: !scrollAttempted });
|
|
358
|
-
scrollAttempted = true;
|
|
359
|
-
if (resolved) return resolved;
|
|
360
|
-
if (!windowAsked) {
|
|
361
|
-
windowAsked = true;
|
|
362
|
-
looking = await this.widenedChain(driver, chain) ?? chain;
|
|
363
|
-
}
|
|
364
|
-
if (Date.now() > deadline) throw await this.timeoutError(driver, element, verb);
|
|
365
|
-
await delay(POLL_INTERVAL_MS);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
/** One resolution pass over the chain — `null` means "nothing yet, keep polling". */
|
|
369
|
-
async resolveChain(driver, chain, cardinality, options) {
|
|
370
|
-
let scope = driver;
|
|
371
|
-
for (const [index, level] of chain.entries()) {
|
|
372
|
-
const matches = await matching(scope, level);
|
|
373
|
-
const count = await matches.length;
|
|
374
|
-
if (count === 0) {
|
|
375
|
-
if (options?.tryScroll) {
|
|
376
|
-
const first = (await matching(scope, level, { anyVisibility: true }))[0];
|
|
377
|
-
if (first !== void 0) try {
|
|
378
|
-
await driver.executeScript("mobile: scroll", [{
|
|
379
|
-
elementId: first.elementId,
|
|
380
|
-
toVisible: true
|
|
381
|
-
}]);
|
|
382
|
-
} catch {}
|
|
383
|
-
}
|
|
384
|
-
return null;
|
|
385
|
-
}
|
|
386
|
-
const isTarget = index === chain.length - 1;
|
|
387
|
-
if (count > 1 && (cardinality === "one" || !isTarget)) throw new AmbiguousElementError(describeMobileAmbiguity({
|
|
388
|
-
element: level,
|
|
389
|
-
matches: await this.captureMatches(matches)
|
|
390
|
-
}));
|
|
391
|
-
const next = matches[0];
|
|
392
|
-
if (next === void 0) return null;
|
|
393
|
-
scope = next;
|
|
394
|
-
}
|
|
395
|
-
return scope;
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* The chain to keep polling with, level by level: a descriptor that
|
|
399
|
-
* designated nothing as a whole label but designates exactly one thing as a
|
|
400
|
-
* SUBSTRING is widened, with the warning that names the label to write. The
|
|
401
|
-
* window is the vocabulary's, not one surface's — chapter 13 — so the
|
|
402
|
-
* screen answers it exactly as a page does.
|
|
403
|
-
*
|
|
404
|
-
* `null` when nothing would change, which is the ordinary wait.
|
|
405
|
-
*/
|
|
406
|
-
async widenedChain(driver, chain) {
|
|
407
|
-
let scope = driver;
|
|
408
|
-
const widened = [];
|
|
409
|
-
let changed = false;
|
|
410
|
-
for (const level of chain) {
|
|
411
|
-
const found = await this.widenedLevel(scope, level);
|
|
412
|
-
changed ||= found !== null;
|
|
413
|
-
const candidate = found ?? level;
|
|
414
|
-
const next = (await matching(scope, candidate))[0];
|
|
415
|
-
if (next === void 0) return null;
|
|
416
|
-
scope = next;
|
|
417
|
-
widened.push(candidate);
|
|
418
|
-
}
|
|
419
|
-
return changed ? widened : null;
|
|
420
|
-
}
|
|
421
|
-
/** One level of the chain, widened and warned about, or `null`. */
|
|
422
|
-
async widenedLevel(scope, level) {
|
|
423
|
-
if (level.exact !== void 0 || level.name === void 0) return null;
|
|
424
|
-
if (await (await matching(scope, level)).length > 0) return null;
|
|
425
|
-
const loose = {
|
|
426
|
-
...level,
|
|
427
|
-
exact: false
|
|
428
|
-
};
|
|
429
|
-
const matches = await matching(scope, loose);
|
|
430
|
-
const first = matches[0];
|
|
431
|
-
if (first === void 0 || await matches.length !== 1) return null;
|
|
432
|
-
const evidence = await captureMatch(first);
|
|
433
|
-
warnSubstringOnly(level, this.options.udid, evidence.label);
|
|
434
|
-
return loose;
|
|
435
|
-
}
|
|
436
|
-
/** Enumerate the candidates' evidence, truncated. */
|
|
437
|
-
async captureMatches(matches) {
|
|
438
|
-
const count = Math.min(await matches.length, MAX_REPORTED_MATCHES);
|
|
439
|
-
const evidence = [];
|
|
440
|
-
for (let index = 0; index < count; index++) {
|
|
441
|
-
const match = matches[index];
|
|
442
|
-
if (match !== void 0) evidence.push(await captureMatch(match));
|
|
443
|
-
}
|
|
444
|
-
return evidence;
|
|
445
|
-
}
|
|
446
|
-
/** Screenshot the failing state into a temp file; never masks the original error. */
|
|
447
|
-
async captureEvidence(driver) {
|
|
448
|
-
try {
|
|
449
|
-
const dir = mkdtempSync(resolve(tmpdir(), "spec-mobile-"));
|
|
450
|
-
const path = resolve(dir, "failure.png");
|
|
451
|
-
writeFileSync(path, Buffer.from(await driver.takeScreenshot(), "base64"));
|
|
452
|
-
return path;
|
|
453
|
-
} catch {
|
|
454
|
-
return null;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
/** The timeout refusal, with a compact excerpt of what IS on screen to aid diagnosis. */
|
|
458
|
-
async timeoutError(driver, element, verb) {
|
|
459
|
-
let excerpt = "";
|
|
460
|
-
try {
|
|
461
|
-
const source = await driver.getPageSource();
|
|
462
|
-
const labels = [...new Set([...source.matchAll(/label="(?<label>[^"]{2,60})"/gu)].map((match) => match.groups?.label ?? ""))].slice(0, 15);
|
|
463
|
-
if (labels.length > 0) excerpt = `\nCurrently on screen: ${labels.join(" | ")}`;
|
|
464
|
-
} catch {}
|
|
465
|
-
return /* @__PURE__ */ new Error(`${verb}(${formatElement(element)}) timed out after ${this.actionTimeoutMs}ms — no visible match.${excerpt}`);
|
|
466
|
-
}
|
|
467
|
-
/** Create the shared driver session (once), with an actionable error when webdriverio is absent. */
|
|
468
|
-
async session(bundleId) {
|
|
469
|
-
if (this.driver) return this.driver;
|
|
470
|
-
const remoteFn = this.remote ?? await loadRemote();
|
|
471
|
-
const url = new URL(this.options.serverUrl);
|
|
472
|
-
this.driver = await remoteFn({
|
|
473
|
-
capabilities: {
|
|
474
|
-
"appium:automationName": "XCUITest",
|
|
475
|
-
"appium:bundleId": bundleId,
|
|
476
|
-
"appium:noReset": true,
|
|
477
|
-
"appium:udid": this.options.udid,
|
|
478
|
-
"appium:wdaLaunchTimeout": this.launchTimeoutMs,
|
|
479
|
-
platformName: "iOS"
|
|
480
|
-
},
|
|
481
|
-
hostname: url.hostname,
|
|
482
|
-
logLevel: "error",
|
|
483
|
-
port: Number(url.port)
|
|
484
|
-
});
|
|
485
|
-
return this.driver;
|
|
486
|
-
}
|
|
487
|
-
};
|
|
488
|
-
//#endregion
|
|
489
|
-
export { AppiumAdapter };
|
package/dist/artifacts.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/** Everything a vitest run generates. */
|
|
2
|
-
const VITEST_ARTIFACTS_DIR = `.artifacts/vitest`;
|
|
3
|
-
/** Where the coverage provider writes its report. */
|
|
4
|
-
const COVERAGE_DIR = `${VITEST_ARTIFACTS_DIR}/coverage`;
|
|
5
|
-
/** Where `sqlite()` caches the schema template it copies per worker. */
|
|
6
|
-
const SQLITE_TEMPLATE_DIR = `${VITEST_ARTIFACTS_DIR}/sqlite`;
|
|
7
|
-
/**
|
|
8
|
-
* Where `context.annotate()` puts what a test attaches, and where Browser Mode
|
|
9
|
-
* drops a failure screenshot. Vitest 5 defaults both to `.vitest/` at the
|
|
10
|
-
* repository ROOT — a second artefact folder beside the one the estate declares,
|
|
11
|
-
* and one no `.gitignore` rule of ours covers.
|
|
12
|
-
*/
|
|
13
|
-
const ATTACHMENTS_DIR = `${VITEST_ARTIFACTS_DIR}/attachments`;
|
|
14
|
-
/** Where the browser provider drops a screenshot of a failing test. */
|
|
15
|
-
const SCREENSHOTS_DIR = `${VITEST_ARTIFACTS_DIR}/screenshots`;
|
|
16
|
-
/**
|
|
17
|
-
* What each file-writing reporter writes, by reporter name.
|
|
18
|
-
*
|
|
19
|
-
* Vitest 5 turned the json and junit reporters into FILE writers (they printed
|
|
20
|
-
* to stdout before) and gave the html and blob ones a `.vitest/` home. Each one
|
|
21
|
-
* is a path the tool pins and a consumer never states, so the preset moves all
|
|
22
|
-
* four at once rather than leaving a repository to discover them one CI run at
|
|
23
|
-
* a time.
|
|
24
|
-
*/
|
|
25
|
-
const REPORTER_OUTPUT_FILES = {
|
|
26
|
-
blob: `${VITEST_ARTIFACTS_DIR}/blob`,
|
|
27
|
-
html: `${VITEST_ARTIFACTS_DIR}/html/index.html`,
|
|
28
|
-
json: `${VITEST_ARTIFACTS_DIR}/json/output.json`,
|
|
29
|
-
junit: `${VITEST_ARTIFACTS_DIR}/junit/output.xml`
|
|
30
|
-
};
|
|
31
|
-
//#endregion
|
|
32
|
-
export { SQLITE_TEMPLATE_DIR as a, SCREENSHOTS_DIR as i, COVERAGE_DIR as n, VITEST_ARTIFACTS_DIR as o, REPORTER_OUTPUT_FILES as r, ATTACHMENTS_DIR as t };
|