swipium 1.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/LICENSE +21 -0
- package/README.md +295 -0
- package/dist/appMap/automationLink.js +74 -0
- package/dist/appMap/automationLink.js.map +1 -0
- package/dist/appMap/build.js +193 -0
- package/dist/appMap/build.js.map +1 -0
- package/dist/appMap/codeIndex.js +73 -0
- package/dist/appMap/codeIndex.js.map +1 -0
- package/dist/appMap/featureModel.js +83 -0
- package/dist/appMap/featureModel.js.map +1 -0
- package/dist/appMap/firstRunApply.js +131 -0
- package/dist/appMap/firstRunApply.js.map +1 -0
- package/dist/appMap/fsWalk.js +75 -0
- package/dist/appMap/fsWalk.js.map +1 -0
- package/dist/appMap/issues.js +227 -0
- package/dist/appMap/issues.js.map +1 -0
- package/dist/appMap/migrations.js +87 -0
- package/dist/appMap/migrations.js.map +1 -0
- package/dist/appMap/prelaunch.js +66 -0
- package/dist/appMap/prelaunch.js.map +1 -0
- package/dist/appMap/projectRegistry.js +70 -0
- package/dist/appMap/projectRegistry.js.map +1 -0
- package/dist/appMap/provenance.js +52 -0
- package/dist/appMap/provenance.js.map +1 -0
- package/dist/appMap/query.js +118 -0
- package/dist/appMap/query.js.map +1 -0
- package/dist/appMap/runtimeMerge.js +207 -0
- package/dist/appMap/runtimeMerge.js.map +1 -0
- package/dist/appMap/schema.js +65 -0
- package/dist/appMap/schema.js.map +1 -0
- package/dist/appMap/screenMatch.js +75 -0
- package/dist/appMap/screenMatch.js.map +1 -0
- package/dist/appMap/staticScan.js +614 -0
- package/dist/appMap/staticScan.js.map +1 -0
- package/dist/appMap/store.js +173 -0
- package/dist/appMap/store.js.map +1 -0
- package/dist/appMap/tsAstScan.js +184 -0
- package/dist/appMap/tsAstScan.js.map +1 -0
- package/dist/artifacts/bundletool.js +199 -0
- package/dist/artifacts/bundletool.js.map +1 -0
- package/dist/artifacts/resolve.js +388 -0
- package/dist/artifacts/resolve.js.map +1 -0
- package/dist/automationGen/appiumModel.js +129 -0
- package/dist/automationGen/appiumModel.js.map +1 -0
- package/dist/automationGen/ciEmitter.js +80 -0
- package/dist/automationGen/ciEmitter.js.map +1 -0
- package/dist/automationGen/jsEmitter.js +440 -0
- package/dist/automationGen/jsEmitter.js.map +1 -0
- package/dist/automationGen/packagePatch.js +56 -0
- package/dist/automationGen/packagePatch.js.map +1 -0
- package/dist/automationGen/projectProfile.js +317 -0
- package/dist/automationGen/projectProfile.js.map +1 -0
- package/dist/automationGen/pythonEmitter.js +386 -0
- package/dist/automationGen/pythonEmitter.js.map +1 -0
- package/dist/automationGen/readmeEmitter.js +66 -0
- package/dist/automationGen/readmeEmitter.js.map +1 -0
- package/dist/automationGen/suitePlan.js +94 -0
- package/dist/automationGen/suitePlan.js.map +1 -0
- package/dist/automationGen/validation.js +150 -0
- package/dist/automationGen/validation.js.map +1 -0
- package/dist/build/parseBuildLog.js +119 -0
- package/dist/build/parseBuildLog.js.map +1 -0
- package/dist/build/plan.js +150 -0
- package/dist/build/plan.js.map +1 -0
- package/dist/ci/preflight.js +85 -0
- package/dist/ci/preflight.js.map +1 -0
- package/dist/cli/init.js +105 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/scan.js +85 -0
- package/dist/cli/scan.js.map +1 -0
- package/dist/cli/suite.js +85 -0
- package/dist/cli/suite.js.map +1 -0
- package/dist/cli/verify.js +62 -0
- package/dist/cli/verify.js.map +1 -0
- package/dist/consent/consent.js +43 -0
- package/dist/consent/consent.js.map +1 -0
- package/dist/context/detect.js +156 -0
- package/dist/context/detect.js.map +1 -0
- package/dist/context/findApps.js +198 -0
- package/dist/context/findApps.js.map +1 -0
- package/dist/context/projectRoot.js +41 -0
- package/dist/context/projectRoot.js.map +1 -0
- package/dist/context/scan.js +167 -0
- package/dist/context/scan.js.map +1 -0
- package/dist/core/target.js +115 -0
- package/dist/core/target.js.map +1 -0
- package/dist/core/targetPlan.js +140 -0
- package/dist/core/targetPlan.js.map +1 -0
- package/dist/drivers/DirectDriver.js +228 -0
- package/dist/drivers/DirectDriver.js.map +1 -0
- package/dist/drivers/Driver.js +4 -0
- package/dist/drivers/Driver.js.map +1 -0
- package/dist/drivers/SimctlDriver.js +127 -0
- package/dist/drivers/SimctlDriver.js.map +1 -0
- package/dist/drivers/WdaDriver.js +250 -0
- package/dist/drivers/WdaDriver.js.map +1 -0
- package/dist/explore/candidates.js +93 -0
- package/dist/explore/candidates.js.map +1 -0
- package/dist/explore/graph.js +116 -0
- package/dist/explore/graph.js.map +1 -0
- package/dist/explore/memory.js +35 -0
- package/dist/explore/memory.js.map +1 -0
- package/dist/explore/planner.js +105 -0
- package/dist/explore/planner.js.map +1 -0
- package/dist/explore/policy.js +85 -0
- package/dist/explore/policy.js.map +1 -0
- package/dist/explore/runner.js +475 -0
- package/dist/explore/runner.js.map +1 -0
- package/dist/explore/signatures.js +35 -0
- package/dist/explore/signatures.js.map +1 -0
- package/dist/explore/suite.js +55 -0
- package/dist/explore/suite.js.map +1 -0
- package/dist/featureTesting/executionBootstrap.js +140 -0
- package/dist/featureTesting/executionBootstrap.js.map +1 -0
- package/dist/firstRun/authStateMachine.js +83 -0
- package/dist/firstRun/authStateMachine.js.map +1 -0
- package/dist/firstRun/classifyScreen.js +201 -0
- package/dist/firstRun/classifyScreen.js.map +1 -0
- package/dist/firstRun/firstRunPlanner.js +207 -0
- package/dist/firstRun/firstRunPlanner.js.map +1 -0
- package/dist/firstRun/firstRunRunner.js +331 -0
- package/dist/firstRun/firstRunRunner.js.map +1 -0
- package/dist/firstRun/generatedDataPolicy.js +215 -0
- package/dist/firstRun/generatedDataPolicy.js.map +1 -0
- package/dist/firstRun/inputPlanner.js +67 -0
- package/dist/firstRun/inputPlanner.js.map +1 -0
- package/dist/firstRun/onboardingStateMachine.js +46 -0
- package/dist/firstRun/onboardingStateMachine.js.map +1 -0
- package/dist/firstRun/paywallPolicy.js +54 -0
- package/dist/firstRun/paywallPolicy.js.map +1 -0
- package/dist/firstRun/types.js +7 -0
- package/dist/firstRun/types.js.map +1 -0
- package/dist/fixtures/catalog.js +131 -0
- package/dist/fixtures/catalog.js.map +1 -0
- package/dist/flows/discover.js +18 -0
- package/dist/flows/discover.js.map +1 -0
- package/dist/flows/generate.js +211 -0
- package/dist/flows/generate.js.map +1 -0
- package/dist/flows/lint.js +97 -0
- package/dist/flows/lint.js.map +1 -0
- package/dist/flows/pack.js +68 -0
- package/dist/flows/pack.js.map +1 -0
- package/dist/flows/run.js +738 -0
- package/dist/flows/run.js.map +1 -0
- package/dist/flows/schema.js +240 -0
- package/dist/flows/schema.js.map +1 -0
- package/dist/flows/seedExec.js +108 -0
- package/dist/flows/seedExec.js.map +1 -0
- package/dist/flows/templates.js +155 -0
- package/dist/flows/templates.js.map +1 -0
- package/dist/flows/waitGuards.js +12 -0
- package/dist/flows/waitGuards.js.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/ios/signing.js +182 -0
- package/dist/ios/signing.js.map +1 -0
- package/dist/issues/classify.js +186 -0
- package/dist/issues/classify.js.map +1 -0
- package/dist/issues/fingerprint.js +130 -0
- package/dist/issues/fingerprint.js.map +1 -0
- package/dist/issues/index.js +240 -0
- package/dist/issues/index.js.map +1 -0
- package/dist/issues/metrics.js +155 -0
- package/dist/issues/metrics.js.map +1 -0
- package/dist/issues/recurrence.js +193 -0
- package/dist/issues/recurrence.js.map +1 -0
- package/dist/issues/report.js +110 -0
- package/dist/issues/report.js.map +1 -0
- package/dist/issues/reportBridge.js +119 -0
- package/dist/issues/reportBridge.js.map +1 -0
- package/dist/issues/schema.js +90 -0
- package/dist/issues/schema.js.map +1 -0
- package/dist/issues/sourceRevision.js +108 -0
- package/dist/issues/sourceRevision.js.map +1 -0
- package/dist/issues/store.js +155 -0
- package/dist/issues/store.js.map +1 -0
- package/dist/lib/android.js +278 -0
- package/dist/lib/android.js.map +1 -0
- package/dist/lib/commandTemplate.js +55 -0
- package/dist/lib/commandTemplate.js.map +1 -0
- package/dist/lib/coordSpace.js +23 -0
- package/dist/lib/coordSpace.js.map +1 -0
- package/dist/lib/device.js +87 -0
- package/dist/lib/device.js.map +1 -0
- package/dist/lib/gitignore.js +22 -0
- package/dist/lib/gitignore.js.map +1 -0
- package/dist/lib/image.js +97 -0
- package/dist/lib/image.js.map +1 -0
- package/dist/lib/logger.js +8 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/metroState.js +45 -0
- package/dist/lib/metroState.js.map +1 -0
- package/dist/lib/needsInput.js +201 -0
- package/dist/lib/needsInput.js.map +1 -0
- package/dist/lib/png.js +103 -0
- package/dist/lib/png.js.map +1 -0
- package/dist/lib/redact.js +21 -0
- package/dist/lib/redact.js.map +1 -0
- package/dist/lib/result.js +87 -0
- package/dist/lib/result.js.map +1 -0
- package/dist/lib/schemaHash.js +65 -0
- package/dist/lib/schemaHash.js.map +1 -0
- package/dist/lib/sensitive.js +13 -0
- package/dist/lib/sensitive.js.map +1 -0
- package/dist/lib/simctl.js +149 -0
- package/dist/lib/simctl.js.map +1 -0
- package/dist/lib/spawn.js +144 -0
- package/dist/lib/spawn.js.map +1 -0
- package/dist/lib/wda.js +347 -0
- package/dist/lib/wda.js.map +1 -0
- package/dist/lib/wdaConfig.js +69 -0
- package/dist/lib/wdaConfig.js.map +1 -0
- package/dist/lib/wdaTune.js +58 -0
- package/dist/lib/wdaTune.js.map +1 -0
- package/dist/oracle/auth.js +22 -0
- package/dist/oracle/auth.js.map +1 -0
- package/dist/oracle/failures.js +193 -0
- package/dist/oracle/failures.js.map +1 -0
- package/dist/oracle/health.js +123 -0
- package/dist/oracle/health.js.map +1 -0
- package/dist/oracle/record.js +36 -0
- package/dist/oracle/record.js.map +1 -0
- package/dist/orchestration/envelope.js +52 -0
- package/dist/orchestration/envelope.js.map +1 -0
- package/dist/orchestration/goal.js +98 -0
- package/dist/orchestration/goal.js.map +1 -0
- package/dist/plan/plan.js +124 -0
- package/dist/plan/plan.js.map +1 -0
- package/dist/prompts/index.js +63 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/report/artifactManifest.js +44 -0
- package/dist/report/artifactManifest.js.map +1 -0
- package/dist/report/coverage.js +60 -0
- package/dist/report/coverage.js.map +1 -0
- package/dist/report/evidence.js +84 -0
- package/dist/report/evidence.js.map +1 -0
- package/dist/report/export.js +447 -0
- package/dist/report/export.js.map +1 -0
- package/dist/report/flake.js +61 -0
- package/dist/report/flake.js.map +1 -0
- package/dist/report/history.js +564 -0
- package/dist/report/history.js.map +1 -0
- package/dist/report/policy.js +98 -0
- package/dist/report/policy.js.map +1 -0
- package/dist/report/qaLevel.js +96 -0
- package/dist/report/qaLevel.js.map +1 -0
- package/dist/report/readiness.js +213 -0
- package/dist/report/readiness.js.map +1 -0
- package/dist/report/summary.js +42 -0
- package/dist/report/summary.js.map +1 -0
- package/dist/report/testCatalog.js +93 -0
- package/dist/report/testCatalog.js.map +1 -0
- package/dist/report/timing.js +37 -0
- package/dist/report/timing.js.map +1 -0
- package/dist/server.js +180 -0
- package/dist/server.js.map +1 -0
- package/dist/services/automationGenerate.js +60 -0
- package/dist/services/automationGenerate.js.map +1 -0
- package/dist/services/build.js +66 -0
- package/dist/services/build.js.map +1 -0
- package/dist/services/preflight.js +51 -0
- package/dist/services/preflight.js.map +1 -0
- package/dist/services/prepareAndroid.js +183 -0
- package/dist/services/prepareAndroid.js.map +1 -0
- package/dist/services/prepareIos.js +162 -0
- package/dist/services/prepareIos.js.map +1 -0
- package/dist/services/report.js +537 -0
- package/dist/services/report.js.map +1 -0
- package/dist/services/smoke.js +120 -0
- package/dist/services/smoke.js.map +1 -0
- package/dist/services/suiteGenerate.js +123 -0
- package/dist/services/suiteGenerate.js.map +1 -0
- package/dist/services/testSuiteKnowledge.js +150 -0
- package/dist/services/testSuiteKnowledge.js.map +1 -0
- package/dist/session/attach.js +47 -0
- package/dist/session/attach.js.map +1 -0
- package/dist/session/progress.js +56 -0
- package/dist/session/progress.js.map +1 -0
- package/dist/session/store.js +329 -0
- package/dist/session/store.js.map +1 -0
- package/dist/snapshot/overlays.js +132 -0
- package/dist/snapshot/overlays.js.map +1 -0
- package/dist/snapshot/parse.js +204 -0
- package/dist/snapshot/parse.js.map +1 -0
- package/dist/snapshot/present.js +12 -0
- package/dist/snapshot/present.js.map +1 -0
- package/dist/snapshot/settle.js +36 -0
- package/dist/snapshot/settle.js.map +1 -0
- package/dist/state/profile.js +318 -0
- package/dist/state/profile.js.map +1 -0
- package/dist/suite/compile.js +176 -0
- package/dist/suite/compile.js.map +1 -0
- package/dist/suite/lint.js +45 -0
- package/dist/suite/lint.js.map +1 -0
- package/dist/suite/pom.js +285 -0
- package/dist/suite/pom.js.map +1 -0
- package/dist/suite/testcase.js +87 -0
- package/dist/suite/testcase.js.map +1 -0
- package/dist/testSuite/generator.js +222 -0
- package/dist/testSuite/generator.js.map +1 -0
- package/dist/testSuite/history.js +44 -0
- package/dist/testSuite/history.js.map +1 -0
- package/dist/testSuite/issueLinks.js +96 -0
- package/dist/testSuite/issueLinks.js.map +1 -0
- package/dist/testSuite/merge.js +190 -0
- package/dist/testSuite/merge.js.map +1 -0
- package/dist/testSuite/schema.js +86 -0
- package/dist/testSuite/schema.js.map +1 -0
- package/dist/testSuite/store.js +116 -0
- package/dist/testSuite/store.js.map +1 -0
- package/dist/testSuite/traceability.js +55 -0
- package/dist/testSuite/traceability.js.map +1 -0
- package/dist/tools/act.js +457 -0
- package/dist/tools/act.js.map +1 -0
- package/dist/tools/agent.js +225 -0
- package/dist/tools/agent.js.map +1 -0
- package/dist/tools/appMap.js +317 -0
- package/dist/tools/appMap.js.map +1 -0
- package/dist/tools/assertVisual.js +74 -0
- package/dist/tools/assertVisual.js.map +1 -0
- package/dist/tools/automationGenerate.js +394 -0
- package/dist/tools/automationGenerate.js.map +1 -0
- package/dist/tools/capabilities.js +114 -0
- package/dist/tools/capabilities.js.map +1 -0
- package/dist/tools/clearOverlay.js +150 -0
- package/dist/tools/clearOverlay.js.map +1 -0
- package/dist/tools/detectContext.js +41 -0
- package/dist/tools/detectContext.js.map +1 -0
- package/dist/tools/doctor.js +116 -0
- package/dist/tools/doctor.js.map +1 -0
- package/dist/tools/explore.js +289 -0
- package/dist/tools/explore.js.map +1 -0
- package/dist/tools/firstRun.js +136 -0
- package/dist/tools/firstRun.js.map +1 -0
- package/dist/tools/flow.js +331 -0
- package/dist/tools/flow.js.map +1 -0
- package/dist/tools/flowGenerate.js +55 -0
- package/dist/tools/flowGenerate.js.map +1 -0
- package/dist/tools/getArtifact.js +39 -0
- package/dist/tools/getArtifact.js.map +1 -0
- package/dist/tools/health.js +41 -0
- package/dist/tools/health.js.map +1 -0
- package/dist/tools/ios.js +492 -0
- package/dist/tools/ios.js.map +1 -0
- package/dist/tools/jobs.js +20 -0
- package/dist/tools/jobs.js.map +1 -0
- package/dist/tools/network.js +50 -0
- package/dist/tools/network.js.map +1 -0
- package/dist/tools/note.js +69 -0
- package/dist/tools/note.js.map +1 -0
- package/dist/tools/plan.js +49 -0
- package/dist/tools/plan.js.map +1 -0
- package/dist/tools/prepareIosTarget.js +99 -0
- package/dist/tools/prepareIosTarget.js.map +1 -0
- package/dist/tools/prepareTarget.js +257 -0
- package/dist/tools/prepareTarget.js.map +1 -0
- package/dist/tools/report.js +42 -0
- package/dist/tools/report.js.map +1 -0
- package/dist/tools/screenRecord.js +24 -0
- package/dist/tools/screenRecord.js.map +1 -0
- package/dist/tools/screenshot.js +63 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/smoke.js +39 -0
- package/dist/tools/smoke.js.map +1 -0
- package/dist/tools/snapshot.js +129 -0
- package/dist/tools/snapshot.js.map +1 -0
- package/dist/tools/startSession.js +169 -0
- package/dist/tools/startSession.js.map +1 -0
- package/dist/tools/state.js +87 -0
- package/dist/tools/state.js.map +1 -0
- package/dist/tools/suite.js +412 -0
- package/dist/tools/suite.js.map +1 -0
- package/dist/tools/testThis.js +774 -0
- package/dist/tools/testThis.js.map +1 -0
- package/dist/tools/wda.js +477 -0
- package/dist/tools/wda.js.map +1 -0
- package/dist/version.js +62 -0
- package/dist/version.js.map +1 -0
- package/dist/visual/ocr.js +72 -0
- package/dist/visual/ocr.js.map +1 -0
- package/dist/visual/provider.js +75 -0
- package/dist/visual/provider.js.map +1 -0
- package/docs/README.md +19 -0
- package/docs/assets/swipium-lockup-ink-on-light.png +0 -0
- package/docs/mcp-server.md +135 -0
- package/docs/tools.md +130 -0
- package/package.json +70 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
// Flow runner v2 (NEXT-PLAN: Flow System V2). Drives a parsed Flow step-by-step using core
|
|
2
|
+
// primitives + the driver directly, WITHOUT re-entering through the model. Fail-fast, never
|
|
3
|
+
// auto-retries (DESIGN §6). v2 adds: setup/teardown phases (teardown always runs), selector-bound
|
|
4
|
+
// inputText, visual steps (image match / diff / visual evidence), device-relative gestures,
|
|
5
|
+
// overlay/network/lifecycle/seed/note steps, and typed per-step failure codes. On failure it
|
|
6
|
+
// captures a screenshot + health so the result stands alone.
|
|
7
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import { isAbsolute, join } from 'node:path';
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { parseSnapshot, signature } from '../snapshot/parse.js';
|
|
11
|
+
import { settle } from '../snapshot/settle.js';
|
|
12
|
+
import { checkHealth } from '../oracle/health.js';
|
|
13
|
+
import { recordHealthFindings } from '../oracle/record.js';
|
|
14
|
+
import { boundsBucket, resolveTarget, resourceIdMatches } from '../core/target.js';
|
|
15
|
+
import { imageDiff, findTemplate } from '../lib/image.js';
|
|
16
|
+
import { captureCoordinateSpace, toDevicePoint } from '../lib/coordSpace.js';
|
|
17
|
+
import { executeSeed } from './seedExec.js';
|
|
18
|
+
import { resolveVars } from './schema.js';
|
|
19
|
+
import { configuredOcrCommand, findOcrRegion, runOcr } from '../visual/ocr.js';
|
|
20
|
+
import { makeRedactor } from '../lib/redact.js';
|
|
21
|
+
const FALLBACK_SWIPE = {
|
|
22
|
+
up: [540, 1500, 540, 600],
|
|
23
|
+
down: [540, 600, 540, 1500],
|
|
24
|
+
left: [800, 1100, 200, 1100],
|
|
25
|
+
right: [200, 1100, 800, 1100],
|
|
26
|
+
};
|
|
27
|
+
const AREA_ANCHOR = { center: [0.5, 0.5], top: [0.5, 0.3], bottom: [0.5, 0.7], left: [0.3, 0.5], right: [0.7, 0.5] };
|
|
28
|
+
const SECRET_VAR_NAME = /pass|secret|token|otp|pin|cvv|key/i;
|
|
29
|
+
/** Device-relative swipe vector (fractions of the screen), falling back to fixed coords if no size. */
|
|
30
|
+
function swipeVector(size, dir, area = 'center', distance = 0.6) {
|
|
31
|
+
if (!size)
|
|
32
|
+
return FALLBACK_SWIPE[dir];
|
|
33
|
+
const [ax, ay] = AREA_ANCHOR[area];
|
|
34
|
+
const cx = size.width * ax;
|
|
35
|
+
const cy = size.height * ay;
|
|
36
|
+
const vd = (size.height * distance) / 2;
|
|
37
|
+
const hd = (size.width * distance) / 2;
|
|
38
|
+
const clampX = (x) => Math.max(1, Math.min(size.width - 1, Math.round(x)));
|
|
39
|
+
const clampY = (y) => Math.max(1, Math.min(size.height - 1, Math.round(y)));
|
|
40
|
+
const v = {
|
|
41
|
+
up: [cx, cy + vd, cx, cy - vd],
|
|
42
|
+
down: [cx, cy - vd, cx, cy + vd],
|
|
43
|
+
left: [cx + hd, cy, cx - hd, cy],
|
|
44
|
+
right: [cx - hd, cy, cx + hd, cy],
|
|
45
|
+
}[dir];
|
|
46
|
+
return [clampX(v[0]), clampY(v[1]), clampX(v[2]), clampY(v[3])];
|
|
47
|
+
}
|
|
48
|
+
function describe(step) {
|
|
49
|
+
switch (step.kind) {
|
|
50
|
+
case 'tap': return `tap ${step.selector}`;
|
|
51
|
+
case 'tapAt': return `tapAt ${step.x},${step.y}`;
|
|
52
|
+
case 'tapImage': return `tapImage ${step.template}`;
|
|
53
|
+
case 'tapOcrText': return `tapOcrText ${JSON.stringify(step.query)}`;
|
|
54
|
+
case 'inputText': return `inputText${step.into ? ` into "${step.into}"` : ''} ${step.secret ? '«secret»' : JSON.stringify(step.value)}`;
|
|
55
|
+
case 'assertVisible': return `assertVisible ${JSON.stringify(step.query)}`;
|
|
56
|
+
case 'assertNotVisible': return `assertNotVisible ${JSON.stringify(step.query)}`;
|
|
57
|
+
case 'assertImage': return `assertImage ${step.template}`;
|
|
58
|
+
case 'assertOcrText': return `assertOcrText ${JSON.stringify(step.query)}`;
|
|
59
|
+
case 'assertVisual': return `assertVisual ${JSON.stringify(step.description)}`;
|
|
60
|
+
case 'assertDiff': return `assertDiff ${step.baseline}`;
|
|
61
|
+
case 'swipe': return `swipe ${step.direction}${step.area ? `@${step.area}` : ''}`;
|
|
62
|
+
case 'scrollTo': return `scrollTo ${JSON.stringify(step.query)}`;
|
|
63
|
+
case 'press': return `press ${step.key}`;
|
|
64
|
+
case 'openUrl': return `openUrl ${step.url}`;
|
|
65
|
+
case 'wait': return step.ms != null ? `wait ${step.ms}ms` : `wait for ${JSON.stringify(step.query)}`;
|
|
66
|
+
case 'waitForIdle': return 'waitForIdle';
|
|
67
|
+
case 'waitForVisible': return `waitForVisible ${JSON.stringify(step.query)}`;
|
|
68
|
+
case 'clearOverlay': return 'clearOverlay';
|
|
69
|
+
case 'networkOffline': return 'networkOffline';
|
|
70
|
+
case 'networkOnline': return 'networkOnline';
|
|
71
|
+
case 'restartApp': return 'restartApp';
|
|
72
|
+
case 'seed': return `seed ${step.fixture}`;
|
|
73
|
+
case 'note': return `note(${step.outcome}) ${JSON.stringify(step.reason)}`;
|
|
74
|
+
case 'screenshot': return 'screenshot';
|
|
75
|
+
case 'prepareTarget': return 'prepareTarget';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function snapshot(session, d) {
|
|
79
|
+
const parsed = parseSnapshot(await d.dumpXml());
|
|
80
|
+
session.lastSnapshot = { fullByRef: parsed.fullByRef, signatures: new Set(parsed.elements.map(signature)), allNodes: parsed.allNodes };
|
|
81
|
+
return parsed;
|
|
82
|
+
}
|
|
83
|
+
function visible(parsed, query) {
|
|
84
|
+
const q = query.toLowerCase();
|
|
85
|
+
return parsed.allNodes.some((n) => n.text?.toLowerCase().includes(q) || n.desc?.toLowerCase().includes(q) || n.id?.toLowerCase().includes(q));
|
|
86
|
+
}
|
|
87
|
+
function nativeSelectorFor(selector) {
|
|
88
|
+
const m = selector.match(/^(accessibility id|name|predicate string|class chain)\s*=\s*(.+)$/i);
|
|
89
|
+
if (!m)
|
|
90
|
+
return null;
|
|
91
|
+
return { using: m[1].toLowerCase(), value: m[2] };
|
|
92
|
+
}
|
|
93
|
+
function resourceIdSelector(selector) {
|
|
94
|
+
return selector.match(/^id\s*=\s*(.+)$/i)?.[1] ?? null;
|
|
95
|
+
}
|
|
96
|
+
function provenanceForStep(flow, stepIndex, kind) {
|
|
97
|
+
const oneBased = stepIndex + 1;
|
|
98
|
+
return (flow.provenance ?? []).find((entry) => (entry.generatedStepIndex === oneBased &&
|
|
99
|
+
(!entry.generatedKind || entry.generatedKind === kind || (kind === 'wait' && entry.generatedKind === 'waitForVisible'))));
|
|
100
|
+
}
|
|
101
|
+
function targetHintsFromProvenance(provenance) {
|
|
102
|
+
if (!provenance)
|
|
103
|
+
return {};
|
|
104
|
+
return {
|
|
105
|
+
packageName: provenance.resourceId?.includes(':id/') ? provenance.resourceId.slice(0, provenance.resourceId.indexOf(':id/')) : undefined,
|
|
106
|
+
className: provenance.className ?? provenance.elementRole,
|
|
107
|
+
textHint: provenance.text ?? provenance.accessibilityLabel,
|
|
108
|
+
boundsHint: provenance.boundsBucket,
|
|
109
|
+
screenSignature: provenance.originalScreenSignature,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function targetForTapSelector(selector, provenance) {
|
|
113
|
+
const m = selector.match(/^id\s*=\s*(.+)$/i);
|
|
114
|
+
if (m)
|
|
115
|
+
return { id: m[1], ...targetHintsFromProvenance(provenance) };
|
|
116
|
+
const native = nativeSelectorFor(selector);
|
|
117
|
+
if (native?.using === 'accessibility id')
|
|
118
|
+
return { id: native.value };
|
|
119
|
+
if (native?.using === 'name')
|
|
120
|
+
return { text: native.value };
|
|
121
|
+
return selector.startsWith('@') ? { ref: selector } : { text: selector };
|
|
122
|
+
}
|
|
123
|
+
async function nativeVisible(d, selector) {
|
|
124
|
+
const native = nativeSelectorFor(selector);
|
|
125
|
+
if (!native)
|
|
126
|
+
return null;
|
|
127
|
+
if (!d.existsBySelector) {
|
|
128
|
+
if (native.using !== 'accessibility id' && native.using !== 'name')
|
|
129
|
+
throw new Error(`${native.using} selectors require backend-native selector support`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return d.existsBySelector(native.using, native.value);
|
|
133
|
+
}
|
|
134
|
+
function fallbackVisibleQuery(selector) {
|
|
135
|
+
const native = nativeSelectorFor(selector);
|
|
136
|
+
const id = selector.match(/^id\s*=\s*(.+)$/i)?.[1];
|
|
137
|
+
if (id)
|
|
138
|
+
return id.includes('/') ? id.slice(id.lastIndexOf('/') + 1) : id;
|
|
139
|
+
return native && (native.using === 'accessibility id' || native.using === 'name') ? native.value : selector;
|
|
140
|
+
}
|
|
141
|
+
function flowScreenSignature(parsed) {
|
|
142
|
+
const sigs = parsed.elements.map(signature);
|
|
143
|
+
if (!sigs.length)
|
|
144
|
+
return undefined;
|
|
145
|
+
return createHash('sha1').update(sigs.sort().join('\n')).digest('hex').slice(0, 16);
|
|
146
|
+
}
|
|
147
|
+
function narrowRawResourceMatches(matches, provenance) {
|
|
148
|
+
let narrowed = matches;
|
|
149
|
+
const tryNarrow = (fn) => {
|
|
150
|
+
const next = narrowed.filter(fn);
|
|
151
|
+
if (next.length)
|
|
152
|
+
narrowed = next;
|
|
153
|
+
};
|
|
154
|
+
if (provenance?.resourceId?.includes(':id/')) {
|
|
155
|
+
const pkg = provenance.resourceId.slice(0, provenance.resourceId.indexOf(':id/'));
|
|
156
|
+
tryNarrow((node) => node.attrs?.['resource-id']?.startsWith(`${pkg}:id/`) === true);
|
|
157
|
+
}
|
|
158
|
+
const cls = provenance?.className ?? provenance?.elementRole;
|
|
159
|
+
if (cls) {
|
|
160
|
+
const wanted = cls.trim().toLowerCase();
|
|
161
|
+
tryNarrow((node) => {
|
|
162
|
+
const got = node.cls.trim().toLowerCase();
|
|
163
|
+
return got === wanted || got.endsWith(`.${wanted}`);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const textHint = provenance?.text ?? provenance?.accessibilityLabel;
|
|
167
|
+
if (textHint) {
|
|
168
|
+
const wanted = textHint.trim().toLowerCase();
|
|
169
|
+
tryNarrow((node) => node.text.trim().toLowerCase() === wanted || node.desc.trim().toLowerCase() === wanted);
|
|
170
|
+
}
|
|
171
|
+
if (provenance?.boundsBucket) {
|
|
172
|
+
tryNarrow((node) => boundsBucket(node.bounds) === provenance.boundsBucket);
|
|
173
|
+
}
|
|
174
|
+
return narrowed;
|
|
175
|
+
}
|
|
176
|
+
async function resourceIdVisibility(session, d, id, provenance) {
|
|
177
|
+
const parsed = await snapshot(session, d);
|
|
178
|
+
const matches = parsed.allNodes.filter((node) => resourceIdMatches(undefined, node.id, id));
|
|
179
|
+
const screenMismatch = provenance?.originalScreenSignature && flowScreenSignature(parsed) !== provenance.originalScreenSignature;
|
|
180
|
+
if (!matches.length)
|
|
181
|
+
return { visible: false, detail: `resource-id "${id}" not visible${screenMismatch ? ' (recorded screen signature differs from current screen)' : ''}` };
|
|
182
|
+
const narrowed = matches.length > 1 ? narrowRawResourceMatches(matches, provenance) : matches;
|
|
183
|
+
if (narrowed.length > 1) {
|
|
184
|
+
const labels = narrowed.map((node, index) => `${index + 1}:${node.cls}:${node.text || node.desc || node.id || 'unlabeled'}`).join(', ');
|
|
185
|
+
return { visible: true, ambiguous: true, detail: `AMBIGUOUS_SELECTOR: resource-id ${id} matched ${narrowed.length} visible nodes (${labels}); add text/class/bounds hints or a unique testID${screenMismatch ? ' (recorded screen signature differs from current screen)' : ''}.` };
|
|
186
|
+
}
|
|
187
|
+
const node = narrowed[0];
|
|
188
|
+
return { visible: node.enabled !== false && node.bounds[2] > node.bounds[0] && node.bounds[3] > node.bounds[1] };
|
|
189
|
+
}
|
|
190
|
+
export function classifyFlowDriverError(error, fallback = 'UNKNOWN') {
|
|
191
|
+
const msg = String(error?.message ?? error);
|
|
192
|
+
if (/not hittable|not hit.?point|not visible.*hittable|is not enabled|element.*obscured|other element.*would receive/i.test(msg))
|
|
193
|
+
return 'ELEMENT_NOT_HITTABLE';
|
|
194
|
+
if (/stale element|stale.*reference|element.*no longer|invalid element/i.test(msg))
|
|
195
|
+
return 'STALE_REF';
|
|
196
|
+
if (/invalid selector|invalid locator|unsupported locator|unsupported selector|locator strategy.*(invalid|unsupported)|selector strategy.*(invalid|unsupported)|predicate.*parse|class chain.*parse/i.test(msg))
|
|
197
|
+
return 'INVALID_SELECTOR';
|
|
198
|
+
if (/AMBIGUOUS_SELECTOR|ambiguous selector|matched \d+ elements/i.test(msg))
|
|
199
|
+
return 'AMBIGUOUS_SELECTOR';
|
|
200
|
+
if (/keyboard.*cover|keyboard.*obstruct|covered by keyboard/i.test(msg))
|
|
201
|
+
return 'KEYBOARD_OBSTRUCTION';
|
|
202
|
+
if (/animation.*idle|idle.*animation|cannot become idle because of animation/i.test(msg))
|
|
203
|
+
return 'ANIMATION_IDLE_BLOCKED';
|
|
204
|
+
if (/timed out waiting.*idle|wait.*idle.*timed out|not idle/i.test(msg))
|
|
205
|
+
return 'UI_IDLE_TIMEOUT';
|
|
206
|
+
if (/wda.*not idle|xctest.*idle|app.*not idle/i.test(msg))
|
|
207
|
+
return 'WDA_APP_NOT_IDLE';
|
|
208
|
+
if (/xpath/i.test(msg))
|
|
209
|
+
return 'WDA_XPATH_REFUSED';
|
|
210
|
+
if (/main thread|event loop.*blocked/i.test(msg))
|
|
211
|
+
return 'WDA_MAIN_THREAD_BUSY';
|
|
212
|
+
if (/hierarchy.*(large|too deep)|snapshot.*too deep|snapshot.*exceeded.*depth|depth.*snapshot|exceeded.*snapshot/i.test(msg))
|
|
213
|
+
return 'WDA_HIERARCHY_TOO_LARGE';
|
|
214
|
+
if (/source.*slow|page source.*slow/i.test(msg))
|
|
215
|
+
return 'WDA_SOURCE_SLOW';
|
|
216
|
+
if (/snapshot|source|dump|hierarchy/i.test(msg))
|
|
217
|
+
return 'SNAPSHOT_FAILED';
|
|
218
|
+
if (/webview|web view|context unavailable/i.test(msg))
|
|
219
|
+
return 'WEBVIEW_UNAVAILABLE';
|
|
220
|
+
if (/no such element|element.*not found|could not resolve element|unable to locate|not visible/i.test(msg))
|
|
221
|
+
return 'ELEMENT_NOT_FOUND';
|
|
222
|
+
if (/WDA HTTP|session not created|invalid session|did not return a sessionId|failed to create.*session/i.test(msg))
|
|
223
|
+
return 'WDA_SESSION_FAILED';
|
|
224
|
+
if (/ECONNREFUSED|fetch failed|timed out|aborted|network|socket|unreachable/i.test(msg))
|
|
225
|
+
return 'WDA_UNREACHABLE';
|
|
226
|
+
return fallback;
|
|
227
|
+
}
|
|
228
|
+
export async function runFlow(sessions, session, d, flow, opts = {}) {
|
|
229
|
+
const vars = { ...Object.fromEntries(session.inputValues.entries()), ...(opts.variables ?? {}) };
|
|
230
|
+
const started = Date.now();
|
|
231
|
+
const steps = [];
|
|
232
|
+
const appId = flow.appId ?? session.appId;
|
|
233
|
+
const timeoutMs = 8000;
|
|
234
|
+
// Ensure session.driver is the driver we're running with — core helpers (resolveTarget) read it.
|
|
235
|
+
session.driver = d;
|
|
236
|
+
const screenSize = await d.screenSize().catch(() => null);
|
|
237
|
+
const mutationConsent = opts.mutationConsent ?? { required: false, approved: true };
|
|
238
|
+
const resolveFlowText = (value) => {
|
|
239
|
+
const resolved = resolveVars(value, vars);
|
|
240
|
+
for (const m of value.matchAll(/\$\{([^}]+)\}/g)) {
|
|
241
|
+
const name = m[1];
|
|
242
|
+
const v = vars[name] ?? process.env[name];
|
|
243
|
+
if (v != null && SECRET_VAR_NAME.test(name))
|
|
244
|
+
session.secrets.add(v);
|
|
245
|
+
}
|
|
246
|
+
return resolved;
|
|
247
|
+
};
|
|
248
|
+
const unresolved = (missing) => ({ ok: false, detail: `unresolved variable(s): ${missing.join(', ')}`, failureCode: 'MISSING_FIXTURE' });
|
|
249
|
+
const shown = (value) => makeRedactor(session.secrets)(value) ?? value;
|
|
250
|
+
const recordFlowMutation = (action, risk, target, status, detail) => {
|
|
251
|
+
sessions.recordMutation(session, {
|
|
252
|
+
tool: 'qa_flow_run',
|
|
253
|
+
action,
|
|
254
|
+
risk,
|
|
255
|
+
target: { flow: flow.name, appId: appId ?? null, ...target },
|
|
256
|
+
consent: mutationConsent,
|
|
257
|
+
status,
|
|
258
|
+
detail,
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
let passed = true;
|
|
262
|
+
let failedAtStep;
|
|
263
|
+
let reason;
|
|
264
|
+
let failureCode;
|
|
265
|
+
const tapImageHit = async (template, minScore) => {
|
|
266
|
+
const tplPath = isAbsolute(template) ? template : join(session.root, template);
|
|
267
|
+
if (!existsSync(tplPath))
|
|
268
|
+
return null;
|
|
269
|
+
const png = await d.screenshot();
|
|
270
|
+
const m = findTemplate(png, readFileSync(tplPath), minScore ?? 0.85);
|
|
271
|
+
if (!m.found)
|
|
272
|
+
return null;
|
|
273
|
+
const cs = await captureCoordinateSpace(d, png);
|
|
274
|
+
return toDevicePoint(cs, m.x, m.y);
|
|
275
|
+
};
|
|
276
|
+
async function execStep(step, stepIndex) {
|
|
277
|
+
const provenance = provenanceForStep(flow, stepIndex, step.kind);
|
|
278
|
+
switch (step.kind) {
|
|
279
|
+
case 'prepareTarget':
|
|
280
|
+
if (!appId)
|
|
281
|
+
return { ok: false, detail: 'no appId (set flow.appId or run qa_prepare_target first)', failureCode: 'NO_ARTIFACT' };
|
|
282
|
+
await d.launchApp(appId);
|
|
283
|
+
await settle(d, { timeoutMs });
|
|
284
|
+
return { ok: true };
|
|
285
|
+
case 'restartApp':
|
|
286
|
+
if (!appId)
|
|
287
|
+
return { ok: false, detail: 'no appId', failureCode: 'NO_ARTIFACT' };
|
|
288
|
+
try {
|
|
289
|
+
await d.terminateApp(appId);
|
|
290
|
+
await d.launchApp(appId);
|
|
291
|
+
await settle(d, { timeoutMs });
|
|
292
|
+
sessions.addEnvChange(session, 'restartApp');
|
|
293
|
+
recordFlowMutation('flow_restart_app', 'low', { stepKind: step.kind }, 'executed');
|
|
294
|
+
return { ok: true };
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
recordFlowMutation('flow_restart_app', 'low', { stepKind: step.kind }, 'blocked', String(e));
|
|
298
|
+
throw e;
|
|
299
|
+
}
|
|
300
|
+
case 'tap': {
|
|
301
|
+
const resolved = resolveFlowText(step.selector);
|
|
302
|
+
if (resolved.missing.length)
|
|
303
|
+
return unresolved(resolved.missing);
|
|
304
|
+
const selector = resolved.out;
|
|
305
|
+
const native = nativeSelectorFor(selector);
|
|
306
|
+
if (native && d.tapBySelector) {
|
|
307
|
+
try {
|
|
308
|
+
await d.tapBySelector(native.using, native.value);
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
return { ok: false, detail: String(e), failureCode: classifyFlowDriverError(e, 'ELEMENT_NOT_FOUND') };
|
|
312
|
+
}
|
|
313
|
+
sessions.bump(session, 'actions');
|
|
314
|
+
await settle(d, { timeoutMs });
|
|
315
|
+
return { ok: true };
|
|
316
|
+
}
|
|
317
|
+
if (native && native.using !== 'accessibility id' && native.using !== 'name') {
|
|
318
|
+
return { ok: false, detail: `${native.using} selectors require backend-native selector support`, failureCode: 'BACKEND_UNSUPPORTED' };
|
|
319
|
+
}
|
|
320
|
+
const t = await resolveTarget(session, targetForTapSelector(selector, provenance));
|
|
321
|
+
if ('error' in t)
|
|
322
|
+
return { ok: false, detail: t.error, failureCode: classifyFlowDriverError(t.error, 'ELEMENT_NOT_FOUND') };
|
|
323
|
+
await d.pressXY(t.x, t.y, 100);
|
|
324
|
+
sessions.bump(session, 'actions');
|
|
325
|
+
await settle(d, { timeoutMs });
|
|
326
|
+
return { ok: true };
|
|
327
|
+
}
|
|
328
|
+
case 'tapAt':
|
|
329
|
+
await d.pressXY(step.x, step.y, 100);
|
|
330
|
+
sessions.bump(session, 'actions');
|
|
331
|
+
await settle(d, { timeoutMs });
|
|
332
|
+
return { ok: true };
|
|
333
|
+
case 'tapImage': {
|
|
334
|
+
const pt = await tapImageHit(step.template, step.minScore);
|
|
335
|
+
if (!pt)
|
|
336
|
+
return { ok: false, detail: `image "${step.template}" not found on screen`, failureCode: 'ELEMENT_NOT_FOUND' };
|
|
337
|
+
await d.pressXY(pt.x, pt.y, 100);
|
|
338
|
+
sessions.bump(session, 'actions');
|
|
339
|
+
await settle(d, { timeoutMs });
|
|
340
|
+
return { ok: true };
|
|
341
|
+
}
|
|
342
|
+
case 'tapOcrText': {
|
|
343
|
+
const resolved = resolveFlowText(step.query);
|
|
344
|
+
if (resolved.missing.length)
|
|
345
|
+
return unresolved(resolved.missing);
|
|
346
|
+
const query = resolved.out;
|
|
347
|
+
if (session.sensitive)
|
|
348
|
+
return { ok: false, detail: 'Sensitive mode refuses OCR visual text search because it would pass a screenshot to an external provider', failureCode: 'UNSAFE_ACTION_REFUSED' };
|
|
349
|
+
const command = configuredOcrCommand(session.root);
|
|
350
|
+
if (!command)
|
|
351
|
+
return { ok: false, detail: 'OCR command is not configured', failureCode: 'VISUAL_ONLY_SCREEN' };
|
|
352
|
+
const hit = findOcrRegion(await runOcr(d, session.root, command), query, step.minConfidence ?? 0.8);
|
|
353
|
+
if (!hit)
|
|
354
|
+
return { ok: false, detail: `OCR text "${shown(query)}" not found`, failureCode: 'ELEMENT_NOT_FOUND' };
|
|
355
|
+
await d.pressXY(hit.devicePoint.x, hit.devicePoint.y, 100);
|
|
356
|
+
sessions.bump(session, 'actions');
|
|
357
|
+
await settle(d, { timeoutMs });
|
|
358
|
+
return { ok: true };
|
|
359
|
+
}
|
|
360
|
+
case 'inputText': {
|
|
361
|
+
const { out, missing } = resolveFlowText(step.value);
|
|
362
|
+
if (missing.length)
|
|
363
|
+
return { ok: false, detail: `unresolved variable(s): ${missing.join(', ')}`, failureCode: 'MISSING_FIXTURE' };
|
|
364
|
+
if (d.kind === 'direct' && /[^\x00-\x7F]/.test(out)) {
|
|
365
|
+
return { ok: false, detail: 'Android DirectDriver text entry is ASCII-safe only for generated replay values', failureCode: 'TEXT_INPUT_UNSUPPORTED' };
|
|
366
|
+
}
|
|
367
|
+
if (step.into) {
|
|
368
|
+
const into = resolveFlowText(step.into);
|
|
369
|
+
if (into.missing.length)
|
|
370
|
+
return unresolved(into.missing);
|
|
371
|
+
const native = nativeSelectorFor(into.out);
|
|
372
|
+
if (native && d.typeBySelector) {
|
|
373
|
+
try {
|
|
374
|
+
if (d.clearBySelector)
|
|
375
|
+
await d.clearBySelector(native.using, native.value);
|
|
376
|
+
await d.typeBySelector(native.using, native.value, out);
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
return { ok: false, detail: String(e), failureCode: classifyFlowDriverError(e, 'ELEMENT_NOT_FOUND') };
|
|
380
|
+
}
|
|
381
|
+
if (step.secret && out)
|
|
382
|
+
session.secrets.add(out);
|
|
383
|
+
sessions.bump(session, 'actions');
|
|
384
|
+
return { ok: true };
|
|
385
|
+
}
|
|
386
|
+
if (native && native.using !== 'accessibility id' && native.using !== 'name') {
|
|
387
|
+
return { ok: false, detail: `${native.using} selectors require backend-native selector support`, failureCode: 'BACKEND_UNSUPPORTED' };
|
|
388
|
+
}
|
|
389
|
+
// selector-bound: focus the named field first (no reliance on existing focus)
|
|
390
|
+
const t = await resolveTarget(session, targetForTapSelector(into.out, provenance));
|
|
391
|
+
if ('error' in t)
|
|
392
|
+
return { ok: false, detail: `field "${shown(into.out)}": ${t.error}`, failureCode: classifyFlowDriverError(t.error, 'ELEMENT_NOT_FOUND') };
|
|
393
|
+
await d.tapXY(t.x, t.y);
|
|
394
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
395
|
+
await d.clearFocusedText(t.textLen);
|
|
396
|
+
}
|
|
397
|
+
if (step.secret && out)
|
|
398
|
+
session.secrets.add(out);
|
|
399
|
+
await d.inputText(out);
|
|
400
|
+
sessions.bump(session, 'actions');
|
|
401
|
+
return { ok: true };
|
|
402
|
+
}
|
|
403
|
+
case 'assertVisible': {
|
|
404
|
+
const resolved = resolveFlowText(step.query);
|
|
405
|
+
if (resolved.missing.length)
|
|
406
|
+
return unresolved(resolved.missing);
|
|
407
|
+
const query = resolved.out;
|
|
408
|
+
try {
|
|
409
|
+
const native = await nativeVisible(d, query);
|
|
410
|
+
if (native != null)
|
|
411
|
+
return native ? { ok: true } : { ok: false, detail: `"${shown(query)}" not visible`, failureCode: 'ASSERTION_FAILED' };
|
|
412
|
+
}
|
|
413
|
+
catch (e) {
|
|
414
|
+
return { ok: false, detail: String(e.message ?? e), failureCode: 'BACKEND_UNSUPPORTED' };
|
|
415
|
+
}
|
|
416
|
+
const id = resourceIdSelector(query);
|
|
417
|
+
if (id) {
|
|
418
|
+
const visibleById = await resourceIdVisibility(session, d, id, provenance);
|
|
419
|
+
if (visibleById.ambiguous)
|
|
420
|
+
return { ok: false, detail: visibleById.detail, failureCode: 'AMBIGUOUS_SELECTOR' };
|
|
421
|
+
return visibleById.visible ? { ok: true } : { ok: false, detail: visibleById.detail ?? `"${shown(query)}" not visible`, failureCode: 'ASSERTION_FAILED' };
|
|
422
|
+
}
|
|
423
|
+
const ok = visible(await snapshot(session, d), fallbackVisibleQuery(query));
|
|
424
|
+
return ok ? { ok } : { ok, detail: `"${shown(query)}" not visible`, failureCode: 'ASSERTION_FAILED' };
|
|
425
|
+
}
|
|
426
|
+
case 'assertNotVisible': {
|
|
427
|
+
const resolved = resolveFlowText(step.query);
|
|
428
|
+
if (resolved.missing.length)
|
|
429
|
+
return unresolved(resolved.missing);
|
|
430
|
+
const query = resolved.out;
|
|
431
|
+
try {
|
|
432
|
+
const native = await nativeVisible(d, query);
|
|
433
|
+
if (native != null)
|
|
434
|
+
return !native ? { ok: true } : { ok: false, detail: `"${shown(query)}" unexpectedly visible`, failureCode: 'ASSERTION_FAILED' };
|
|
435
|
+
}
|
|
436
|
+
catch (e) {
|
|
437
|
+
return { ok: false, detail: String(e.message ?? e), failureCode: 'BACKEND_UNSUPPORTED' };
|
|
438
|
+
}
|
|
439
|
+
const id = resourceIdSelector(query);
|
|
440
|
+
if (id) {
|
|
441
|
+
const visibleById = await resourceIdVisibility(session, d, id, provenance);
|
|
442
|
+
if (visibleById.ambiguous)
|
|
443
|
+
return { ok: false, detail: visibleById.detail, failureCode: 'AMBIGUOUS_SELECTOR' };
|
|
444
|
+
return !visibleById.visible ? { ok: true } : { ok: false, detail: `"${shown(query)}" unexpectedly visible`, failureCode: 'ASSERTION_FAILED' };
|
|
445
|
+
}
|
|
446
|
+
const ok = !visible(await snapshot(session, d), fallbackVisibleQuery(query));
|
|
447
|
+
return ok ? { ok } : { ok, detail: `"${shown(query)}" unexpectedly visible`, failureCode: 'ASSERTION_FAILED' };
|
|
448
|
+
}
|
|
449
|
+
case 'assertImage': {
|
|
450
|
+
const tplPath = isAbsolute(step.template) ? step.template : join(session.root, step.template);
|
|
451
|
+
if (!existsSync(tplPath))
|
|
452
|
+
return { ok: false, detail: `template not found: ${tplPath}`, failureCode: 'ASSERTION_FAILED' };
|
|
453
|
+
const m = findTemplate(await d.screenshot(), readFileSync(tplPath), step.minScore ?? 0.85);
|
|
454
|
+
return m.found ? { ok: true } : { ok: false, detail: `image "${step.template}" not visible (best score ${m.score})`, failureCode: 'ASSERTION_FAILED' };
|
|
455
|
+
}
|
|
456
|
+
case 'assertOcrText': {
|
|
457
|
+
const resolved = resolveFlowText(step.query);
|
|
458
|
+
if (resolved.missing.length)
|
|
459
|
+
return unresolved(resolved.missing);
|
|
460
|
+
const query = resolved.out;
|
|
461
|
+
if (session.sensitive)
|
|
462
|
+
return { ok: false, detail: 'Sensitive mode refuses OCR visual text assertion because it would pass a screenshot to an external provider', failureCode: 'UNSAFE_ACTION_REFUSED' };
|
|
463
|
+
const command = configuredOcrCommand(session.root);
|
|
464
|
+
if (!command)
|
|
465
|
+
return { ok: false, detail: 'OCR command is not configured', failureCode: 'VISUAL_ONLY_SCREEN' };
|
|
466
|
+
const hit = findOcrRegion(await runOcr(d, session.root, command), query, step.minConfidence ?? 0.8);
|
|
467
|
+
return hit ? { ok: true } : { ok: false, detail: `OCR text "${shown(query)}" not found`, failureCode: 'ASSERTION_FAILED' };
|
|
468
|
+
}
|
|
469
|
+
case 'assertVisual': {
|
|
470
|
+
// human-readable visual checkpoint: capture evidence + record a visual note; passes.
|
|
471
|
+
const png = await d.screenshot();
|
|
472
|
+
const uri = sessions.saveArtifact(session, 'screenshot', `flow-visual-${Date.now()}.png`, png, 'image/png', `visual checkpoint: ${step.description}`);
|
|
473
|
+
sessions.bump(session, 'screenshots');
|
|
474
|
+
sessions.addNote(session, { at: Date.now(), workflow: `${flow.name}:visual`, outcome: 'pass', reason: step.description, method: 'visual', verifiedVisually: true, artifactUris: [uri] });
|
|
475
|
+
return { ok: true };
|
|
476
|
+
}
|
|
477
|
+
case 'assertDiff': {
|
|
478
|
+
const basePath = join(session.root, '.swipium', 'baselines', `${step.baseline}.png`);
|
|
479
|
+
if (!existsSync(basePath))
|
|
480
|
+
return { ok: false, detail: `no baseline "${step.baseline}" (create a visual baseline first)`, failureCode: 'MISSING_FIXTURE' };
|
|
481
|
+
const res = imageDiff(readFileSync(basePath), await d.screenshot());
|
|
482
|
+
const tol = step.threshold ?? 0.02;
|
|
483
|
+
return res.comparable && res.ratio <= tol ? { ok: true } : { ok: false, detail: res.comparable ? `${(res.ratio * 100).toFixed(2)}% changed > ${(tol * 100).toFixed(1)}%` : `not comparable: ${res.reason}`, failureCode: 'ASSERTION_FAILED' };
|
|
484
|
+
}
|
|
485
|
+
case 'swipe': {
|
|
486
|
+
const v = swipeVector(screenSize, step.direction, step.area, step.distance);
|
|
487
|
+
await d.swipe(v[0], v[1], v[2], v[3], 300);
|
|
488
|
+
sessions.bump(session, 'actions');
|
|
489
|
+
await settle(d, { timeoutMs });
|
|
490
|
+
return { ok: true };
|
|
491
|
+
}
|
|
492
|
+
case 'scrollTo': {
|
|
493
|
+
const resolved = resolveFlowText(step.query);
|
|
494
|
+
if (resolved.missing.length)
|
|
495
|
+
return unresolved(resolved.missing);
|
|
496
|
+
const query = resolved.out;
|
|
497
|
+
for (let n = 0; n < 8; n++) {
|
|
498
|
+
try {
|
|
499
|
+
const native = await nativeVisible(d, query);
|
|
500
|
+
if (native === true)
|
|
501
|
+
return { ok: true };
|
|
502
|
+
if (native === false) {
|
|
503
|
+
const v = swipeVector(screenSize, 'up');
|
|
504
|
+
await d.swipe(v[0], v[1], v[2], v[3], 300);
|
|
505
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch (e) {
|
|
510
|
+
return { ok: false, detail: String(e.message ?? e), failureCode: 'BACKEND_UNSUPPORTED' };
|
|
511
|
+
}
|
|
512
|
+
const id = resourceIdSelector(query);
|
|
513
|
+
if (id) {
|
|
514
|
+
const visibleById = await resourceIdVisibility(session, d, id, provenance);
|
|
515
|
+
if (visibleById.ambiguous)
|
|
516
|
+
return { ok: false, detail: visibleById.detail, failureCode: 'AMBIGUOUS_SELECTOR' };
|
|
517
|
+
if (visibleById.visible)
|
|
518
|
+
return { ok: true };
|
|
519
|
+
}
|
|
520
|
+
else if (visible(await snapshot(session, d), fallbackVisibleQuery(query))) {
|
|
521
|
+
return { ok: true };
|
|
522
|
+
}
|
|
523
|
+
const v = swipeVector(screenSize, 'up');
|
|
524
|
+
await d.swipe(v[0], v[1], v[2], v[3], 300);
|
|
525
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
526
|
+
}
|
|
527
|
+
return { ok: false, detail: `"${shown(query)}" not found after scrolling`, failureCode: 'ELEMENT_NOT_FOUND' };
|
|
528
|
+
}
|
|
529
|
+
case 'press':
|
|
530
|
+
await d.pressKey(step.key);
|
|
531
|
+
sessions.bump(session, 'actions');
|
|
532
|
+
await settle(d, { timeoutMs });
|
|
533
|
+
return { ok: true };
|
|
534
|
+
case 'openUrl': {
|
|
535
|
+
const { out, missing } = resolveFlowText(step.url);
|
|
536
|
+
if (missing.length)
|
|
537
|
+
return { ok: false, detail: `unresolved variable(s): ${missing.join(', ')}`, failureCode: 'MISSING_FIXTURE' };
|
|
538
|
+
await d.openUrl(out);
|
|
539
|
+
await settle(d, { timeoutMs });
|
|
540
|
+
return { ok: true };
|
|
541
|
+
}
|
|
542
|
+
case 'wait':
|
|
543
|
+
if (step.ms != null) {
|
|
544
|
+
const waitStarted = Date.now();
|
|
545
|
+
await new Promise((r) => setTimeout(r, step.ms));
|
|
546
|
+
sessions.addMilestoneDuration(session, 'wait_ms', Date.now() - waitStarted);
|
|
547
|
+
return { ok: true };
|
|
548
|
+
}
|
|
549
|
+
// falls through to query-wait
|
|
550
|
+
case 'waitForVisible': {
|
|
551
|
+
const rawQuery = step.kind === 'waitForVisible' ? step.query : step.query;
|
|
552
|
+
const resolved = resolveFlowText(rawQuery);
|
|
553
|
+
if (resolved.missing.length)
|
|
554
|
+
return unresolved(resolved.missing);
|
|
555
|
+
const query = resolved.out;
|
|
556
|
+
const deadline = Date.now() + (step.kind === 'waitForVisible' ? step.timeoutMs ?? timeoutMs : timeoutMs);
|
|
557
|
+
const waitStarted = Date.now();
|
|
558
|
+
while (Date.now() < deadline) {
|
|
559
|
+
try {
|
|
560
|
+
const native = await nativeVisible(d, query);
|
|
561
|
+
if (native === true) {
|
|
562
|
+
sessions.addMilestoneDuration(session, 'wait_ms', Date.now() - waitStarted);
|
|
563
|
+
return { ok: true };
|
|
564
|
+
}
|
|
565
|
+
if (native === false) {
|
|
566
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch (e) {
|
|
571
|
+
sessions.addMilestoneDuration(session, 'wait_ms', Date.now() - waitStarted);
|
|
572
|
+
return { ok: false, detail: String(e.message ?? e), failureCode: 'BACKEND_UNSUPPORTED' };
|
|
573
|
+
}
|
|
574
|
+
const id = resourceIdSelector(query);
|
|
575
|
+
if (id) {
|
|
576
|
+
const visibleById = await resourceIdVisibility(session, d, id, provenance);
|
|
577
|
+
if (visibleById.ambiguous) {
|
|
578
|
+
sessions.addMilestoneDuration(session, 'wait_ms', Date.now() - waitStarted);
|
|
579
|
+
return { ok: false, detail: visibleById.detail, failureCode: 'AMBIGUOUS_SELECTOR' };
|
|
580
|
+
}
|
|
581
|
+
if (!visibleById.visible) {
|
|
582
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else if (!visible(await snapshot(session, d), fallbackVisibleQuery(query))) {
|
|
587
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
{
|
|
591
|
+
sessions.addMilestoneDuration(session, 'wait_ms', Date.now() - waitStarted);
|
|
592
|
+
return { ok: true };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
sessions.addMilestoneDuration(session, 'wait_ms', Date.now() - waitStarted);
|
|
596
|
+
return { ok: false, detail: `timed out waiting for "${shown(query)}"`, failureCode: 'ASSERTION_FAILED' };
|
|
597
|
+
}
|
|
598
|
+
case 'waitForIdle': {
|
|
599
|
+
const waitStarted = Date.now();
|
|
600
|
+
await settle(d, { timeoutMs: step.timeoutMs ?? timeoutMs });
|
|
601
|
+
sessions.addMilestoneDuration(session, 'wait_ms', Date.now() - waitStarted);
|
|
602
|
+
return { ok: true };
|
|
603
|
+
}
|
|
604
|
+
case 'clearOverlay': {
|
|
605
|
+
// best-effort: hide the keyboard / dismiss a dialog with BACK (a flow cleanup, always ok)
|
|
606
|
+
if (await d.imeShown().catch(() => false))
|
|
607
|
+
await d.pressKey('back');
|
|
608
|
+
else
|
|
609
|
+
await d.pressKey('back');
|
|
610
|
+
await settle(d, { timeoutMs });
|
|
611
|
+
return { ok: true };
|
|
612
|
+
}
|
|
613
|
+
case 'networkOffline':
|
|
614
|
+
case 'networkOnline': {
|
|
615
|
+
const offline = step.kind === 'networkOffline';
|
|
616
|
+
const action = offline ? 'flow_network_offline' : 'flow_network_online';
|
|
617
|
+
try {
|
|
618
|
+
if (offline && !session.network)
|
|
619
|
+
session.network = { changed: true, originalAirplane: await d.airplaneOn().catch(() => false) };
|
|
620
|
+
await d.setAirplane(offline);
|
|
621
|
+
if (session.network)
|
|
622
|
+
session.network.changed = true;
|
|
623
|
+
sessions.addEnvChange(session, `network ${offline ? 'offline' : 'online'}`);
|
|
624
|
+
sessions.persist(session);
|
|
625
|
+
recordFlowMutation(action, 'medium', { stepKind: step.kind, to: offline ? 'offline' : 'online' }, 'executed');
|
|
626
|
+
return { ok: true };
|
|
627
|
+
}
|
|
628
|
+
catch (e) {
|
|
629
|
+
recordFlowMutation(action, 'medium', { stepKind: step.kind, to: offline ? 'offline' : 'online' }, 'blocked', String(e));
|
|
630
|
+
throw e;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
case 'seed': {
|
|
634
|
+
const fixture = session.fixtures.find((f) => f.name === step.fixture);
|
|
635
|
+
if (!fixture?.seed)
|
|
636
|
+
return { ok: false, detail: `fixture "${step.fixture}" has no seed spec`, failureCode: 'SEED_FAILED' };
|
|
637
|
+
const r = await executeSeed(sessions, session, d, step.fixture, fixture.seed);
|
|
638
|
+
const risk = fixture.seed.type === 'script' ? 'high' : fixture.seed.type === 'api' ? 'medium' : 'low';
|
|
639
|
+
if (!r.ok) {
|
|
640
|
+
sessions.addNote(session, { at: Date.now(), workflow: `seed:${step.fixture}`, outcome: 'blocked', category: 'missing_test_data', reason: `seed failed: ${r.detail}`, requiredState: fixture.requiredState, recommendedSetup: fixture.recommendedSetup });
|
|
641
|
+
recordFlowMutation('flow_seed_state', risk, { stepKind: step.kind, fixture: step.fixture, seedType: fixture.seed.type }, 'blocked', r.detail);
|
|
642
|
+
return { ok: false, detail: `seed "${step.fixture}" failed: ${r.detail}`, failureCode: 'SEED_FAILED' };
|
|
643
|
+
}
|
|
644
|
+
recordFlowMutation('flow_seed_state', risk, { stepKind: step.kind, fixture: step.fixture, seedType: fixture.seed.type }, 'executed', r.warnings.join(' ') || undefined);
|
|
645
|
+
return { ok: true };
|
|
646
|
+
}
|
|
647
|
+
case 'note':
|
|
648
|
+
sessions.addNote(session, { at: Date.now(), workflow: `${flow.name}:note`, outcome: step.outcome, reason: step.reason });
|
|
649
|
+
return { ok: true };
|
|
650
|
+
case 'screenshot': {
|
|
651
|
+
const png = await d.screenshot();
|
|
652
|
+
sessions.saveArtifact(session, 'screenshot', `flow-shot-${Date.now()}.png`, png, 'image/png', step.reason ?? 'flow screenshot');
|
|
653
|
+
sessions.bump(session, 'screenshots');
|
|
654
|
+
return { ok: true };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
let recordedFlowRuntime = false;
|
|
659
|
+
const finish = () => {
|
|
660
|
+
const elapsedMs = Date.now() - started;
|
|
661
|
+
if (!recordedFlowRuntime) {
|
|
662
|
+
sessions.addMilestoneDuration(session, 'flow_runtime_ms', elapsedMs);
|
|
663
|
+
recordedFlowRuntime = true;
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
name: flow.name,
|
|
667
|
+
appId,
|
|
668
|
+
passed,
|
|
669
|
+
failedAtStep,
|
|
670
|
+
reason,
|
|
671
|
+
failureCode,
|
|
672
|
+
steps,
|
|
673
|
+
durationSec: Math.round(elapsedMs / 100) / 10,
|
|
674
|
+
counters: session.counters,
|
|
675
|
+
};
|
|
676
|
+
};
|
|
677
|
+
// Run a list of steps; returns the failing global index, or -1 if all passed.
|
|
678
|
+
const runPhase = async (list, phase, base, failFast) => {
|
|
679
|
+
for (let j = 0; j < list.length; j++) {
|
|
680
|
+
const step = list[j];
|
|
681
|
+
const index = base + j;
|
|
682
|
+
const summary = describe(step);
|
|
683
|
+
const budget = sessions.budgetStop(session);
|
|
684
|
+
if (budget && phase !== 'teardown') {
|
|
685
|
+
steps.push({ index, phase, kind: step.kind, summary, ok: false, durationSec: 0, detail: budget });
|
|
686
|
+
passed = false;
|
|
687
|
+
failedAtStep = index;
|
|
688
|
+
reason = `budget: ${budget}`;
|
|
689
|
+
return index;
|
|
690
|
+
}
|
|
691
|
+
let outcome;
|
|
692
|
+
const stepStarted = Date.now();
|
|
693
|
+
try {
|
|
694
|
+
outcome = await execStep(step, index);
|
|
695
|
+
}
|
|
696
|
+
catch (e) {
|
|
697
|
+
outcome = { ok: false, detail: String(e), failureCode: classifyFlowDriverError(e) };
|
|
698
|
+
}
|
|
699
|
+
const rec = { index, phase, kind: step.kind, summary, ok: outcome.ok, durationSec: Math.round((Date.now() - stepStarted) / 100) / 10, detail: outcome.detail, failureCode: outcome.failureCode };
|
|
700
|
+
steps.push(rec);
|
|
701
|
+
if (!outcome.ok && failFast) {
|
|
702
|
+
passed = false;
|
|
703
|
+
failedAtStep = index;
|
|
704
|
+
reason = outcome.detail ?? 'step failed';
|
|
705
|
+
failureCode = outcome.failureCode;
|
|
706
|
+
try {
|
|
707
|
+
const png = await d.screenshot();
|
|
708
|
+
rec.screenshotUri = sessions.saveArtifact(session, 'screenshot', `flow-fail-${index}-${Date.now()}.png`, png, 'image/png', `flow ${flow.name} failed at step ${index}: ${summary}`);
|
|
709
|
+
}
|
|
710
|
+
catch { /* best-effort */ }
|
|
711
|
+
return index;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return -1;
|
|
715
|
+
};
|
|
716
|
+
// setup → main → teardown (teardown ALWAYS runs, even after a failure).
|
|
717
|
+
let nextBase = 0;
|
|
718
|
+
const setupFail = await runPhase(flow.setup, 'setup', nextBase, true);
|
|
719
|
+
nextBase += flow.setup.length;
|
|
720
|
+
if (setupFail < 0) {
|
|
721
|
+
await runPhase(flow.steps, 'main', nextBase, true);
|
|
722
|
+
}
|
|
723
|
+
nextBase += flow.steps.length;
|
|
724
|
+
await runPhase(flow.teardown, 'teardown', nextBase, false); // teardown failures are noted, not fatal
|
|
725
|
+
// Final health for the record (best-effort).
|
|
726
|
+
try {
|
|
727
|
+
const health = await checkHealth(d, appId);
|
|
728
|
+
await recordHealthFindings(sessions, session, health.findings, d, health.foreground);
|
|
729
|
+
const r = finish();
|
|
730
|
+
r.nativeHealth = health.nativeHealthy ? 'ok' : health.nativeStatus;
|
|
731
|
+
r.appHealth = health.appStatus;
|
|
732
|
+
return r;
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
return finish();
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
//# sourceMappingURL=run.js.map
|