-
-
Notifications
You must be signed in to change notification settings - Fork 277
Expand file tree
/
Copy pathbrowserstack-device-features.ts
More file actions
241 lines (230 loc) · 8.26 KB
/
Copy pathbrowserstack-device-features.ts
File metadata and controls
241 lines (230 loc) · 8.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
import {
PROVIDER_DEVICE_ORIENTATIONS,
type CloudProviderProfileFields,
} from '@agent-device/contracts/remote';
import { AppError } from '@agent-device/kernel/errors';
import type { CloudWebDriverPlatform } from './runtime.ts';
/**
* BrowserStack "device feature" session capabilities.
*
* These are vendor capabilities: on the W3C wire they belong inside `bstack:options`, not at the
* top level. BrowserStack's YAML config lists them unnested and its SDK moves them — agent-device
* talks to the hub directly, so it nests them here.
*
* The table is the contract. Adding a capability means adding a spec row, not a branch in the
* capability builder or the flag reader.
*/
export type BrowserStackDeviceFeatureFields = Pick<
CloudProviderProfileFields,
| 'providerDeviceOrientation'
| 'providerGeoLocation'
| 'providerTimezone'
| 'providerLanguage'
| 'providerLocale'
| 'providerNetworkProfile'
| 'providerCustomNetwork'
| 'providerNoResignApp'
>;
type BrowserStackDeviceFeatureSpec = {
/** `CliFlags`/profile field this capability is configured from. */
field: keyof BrowserStackDeviceFeatureFields;
/** Key emitted inside `bstack:options`. */
capability: string;
/** Canonical CLI flag, so an error can name a recovery action. */
flag: string;
/**
* `negated-boolean` is a flag whose presence means "turn the capability off" — BrowserStack
* defaults `resignApp` to true, so the only useful instruction is the opt-out.
*/
type: 'string' | 'enum' | 'negated-boolean';
enumValues?: readonly string[];
/** Platform the capability applies to; absent means both. */
platform?: CloudWebDriverPlatform;
};
export const BROWSERSTACK_DEVICE_FEATURE_SPECS: readonly BrowserStackDeviceFeatureSpec[] = [
{
field: 'providerDeviceOrientation',
capability: 'deviceOrientation',
flag: '--provider-device-orientation',
type: 'enum',
enumValues: PROVIDER_DEVICE_ORIENTATIONS,
},
{
field: 'providerGeoLocation',
capability: 'geoLocation',
flag: '--provider-geo-location',
type: 'string',
},
{
field: 'providerTimezone',
capability: 'timezone',
flag: '--provider-timezone',
type: 'string',
},
{
field: 'providerLanguage',
capability: 'language',
flag: '--provider-language',
type: 'string',
},
{
field: 'providerLocale',
capability: 'locale',
flag: '--provider-locale',
type: 'string',
},
{
field: 'providerNetworkProfile',
capability: 'networkProfile',
flag: '--provider-network-profile',
type: 'string',
},
{
field: 'providerCustomNetwork',
capability: 'customNetwork',
flag: '--provider-custom-network',
type: 'string',
},
{
// BrowserStack re-signs uploaded iOS apps with its own provisioning profile, which strips
// entitlements. Opting out keeps entitlement-dependent features (push notifications) testable.
field: 'providerNoResignApp',
capability: 'resignApp',
flag: '--provider-no-resign-app',
type: 'negated-boolean',
platform: 'ios',
},
];
/**
* Builds the `bstack:options` fragment for the configured device features.
*
* Rejects combinations BrowserStack cannot act on unambiguously, so a failure names a flag rather
* than surfacing as a silently ignored capability on a remote device.
*/
export function buildBrowserStackDeviceFeatureCapabilities(
fields: BrowserStackDeviceFeatureFields,
platform: CloudWebDriverPlatform,
): Record<string, unknown> {
requireCompatibleNetworkFields(fields);
const capabilities: Record<string, unknown> = {};
for (const spec of BROWSERSTACK_DEVICE_FEATURE_SPECS) {
const value = fields[spec.field];
if (value === undefined || value === false || value === '') continue;
requireSupportedPlatform(spec, platform);
capabilities[spec.capability] = spec.type === 'negated-boolean' ? false : value;
}
return capabilities;
}
/**
* Canonical CLI flags for every device-feature capability set on `flags`.
*
* These capabilities are BrowserStack-owned. Other providers have no equivalent, so a caller who
* passes them to AWS Device Farm would otherwise have them accepted, persisted into the profile,
* and then silently dropped — the session runs with provider defaults and nothing says why.
* Callers use this to reject them at the point the provider is known.
*/
function browserStackOnlyDeviceFeatureFlags(flags: Record<string, unknown> | undefined): string[] {
return BROWSERSTACK_DEVICE_FEATURE_SPECS.filter((spec) => {
const value = flags?.[spec.field];
return value !== undefined && value !== false && value !== '';
}).map((spec) => spec.flag);
}
/**
* Fails when a non-BrowserStack provider was given BrowserStack-owned device features.
*
* Called from both the CLI profile builder and the provider's own session preparation. The second
* is the one that actually closes the hole: the typed client and hand-authored remote-config
* profiles reach session preparation without passing through `connect`, so a CLI-only check leaves
* those routes accepting the capabilities and dropping them.
*/
export function rejectBrowserStackOnlyDeviceFeatures(
flags: Record<string, unknown> | undefined,
provider: string,
): void {
const configured = browserStackOnlyDeviceFeatureFlags(flags);
if (configured.length === 0) return;
const plural = configured.length !== 1;
throw new AppError(
'INVALID_ARGS',
`${configured.join(', ')} ${plural ? 'are' : 'is'} only supported by BrowserStack, not ${provider}.`,
{
hint: `Drop ${plural ? 'those flags' : 'the flag'} or use the browserstack provider.`,
provider,
flags: configured,
},
);
}
/**
* Reads device-feature fields off an untyped flag bag (a daemon request), so the daemon-side
* capability build and the CLI-side profile build stay driven by the same table.
*
* This is a trust boundary: enum values are validated here rather than forwarded to the hub, where
* an unrecognized value is accepted and then ignored.
*/
export function readBrowserStackDeviceFeatureFields(
flags: Record<string, unknown> | undefined,
): BrowserStackDeviceFeatureFields {
const fields: BrowserStackDeviceFeatureFields = {};
for (const spec of BROWSERSTACK_DEVICE_FEATURE_SPECS) {
const value = flags?.[spec.field];
if (spec.type === 'negated-boolean') {
if (value === true) fields.providerNoResignApp = true;
continue;
}
if (typeof value !== 'string' || value.length === 0) continue;
assignStringField(fields, spec, value);
}
return fields;
}
function assignStringField(
fields: BrowserStackDeviceFeatureFields,
spec: BrowserStackDeviceFeatureSpec,
value: string,
): void {
if (spec.field === 'providerDeviceOrientation') {
fields.providerDeviceOrientation = requireDeviceOrientation(spec, value);
return;
}
if (spec.field === 'providerNoResignApp') return;
fields[spec.field] = value;
}
function requireDeviceOrientation(
spec: BrowserStackDeviceFeatureSpec,
value: string,
): (typeof PROVIDER_DEVICE_ORIENTATIONS)[number] {
const match = PROVIDER_DEVICE_ORIENTATIONS.find((orientation) => orientation === value);
if (match) return match;
throw new AppError('INVALID_ARGS', `Invalid ${spec.flag} value: ${value}.`, {
hint: `Use ${PROVIDER_DEVICE_ORIENTATIONS.join('|')}.`,
flag: spec.flag,
capability: spec.capability,
});
}
function requireSupportedPlatform(
spec: BrowserStackDeviceFeatureSpec,
platform: CloudWebDriverPlatform,
): void {
if (!spec.platform || spec.platform === platform) return;
throw new AppError(
'INVALID_ARGS',
`${spec.flag} applies to ${spec.platform} sessions only, but the session platform is ${platform}.`,
{
hint: `Drop ${spec.flag} or connect with --platform ${spec.platform}.`,
flag: spec.flag,
capability: spec.capability,
platform,
},
);
}
function requireCompatibleNetworkFields(fields: BrowserStackDeviceFeatureFields): void {
if (!fields.providerNetworkProfile || !fields.providerCustomNetwork) return;
throw new AppError(
'INVALID_ARGS',
'BrowserStack accepts either a named network profile or a custom network shape, not both.',
{
hint: 'Pass only one of --provider-network-profile or --provider-custom-network.',
networkProfile: fields.providerNetworkProfile,
customNetwork: fields.providerCustomNetwork,
},
);
}