browserstack-node-sdk 1.65.6 → 1.66.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/ats/src/cloudproviders/AWS.js +1 -1
- package/ats/src/cloudproviders/Azure.js +1 -1
- package/ats/src/cloudproviders/CloudProvider.js +1 -1
- package/ats/src/cloudproviders/GoogleCloudProvider.js +1 -1
- package/ats/src/cloudproviders/utils.js +1 -1
- package/ats/src/commands/configure.js +1 -1
- package/ats/src/commands/connect.js +1 -1
- package/ats/src/commands/connect_commands/grid.js +1 -1
- package/ats/src/commands/create.js +1 -1
- package/ats/src/commands/create_commands/grid.js +1 -1
- package/ats/src/commands/delete.js +1 -1
- package/ats/src/commands/delete_commands/grid.js +1 -1
- package/ats/src/commands/init.js +1 -1
- package/ats/src/commands/list.js +1 -1
- package/ats/src/commands/list_commands/grid_profiles.js +1 -1
- package/ats/src/commands/list_commands/grids.js +1 -1
- package/ats/src/commands/precheck.js +1 -1
- package/ats/src/commands/precheck_commands/setup_precheck.js +1 -1
- package/ats/src/commands/start.js +1 -1
- package/ats/src/commands/start_commands/grid.js +1 -1
- package/ats/src/commands/stop.js +1 -1
- package/ats/src/commands/stop_commands/grid.js +1 -1
- package/ats/src/config/constants.js +1 -1
- package/ats/src/config/logger.js +1 -1
- package/ats/src/index.js +1 -1
- package/ats/src/utils/AWSUtils.js +1 -1
- package/ats/src/utils/createAutoscalerStack.js +1 -1
- package/ats/src/utils/createClusterStack.js +1 -1
- package/ats/src/utils/createHubHaStack.js +1 -1
- package/ats/src/utils/createk8sClient.js +1 -1
- package/ats/src/utils/deleteEFSMountTargets.js +1 -1
- package/ats/src/utils/deleteStack.js +1 -1
- package/ats/src/utils/describeStack.js +1 -1
- package/ats/src/utils/gridComponents.js +1 -1
- package/ats/src/utils/hstUtils.js +1 -1
- package/ats/src/utils/instrumentationUtils.js +1 -1
- package/ats/src/utils/userInput.js +1 -1
- package/ats/src/utils/utilityMethods.js +1 -1
- package/ats/src/utils/webSocketUtils.js +1 -1
- package/generated/sdk-messages_pb.d.ts +118 -0
- package/generated/sdk-messages_pb.js +908 -0
- package/generated/sdk_grpc_pb.d.ts +10 -0
- package/generated/sdk_grpc_pb.js +74 -0
- package/package.json +1 -1
- package/src/bin/codeceptjs/BrowserStackPlugin.js +1 -1
- package/src/bin/codeceptjs/command.js +1 -1
- package/src/bin/codeceptjs/helper.js +1 -1
- package/src/bin/codeceptjs/o11yHelper.js +1 -1
- package/src/bin/cucumber-js/command.js +1 -1
- package/src/bin/cucumber-js/formatter/custom_formatter.js +1 -1
- package/src/bin/cucumber-js/formatter/custom_formatter_old_versions.js +1 -1
- package/src/bin/cucumber-js/test-observability.js +1 -1
- package/src/bin/cucumber-js/test.setup.js +1 -1
- package/src/bin/cucumber-js/test_old.setup.js +1 -1
- package/src/bin/cucumber-js/timeout.setup.js +1 -1
- package/src/bin/detox/a11ySetup.js +1 -0
- package/src/bin/detox/command.js +2 -0
- package/src/bin/detox/modifyDetoxConf.js +1 -0
- package/src/bin/detox/traEmitter.js +1 -0
- package/src/bin/integrations/command.js +1 -1
- package/src/bin/integrations/constants.js +1 -1
- package/src/bin/jasmine/bstack-reporter/customReporter.js +1 -1
- package/src/bin/jasmine/command.js +1 -1
- package/src/bin/jasmine/test.setup.js +1 -1
- package/src/bin/jasmine/testRunMap.js +1 -1
- package/src/bin/jest/command.js +1 -1
- package/src/bin/jest/customImplements/customCircus.js +1 -1
- package/src/bin/jest/customImplements/customEnvironment.js +1 -1
- package/src/bin/jest/customImplements/customJestAdapterInit.js +1 -1
- package/src/bin/jest/customImplements/customSequencer.js +1 -1
- package/src/bin/jest/customImplements/customTestRunnerTemplate.js +1 -1
- package/src/bin/jest/jest.setup.js +1 -1
- package/src/bin/jest/test-observability/data-hooks.js +1 -1
- package/src/bin/jest/test-observability/helper.js +1 -1
- package/src/bin/jest/test-observability/obsReporter.js +1 -1
- package/src/bin/jest/test-observability/test-details.js +1 -1
- package/src/bin/jest/test.setup.js +1 -1
- package/src/bin/jest/test_before.setup.js +1 -1
- package/src/bin/launcher/launcher.js +1 -1
- package/src/bin/load/index.js +1 -1
- package/src/bin/load/loadErrors.js +1 -1
- package/src/bin/load/loadUtils.js +1 -1
- package/src/bin/mocha/accessibility-automation/helper.js +1 -1
- package/src/bin/mocha/bstack-reporter/customReporter.js +1 -1
- package/src/bin/mocha/command.js +1 -1
- package/src/bin/mocha/test-observability/data-hooks.js +1 -1
- package/src/bin/mocha/test-observability/mochaUtils.js +1 -1
- package/src/bin/mocha/test.setup.js +1 -1
- package/src/bin/newman/command.js +1 -1
- package/src/bin/newman/helper.js +1 -1
- package/src/bin/newman/reporter/index.js +1 -1
- package/src/bin/newman/scriptRunner.js +1 -1
- package/src/bin/nightwatch/command.js +1 -1
- package/src/bin/node-generic/client.js +1 -1
- package/src/bin/playwright/accessibility-helper.js +1 -1
- package/src/bin/playwright/command.js +1 -1
- package/src/bin/playwright/globalSetup.js +1 -1
- package/src/bin/playwright/mock-process.js +1 -1
- package/src/bin/playwright/modifyPlaywrightConf.js +1 -1
- package/src/bin/playwright/patches.js +1 -1
- package/src/bin/playwright/reporter/pwUtils.js +1 -1
- package/src/bin/playwright/reporter/reporter.js +1 -1
- package/src/bin/playwright/reporter/test-details.js +1 -1
- package/src/bin/playwright/reporterArgs.js +1 -1
- package/src/bin/playwright/sessionDetails.js +1 -1
- package/src/bin/puppeteer/patches.js +1 -1
- package/src/bin/puppeteer/sessionDetails.js +1 -1
- package/src/bin/puppeteer/test.setup.js +1 -1
- package/src/bin/runner.js +1 -1
- package/src/bin/setup.js +1 -1
- package/src/bin/test-runner/index.js +1 -1
- package/src/bin/test-runner/runTest.js +1 -1
- package/src/bin/test-runner/testWorker.js +1 -1
- package/src/bin/testcafe/a11y-runner.js +1 -1
- package/src/bin/testcafe/command.js +1 -1
- package/src/bin/testcafe/reporter/index.js +1 -1
- package/src/bin/testcafe/reporter/tcUtils.js +1 -1
- package/src/bin/testcafe/session-registry.js +1 -1
- package/src/bin/testcafe/stdout-interceptor.js +1 -1
- package/src/bin/testcafe/testcafe-action-patcher.js +1 -1
- package/src/bin/testcafe/w3c-driver.js +1 -1
- package/src/bin/utils/apiUtilityMethods.js +1 -1
- package/src/bin/utils/clientTroubleshootingService.js +1 -1
- package/src/bin/utils/constants.js +1 -1
- package/src/bin/utils/log4jsAppender.js +1 -1
- package/src/bin/utils/logPatcher.js +1 -1
- package/src/bin/utils/logReportingAPI.js +1 -1
- package/src/bin/utils/playwright-patcher.js +1 -1
- package/src/bin/utils/puppeteer-patcher.js +1 -1
- package/src/bin/utils/sharedPlaywrightSessionDetails.js +1 -1
- package/src/bin/utils/sharedUtils.js +1 -1
- package/src/bin/v2/TestInfo.js +1 -1
- package/src/bin/v2/browserstackCLI.js +1 -1
- package/src/bin/v2/cliUtils.js +1 -1
- package/src/bin/v2/customTagManager.js +1 -1
- package/src/bin/v2/eventDispatcher.js +1 -1
- package/src/bin/v2/framework/automationFramework.js +1 -1
- package/src/bin/v2/framework/constants/automationFrameworkConstants.js +1 -1
- package/src/bin/v2/framework/constants/testFrameworkConstants.js +1 -1
- package/src/bin/v2/framework/cucumberTestFramework.js +1 -1
- package/src/bin/v2/framework/jestTestFramework.js +1 -1
- package/src/bin/v2/framework/newmanTestFramework.js +1 -1
- package/src/bin/v2/framework/nodeGenericTestFramework.js +1 -1
- package/src/bin/v2/framework/playwrightAutomationFramework.js +1 -1
- package/src/bin/v2/framework/playwrightTestFramework.js +1 -1
- package/src/bin/v2/framework/puppeteerAutomationFramework.js +1 -1
- package/src/bin/v2/framework/testFramework.js +1 -1
- package/src/bin/v2/framework/testcafeAutomationFramework.js +1 -1
- package/src/bin/v2/framework/testcafeTestFramework.js +1 -1
- package/src/bin/v2/grpcClient.js +1 -1
- package/src/bin/v2/instances/automationFrameworkInstance.js +1 -1
- package/src/bin/v2/instances/testFrameworkInstance.js +1 -1
- package/src/bin/v2/instances/trackedContext.js +1 -1
- package/src/bin/v2/instances/trackedInstance.js +1 -1
- package/src/bin/v2/modules/accessibilityModule.js +1 -1
- package/src/bin/v2/modules/baseModule.js +1 -1
- package/src/bin/v2/modules/loadTestModule.js +1 -1
- package/src/bin/v2/modules/observabilityModule.js +1 -1
- package/src/bin/v2/modules/playwrightDriverModule.js +1 -1
- package/src/bin/v2/modules/puppeteerDriverModule.js +1 -1
- package/src/bin/v2/modules/testcafeDriverModule.js +1 -1
- package/src/bin/v2/modules/testhubModule.js +1 -1
- package/src/bin/v2/sdkErrorHandler.js +1 -1
- package/src/bin/v2/state/automationFrameworkState.js +1 -1
- package/src/bin/v2/state/hookState.js +1 -1
- package/src/bin/v2/state/testFrameworkState.js +1 -1
- package/src/bin/v2/uploadAttachmentsManager.js +1 -1
- package/src/bin/v2/utils/cucumber-utils.js +1 -1
- package/src/bin/v2/utils/jest-utils.js +1 -1
- package/src/bin/vanilla-js/command.js +1 -1
- package/src/browserStackSdk.js +1 -1
- package/src/detox-driver/a11y.js +574 -0
- package/src/detox-driver/allocationDriver.js +26 -0
- package/src/detox-driver/cloudPlatformAction.js +111 -0
- package/src/detox-driver/environment.js +253 -0
- package/src/detox-driver/expect.js +44 -0
- package/src/detox-driver/index.js +12 -0
- package/src/detox-driver/login-timeout-preload.js +25 -0
- package/src/detox-driver/runtimeDriver.js +351 -0
- package/src/detox-driver/session-reporter.js +41 -0
- package/src/detox-driver/tra-capture.js +81 -0
- package/src/helpers/BrowserStackSetup.js +1 -1
- package/src/helpers/accessibility-automation/accessibilityRetryHelper.js +1 -1
- package/src/helpers/accessibility-automation/constants.js +1 -1
- package/src/helpers/accessibility-automation/helper.js +1 -1
- package/src/helpers/accessibility-automation/scripts.js +1 -1
- package/src/helpers/ats/constants.js +1 -1
- package/src/helpers/ats/helper.js +1 -1
- package/src/helpers/caCertHelper.js +1 -1
- package/src/helpers/capsMapping.js +1 -1
- package/src/helpers/central-scanner/constants.js +1 -1
- package/src/helpers/central-scanner/helpers.js +1 -1
- package/src/helpers/classLogger.js +1 -1
- package/src/helpers/driverInitErrorReporter.js +1 -1
- package/src/helpers/globals.js +1 -1
- package/src/helpers/helper.js +1 -1
- package/src/helpers/logger.js +1 -1
- package/src/helpers/metadata.js +1 -1
- package/src/helpers/patchHelper.js +1 -1
- package/src/helpers/patchHelpers.js +1 -1
- package/src/helpers/percy/Percy.js +1 -1
- package/src/helpers/percy/PercyBinary.js +1 -1
- package/src/helpers/percy/PercyCaptureMap.js +1 -1
- package/src/helpers/percy/PercyCaptureModes.js +1 -1
- package/src/helpers/percy/PercySDK.js +1 -1
- package/src/helpers/percy/index.js +1 -1
- package/src/helpers/percy/utils.js +1 -1
- package/src/helpers/performance/constants.js +1 -1
- package/src/helpers/performance/performance-tester.js +1 -1
- package/src/helpers/playwright-helper.js +1 -1
- package/src/helpers/processHelper.js +1 -1
- package/src/helpers/puppeteer-helper.js +1 -1
- package/src/helpers/request-spy.js +1 -1
- package/src/helpers/setKeepAlive.js +1 -1
- package/src/helpers/stackFrameUtils.js +1 -1
- package/src/helpers/test-observability/constants.js +1 -1
- package/src/helpers/test-observability/error-handler.js +1 -1
- package/src/helpers/test-observability/requestQueueHandler.js +1 -1
- package/src/helpers/test-observability/sessionHandler.js +1 -1
- package/src/helpers/test-observability/utils.js +1 -1
- package/src/helpers/test-orchestration/constants.js +1 -1
- package/src/helpers/test-orchestration/failfastManager.js +1 -1
- package/src/helpers/test-orchestration/fileHelper.js +1 -1
- package/src/helpers/test-orchestration/orchestrationUtils.js +1 -1
- package/src/helpers/test-orchestration/requestUtils.js +1 -1
- package/src/helpers/test-orchestration/testOrchestrationHandler.js +1 -1
- package/src/helpers/test-orchestration/testOrderingServerHandler.js +1 -1
- package/src/helpers/testhub/constants.js +1 -1
- package/src/helpers/testhub/testhubHandler.js +1 -1
- package/src/helpers/testhub/utils.js +1 -1
- package/src/helpers/utilities.js +1 -1
- package/src/helpers/w3cMapping.js +1 -1
- package/src/hubAllocationMethods.js +1 -1
- package/src/index.js +1 -1
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// App Accessibility scan hook — runs inside the detox jest process (Node realm), NOT the
|
|
4
|
+
// SDK. Same realm rules as tra-capture/session-reporter: node builtins only, and every
|
|
5
|
+
// path is best-effort. A11y is instrumentation; a scan that cannot be armed, routed or
|
|
6
|
+
// answered must leave the customer's test result byte-identical to a run without it.
|
|
7
|
+
//
|
|
8
|
+
// The arming data (scan script, build uuid, tokens, command list) is resolved in the SDK
|
|
9
|
+
// main process and handed over as a JSON file named by BROWSERSTACK_DETOX_A11Y_CONFIG —
|
|
10
|
+
// the detox child inherits the parent env wholesale. A file rather than an env value
|
|
11
|
+
// because the scan script is an opaque server-supplied string of unbounded length.
|
|
12
|
+
//
|
|
13
|
+
// The scan itself is NOT ours to compose: the server ships a `scan` script carrying a
|
|
14
|
+
// printf-style `%s`, and every App-A11y SDK substitutes a JSON args object into it and
|
|
15
|
+
// hands the result to the framework's execute primitive (java `String.format`, csharp
|
|
16
|
+
// `.Replace("%s", …)`, ruby `format_string`). Detox has no execute primitive and no
|
|
17
|
+
// WebDriver session, so the substituted string is relayed over the one device-side
|
|
18
|
+
// channel that exists — the detox WS bridge, whose `CloudPlatform` action the Hub
|
|
19
|
+
// already intercepts. Which frame shape the Hub routes is discovered at runtime by the
|
|
20
|
+
// candidate ladder below, because it is not documented anywhere we can read.
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const https = require('https');
|
|
23
|
+
|
|
24
|
+
const LOG_PREFIX = '[BrowserStack App A11y]';
|
|
25
|
+
|
|
26
|
+
// Deliberately shorter than CloudPlatformAction's 90s: an unrouted frame must not hold a
|
|
27
|
+
// test for a minute and a half, and a scan the far side never answers has to be written
|
|
28
|
+
// off while the run is still cheap to save.
|
|
29
|
+
const SCAN_TIMEOUT_MS = 30000;
|
|
30
|
+
|
|
31
|
+
const loggedOnce = new Set();
|
|
32
|
+
let config = null;
|
|
33
|
+
let disabled = '';
|
|
34
|
+
let routedCandidate = null;
|
|
35
|
+
let laddersWalked = false;
|
|
36
|
+
let scansAttempted = 0;
|
|
37
|
+
let scansRouted = 0;
|
|
38
|
+
// The WS client is reachable only through the runtime driver — the expect/matcher surfaces
|
|
39
|
+
// are handed an invocationManager, not a client. The driver registers it on construction,
|
|
40
|
+
// which always precedes any element action.
|
|
41
|
+
let registeredClient = null;
|
|
42
|
+
|
|
43
|
+
const registerClient = (client) => {
|
|
44
|
+
registeredClient = client || registeredClient;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const log = (message) => {
|
|
48
|
+
try {
|
|
49
|
+
console.log(`${LOG_PREFIX} ${message}`);
|
|
50
|
+
} catch (err) { /* a log must never break a run */ }
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const logOnce = (key, message) => {
|
|
54
|
+
if (loggedOnce.has(key)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
loggedOnce.add(key);
|
|
58
|
+
log(message);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const brief = (value, limit = 600) => {
|
|
62
|
+
try {
|
|
63
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value);
|
|
64
|
+
if (text === undefined) {
|
|
65
|
+
return String(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return text.length > limit ? `${text.slice(0, limit)}…(${text.length} chars)` : text;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return String(value);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Command entries arrive in the shape App Accessibility serves them:
|
|
75
|
+
// {library, method, module, name, class}. `class` and the trailing segment of `module`
|
|
76
|
+
// both name the surface; either is accepted because only one of them is guaranteed by
|
|
77
|
+
// the served payload. `name` is what the scan reports as its `method` — the served set
|
|
78
|
+
// carries BOTH bare and disambiguated names for the same method on different surfaces
|
|
79
|
+
// (`tap` on NativeElement vs `deviceTap` on RuntimeDevice), which is exactly why the
|
|
80
|
+
// match key is (surface, method) and never the name alone.
|
|
81
|
+
const normaliseCommands = (raw) => {
|
|
82
|
+
const commands = [];
|
|
83
|
+
if (!Array.isArray(raw)) {
|
|
84
|
+
return commands;
|
|
85
|
+
}
|
|
86
|
+
raw.forEach((entry) => {
|
|
87
|
+
if (!entry || !entry.method) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const moduleTail = String(entry.module || '').split('/').pop();
|
|
91
|
+
const surface = String(entry.class || moduleTail || '');
|
|
92
|
+
if (!surface) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
commands.push({
|
|
96
|
+
surface: surface.toLowerCase(),
|
|
97
|
+
method: String(entry.method),
|
|
98
|
+
name: String(entry.name || entry.method),
|
|
99
|
+
source: entry.bstackLocal === true ? 'sdk-local' : 'served'
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return commands;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const loadConfig = () => {
|
|
107
|
+
if (config !== null) {
|
|
108
|
+
return config;
|
|
109
|
+
}
|
|
110
|
+
config = false;
|
|
111
|
+
try {
|
|
112
|
+
const file = process.env.BROWSERSTACK_DETOX_A11Y_CONFIG;
|
|
113
|
+
if (!file || !fs.existsSync(file)) {
|
|
114
|
+
return config;
|
|
115
|
+
}
|
|
116
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
117
|
+
if (!raw || !raw.scanScript) {
|
|
118
|
+
log('armed but the build carried no scan script — no scans for this run.');
|
|
119
|
+
|
|
120
|
+
return config;
|
|
121
|
+
}
|
|
122
|
+
const commands = normaliseCommands(raw.commands);
|
|
123
|
+
if (!commands.length) {
|
|
124
|
+
log('armed but the build carried no commands to wrap — no scans for this run.');
|
|
125
|
+
|
|
126
|
+
return config;
|
|
127
|
+
}
|
|
128
|
+
config = {
|
|
129
|
+
scanScript: String(raw.scanScript),
|
|
130
|
+
thBuildUuid: String(raw.thBuildUuid || ''),
|
|
131
|
+
thJwtToken: String(raw.thJwtToken || ''),
|
|
132
|
+
authHeader: String(raw.authHeader || ''),
|
|
133
|
+
centralAuthHeader: String(raw.centralAuthHeader || ''),
|
|
134
|
+
commands,
|
|
135
|
+
hub: raw.hub && raw.hub.user && raw.hub.key ? raw.hub : null,
|
|
136
|
+
probe: raw.probe === true
|
|
137
|
+
};
|
|
138
|
+
const bySource = commands.reduce((acc, cmd) => {
|
|
139
|
+
acc[cmd.source] = (acc[cmd.source] || 0) + 1;
|
|
140
|
+
|
|
141
|
+
return acc;
|
|
142
|
+
}, {});
|
|
143
|
+
log(`armed — ${commands.length} command(s) to wrap (`
|
|
144
|
+
+ `${Object.keys(bySource).map((k) => `${k}: ${bySource[k]}`).join(', ')}`
|
|
145
|
+
+ `), build ${config.thBuildUuid || '<unknown>'}`
|
|
146
|
+
+ `, hub relay ${config.hub ? config.hub.host : 'unavailable (no credentials)'}`
|
|
147
|
+
+ `${config.probe ? ', transport probe ON' : ''}.`);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
log(`could not read the arming data (${err && err.message}) — no scans for this run.`);
|
|
150
|
+
config = false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return config;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const enabled = () => Boolean(loadConfig()) && !disabled;
|
|
157
|
+
|
|
158
|
+
// Which command name (if any) a call on `surface` maps to. Returns null for anything not
|
|
159
|
+
// in the list — matchers, expectations and every unmapped method stay untouched.
|
|
160
|
+
const commandNameFor = (surface, method) => {
|
|
161
|
+
const cfg = loadConfig();
|
|
162
|
+
if (!cfg) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const key = String(surface || '').toLowerCase();
|
|
166
|
+
const hit = cfg.commands.find((cmd) => cmd.surface === key && cmd.method === method);
|
|
167
|
+
|
|
168
|
+
return hit ? hit : null;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// The scan args, exactly the set the reference SDKs put on the wire for the app branch
|
|
172
|
+
// (javaagent AccessibilityModule.getParamsForScanForCli). `method` is the matched command
|
|
173
|
+
// name; the backend does not validate it against a closed set, so the observed values
|
|
174
|
+
// differ across SDKs (a test-method name from TestNG, command names from WDIO).
|
|
175
|
+
const scanArgs = (cfg, testUuid, commandName) => {
|
|
176
|
+
const args = {
|
|
177
|
+
thTestRunUuid: testUuid,
|
|
178
|
+
thBuildUuid: cfg.thBuildUuid,
|
|
179
|
+
thJwtToken: cfg.thJwtToken,
|
|
180
|
+
authHeader: cfg.authHeader,
|
|
181
|
+
method: commandName,
|
|
182
|
+
scanTimestamp: String(Date.now())
|
|
183
|
+
};
|
|
184
|
+
// The reference SDKs send the CENTRAL SDK JWT here — verified byte-identical to
|
|
185
|
+
// `thJwtToken` in a working TestNG scan (both the 840-char `iss: www.browserstack.com`
|
|
186
|
+
// token; `authHeader` is the separate build-scoped a11y token). Omitting it is why a scan
|
|
187
|
+
// could be accepted (`scan_pending: true`) and still never produce a snapshot: the far
|
|
188
|
+
// side has the build token but not the central one.
|
|
189
|
+
const central = cfg.centralAuthHeader || cfg.thJwtToken;
|
|
190
|
+
if (central) {
|
|
191
|
+
args.centralAuthHeader = central;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return args;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Candidate frame shapes for relaying the scan, most-likely first. Every one rides the
|
|
198
|
+
// `CloudPlatform` action the Hub already intercepts for launchApp/terminateApp, so an
|
|
199
|
+
// unrecognised shape is answered (or refused) by the Hub rather than forwarded to the
|
|
200
|
+
// stock upstream detox-server, which would reject an unknown action type. The
|
|
201
|
+
// executor-string variants carry the substituted script verbatim; the argument variants
|
|
202
|
+
// carry the parsed action + arguments, which is the shape app-automate's own spec names
|
|
203
|
+
// for a `device.browserstackExecutor({action, arguments})` helper.
|
|
204
|
+
// The Hub dispatches on `method` and answers an unknown one with a well-formed
|
|
205
|
+
// `{success:false, message:'method <x> not supported'}` — cheaply, in well under a second,
|
|
206
|
+
// with no effect on the socket. That makes a name sweep both safe and conclusive, so the
|
|
207
|
+
// probe list is deliberately broad: the entry name is app-automate's to choose and is not
|
|
208
|
+
// documented anywhere readable from here.
|
|
209
|
+
// The scan's other possible carrier, and the only one that needs nothing from the Hub: the
|
|
210
|
+
// `cloudSessionId` the bridge returns on every CloudPlatform response is a real Hub session
|
|
211
|
+
// key — a request against a FINISHED one is answered `{"status":13,"value":{"message":
|
|
212
|
+
// "Session not started or terminated"}}`, i.e. looked up and found absent, not refused as
|
|
213
|
+
// unroutable. If the record is live for the duration of the session, the standard WebDriver
|
|
214
|
+
// execute endpoint reaches the same `jsExecutor` → `appAllyHandler.appAllyScan` path every
|
|
215
|
+
// Appium SDK uses, with the executor string verbatim and no new Hub route.
|
|
216
|
+
//
|
|
217
|
+
// Never rejects; the outcome is the status line and the body, because a refusal's SHAPE is
|
|
218
|
+
// what distinguishes "no such session" from "no such command" from "not allowed here".
|
|
219
|
+
const httpPost = (hub, pathname, body, timeoutMs) => new Promise((resolve) => {
|
|
220
|
+
let settled = false;
|
|
221
|
+
const done = (outcome) => {
|
|
222
|
+
if (!settled) {
|
|
223
|
+
settled = true;
|
|
224
|
+
resolve(outcome);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
try {
|
|
228
|
+
const request = https.request({
|
|
229
|
+
host: hub.host,
|
|
230
|
+
port: 443,
|
|
231
|
+
method: 'POST',
|
|
232
|
+
path: pathname,
|
|
233
|
+
headers: {
|
|
234
|
+
'Content-Type': 'application/json',
|
|
235
|
+
'Content-Length': Buffer.byteLength(body),
|
|
236
|
+
Authorization: `Basic ${Buffer.from(`${hub.user}:${hub.key}`).toString('base64')}`
|
|
237
|
+
},
|
|
238
|
+
timeout: timeoutMs
|
|
239
|
+
}, (response) => {
|
|
240
|
+
let raw = '';
|
|
241
|
+
response.setEncoding('utf8');
|
|
242
|
+
response.on('data', (chunk) => {
|
|
243
|
+
raw += chunk;
|
|
244
|
+
});
|
|
245
|
+
response.on('end', () => done({status: response.statusCode, body: raw}));
|
|
246
|
+
});
|
|
247
|
+
request.on('timeout', () => {
|
|
248
|
+
request.destroy(new Error(`no answer within ${timeoutMs}ms`));
|
|
249
|
+
});
|
|
250
|
+
request.on('error', (err) => done({error: (err && err.message) || String(err)}));
|
|
251
|
+
request.end(body);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
done({error: (err && err.message) || String(err)});
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const httpScan = async (cfg, candidate, script, timeoutMs) => {
|
|
258
|
+
if (!cfg.hub) {
|
|
259
|
+
return {routed: false, fatal: false, detail: 'no hub credentials in the arming data'};
|
|
260
|
+
}
|
|
261
|
+
const sessionId = require('./cloudPlatformAction').getCloudSessionId();
|
|
262
|
+
if (!sessionId) {
|
|
263
|
+
return {routed: false, fatal: false, detail: 'no cloud session id seen on the bridge yet'};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const result = await httpPost(
|
|
267
|
+
cfg.hub, `/wd/hub/session/${sessionId}${candidate.suffix}`, JSON.stringify({script, args: []}), timeoutMs
|
|
268
|
+
);
|
|
269
|
+
if (result.error) {
|
|
270
|
+
return {routed: false, fatal: false, detail: `transport error: ${result.error}`};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let parsed = null;
|
|
274
|
+
try {
|
|
275
|
+
parsed = JSON.parse(result.body);
|
|
276
|
+
} catch (err) { /* a non-JSON body is reported verbatim below */ }
|
|
277
|
+
const value = parsed && parsed.value;
|
|
278
|
+
// W3C reports failure as `value.error`; the JSONWire dialect this Hub also speaks reports
|
|
279
|
+
// it as a non-zero top-level `status`. A refusal is never a route.
|
|
280
|
+
const errored = result.status !== 200
|
|
281
|
+
|| (value && typeof value === 'object' && value.error)
|
|
282
|
+
|| (parsed && parsed.status);
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
routed: !errored,
|
|
286
|
+
fatal: false,
|
|
287
|
+
detail: `HTTP ${result.status} session=${sessionId} body=${brief(result.body)}`
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const CANDIDATES = [
|
|
292
|
+
// First because it is the only shape that could make this SDK-only, and because an HTTP
|
|
293
|
+
// call is off the WS socket entirely — it cannot disturb an in-flight detox action.
|
|
294
|
+
{id: 'http:wd-execute-sync', kind: 'http', suffix: '/execute/sync'},
|
|
295
|
+
{id: 'http:wd-execute', kind: 'http', suffix: '/execute'},
|
|
296
|
+
|
|
297
|
+
{id: 'cloudPlatform:appAllyScan', params: (script, args) => ({method: 'appAllyScan', arguments: args})},
|
|
298
|
+
{id: 'cloudPlatform:browserstackExecutor+action', params: (script, args) => ({method: 'browserstackExecutor', action: 'appAllyScan', arguments: args})},
|
|
299
|
+
{id: 'cloudPlatform:browserstackExecutor+script', params: (script) => ({method: 'browserstackExecutor', script})},
|
|
300
|
+
{id: 'cloudPlatform:executeScript', params: (script) => ({method: 'executeScript', script})},
|
|
301
|
+
{id: 'cloudPlatform:accessibilityScan', params: (script, args) => ({method: 'accessibilityScan', arguments: args})},
|
|
302
|
+
|
|
303
|
+
{id: 'cloudPlatform:appAllyScan+script', probeOnly: true, params: (script) => ({method: 'appAllyScan', script})},
|
|
304
|
+
{id: 'cloudPlatform:appAccessibilityScan', probeOnly: true, params: (script, args) => ({method: 'appAccessibilityScan', arguments: args})},
|
|
305
|
+
{id: 'cloudPlatform:performScan', probeOnly: true, params: (script, args) => ({method: 'performScan', arguments: args})},
|
|
306
|
+
{id: 'cloudPlatform:jsExecutor', probeOnly: true, params: (script) => ({method: 'jsExecutor', script})},
|
|
307
|
+
{id: 'cloudPlatform:customExecutor', probeOnly: true, params: (script) => ({method: 'customExecutor', script})},
|
|
308
|
+
{id: 'cloudPlatform:execute', probeOnly: true, params: (script) => ({method: 'execute', script})},
|
|
309
|
+
{id: 'cloudPlatform:sendRequestToPlatform', probeOnly: true, params: (script) => ({method: 'sendRequestToPlatform', script})},
|
|
310
|
+
{id: 'cloudPlatform:accessibility_info', probeOnly: true, params: (script, args) => ({method: 'accessibility_info', arguments: args})},
|
|
311
|
+
|
|
312
|
+
// Deliberately last: a frame whose ACTION TYPE the Hub does not intercept is relayed
|
|
313
|
+
// generically to the stock upstream detox-server, and an unknown type there answers with
|
|
314
|
+
// a frame detox's own _onMessage cannot place — which rejects every pending request and
|
|
315
|
+
// takes the run down. Worth measuring once under the probe flag; never on a customer run.
|
|
316
|
+
{id: 'action:browserstackExecutor', probeOnly: true, actionType: 'browserstackExecutor', params: (script, args) => ({action: 'appAllyScan', arguments: args})}
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
// A response frame the Hub never answered leaves its entry in the WS client's in-flight
|
|
320
|
+
// map (detox's InflightRequest.reject does not remove it), and the NEXT send then sees two
|
|
321
|
+
// atomic requests in flight and rejects BOTH — which would fail the customer's next
|
|
322
|
+
// command. Clearing our own entry the way detox's own resetInFlightPromises does, and
|
|
323
|
+
// recording it as aborted so a late answer is logged rather than thrown as an unexpected
|
|
324
|
+
// message, is what keeps a dead scan from taking the run with it.
|
|
325
|
+
const releaseInflight = (client, messageId) => {
|
|
326
|
+
try {
|
|
327
|
+
const ws = client && client._asyncWebSocket;
|
|
328
|
+
if (!ws || messageId === undefined || messageId === null) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (ws.inFlightPromises && Object.prototype.hasOwnProperty.call(ws.inFlightPromises, messageId)) {
|
|
332
|
+
delete ws.inFlightPromises[messageId];
|
|
333
|
+
}
|
|
334
|
+
if (ws._abortedMessageIds && typeof ws._abortedMessageIds.add === 'function') {
|
|
335
|
+
ws._abortedMessageIds.add(+messageId);
|
|
336
|
+
}
|
|
337
|
+
} catch (err) { /* best-effort */ }
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// Outcome shape: {routed:boolean, fatal:boolean, detail:string}. `fatal` means the socket
|
|
341
|
+
// is no longer safe to send scans on, so the ladder stops and scanning is disabled for the
|
|
342
|
+
// rest of the session — one bad frame, never a cascade.
|
|
343
|
+
const attempt = async (client, actions, candidate, script, args) => {
|
|
344
|
+
if (candidate.kind === 'http') {
|
|
345
|
+
return httpScan(loadConfig(), candidate, script, SCAN_TIMEOUT_MS);
|
|
346
|
+
}
|
|
347
|
+
// Construction is INSIDE the try on purpose: `candidate.params()` and the Action constructor
|
|
348
|
+
// are the caller's own code running inside a real device action's promise chain, so a throw
|
|
349
|
+
// here would fail the customer's command — the exact outcome this file exists to prevent.
|
|
350
|
+
let action;
|
|
351
|
+
try {
|
|
352
|
+
action = new actions.ScanRelayAction(
|
|
353
|
+
candidate.params(script, args), SCAN_TIMEOUT_MS, candidate.actionType
|
|
354
|
+
);
|
|
355
|
+
const response = await client.sendAction(action);
|
|
356
|
+
const type = response && response.type;
|
|
357
|
+
const envelopeAccepted = type === 'CloudPlatform' || type === 'browserstackExecutor';
|
|
358
|
+
// The Hub answers an unmapped method with a well-formed frame of the RIGHT type and
|
|
359
|
+
// `success: false` ("method <x> not supported"). Reading the type alone would score
|
|
360
|
+
// that as a working transport and stop the search at the first refusal.
|
|
361
|
+
const inner = (response && response.response) || {};
|
|
362
|
+
const refused = inner.success === false;
|
|
363
|
+
|
|
364
|
+
if (envelopeAccepted && !refused) {
|
|
365
|
+
return {routed: true, fatal: false, detail: `type=${type} response=${brief(response)}`};
|
|
366
|
+
}
|
|
367
|
+
if (envelopeAccepted) {
|
|
368
|
+
return {routed: false, fatal: false, detail: `Hub refused: ${inner.message || brief(response)}`};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {routed: false, fatal: false, detail: `unexpected response type=${type} response=${brief(response)}`};
|
|
372
|
+
} catch (err) {
|
|
373
|
+
const message = String((err && err.message) || err);
|
|
374
|
+
const timedOut = message.includes('has not received a response within');
|
|
375
|
+
// `action` is undefined if construction itself threw — there is no in-flight entry to
|
|
376
|
+
// release in that case, and dereferencing it here would turn a handled failure into an
|
|
377
|
+
// unhandled one.
|
|
378
|
+
if (timedOut && action) {
|
|
379
|
+
releaseInflight(client, action.messageId);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {routed: false, fatal: timedOut, detail: `${timedOut ? 'timeout' : 'error'}: ${brief(message)}`};
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// Fire one scan for a completed command. Awaited by the caller on purpose: detox rejects
|
|
387
|
+
// every in-flight atomic action the moment a second one is sent, so a fire-and-forget scan
|
|
388
|
+
// would fail the customer's next command. Never throws.
|
|
389
|
+
const scan = async (client, commandName, testUuid) => {
|
|
390
|
+
const cfg = loadConfig();
|
|
391
|
+
if (!cfg || disabled || !client) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// A scan with no test to attach to is an orphan the dashboard cannot place. Hook-scope
|
|
396
|
+
// uuids are not published, so a beforeAll/beforeEach action is suppressed rather than
|
|
397
|
+
// reported against an empty uuid.
|
|
398
|
+
if (!testUuid) {
|
|
399
|
+
logOnce('no-uuid', `no scan for "${commandName}" and any other command outside a test body`
|
|
400
|
+
+ ' — a scan can only be attributed to a running test.');
|
|
401
|
+
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let args;
|
|
406
|
+
let script;
|
|
407
|
+
try {
|
|
408
|
+
// Built once: the script-carrying and argument-carrying relay shapes must describe the
|
|
409
|
+
// same scan, and a second scanArgs() call would give them different scanTimestamps.
|
|
410
|
+
args = scanArgs(cfg, testUuid, commandName);
|
|
411
|
+
// Function replacement: `$&`-style sequences inside a JWT would otherwise be read as
|
|
412
|
+
// replacement patterns.
|
|
413
|
+
const json = JSON.stringify(args);
|
|
414
|
+
script = cfg.scanScript.includes('%s') ? cfg.scanScript.replace('%s', () => json) : cfg.scanScript;
|
|
415
|
+
} catch (err) {
|
|
416
|
+
log(`could not build the scan payload for "${commandName}" (${err && err.message}); skipping.`);
|
|
417
|
+
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const actions = require('./cloudPlatformAction');
|
|
422
|
+
scansAttempted++;
|
|
423
|
+
|
|
424
|
+
// Once a candidate has routed, only that one is used. The full ladder is walked exactly
|
|
425
|
+
// once — a detox cloud session allocates rarely enough that one-shape-per-run would cost
|
|
426
|
+
// days, so the first scan of a probe run measures every shape and later scans use the
|
|
427
|
+
// winner. Outside probe mode the ladder stops at the first shape that answers.
|
|
428
|
+
const sweeping = cfg.probe && !laddersWalked;
|
|
429
|
+
const ladder = (routedCandidate && !sweeping)
|
|
430
|
+
? [routedCandidate]
|
|
431
|
+
: CANDIDATES.filter((candidate) => sweeping || !candidate.probeOnly);
|
|
432
|
+
let routed = false;
|
|
433
|
+
|
|
434
|
+
for (const candidate of ladder) {
|
|
435
|
+
const outcome = await attempt(client, actions, candidate, script, args);
|
|
436
|
+
const line = `scan for "${commandName}" via ${candidate.id} → ${outcome.routed ? 'ROUTED' : 'no route'} — ${outcome.detail}`;
|
|
437
|
+
if (outcome.routed || sweeping) {
|
|
438
|
+
log(line);
|
|
439
|
+
} else {
|
|
440
|
+
logOnce(`nr:${candidate.id}`, line);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (outcome.routed) {
|
|
444
|
+
routed = true;
|
|
445
|
+
if (!routedCandidate) {
|
|
446
|
+
routedCandidate = candidate;
|
|
447
|
+
log(`transport resolved: ${candidate.id}`);
|
|
448
|
+
}
|
|
449
|
+
if (!sweeping) {
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (outcome.fatal) {
|
|
455
|
+
disabled = `transport timed out on ${candidate.id}`;
|
|
456
|
+
log(`scans DISABLED for the rest of this session (${disabled}). The test run is unaffected.`);
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (sweeping) {
|
|
462
|
+
laddersWalked = true;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (routed) {
|
|
466
|
+
scansRouted++;
|
|
467
|
+
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// A refused method is a static property of the Hub's route table, not a transient
|
|
472
|
+
// failure — retrying it on every subsequent command would add a round trip per command
|
|
473
|
+
// and change nothing. Stop after one exhausted pass, and say plainly that this run
|
|
474
|
+
// produced NO scans (as distinct from scans that were attempted and failed), because
|
|
475
|
+
// that distinction is the whole difference for whoever picks up the support ticket.
|
|
476
|
+
if (!disabled) {
|
|
477
|
+
disabled = 'the Hub has no route for a detox App Accessibility scan';
|
|
478
|
+
log(`NO SCANS for this run: ${ladder.length} relay shape(s) tried and every one was refused`
|
|
479
|
+
+ ` by the Hub (${ladder.map((c) => c.id).join(', ')}). The App Accessibility build was created`
|
|
480
|
+
+ ' and armed correctly; the scan relay is not routed. Test results are unaffected.');
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// Recursive, path-accumulating proxy over one of detox's element objects. Applied ONLY
|
|
485
|
+
// when a11y is armed, so the disabled path returns detox's own objects untouched.
|
|
486
|
+
//
|
|
487
|
+
// Two source hazards it exists to absorb: `NativeElement.atIndex()` and
|
|
488
|
+
// `NativeWaitForElement`'s `get not` both `return this` — the raw target — so a chained
|
|
489
|
+
// call would escape the wrapper unless a result identical to the target is re-wrapped.
|
|
490
|
+
// Methods are applied to the TARGET, not the proxy, so detox's own internal `this.…`
|
|
491
|
+
// calls never re-enter the hook and no action is counted twice.
|
|
492
|
+
const wrap = (target, surface) => {
|
|
493
|
+
if (!target || (typeof target !== 'object' && typeof target !== 'function')) {
|
|
494
|
+
return target;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const proxy = new Proxy(target, {
|
|
498
|
+
get(obj, prop) {
|
|
499
|
+
const value = obj[prop];
|
|
500
|
+
if (typeof value !== 'function') {
|
|
501
|
+
return value;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const command = commandNameFor(surface, String(prop));
|
|
505
|
+
|
|
506
|
+
return function wrapped(...args) {
|
|
507
|
+
const result = value.apply(obj, args);
|
|
508
|
+
|
|
509
|
+
// Synchronous chaining helper (atIndex, WebViewElement.element): re-wrap so the
|
|
510
|
+
// chain stays instrumented all the way to the leaf action.
|
|
511
|
+
if (!(result && typeof result.then === 'function')) {
|
|
512
|
+
return result === obj ? proxy : wrapChainResult(result);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return result.then(async (resolved) => {
|
|
516
|
+
if (command) {
|
|
517
|
+
// Post-settle by construction: a native invocation resolves only after
|
|
518
|
+
// Espresso's idling resources are idle, so the screen the scan captures is the
|
|
519
|
+
// one the command produced. Web-view actions are detox's own documented
|
|
520
|
+
// exception — it adds a hardcoded 500ms and guarantees nothing.
|
|
521
|
+
await scan(registeredClient, command.name, currentTestUuid());
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return resolved === obj ? proxy : wrapChainResult(resolved);
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
return proxy;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// A method that returns a *different* element object (WebViewElement.element() →
|
|
534
|
+
// WebElement) has to hand back a proxy over the new surface, or the leaf action is never
|
|
535
|
+
// seen. Anything that is not one of the wrapped surfaces is returned untouched.
|
|
536
|
+
const WRAPPED_SURFACES = ['NativeElement', 'WebElement', 'WebViewElement'];
|
|
537
|
+
|
|
538
|
+
const wrapChainResult = (result) => {
|
|
539
|
+
try {
|
|
540
|
+
if (!result || typeof result !== 'object') {
|
|
541
|
+
return result;
|
|
542
|
+
}
|
|
543
|
+
const name = result.constructor && result.constructor.name;
|
|
544
|
+
if (WRAPPED_SURFACES.includes(name)) {
|
|
545
|
+
return wrap(result, name);
|
|
546
|
+
}
|
|
547
|
+
} catch (err) { /* best-effort */ }
|
|
548
|
+
|
|
549
|
+
return result;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// The owning test's uuid, published per `it()` on the Node-realm global by the SDK's jest
|
|
553
|
+
// environment (the same channel __BSTACK_DETOX_SPEC__ uses) and cleared at test_done —
|
|
554
|
+
// so its absence is precisely the "outside a test body" case R50 requires be suppressed.
|
|
555
|
+
const currentTestUuid = () => {
|
|
556
|
+
try {
|
|
557
|
+
return global.__BSTACK_DETOX_TEST_UUID__ || '';
|
|
558
|
+
} catch (err) {
|
|
559
|
+
return '';
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const summary = () => ({scansAttempted, scansRouted, disabled, transport: routedCandidate && routedCandidate.id});
|
|
564
|
+
|
|
565
|
+
module.exports = {
|
|
566
|
+
enabled,
|
|
567
|
+
commandNameFor,
|
|
568
|
+
scan,
|
|
569
|
+
wrap,
|
|
570
|
+
registerClient,
|
|
571
|
+
currentTestUuid,
|
|
572
|
+
summary,
|
|
573
|
+
SCAN_TIMEOUT_MS
|
|
574
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Vanilla Detox's external-module contract (devices/allocation/factories/external.js)
|
|
4
|
+
// requires a `DeviceAllocationDriverClass`. BrowserStack cloud sessions don't allocate
|
|
5
|
+
// a local device — the real "allocation" happens server-side when the runtime driver's
|
|
6
|
+
// launchApp opens the cloud WebSocket session — so this driver is a no-op shell that
|
|
7
|
+
// satisfies the AllocationDriverBase interface (init/allocate/postAllocate/free).
|
|
8
|
+
class CloudAllocationDriver {
|
|
9
|
+
async init() {}
|
|
10
|
+
|
|
11
|
+
// Cookie shape must match detox's AndroidDeviceCookie ({id, adbName}) since
|
|
12
|
+
// RuntimeDevice/DeviceAllocator key off cookie.id — nulls are fine, there is no
|
|
13
|
+
// local adb device to name.
|
|
14
|
+
async allocate(_deviceConfig) {
|
|
15
|
+
return {id: null, adbName: null};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async postAllocate(deviceCookie) {
|
|
19
|
+
return deviceCookie;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// No real device to release.
|
|
23
|
+
async free(_cookie, _options) {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = CloudAllocationDriver;
|