@devicecloud.dev/dcd 5.6.0-beta.3 → 5.6.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/commands/artifacts.d.ts +1 -1
- package/dist/commands/artifacts.js +1 -1
- package/dist/commands/cloud.d.ts +5 -5
- package/dist/commands/cloud.js +14 -6
- package/dist/commands/switch-org.js +5 -1
- package/dist/commands/upload.js +5 -1
- package/dist/commands/whoami.d.ts +7 -1
- package/dist/commands/whoami.js +26 -2
- package/dist/config/flags/device.flags.d.ts +3 -3
- package/dist/config/flags/device.flags.js +5 -3
- package/dist/config/flags/execution.flags.d.ts +1 -1
- package/dist/config/flags/execution.flags.js +1 -1
- package/dist/config/flags/output.flags.d.ts +1 -1
- package/dist/config/flags/output.flags.js +1 -1
- package/dist/constants.d.ts +5 -5
- package/dist/gateways/api-gateway.js +2 -1
- package/dist/gateways/realtime-gateway.d.ts +7 -0
- package/dist/gateways/realtime-gateway.js +12 -1
- package/dist/mcp/context.d.ts +14 -3
- package/dist/mcp/context.js +15 -3
- package/dist/mcp/tools/download-artifacts.js +9 -6
- package/dist/mcp/tools/list-devices.js +1 -1
- package/dist/mcp/tools/run-cloud-test.js +19 -3
- package/dist/services/device-validation.service.d.ts +17 -2
- package/dist/services/device-validation.service.js +93 -12
- package/dist/services/notices.service.js +3 -0
- package/dist/services/report-download.service.js +9 -3
- package/dist/services/results-polling.service.d.ts +21 -0
- package/dist/services/results-polling.service.js +55 -3
- package/dist/services/test-submission.service.d.ts +11 -0
- package/dist/services/test-submission.service.js +12 -5
- package/dist/types/domain/auth.types.d.ts +6 -0
- package/dist/types/domain/device.types.d.ts +18 -4
- package/dist/types/domain/device.types.js +17 -3
- package/dist/types/generated/schema.types.d.ts +127 -167
- package/dist/utils/auth.d.ts +33 -0
- package/dist/utils/auth.js +61 -2
- package/package.json +2 -2
|
@@ -1,5 +1,35 @@
|
|
|
1
|
+
const DEVICES_DOCS_URL = 'https://docs.devicecloud.dev/getting-started/devices-configuration';
|
|
2
|
+
/** `dcd cloud`'s spelling; the MCP tool passes its own parameter names. */
|
|
3
|
+
const CLI_ARG_NAMES = {
|
|
4
|
+
androidApiLevel: '--android-api-level',
|
|
5
|
+
androidDevice: '--android-device',
|
|
6
|
+
iOSDevice: '--ios-device',
|
|
7
|
+
iOSVersion: '--ios-version',
|
|
8
|
+
};
|
|
9
|
+
/** The versions a lookup offers for one device; empty when it offers none. */
|
|
10
|
+
function versionsFor(lookup, device) {
|
|
11
|
+
// hasOwn, so a slug like "constructor" can't resolve to a prototype member.
|
|
12
|
+
if (!lookup || !Object.hasOwn(lookup, device))
|
|
13
|
+
return [];
|
|
14
|
+
const versions = lookup[device];
|
|
15
|
+
return Array.isArray(versions) ? versions : [];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Devices the lookup can actually run, in the API's order. A rollout gate can
|
|
19
|
+
* leave a device with no versions (every non-Play device has an empty Google
|
|
20
|
+
* Play list), and those must not be offered as alternatives.
|
|
21
|
+
*/
|
|
22
|
+
function offeredDevices(lookup) {
|
|
23
|
+
return Object.keys(lookup ?? {}).filter((device) => versionsFor(lookup, device).length > 0);
|
|
24
|
+
}
|
|
1
25
|
/**
|
|
2
|
-
*
|
|
26
|
+
* Validates requested devices against the compatibility matrix the API serves
|
|
27
|
+
* from `GET /results/compatibility/data`, which is the only authority on what
|
|
28
|
+
* can run. Nothing here consults the CLI's device and OS enums
|
|
29
|
+
* (src/types/domain/device.types.ts) — those only feed `--help` — so a device
|
|
30
|
+
* or OS version the API adds is accepted without a CLI release, and one it
|
|
31
|
+
* withdraws is refused, before anything is uploaded, with the list the API
|
|
32
|
+
* offers today.
|
|
3
33
|
*/
|
|
4
34
|
export class DeviceValidationService {
|
|
5
35
|
/**
|
|
@@ -13,6 +43,10 @@ export class DeviceValidationService {
|
|
|
13
43
|
* @throws Error if device/API level combination is not supported
|
|
14
44
|
*/
|
|
15
45
|
validateAndroidDevice(androidApiLevel, androidDevice, googlePlay, compatibilityData, options = {}) {
|
|
46
|
+
const lookup = googlePlay
|
|
47
|
+
? compatibilityData.androidPlay
|
|
48
|
+
: compatibilityData.android;
|
|
49
|
+
const argNames = options.argNames ?? CLI_ARG_NAMES;
|
|
16
50
|
this.validateDevice({
|
|
17
51
|
debugLines: (deviceID, version, supportedVersions) => [
|
|
18
52
|
`[DEBUG] Android device: ${deviceID}`,
|
|
@@ -20,16 +54,27 @@ export class DeviceValidationService {
|
|
|
20
54
|
`[DEBUG] Google Play enabled: ${googlePlay}`,
|
|
21
55
|
`[DEBUG] Supported Android versions: ${supportedVersions.join(', ')}`,
|
|
22
56
|
],
|
|
57
|
+
// Mirrors the API, which applies these same global defaults to a
|
|
58
|
+
// request that omits either half (test-request-validator.service.ts)
|
|
59
|
+
// and then rejects an incompatible pair.
|
|
23
60
|
defaultDevice: 'pixel-7',
|
|
24
61
|
defaultVersion: '34',
|
|
25
62
|
device: androidDevice,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
63
|
+
deviceArg: argNames.androidDevice,
|
|
64
|
+
deviceKind: googlePlay ? 'Google Play' : 'Android',
|
|
65
|
+
formatVersion: (version) => `API level ${version}`,
|
|
66
|
+
lookup,
|
|
30
67
|
options,
|
|
68
|
+
platform: 'Android',
|
|
69
|
+
// A real device that merely has no Google Play image deserves a sharper
|
|
70
|
+
// answer than "not supported".
|
|
71
|
+
unknownDeviceMessage: (deviceID) => googlePlay && versionsFor(compatibilityData.android, deviceID).length > 0
|
|
72
|
+
? `Android device "${deviceID}" is not available with Google Play. Google Play devices: ${offeredDevices(lookup).join(', ')}. See ${DEVICES_DOCS_URL}`
|
|
73
|
+
: undefined,
|
|
31
74
|
unsupportedVersionMessage: (deviceID, supportedVersions) => `${deviceID} ${googlePlay ? '(Play Store) ' : ''}only supports these Android API levels: ${supportedVersions.join(', ')}`,
|
|
32
75
|
version: androidApiLevel,
|
|
76
|
+
versionArg: argNames.androidApiLevel,
|
|
77
|
+
versionName: 'Android API level',
|
|
33
78
|
});
|
|
34
79
|
}
|
|
35
80
|
/**
|
|
@@ -42,20 +87,27 @@ export class DeviceValidationService {
|
|
|
42
87
|
* @throws Error if device/version combination is not supported
|
|
43
88
|
*/
|
|
44
89
|
validateiOSDevice(iOSVersion, iOSDevice, compatibilityData, options = {}) {
|
|
90
|
+
const argNames = options.argNames ?? CLI_ARG_NAMES;
|
|
45
91
|
this.validateDevice({
|
|
46
92
|
debugLines: (deviceID, version, supportedVersions) => [
|
|
47
93
|
`[DEBUG] iOS device: ${deviceID}`,
|
|
48
94
|
`[DEBUG] iOS version: ${version}`,
|
|
49
95
|
`[DEBUG] Supported iOS versions: ${supportedVersions.join(', ')}`,
|
|
50
96
|
],
|
|
97
|
+
// Mirrors the API's global defaults, as for Android above.
|
|
51
98
|
defaultDevice: 'iphone-14',
|
|
52
99
|
defaultVersion: '17',
|
|
53
100
|
device: iOSDevice,
|
|
101
|
+
deviceArg: argNames.iOSDevice,
|
|
102
|
+
deviceKind: 'iOS',
|
|
103
|
+
formatVersion: (version) => `iOS ${version}`,
|
|
54
104
|
lookup: compatibilityData?.ios,
|
|
55
|
-
noSupportMessage: (deviceID) => `Device ${deviceID} is not supported. Please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`,
|
|
56
105
|
options,
|
|
106
|
+
platform: 'iOS',
|
|
57
107
|
unsupportedVersionMessage: (deviceID, supportedVersions) => `${deviceID} only supports these iOS versions: ${supportedVersions.join(', ')}`,
|
|
58
108
|
version: iOSVersion,
|
|
109
|
+
versionArg: argNames.iOSVersion,
|
|
110
|
+
versionName: 'iOS version',
|
|
59
111
|
});
|
|
60
112
|
}
|
|
61
113
|
/**
|
|
@@ -66,20 +118,49 @@ export class DeviceValidationService {
|
|
|
66
118
|
* @throws Error if device/version combination is not supported
|
|
67
119
|
*/
|
|
68
120
|
validateDevice(config) {
|
|
69
|
-
const { debugLines, defaultDevice, defaultVersion, device,
|
|
121
|
+
const { debugLines, defaultDevice, defaultVersion, device, deviceArg, deviceKind, formatVersion, lookup, options, platform, unknownDeviceMessage, unsupportedVersionMessage, version, versionArg, versionName, } = config;
|
|
70
122
|
const { debug = false, logger } = options;
|
|
71
123
|
if (!version && !device) {
|
|
72
124
|
return;
|
|
73
125
|
}
|
|
74
126
|
const deviceID = device || defaultDevice;
|
|
75
|
-
const supportedVersions = lookup
|
|
127
|
+
const supportedVersions = versionsFor(lookup, deviceID);
|
|
76
128
|
const requestedVersion = version || defaultVersion;
|
|
77
129
|
if (supportedVersions.length === 0) {
|
|
78
|
-
|
|
130
|
+
if (!device) {
|
|
131
|
+
// Only a version was requested, and the API no longer offers the
|
|
132
|
+
// device the CLI assumes it defaults to. That assumption is what's
|
|
133
|
+
// stale, not the request: the API resolves and validates its own
|
|
134
|
+
// default device when the run is submitted.
|
|
135
|
+
if (debug && logger) {
|
|
136
|
+
logger(`[DEBUG] Default ${platform} device ${deviceID} is not in the compatibility data; leaving the ${versionName} check to the API`);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const offered = offeredDevices(lookup);
|
|
141
|
+
throw new Error(unknownDeviceMessage?.(deviceID) ??
|
|
142
|
+
(offered.length > 0
|
|
143
|
+
? `${platform} device "${deviceID}" is not supported. Supported ${deviceKind} devices: ${offered.join(', ')}. See ${DEVICES_DOCS_URL}`
|
|
144
|
+
: `${platform} device "${deviceID}" is not supported. See ${DEVICES_DOCS_URL}`));
|
|
79
145
|
}
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
146
|
+
if (!supportedVersions.includes(requestedVersion)) {
|
|
147
|
+
const unsupported = unsupportedVersionMessage(deviceID, supportedVersions);
|
|
148
|
+
if (!version) {
|
|
149
|
+
// Device-only request. The API fills in the same global default
|
|
150
|
+
// version rather than one this device runs, so the submission would
|
|
151
|
+
// be refused there too — say so, and how to avoid it.
|
|
152
|
+
throw new Error(`${unsupported}. No ${versionName} was given, so the default (${defaultVersion}) was checked: pass one of those explicitly with ${versionArg}`);
|
|
153
|
+
}
|
|
154
|
+
if (!device) {
|
|
155
|
+
// Version-only request: name the device that was assumed, and the
|
|
156
|
+
// devices that do offer the requested version.
|
|
157
|
+
const devicesWithVersion = offeredDevices(lookup).filter((d) => versionsFor(lookup, d).includes(requestedVersion));
|
|
158
|
+
throw new Error(`${unsupported}. No ${platform} device was given, so the default (${deviceID}) was checked: ` +
|
|
159
|
+
(devicesWithVersion.length > 0
|
|
160
|
+
? `pass one that offers ${formatVersion(requestedVersion)} with ${deviceArg}: ${devicesWithVersion.join(', ')}`
|
|
161
|
+
: `no ${deviceKind} device offers ${formatVersion(requestedVersion)}`));
|
|
162
|
+
}
|
|
163
|
+
throw new Error(unsupported);
|
|
83
164
|
}
|
|
84
165
|
if (debug && logger) {
|
|
85
166
|
for (const line of debugLines(deviceID, requestedVersion, supportedVersions)) {
|
|
@@ -80,6 +80,9 @@ function renderNotice(notice, opts) {
|
|
|
80
80
|
export function platformFromAppFile(appFile) {
|
|
81
81
|
if (!appFile)
|
|
82
82
|
return undefined;
|
|
83
|
+
// Expo iOS simulator builds (`dcd cloud` extracts these to a .app first).
|
|
84
|
+
if (appFile.split('?')[0].toLowerCase().endsWith('.tar.gz'))
|
|
85
|
+
return 'ios';
|
|
83
86
|
const ext = appFile.split('?')[0].toLowerCase().match(/.([a-z0-9]+)$/)?.[1];
|
|
84
87
|
if (ext === 'apk' || ext === 'aab')
|
|
85
88
|
return 'android';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import { ApiGateway } from '../gateways/api-gateway.js';
|
|
3
|
+
import { refreshAuth } from '../utils/auth.js';
|
|
3
4
|
/**
|
|
4
5
|
* Service for downloading test artifacts and reports
|
|
5
6
|
*/
|
|
@@ -15,7 +16,9 @@ export class ReportDownloadService {
|
|
|
15
16
|
if (debug && logger) {
|
|
16
17
|
logger(`[DEBUG] Downloading artifacts: ${downloadType}`);
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
+
// `dcd cloud` downloads after polling, which can outlast a `dcd login`
|
|
20
|
+
// token; a failed refresh lands in the warning below like any failure.
|
|
21
|
+
await ApiGateway.downloadArtifactsZip(apiUrl, await refreshAuth(auth), uploadId, downloadType, artifactsPath);
|
|
19
22
|
if (logger) {
|
|
20
23
|
logger('\n');
|
|
21
24
|
logger(`Test artifacts have been downloaded to ${artifactsPath}`);
|
|
@@ -54,7 +57,9 @@ export class ReportDownloadService {
|
|
|
54
57
|
}
|
|
55
58
|
case 'html':
|
|
56
59
|
case 'html-detailed': {
|
|
57
|
-
|
|
60
|
+
// The HTML report is a ZIP (report.html plus its screenshots and
|
|
61
|
+
// assets), so the default name says so. An explicit path is kept as-is.
|
|
62
|
+
const htmlReportPath = path.resolve(process.cwd(), htmlPath || 'report.zip');
|
|
58
63
|
await this.downloadReport('html', htmlReportPath, {
|
|
59
64
|
...downloadOptions,
|
|
60
65
|
warnLogger,
|
|
@@ -81,7 +86,8 @@ export class ReportDownloadService {
|
|
|
81
86
|
if (debug && logger) {
|
|
82
87
|
logger(`[DEBUG] Downloading ${type.toUpperCase()} report`);
|
|
83
88
|
}
|
|
84
|
-
|
|
89
|
+
// As for artifacts: this can run after a poll that outlasted the token.
|
|
90
|
+
await ApiGateway.downloadReportGeneric(apiUrl, await refreshAuth(auth), uploadId, type, filePath);
|
|
85
91
|
if (logger) {
|
|
86
92
|
logger(`${type.toUpperCase()} test report has been downloaded to ${filePath}`);
|
|
87
93
|
}
|
|
@@ -6,6 +6,14 @@ export declare class RunFailedError extends Error {
|
|
|
6
6
|
result: PollingResult;
|
|
7
7
|
constructor(result: PollingResult);
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* The `dcd login` session behind a poll expired and only a new login can fix
|
|
11
|
+
* it, so the poll stops at once instead of spending its retry budget — meant
|
|
12
|
+
* for network blips — on a failure that can't clear.
|
|
13
|
+
*/
|
|
14
|
+
export declare class PollingAuthError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
9
17
|
export interface PollingOptions {
|
|
10
18
|
auth: AuthContext;
|
|
11
19
|
apiUrl: string;
|
|
@@ -115,6 +123,19 @@ export declare class ResultsPollingService {
|
|
|
115
123
|
* @returns Promise resolving to test results
|
|
116
124
|
*/
|
|
117
125
|
private fetchAndLogResults;
|
|
126
|
+
/**
|
|
127
|
+
* Swap in a fresh `dcd login` token when the current one is about to
|
|
128
|
+
* expire, and hand it to the realtime socket too; API-key auth comes back
|
|
129
|
+
* as-is. A transient refresh failure is thrown as-is, to be retried like
|
|
130
|
+
* any failed poll; one only `dcd login` can fix becomes a PollingAuthError.
|
|
131
|
+
* @param auth The credential the previous poll used
|
|
132
|
+
* @param uploadId Upload being polled, for the reconnect hint
|
|
133
|
+
* @param subscription Realtime subscription to pass a new token to
|
|
134
|
+
* @param debug Whether debug logging is enabled
|
|
135
|
+
* @param logger Optional logger function
|
|
136
|
+
* @returns The credential for the next poll
|
|
137
|
+
*/
|
|
138
|
+
private refreshPollingAuth;
|
|
118
139
|
private filterLatestResults;
|
|
119
140
|
/**
|
|
120
141
|
* Handle completed tests and return final result
|
|
@@ -2,6 +2,7 @@ import * as path from 'node:path';
|
|
|
2
2
|
import { ApiGateway } from '../gateways/api-gateway.js';
|
|
3
3
|
import { RealtimeResultsGateway, } from '../gateways/realtime-gateway.js';
|
|
4
4
|
import { formatDurationSeconds } from '../methods.js';
|
|
5
|
+
import { isDefinitiveAuthFailure, refreshAuth } from '../utils/auth.js';
|
|
5
6
|
import { checkInternetConnectivity } from '../utils/connectivity.js';
|
|
6
7
|
import { isCI } from '../utils/ci.js';
|
|
7
8
|
import { ux } from '../utils/progress.js';
|
|
@@ -18,6 +19,17 @@ export class RunFailedError extends Error {
|
|
|
18
19
|
this.name = 'RunFailedError';
|
|
19
20
|
}
|
|
20
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* The `dcd login` session behind a poll expired and only a new login can fix
|
|
24
|
+
* it, so the poll stops at once instead of spending its retry budget — meant
|
|
25
|
+
* for network blips — on a failure that can't clear.
|
|
26
|
+
*/
|
|
27
|
+
export class PollingAuthError extends Error {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'PollingAuthError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
21
33
|
/**
|
|
22
34
|
* Structured device for a result row. Prefers the friendly deviceName/osVersion
|
|
23
35
|
* the per-flow targeting / matrix fan-out stamps onto each result's config
|
|
@@ -115,7 +127,10 @@ export class ResultsPollingService {
|
|
|
115
127
|
}
|
|
116
128
|
}
|
|
117
129
|
async pollLoop(options, testMetadata) {
|
|
118
|
-
const { apiUrl,
|
|
130
|
+
const { apiUrl, uploadId, consoleUrl, quiet = false, json = false, debug = false, logger } = options;
|
|
131
|
+
// Re-checked before every poll: a run can outlast a `dcd login` access
|
|
132
|
+
// token (about an hour). API-key auth passes through unchanged.
|
|
133
|
+
let { auth } = options;
|
|
119
134
|
this.initializePollingDisplay(json, logger);
|
|
120
135
|
let sequentialPollFailures = 0;
|
|
121
136
|
const pollIntervalMs = auth.mode === 'bearer'
|
|
@@ -213,6 +228,7 @@ export class ResultsPollingService {
|
|
|
213
228
|
try {
|
|
214
229
|
nextPollAt = null;
|
|
215
230
|
renderStatus();
|
|
231
|
+
auth = await this.refreshPollingAuth(auth, uploadId, subscription, debug, logger);
|
|
216
232
|
const updatedResults = await this.fetchAndLogResults(apiUrl, auth, uploadId, debug, logger);
|
|
217
233
|
const { summary } = this.calculateStatusSummary(updatedResults);
|
|
218
234
|
if (!json) {
|
|
@@ -238,8 +254,9 @@ export class ResultsPollingService {
|
|
|
238
254
|
await waitForNextPoll(pollIntervalMs);
|
|
239
255
|
}
|
|
240
256
|
catch (error) {
|
|
241
|
-
// Re-throw RunFailedError immediately (test failures, not polling
|
|
242
|
-
|
|
257
|
+
// Re-throw RunFailedError immediately (test failures, not polling
|
|
258
|
+
// errors), and an expired login no retry can fix.
|
|
259
|
+
if (error instanceof RunFailedError || error instanceof PollingAuthError) {
|
|
243
260
|
throw error;
|
|
244
261
|
}
|
|
245
262
|
sequentialPollFailures++;
|
|
@@ -399,6 +416,41 @@ export class ResultsPollingService {
|
|
|
399
416
|
}
|
|
400
417
|
return updatedResults;
|
|
401
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Swap in a fresh `dcd login` token when the current one is about to
|
|
421
|
+
* expire, and hand it to the realtime socket too; API-key auth comes back
|
|
422
|
+
* as-is. A transient refresh failure is thrown as-is, to be retried like
|
|
423
|
+
* any failed poll; one only `dcd login` can fix becomes a PollingAuthError.
|
|
424
|
+
* @param auth The credential the previous poll used
|
|
425
|
+
* @param uploadId Upload being polled, for the reconnect hint
|
|
426
|
+
* @param subscription Realtime subscription to pass a new token to
|
|
427
|
+
* @param debug Whether debug logging is enabled
|
|
428
|
+
* @param logger Optional logger function
|
|
429
|
+
* @returns The credential for the next poll
|
|
430
|
+
*/
|
|
431
|
+
async refreshPollingAuth(auth, uploadId, subscription, debug, logger) {
|
|
432
|
+
let next;
|
|
433
|
+
try {
|
|
434
|
+
next = await refreshAuth(auth);
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
if (isDefinitiveAuthFailure(error)) {
|
|
438
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
439
|
+
throw new PollingAuthError(`Your dcd login session expired and could not be refreshed: ${reason}\n\n` +
|
|
440
|
+
`The test is still running in the cloud. Run \`dcd login\`, then reconnect with:\n dcd status --upload-id ${uploadId}`);
|
|
441
|
+
}
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
if (next !== auth) {
|
|
445
|
+
if (debug && logger) {
|
|
446
|
+
logger('[DEBUG] Refreshed the dcd login session for polling');
|
|
447
|
+
}
|
|
448
|
+
if (next.accessToken && next.accessToken !== auth.accessToken) {
|
|
449
|
+
subscription?.updateAccessToken(next.accessToken);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return next;
|
|
453
|
+
}
|
|
402
454
|
filterLatestResults(results) {
|
|
403
455
|
// Resolve each row to the root of its retry chain (a retry's `retry_of`
|
|
404
456
|
// may point at the previous retry rather than the original attempt), then
|
|
@@ -6,6 +6,12 @@ export interface TestSubmissionConfig {
|
|
|
6
6
|
androidNoSnapshot?: boolean;
|
|
7
7
|
apiUrl?: string;
|
|
8
8
|
appBinaryId: string;
|
|
9
|
+
/**
|
|
10
|
+
* Platform of the app binary, when the CLI can tell from a local file
|
|
11
|
+
* (.apk → android; .app / .zip / Expo .tar.gz → ios). Unknown for
|
|
12
|
+
* --app-binary-id, which falls back to the device flags.
|
|
13
|
+
*/
|
|
14
|
+
appPlatform?: 'android' | 'ios';
|
|
9
15
|
/**
|
|
10
16
|
* Ask the API to cancel the still-queued tests of the previous run from
|
|
11
17
|
* the same CI context. Sent as its own field rather than inside `config`,
|
|
@@ -19,6 +25,11 @@ export interface TestSubmissionConfig {
|
|
|
19
25
|
debug?: boolean;
|
|
20
26
|
deviceLocale?: string;
|
|
21
27
|
deviceMatrix?: DeviceMatrixConfig[];
|
|
28
|
+
/**
|
|
29
|
+
* --disable-animations as given on the command line: true / false when the
|
|
30
|
+
* flag (or --no-disable-animations) was passed, undefined when it wasn't —
|
|
31
|
+
* only then does the workspace config's per-platform value apply.
|
|
32
|
+
*/
|
|
22
33
|
disableAnimations?: boolean;
|
|
23
34
|
/**
|
|
24
35
|
* Encrypt the flow zip and env vars before upload (#1151/#1152), each with its
|
|
@@ -20,7 +20,7 @@ export class TestSubmissionService {
|
|
|
20
20
|
* @returns The flow zip buffer, its SHA-256, and the string-encoded fields
|
|
21
21
|
*/
|
|
22
22
|
async buildTestPayload(config) {
|
|
23
|
-
const { apiUrl, appBinaryId, encrypt = false, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], cancelPrevious = false, googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, retry, continueOnFailure = true, renderEngine, report, showCrosshairs, maestroChromeOnboarding, raw, disableAnimations, deviceMatrix, debug = false, logger, } = config;
|
|
23
|
+
const { apiUrl, appBinaryId, appPlatform, encrypt = false, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], cancelPrevious = false, googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, retry, continueOnFailure = true, renderEngine, report, showCrosshairs, maestroChromeOnboarding, raw, disableAnimations, deviceMatrix, debug = false, logger, } = config;
|
|
24
24
|
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, includedFiles = [], referencedFiles, sequence, workspaceConfig, } = executionPlan;
|
|
25
25
|
const { flows: sequentialFlows = [] } = sequence ?? {};
|
|
26
26
|
const envObject = this.parseKeyValuePairs(env);
|
|
@@ -120,18 +120,25 @@ export class TestSubmissionService {
|
|
|
120
120
|
fields.deviceMatrix = JSON.stringify(deviceMatrix);
|
|
121
121
|
}
|
|
122
122
|
// Platform used only to pick which workspace-config disableAnimations flag
|
|
123
|
-
// applies.
|
|
124
|
-
//
|
|
123
|
+
// applies. The binary decides when the CLI knows it — otherwise an iOS run
|
|
124
|
+
// without --ios-device/--ios-version read the android block. For an
|
|
125
|
+
// --app-binary-id, a device matrix (single-platform; its first cell
|
|
126
|
+
// decides) or the scalar iOS flags are the best evidence left.
|
|
125
127
|
const matrixPlatform = deviceMatrix && deviceMatrix.length > 0
|
|
126
128
|
? 'iOSDevice' in deviceMatrix[0]
|
|
127
129
|
? 'ios'
|
|
128
130
|
: 'android'
|
|
129
131
|
: undefined;
|
|
130
|
-
const targetPlatform =
|
|
132
|
+
const targetPlatform = appPlatform ??
|
|
133
|
+
matrixPlatform ??
|
|
134
|
+
(iOSDevice || iOSVersion ? 'ios' : 'android');
|
|
131
135
|
const configYamlDisableAnimations = targetPlatform === 'ios'
|
|
132
136
|
? Boolean(workspaceConfig?.platform?.ios?.disableAnimations)
|
|
133
137
|
: Boolean(workspaceConfig?.platform?.android?.disableAnimations);
|
|
134
|
-
|
|
138
|
+
// An explicit --disable-animations / --no-disable-animations wins over
|
|
139
|
+
// config.yaml; previously `flag || config` meant the flag could only ever
|
|
140
|
+
// turn animations off, never keep them on against the config.
|
|
141
|
+
const effectiveDisableAnimations = disableAnimations ?? configYamlDisableAnimations;
|
|
135
142
|
const configPayload = {
|
|
136
143
|
allExcludeTags,
|
|
137
144
|
allIncludeTags,
|
|
@@ -13,6 +13,12 @@ export interface AuthContext {
|
|
|
13
13
|
accessToken?: string;
|
|
14
14
|
/** Environment the session belongs to — present when mode === 'bearer'. */
|
|
15
15
|
env?: DcdEnvName;
|
|
16
|
+
/**
|
|
17
|
+
* Unix epoch seconds at which `accessToken` expires — present when
|
|
18
|
+
* mode === 'bearer'. Long-lived callers check it (utils/auth.ts
|
|
19
|
+
* isAuthExpiring / refreshAuth) because the headers below stop working then.
|
|
20
|
+
*/
|
|
21
|
+
expiresAt?: number;
|
|
16
22
|
/** Present when mode === 'bearer'. */
|
|
17
23
|
orgId?: string;
|
|
18
24
|
/** Present when mode === 'bearer'. */
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Device type definitions - should be kept in sync with API
|
|
3
|
-
* @see
|
|
3
|
+
* @see dcd/api/src/common/types/device.types.ts
|
|
4
|
+
*
|
|
5
|
+
* These drive the `--ios-device` / `--ios-version` / `--android-device` /
|
|
6
|
+
* `--android-api-level` help text and nothing else: validation runs against
|
|
7
|
+
* the live compatibility payload (device-validation.service.ts). A value the
|
|
8
|
+
* API has dropped gets a "not supported" error listing what it offers now, and
|
|
9
|
+
* one it has added is accepted even if it is missing here — so keeping these
|
|
10
|
+
* lists current only improves the help text; it never gates a run.
|
|
11
|
+
*
|
|
12
|
+
* `iphone-14-pro` and `iphone-15-pro` were listed here but have never existed
|
|
13
|
+
* in the API enum; removed 2026-09-21 rather than carried forward.
|
|
4
14
|
*/
|
|
5
15
|
export declare enum EiOSDevices {
|
|
6
16
|
'ipad-pro-6th-gen' = "ipad-pro-6th-gen",
|
|
17
|
+
'ipad-pro-m5-11' = "ipad-pro-m5-11",
|
|
18
|
+
'ipad-pro-m5-13' = "ipad-pro-m5-13",
|
|
7
19
|
'iphone-14' = "iphone-14",
|
|
8
|
-
'iphone-14-pro' = "iphone-14-pro",
|
|
9
20
|
'iphone-15' = "iphone-15",
|
|
10
|
-
'iphone-15-pro' = "iphone-15-pro",
|
|
11
21
|
'iphone-16' = "iphone-16",
|
|
12
22
|
'iphone-16-plus' = "iphone-16-plus",
|
|
13
23
|
'iphone-16-pro' = "iphone-16-pro",
|
|
14
|
-
'iphone-16-pro-max' = "iphone-16-pro-max"
|
|
24
|
+
'iphone-16-pro-max' = "iphone-16-pro-max",
|
|
25
|
+
'iphone-17' = "iphone-17",
|
|
26
|
+
'iphone-18-pro' = "iphone-18-pro",
|
|
27
|
+
'iphone-18-pro-max' = "iphone-18-pro-max",
|
|
28
|
+
'iphone-air' = "iphone-air"
|
|
15
29
|
}
|
|
16
30
|
export declare enum EAndroidDevices {
|
|
17
31
|
'generic-tablet' = "generic-tablet",
|
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Device type definitions - should be kept in sync with API
|
|
3
|
-
* @see
|
|
3
|
+
* @see dcd/api/src/common/types/device.types.ts
|
|
4
|
+
*
|
|
5
|
+
* These drive the `--ios-device` / `--ios-version` / `--android-device` /
|
|
6
|
+
* `--android-api-level` help text and nothing else: validation runs against
|
|
7
|
+
* the live compatibility payload (device-validation.service.ts). A value the
|
|
8
|
+
* API has dropped gets a "not supported" error listing what it offers now, and
|
|
9
|
+
* one it has added is accepted even if it is missing here — so keeping these
|
|
10
|
+
* lists current only improves the help text; it never gates a run.
|
|
11
|
+
*
|
|
12
|
+
* `iphone-14-pro` and `iphone-15-pro` were listed here but have never existed
|
|
13
|
+
* in the API enum; removed 2026-09-21 rather than carried forward.
|
|
4
14
|
*/
|
|
5
15
|
export var EiOSDevices;
|
|
6
16
|
(function (EiOSDevices) {
|
|
7
17
|
EiOSDevices["ipad-pro-6th-gen"] = "ipad-pro-6th-gen";
|
|
18
|
+
EiOSDevices["ipad-pro-m5-11"] = "ipad-pro-m5-11";
|
|
19
|
+
EiOSDevices["ipad-pro-m5-13"] = "ipad-pro-m5-13";
|
|
8
20
|
EiOSDevices["iphone-14"] = "iphone-14";
|
|
9
|
-
EiOSDevices["iphone-14-pro"] = "iphone-14-pro";
|
|
10
21
|
EiOSDevices["iphone-15"] = "iphone-15";
|
|
11
|
-
EiOSDevices["iphone-15-pro"] = "iphone-15-pro";
|
|
12
22
|
EiOSDevices["iphone-16"] = "iphone-16";
|
|
13
23
|
EiOSDevices["iphone-16-plus"] = "iphone-16-plus";
|
|
14
24
|
EiOSDevices["iphone-16-pro"] = "iphone-16-pro";
|
|
15
25
|
EiOSDevices["iphone-16-pro-max"] = "iphone-16-pro-max";
|
|
26
|
+
EiOSDevices["iphone-17"] = "iphone-17";
|
|
27
|
+
EiOSDevices["iphone-18-pro"] = "iphone-18-pro";
|
|
28
|
+
EiOSDevices["iphone-18-pro-max"] = "iphone-18-pro-max";
|
|
29
|
+
EiOSDevices["iphone-air"] = "iphone-air";
|
|
16
30
|
})(EiOSDevices || (EiOSDevices = {}));
|
|
17
31
|
export var EAndroidDevices;
|
|
18
32
|
(function (EAndroidDevices) {
|