@testspectra/react-native 0.0.0-stage → 1.2.15
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.md +48 -0
- package/dist/index.d.ts +109 -0
- package/dist/index.js +3662 -0
- package/dist/mock/react_native_shim.cjs +219 -0
- package/dist/mock/react_native_shim.js +190 -0
- package/package.json +50 -3
- package/README.md +0 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,3662 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
5
|
+
}) : x)(function(x) {
|
|
6
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
7
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
8
|
+
});
|
|
9
|
+
var __esm = (fn, res, err) => function __init() {
|
|
10
|
+
if (err) throw err[0];
|
|
11
|
+
try {
|
|
12
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
13
|
+
} catch (e) {
|
|
14
|
+
throw err = [e], e;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var __export = (target, all) => {
|
|
18
|
+
for (var name in all)
|
|
19
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/mock/react_native_shim.ts
|
|
23
|
+
var react_native_shim_exports = {};
|
|
24
|
+
__export(react_native_shim_exports, {
|
|
25
|
+
ActivityIndicator: () => ActivityIndicator,
|
|
26
|
+
Alert: () => Alert,
|
|
27
|
+
Animated: () => Animated,
|
|
28
|
+
AppState: () => AppState,
|
|
29
|
+
BackHandler: () => BackHandler,
|
|
30
|
+
Dimensions: () => Dimensions,
|
|
31
|
+
FlatList: () => FlatList,
|
|
32
|
+
Image: () => Image,
|
|
33
|
+
Keyboard: () => Keyboard,
|
|
34
|
+
Linking: () => Linking,
|
|
35
|
+
Modal: () => Modal,
|
|
36
|
+
PixelRatio: () => PixelRatio,
|
|
37
|
+
Platform: () => Platform,
|
|
38
|
+
Pressable: () => Pressable,
|
|
39
|
+
SafeAreaView: () => SafeAreaView,
|
|
40
|
+
ScrollView: () => ScrollView,
|
|
41
|
+
SectionList: () => SectionList,
|
|
42
|
+
StatusBar: () => StatusBar,
|
|
43
|
+
StyleSheet: () => StyleSheet,
|
|
44
|
+
Text: () => Text,
|
|
45
|
+
TextInput: () => TextInput,
|
|
46
|
+
TouchableHighlight: () => TouchableHighlight,
|
|
47
|
+
TouchableOpacity: () => TouchableOpacity,
|
|
48
|
+
TouchableWithoutFeedback: () => TouchableWithoutFeedback,
|
|
49
|
+
UIManager: () => UIManager,
|
|
50
|
+
View: () => View,
|
|
51
|
+
default: () => react_native_shim_default,
|
|
52
|
+
useWindowDimensions: () => useWindowDimensions
|
|
53
|
+
});
|
|
54
|
+
import React from "react";
|
|
55
|
+
function createComponent(displayName) {
|
|
56
|
+
const Comp = React.forwardRef((props, ref) => {
|
|
57
|
+
return React.createElement(displayName, { ...props, ref });
|
|
58
|
+
});
|
|
59
|
+
Comp.displayName = displayName;
|
|
60
|
+
return Comp;
|
|
61
|
+
}
|
|
62
|
+
function useWindowDimensions() {
|
|
63
|
+
return { width: 412, height: 915, scale: 2.625, fontScale: 1 };
|
|
64
|
+
}
|
|
65
|
+
var View, Text, TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, Pressable, TextInput, ScrollView, FlatList, SectionList, ActivityIndicator, Modal, Image, SafeAreaView, StatusBar, StyleSheet, Platform, Dimensions, PixelRatio, Keyboard, UIManager, Alert, AppState, Linking, BackHandler, Animated, react_native_shim_default;
|
|
66
|
+
var init_react_native_shim = __esm({
|
|
67
|
+
"src/mock/react_native_shim.ts"() {
|
|
68
|
+
"use strict";
|
|
69
|
+
View = createComponent("View");
|
|
70
|
+
Text = createComponent("Text");
|
|
71
|
+
TouchableOpacity = createComponent("TouchableOpacity");
|
|
72
|
+
TouchableHighlight = createComponent("TouchableHighlight");
|
|
73
|
+
TouchableWithoutFeedback = createComponent("TouchableWithoutFeedback");
|
|
74
|
+
Pressable = createComponent("Pressable");
|
|
75
|
+
TextInput = createComponent("TextInput");
|
|
76
|
+
ScrollView = createComponent("ScrollView");
|
|
77
|
+
FlatList = createComponent("FlatList");
|
|
78
|
+
SectionList = createComponent("SectionList");
|
|
79
|
+
ActivityIndicator = createComponent("ActivityIndicator");
|
|
80
|
+
Modal = createComponent("Modal");
|
|
81
|
+
Image = createComponent("Image");
|
|
82
|
+
SafeAreaView = createComponent("SafeAreaView");
|
|
83
|
+
StatusBar = createComponent("StatusBar");
|
|
84
|
+
StyleSheet = {
|
|
85
|
+
create: (styles2) => styles2,
|
|
86
|
+
flatten: (style) => {
|
|
87
|
+
if (Array.isArray(style)) {
|
|
88
|
+
return Object.assign({}, ...style.filter(Boolean).map(StyleSheet.flatten));
|
|
89
|
+
}
|
|
90
|
+
return style || {};
|
|
91
|
+
},
|
|
92
|
+
hairlineWidth: 1,
|
|
93
|
+
absoluteFill: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0 },
|
|
94
|
+
absoluteFillObject: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }
|
|
95
|
+
};
|
|
96
|
+
Platform = {
|
|
97
|
+
OS: "android",
|
|
98
|
+
select: (obj) => obj?.android ?? obj?.default ?? obj?.ios,
|
|
99
|
+
isTesting: true,
|
|
100
|
+
Version: 34
|
|
101
|
+
};
|
|
102
|
+
Dimensions = {
|
|
103
|
+
get: (_dim) => ({
|
|
104
|
+
width: 412,
|
|
105
|
+
height: 915,
|
|
106
|
+
scale: 2.625,
|
|
107
|
+
fontScale: 1
|
|
108
|
+
}),
|
|
109
|
+
addEventListener: () => ({ remove: () => {
|
|
110
|
+
} }),
|
|
111
|
+
removeEventListener: () => {
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
PixelRatio = {
|
|
115
|
+
get: () => 2.625,
|
|
116
|
+
getFontScale: () => 1,
|
|
117
|
+
getPixelSizeForLayoutSize: (layoutSize) => Math.round(layoutSize * 2.625),
|
|
118
|
+
roundToNearestPixel: (layoutSize) => Math.round(layoutSize)
|
|
119
|
+
};
|
|
120
|
+
Keyboard = {
|
|
121
|
+
dismiss: () => {
|
|
122
|
+
},
|
|
123
|
+
addListener: () => ({ remove: () => {
|
|
124
|
+
} }),
|
|
125
|
+
removeListener: () => {
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
UIManager = {
|
|
129
|
+
measure: (_node, cb) => {
|
|
130
|
+
cb(0, 0, 100, 40, 50, 100);
|
|
131
|
+
},
|
|
132
|
+
measureInWindow: (_node, cb) => {
|
|
133
|
+
cb(50, 100, 100, 40);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
Alert = {
|
|
137
|
+
alert: () => {
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
AppState = {
|
|
141
|
+
currentState: "active",
|
|
142
|
+
addEventListener: () => ({ remove: () => {
|
|
143
|
+
} })
|
|
144
|
+
};
|
|
145
|
+
Linking = {
|
|
146
|
+
openURL: () => Promise.resolve(),
|
|
147
|
+
canOpenURL: () => Promise.resolve(true),
|
|
148
|
+
getInitialURL: () => Promise.resolve(null)
|
|
149
|
+
};
|
|
150
|
+
BackHandler = {
|
|
151
|
+
addEventListener: () => ({ remove: () => {
|
|
152
|
+
} }),
|
|
153
|
+
exitApp: () => {
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
Animated = {
|
|
157
|
+
Value: class AnimatedValue {
|
|
158
|
+
constructor(val) {
|
|
159
|
+
this.val = val;
|
|
160
|
+
}
|
|
161
|
+
val;
|
|
162
|
+
setValue(v) {
|
|
163
|
+
this.val = v;
|
|
164
|
+
}
|
|
165
|
+
interpolate(_config) {
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
timing: (_val, _config) => ({
|
|
170
|
+
start: (cb) => cb?.({ finished: true }),
|
|
171
|
+
stop: () => {
|
|
172
|
+
}
|
|
173
|
+
}),
|
|
174
|
+
spring: (_val, _config) => ({
|
|
175
|
+
start: (cb) => cb?.({ finished: true }),
|
|
176
|
+
stop: () => {
|
|
177
|
+
}
|
|
178
|
+
}),
|
|
179
|
+
View,
|
|
180
|
+
Text,
|
|
181
|
+
Image,
|
|
182
|
+
ScrollView,
|
|
183
|
+
createAnimatedComponent: (c) => c
|
|
184
|
+
};
|
|
185
|
+
react_native_shim_default = {
|
|
186
|
+
View,
|
|
187
|
+
Text,
|
|
188
|
+
TouchableOpacity,
|
|
189
|
+
TouchableHighlight,
|
|
190
|
+
TouchableWithoutFeedback,
|
|
191
|
+
Pressable,
|
|
192
|
+
TextInput,
|
|
193
|
+
ScrollView,
|
|
194
|
+
FlatList,
|
|
195
|
+
SectionList,
|
|
196
|
+
ActivityIndicator,
|
|
197
|
+
Modal,
|
|
198
|
+
Image,
|
|
199
|
+
SafeAreaView,
|
|
200
|
+
StatusBar,
|
|
201
|
+
StyleSheet,
|
|
202
|
+
Platform,
|
|
203
|
+
Dimensions,
|
|
204
|
+
useWindowDimensions,
|
|
205
|
+
PixelRatio,
|
|
206
|
+
Keyboard,
|
|
207
|
+
UIManager,
|
|
208
|
+
Alert,
|
|
209
|
+
AppState,
|
|
210
|
+
Linking,
|
|
211
|
+
BackHandler,
|
|
212
|
+
Animated
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// src/bridge/tree_registry.ts
|
|
218
|
+
function registerRootNode(node) {
|
|
219
|
+
registeredRootNode = node;
|
|
220
|
+
}
|
|
221
|
+
function setMountedRootRef(ref) {
|
|
222
|
+
rootInstanceRef = ref;
|
|
223
|
+
}
|
|
224
|
+
function setMountedElement(element) {
|
|
225
|
+
mountedElement = element;
|
|
226
|
+
}
|
|
227
|
+
function clearMountedTree() {
|
|
228
|
+
registeredRootNode = null;
|
|
229
|
+
rootInstanceRef = null;
|
|
230
|
+
mountedElement = null;
|
|
231
|
+
if (typeof globalThis.__SPECTRA_MOUNTED_TREE__ !== "undefined") {
|
|
232
|
+
delete globalThis.__SPECTRA_MOUNTED_TREE__;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function getFiberFromInstance(inst) {
|
|
236
|
+
if (!inst) return null;
|
|
237
|
+
if (inst._reactInternals) return inst._reactInternals;
|
|
238
|
+
if (inst._internalFiber) return inst._internalFiber;
|
|
239
|
+
if (typeof inst === "object") {
|
|
240
|
+
for (const key of Object.keys(inst)) {
|
|
241
|
+
if (key.startsWith("__reactFiber$")) {
|
|
242
|
+
return inst[key];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
function buildTreeFromFiber(fiber, parent = null) {
|
|
249
|
+
if (!fiber) return null;
|
|
250
|
+
const node = {
|
|
251
|
+
type: fiber.type,
|
|
252
|
+
props: fiber.memoizedProps || fiber.pendingProps || {},
|
|
253
|
+
parent,
|
|
254
|
+
children: [],
|
|
255
|
+
fiber,
|
|
256
|
+
instance: fiber.stateNode
|
|
257
|
+
};
|
|
258
|
+
let child = fiber.child;
|
|
259
|
+
while (child) {
|
|
260
|
+
const childNode = buildTreeFromFiber(child, node);
|
|
261
|
+
if (childNode) {
|
|
262
|
+
node.children.push(childNode);
|
|
263
|
+
}
|
|
264
|
+
child = child.sibling;
|
|
265
|
+
}
|
|
266
|
+
return node;
|
|
267
|
+
}
|
|
268
|
+
function buildTreeFromReactElement(element, parent = null) {
|
|
269
|
+
if (element == null || typeof element === "boolean") return null;
|
|
270
|
+
if (typeof element === "string" || typeof element === "number") {
|
|
271
|
+
return {
|
|
272
|
+
type: "Text",
|
|
273
|
+
props: { children: String(element) },
|
|
274
|
+
parent,
|
|
275
|
+
children: []
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (Array.isArray(element)) {
|
|
279
|
+
const fragmentNode = {
|
|
280
|
+
type: "Fragment",
|
|
281
|
+
props: {},
|
|
282
|
+
parent,
|
|
283
|
+
children: []
|
|
284
|
+
};
|
|
285
|
+
for (const item of element) {
|
|
286
|
+
const child = buildTreeFromReactElement(item, fragmentNode);
|
|
287
|
+
if (child) fragmentNode.children.push(child);
|
|
288
|
+
}
|
|
289
|
+
return fragmentNode;
|
|
290
|
+
}
|
|
291
|
+
if (typeof element === "object" && element.type) {
|
|
292
|
+
const props = element.props || {};
|
|
293
|
+
const node = {
|
|
294
|
+
type: element.type,
|
|
295
|
+
props,
|
|
296
|
+
parent,
|
|
297
|
+
children: []
|
|
298
|
+
};
|
|
299
|
+
if (typeof element.type === "function" && (!props.children || Array.isArray(props.children) && props.children.length === 0)) {
|
|
300
|
+
try {
|
|
301
|
+
const rendered = element.type(props);
|
|
302
|
+
if (rendered) {
|
|
303
|
+
const renderedChild = buildTreeFromReactElement(rendered, node);
|
|
304
|
+
if (renderedChild) {
|
|
305
|
+
node.children.push(renderedChild);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (props.children) {
|
|
312
|
+
const rawChildren = Array.isArray(props.children) ? props.children : [props.children];
|
|
313
|
+
for (const child of rawChildren) {
|
|
314
|
+
const childNode = buildTreeFromReactElement(child, node);
|
|
315
|
+
if (childNode) {
|
|
316
|
+
node.children.push(childNode);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return node;
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
function getMountedTree() {
|
|
325
|
+
if (registeredRootNode) return registeredRootNode;
|
|
326
|
+
if (typeof globalThis.__SPECTRA_MOUNTED_TREE__ !== "undefined") {
|
|
327
|
+
return globalThis.__SPECTRA_MOUNTED_TREE__;
|
|
328
|
+
}
|
|
329
|
+
if (rootInstanceRef) {
|
|
330
|
+
const fiber = getFiberFromInstance(rootInstanceRef);
|
|
331
|
+
if (fiber) {
|
|
332
|
+
const tree = buildTreeFromFiber(fiber);
|
|
333
|
+
if (tree) return tree;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (mountedElement) {
|
|
337
|
+
return buildTreeFromReactElement(mountedElement);
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
function extractText(node) {
|
|
342
|
+
const parts = [];
|
|
343
|
+
function collect(val) {
|
|
344
|
+
if (val == null || typeof val === "boolean") return;
|
|
345
|
+
if (typeof val === "string" || typeof val === "number") {
|
|
346
|
+
parts.push(String(val));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (Array.isArray(val)) {
|
|
350
|
+
for (const item of val) collect(item);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (typeof val === "object") {
|
|
354
|
+
if (val.props && val.props.children !== void 0) {
|
|
355
|
+
collect(val.props.children);
|
|
356
|
+
} else if (val.children && Array.isArray(val.children)) {
|
|
357
|
+
for (const c of val.children) collect(c);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function walk(curr) {
|
|
362
|
+
if (!curr) return;
|
|
363
|
+
if (curr.children && curr.children.length > 0) {
|
|
364
|
+
for (const childNode of curr.children) {
|
|
365
|
+
walk(childNode);
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (curr.props?.children !== void 0) {
|
|
370
|
+
collect(curr.props.children);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
walk(node);
|
|
374
|
+
if (parts.length === 0 && node.props?.value != null) {
|
|
375
|
+
return String(node.props.value);
|
|
376
|
+
}
|
|
377
|
+
return parts.join("");
|
|
378
|
+
}
|
|
379
|
+
function flattenStyle(style) {
|
|
380
|
+
if (!style) return null;
|
|
381
|
+
if (Array.isArray(style)) {
|
|
382
|
+
const result = {};
|
|
383
|
+
for (const s of style) {
|
|
384
|
+
if (s) {
|
|
385
|
+
const flat = flattenStyle(s);
|
|
386
|
+
if (flat) Object.assign(result, flat);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
if (typeof style === "object") return style;
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
var registeredRootNode, rootInstanceRef, mountedElement;
|
|
395
|
+
var init_tree_registry = __esm({
|
|
396
|
+
"src/bridge/tree_registry.ts"() {
|
|
397
|
+
"use strict";
|
|
398
|
+
registeredRootNode = null;
|
|
399
|
+
rootInstanceRef = null;
|
|
400
|
+
mountedElement = null;
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// src/runtime/harness_root.tsx
|
|
405
|
+
import React2, { useState, useEffect, useRef } from "react";
|
|
406
|
+
import { jsx } from "react/jsx-runtime";
|
|
407
|
+
function getReactNativePrimitives() {
|
|
408
|
+
try {
|
|
409
|
+
if (typeof __require !== "undefined") {
|
|
410
|
+
const rn = __require("react-native");
|
|
411
|
+
if (rn && rn.View && rn.StyleSheet) {
|
|
412
|
+
return { View: rn.View, StyleSheet: rn.StyleSheet };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
View: ((props) => React2.createElement("View", props)),
|
|
419
|
+
StyleSheet: { create: (styles2) => styles2 }
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function registerMountHandlers(mount2, unmount2) {
|
|
423
|
+
activeMountListener = mount2;
|
|
424
|
+
activeUnmountListener = unmount2;
|
|
425
|
+
return () => {
|
|
426
|
+
activeMountListener = null;
|
|
427
|
+
activeUnmountListener = null;
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function getActiveMountListener() {
|
|
431
|
+
return activeMountListener;
|
|
432
|
+
}
|
|
433
|
+
function getActiveUnmountListener() {
|
|
434
|
+
return activeUnmountListener;
|
|
435
|
+
}
|
|
436
|
+
function SpectraHarnessRoot() {
|
|
437
|
+
let currentElement = null;
|
|
438
|
+
let setCurrentElement = () => {
|
|
439
|
+
};
|
|
440
|
+
try {
|
|
441
|
+
const [curr, setCurr] = useState(null);
|
|
442
|
+
currentElement = curr;
|
|
443
|
+
setCurrentElement = setCurr;
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
useEffect(() => {
|
|
448
|
+
return registerMountHandlers(
|
|
449
|
+
(element) => setCurrentElement(element),
|
|
450
|
+
() => setCurrentElement(null)
|
|
451
|
+
);
|
|
452
|
+
}, []);
|
|
453
|
+
} catch {
|
|
454
|
+
}
|
|
455
|
+
let rootRef = null;
|
|
456
|
+
try {
|
|
457
|
+
rootRef = useRef(null);
|
|
458
|
+
useEffect(() => {
|
|
459
|
+
if (rootRef.current) {
|
|
460
|
+
setMountedRootRef(rootRef.current);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
} catch {
|
|
464
|
+
}
|
|
465
|
+
return /* @__PURE__ */ jsx(
|
|
466
|
+
View2,
|
|
467
|
+
{
|
|
468
|
+
ref: rootRef,
|
|
469
|
+
testID: "spectra-root",
|
|
470
|
+
accessibilityLabel: "spectra-root",
|
|
471
|
+
style: styles.container,
|
|
472
|
+
accessible: false,
|
|
473
|
+
children: currentElement
|
|
474
|
+
}
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
var View2, StyleSheet2, activeMountListener, activeUnmountListener, styles;
|
|
478
|
+
var init_harness_root = __esm({
|
|
479
|
+
"src/runtime/harness_root.tsx"() {
|
|
480
|
+
"use strict";
|
|
481
|
+
init_tree_registry();
|
|
482
|
+
({ View: View2, StyleSheet: StyleSheet2 } = getReactNativePrimitives());
|
|
483
|
+
activeMountListener = null;
|
|
484
|
+
activeUnmountListener = null;
|
|
485
|
+
styles = StyleSheet2.create({
|
|
486
|
+
container: {
|
|
487
|
+
flex: 1,
|
|
488
|
+
backgroundColor: "#ffffff"
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// src/runner/in_app_runner.ts
|
|
495
|
+
function beforeEach(fn) {
|
|
496
|
+
registeredBeforeEach.push(fn);
|
|
497
|
+
}
|
|
498
|
+
function afterEach(fn) {
|
|
499
|
+
registeredAfterEach.push(fn);
|
|
500
|
+
}
|
|
501
|
+
function cleanup() {
|
|
502
|
+
const spectra2 = globalThis.Spectra;
|
|
503
|
+
if (typeof spectra2?.unmount === "function") {
|
|
504
|
+
try {
|
|
505
|
+
spectra2.unmount();
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
clearMountedTree();
|
|
510
|
+
}
|
|
511
|
+
function setSocketEmitter(emitter) {
|
|
512
|
+
activeSocketEmitter = emitter;
|
|
513
|
+
}
|
|
514
|
+
function getSocketEmitter() {
|
|
515
|
+
return activeSocketEmitter;
|
|
516
|
+
}
|
|
517
|
+
function emit(tag, payload) {
|
|
518
|
+
if (activeSocketEmitter) {
|
|
519
|
+
try {
|
|
520
|
+
activeSocketEmitter(tag, payload);
|
|
521
|
+
return;
|
|
522
|
+
} catch {
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
console.log(`[${tag}] ${payload}`);
|
|
526
|
+
}
|
|
527
|
+
var registeredTests, registeredBeforeEach, registeredAfterEach, it, activeSocketEmitter, mobileRunner;
|
|
528
|
+
var init_in_app_runner = __esm({
|
|
529
|
+
"src/runner/in_app_runner.ts"() {
|
|
530
|
+
"use strict";
|
|
531
|
+
init_tree_registry();
|
|
532
|
+
registeredTests = [];
|
|
533
|
+
registeredBeforeEach = [];
|
|
534
|
+
registeredAfterEach = [];
|
|
535
|
+
it = function(title, fn) {
|
|
536
|
+
registeredTests.push({ title, fn });
|
|
537
|
+
};
|
|
538
|
+
it.only = function(title, fn) {
|
|
539
|
+
registeredTests.push({ title, fn });
|
|
540
|
+
};
|
|
541
|
+
it.skip = function(_title, _fn) {
|
|
542
|
+
};
|
|
543
|
+
globalThis["it"] = it;
|
|
544
|
+
globalThis["beforeEach"] = beforeEach;
|
|
545
|
+
globalThis["afterEach"] = afterEach;
|
|
546
|
+
activeSocketEmitter = null;
|
|
547
|
+
mobileRunner = {
|
|
548
|
+
async run(specPath, options) {
|
|
549
|
+
emit("TESTSPECTRA_SUITE_START", JSON.stringify({ file: specPath }));
|
|
550
|
+
try {
|
|
551
|
+
globalThis.__TS_RUNTIME__?.activeDriver?.clearMocks?.();
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
if (options?.bundleSource) {
|
|
555
|
+
try {
|
|
556
|
+
const executeSpec = new Function("require", "globalThis", options.bundleSource);
|
|
557
|
+
executeSpec(typeof __require !== "undefined" ? __require : void 0, globalThis);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
emit("TESTSPECTRA_SUITE_ERROR", JSON.stringify({ file: specPath, error: String(err) }));
|
|
560
|
+
return { passed: 0, failed: 1 };
|
|
561
|
+
}
|
|
562
|
+
} else if (typeof fetch === "function") {
|
|
563
|
+
const host = options?.host ?? "127.0.0.1";
|
|
564
|
+
const port = options?.port ?? 9245;
|
|
565
|
+
const url = options?.bundleUrl ?? `http://${host}:${port}/bundle?file=${encodeURIComponent(specPath)}`;
|
|
566
|
+
try {
|
|
567
|
+
const res = await fetch(url);
|
|
568
|
+
if (res.ok) {
|
|
569
|
+
const bundleCode = await res.text();
|
|
570
|
+
const executeSpec = new Function("require", "globalThis", bundleCode);
|
|
571
|
+
executeSpec(typeof __require !== "undefined" ? __require : void 0, globalThis);
|
|
572
|
+
}
|
|
573
|
+
} catch (err) {
|
|
574
|
+
if (registeredTests.length === 0) {
|
|
575
|
+
emit(
|
|
576
|
+
"TESTSPECTRA_SUITE_ERROR",
|
|
577
|
+
JSON.stringify({ file: specPath, error: `Failed to fetch spec bundle from ${url}: ${String(err)}` })
|
|
578
|
+
);
|
|
579
|
+
return { passed: 0, failed: 1 };
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
let passed = 0;
|
|
584
|
+
let failed = 0;
|
|
585
|
+
for (const { title, fn } of registeredTests) {
|
|
586
|
+
emit("TESTSPECTRA_TEST_START", title);
|
|
587
|
+
globalThis["__CURRENT_TEST_TITLE__"] = title;
|
|
588
|
+
const tStart = Date.now();
|
|
589
|
+
try {
|
|
590
|
+
for (const hook of registeredBeforeEach) await hook();
|
|
591
|
+
await fn();
|
|
592
|
+
for (const hook of registeredAfterEach) await hook();
|
|
593
|
+
const duration = Date.now() - tStart;
|
|
594
|
+
emit("TESTSPECTRA_TEST_PASS", `${title} (${duration}ms)`);
|
|
595
|
+
passed++;
|
|
596
|
+
} catch (err) {
|
|
597
|
+
const duration = Date.now() - tStart;
|
|
598
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
599
|
+
emit("TESTSPECTRA_TEST_ERROR", message);
|
|
600
|
+
emit("TESTSPECTRA_TEST_FAIL", `${title} (${duration}ms)`);
|
|
601
|
+
failed++;
|
|
602
|
+
} finally {
|
|
603
|
+
cleanup();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
registeredTests.length = 0;
|
|
607
|
+
registeredBeforeEach.length = 0;
|
|
608
|
+
registeredAfterEach.length = 0;
|
|
609
|
+
emit("TESTSPECTRA_SUITE_END", JSON.stringify({ file: specPath, passed, failed }));
|
|
610
|
+
return { passed, failed };
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
globalThis["__SPECTRA_MOBILE_RUNNER__"] = mobileRunner;
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// ../../core/worker-runtime/src/drivers/driver_context.ts
|
|
618
|
+
function setActiveDriver(driver) {
|
|
619
|
+
activeDriver = driver;
|
|
620
|
+
}
|
|
621
|
+
function getActiveDriver() {
|
|
622
|
+
if (!activeDriver) {
|
|
623
|
+
throw new Error("No active driver bound to worker runtime");
|
|
624
|
+
}
|
|
625
|
+
return activeDriver;
|
|
626
|
+
}
|
|
627
|
+
var activeDriver;
|
|
628
|
+
var init_driver_context = __esm({
|
|
629
|
+
"../../core/worker-runtime/src/drivers/driver_context.ts"() {
|
|
630
|
+
"use strict";
|
|
631
|
+
activeDriver = null;
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// ../../core/worker-runtime/src/harness/config.ts
|
|
636
|
+
function getWorkerConfig() {
|
|
637
|
+
if (currentConfig) {
|
|
638
|
+
return currentConfig;
|
|
639
|
+
}
|
|
640
|
+
if (globalThis.__TESTSPECTRA_CONFIG__) {
|
|
641
|
+
currentConfig = globalThis.__TESTSPECTRA_CONFIG__;
|
|
642
|
+
return currentConfig;
|
|
643
|
+
}
|
|
644
|
+
if (typeof process !== "undefined" && process.env?.TESTSPECTRA_CONFIG) {
|
|
645
|
+
try {
|
|
646
|
+
currentConfig = JSON.parse(process.env.TESTSPECTRA_CONFIG);
|
|
647
|
+
return currentConfig;
|
|
648
|
+
} catch {
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
currentConfig = {
|
|
652
|
+
rootDir: typeof process !== "undefined" ? process.cwd() : "",
|
|
653
|
+
testFilePath: "",
|
|
654
|
+
activePlatform: DEFAULT_WORKER_CONFIG.activePlatform,
|
|
655
|
+
configuredBaseUrl: DEFAULT_WORKER_CONFIG.configuredBaseUrl,
|
|
656
|
+
cdpWsUrl: "",
|
|
657
|
+
driverServerUrl: DEFAULT_WORKER_CONFIG.driverServerUrl,
|
|
658
|
+
manifest: {
|
|
659
|
+
page_objects: [],
|
|
660
|
+
fixtures: [],
|
|
661
|
+
steps: [],
|
|
662
|
+
actions: [],
|
|
663
|
+
global_hooks: [],
|
|
664
|
+
suite_hooks: {}
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
return currentConfig;
|
|
668
|
+
}
|
|
669
|
+
var DEFAULT_WORKER_CONFIG, currentConfig;
|
|
670
|
+
var init_config = __esm({
|
|
671
|
+
"../../core/worker-runtime/src/harness/config.ts"() {
|
|
672
|
+
"use strict";
|
|
673
|
+
DEFAULT_WORKER_CONFIG = {
|
|
674
|
+
activePlatform: "web",
|
|
675
|
+
configuredBaseUrl: "http://localhost:3000",
|
|
676
|
+
driverServerUrl: "tcp://127.0.0.1:8200",
|
|
677
|
+
defaultElementWaitMs: 5e3,
|
|
678
|
+
defaultNavigationWaitMs: 1e4,
|
|
679
|
+
defaultPollIntervalMs: 20,
|
|
680
|
+
defaultReadyPollIntervalMs: 50
|
|
681
|
+
};
|
|
682
|
+
currentConfig = null;
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// ../../core/worker-runtime/src/harness/reporter.ts
|
|
687
|
+
function recordDuration(label, category, durationMs, extra) {
|
|
688
|
+
if (activeDurationSink) {
|
|
689
|
+
activeDurationSink(label, category, durationMs, extra);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function getImplicitWaitMs() {
|
|
693
|
+
const config = getWorkerConfig();
|
|
694
|
+
return config.implicitWaitMs ?? DEFAULT_WORKER_CONFIG.defaultElementWaitMs;
|
|
695
|
+
}
|
|
696
|
+
function getAssertionTimeoutMs() {
|
|
697
|
+
return Math.min(getImplicitWaitMs(), 2e3);
|
|
698
|
+
}
|
|
699
|
+
function emitEvent(tag, payload) {
|
|
700
|
+
console.log(`[${tag}] ${payload}`);
|
|
701
|
+
}
|
|
702
|
+
async function trackStep(category, payloadObj, fn) {
|
|
703
|
+
const config = getWorkerConfig();
|
|
704
|
+
const currentTest = globalThis.__CURRENT_TEST_TITLE__ || globalThis.__CURRENT_TEST_ID__ || "";
|
|
705
|
+
const workerId = globalThis.__TESTSPECTRA_WORKER_ID__;
|
|
706
|
+
if (currentTest && !payloadObj.test) payloadObj.test = currentTest;
|
|
707
|
+
if (workerId !== void 0 && payloadObj.workerId === void 0) payloadObj.workerId = workerId;
|
|
708
|
+
const payload = JSON.stringify(payloadObj);
|
|
709
|
+
emitEvent("TESTSPECTRA_STEP_START", payload);
|
|
710
|
+
const start = Date.now();
|
|
711
|
+
try {
|
|
712
|
+
const res = await fn();
|
|
713
|
+
const duration = Date.now() - start;
|
|
714
|
+
emitEvent("TESTSPECTRA_STEP_PASS", `${payload} (${duration}ms)`);
|
|
715
|
+
recordDuration(`step:${payloadObj.key}`, category, duration, { test: currentTest, target: payloadObj.target });
|
|
716
|
+
const delay = config.stepDelayMs || 0;
|
|
717
|
+
if (delay > 0) {
|
|
718
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
719
|
+
}
|
|
720
|
+
return res;
|
|
721
|
+
} catch (err) {
|
|
722
|
+
const duration = Date.now() - start;
|
|
723
|
+
emitEvent("TESTSPECTRA_STEP_FAIL", `${payload} (${duration}ms)`);
|
|
724
|
+
recordDuration(`step:${payloadObj.key}`, category, duration, {
|
|
725
|
+
test: currentTest,
|
|
726
|
+
target: payloadObj.target,
|
|
727
|
+
failed: true
|
|
728
|
+
});
|
|
729
|
+
emitEvent("TESTSPECTRA_TEST_ERROR", err?.message || String(err));
|
|
730
|
+
throw err;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async function pollCondition(fn, timeoutMs, intervalMs = 15) {
|
|
734
|
+
const effectiveTimeout = timeoutMs ?? getImplicitWaitMs();
|
|
735
|
+
const start = Date.now();
|
|
736
|
+
while (Date.now() - start < effectiveTimeout) {
|
|
737
|
+
try {
|
|
738
|
+
if (await fn()) return true;
|
|
739
|
+
} catch {
|
|
740
|
+
}
|
|
741
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
return await fn();
|
|
745
|
+
} catch {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
var activeDurationSink;
|
|
750
|
+
var init_reporter = __esm({
|
|
751
|
+
"../../core/worker-runtime/src/harness/reporter.ts"() {
|
|
752
|
+
"use strict";
|
|
753
|
+
init_config();
|
|
754
|
+
activeDurationSink = null;
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// ../../core/worker-runtime/src/dsl/assertions.ts
|
|
759
|
+
function createElementAssertions(selector, index, state) {
|
|
760
|
+
const driver = () => getActiveDriver();
|
|
761
|
+
const selectorText4 = typeof selector === "string" ? selector : selector.selector;
|
|
762
|
+
async function shouldBeVisible(options) {
|
|
763
|
+
return trackStep("assertion", { type: "assertion", key: "be.visible", target: selector, args: [] }, async () => {
|
|
764
|
+
const ok = await pollCondition(() => state.isVisible(), options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
765
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" to be visible`);
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
async function shouldNotBeVisible(options) {
|
|
769
|
+
return trackStep(
|
|
770
|
+
"assertion",
|
|
771
|
+
{ type: "assertion", key: "not.be.visible", target: selector, args: [] },
|
|
772
|
+
async () => {
|
|
773
|
+
const ok = await pollCondition(
|
|
774
|
+
async () => !await state.isVisible(),
|
|
775
|
+
options?.timeoutMs ?? getAssertionTimeoutMs()
|
|
776
|
+
);
|
|
777
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" not to be visible`);
|
|
778
|
+
}
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
async function shouldExist(options) {
|
|
782
|
+
return trackStep("assertion", { type: "assertion", key: "exist", target: selector, args: [] }, async () => {
|
|
783
|
+
const ok = await pollCondition(() => state.isExisting(), options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
784
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" to exist in DOM`);
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
async function shouldNotExist(options) {
|
|
788
|
+
return trackStep("assertion", { type: "assertion", key: "not.exist", target: selector, args: [] }, async () => {
|
|
789
|
+
const ok = await pollCondition(
|
|
790
|
+
async () => !await state.isExisting(),
|
|
791
|
+
options?.timeoutMs ?? getAssertionTimeoutMs()
|
|
792
|
+
);
|
|
793
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" not to exist in DOM`);
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
async function shouldBeEnabled(options) {
|
|
797
|
+
return trackStep("assertion", { type: "assertion", key: "be.enabled", target: selector, args: [] }, async () => {
|
|
798
|
+
const ok = await pollCondition(() => state.isEnabled(), options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
799
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" to be enabled`);
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
async function shouldBeDisabled(options) {
|
|
803
|
+
return trackStep("assertion", { type: "assertion", key: "be.disabled", target: selector, args: [] }, async () => {
|
|
804
|
+
const ok = await pollCondition(
|
|
805
|
+
async () => !await state.isEnabled(),
|
|
806
|
+
options?.timeoutMs ?? getAssertionTimeoutMs()
|
|
807
|
+
);
|
|
808
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" to be disabled`);
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
async function shouldBeClickable(options) {
|
|
812
|
+
return trackStep("assertion", { type: "assertion", key: "be.clickable", target: selector, args: [] }, async () => {
|
|
813
|
+
const ok = await pollCondition(
|
|
814
|
+
() => driver().isClickable(selector, index),
|
|
815
|
+
options?.timeoutMs ?? getAssertionTimeoutMs()
|
|
816
|
+
);
|
|
817
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" to be clickable`);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
async function shouldNotBeClickable(options) {
|
|
821
|
+
return trackStep(
|
|
822
|
+
"assertion",
|
|
823
|
+
{ type: "assertion", key: "not.be.clickable", target: selector, args: [] },
|
|
824
|
+
async () => {
|
|
825
|
+
const ok = await pollCondition(
|
|
826
|
+
async () => !await driver().isClickable(selector, index),
|
|
827
|
+
options?.timeoutMs ?? getAssertionTimeoutMs()
|
|
828
|
+
);
|
|
829
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" not to be clickable`);
|
|
830
|
+
}
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
async function shouldBeChecked(options) {
|
|
834
|
+
return trackStep("assertion", { type: "assertion", key: "be.checked", target: selector, args: [] }, async () => {
|
|
835
|
+
const ok = await pollCondition(() => state.isChecked(), options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
836
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" to be checked`);
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
async function shouldNotBeChecked(options) {
|
|
840
|
+
return trackStep(
|
|
841
|
+
"assertion",
|
|
842
|
+
{ type: "assertion", key: "not.be.checked", target: selector, args: [] },
|
|
843
|
+
async () => {
|
|
844
|
+
const ok = await pollCondition(
|
|
845
|
+
async () => !await state.isChecked(),
|
|
846
|
+
options?.timeoutMs ?? getAssertionTimeoutMs()
|
|
847
|
+
);
|
|
848
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" not to be checked`);
|
|
849
|
+
}
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
async function shouldBeFocused(options) {
|
|
853
|
+
return trackStep("assertion", { type: "assertion", key: "be.focused", target: selector, args: [] }, async () => {
|
|
854
|
+
const ok = await pollCondition(() => state.isFocused(), options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
855
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" to be focused`);
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
async function shouldNotBeFocused(options) {
|
|
859
|
+
return trackStep(
|
|
860
|
+
"assertion",
|
|
861
|
+
{ type: "assertion", key: "not.be.focused", target: selector, args: [] },
|
|
862
|
+
async () => {
|
|
863
|
+
const ok = await pollCondition(
|
|
864
|
+
async () => !await state.isFocused(),
|
|
865
|
+
options?.timeoutMs ?? getAssertionTimeoutMs()
|
|
866
|
+
);
|
|
867
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" not to be focused`);
|
|
868
|
+
}
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
async function shouldHaveValue(value, options) {
|
|
872
|
+
return trackStep(
|
|
873
|
+
"assertion",
|
|
874
|
+
{ type: "assertion", key: "have.value", target: selector, args: [value] },
|
|
875
|
+
async () => {
|
|
876
|
+
let actual = "";
|
|
877
|
+
const ok = await pollCondition(async () => {
|
|
878
|
+
actual = await state.getValue();
|
|
879
|
+
return actual === String(value);
|
|
880
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
881
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" to have value "${value}", but got "${actual}"`);
|
|
882
|
+
}
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
async function shouldNotHaveValue(value, options) {
|
|
886
|
+
return trackStep(
|
|
887
|
+
"assertion",
|
|
888
|
+
{ type: "assertion", key: "not.have.value", target: selector, args: [value] },
|
|
889
|
+
async () => {
|
|
890
|
+
const ok = await pollCondition(async () => {
|
|
891
|
+
const actual = await state.getValue();
|
|
892
|
+
return actual !== String(value);
|
|
893
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
894
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" not to have value "${value}"`);
|
|
895
|
+
}
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
async function shouldContainValue(value, options) {
|
|
899
|
+
return trackStep(
|
|
900
|
+
"assertion",
|
|
901
|
+
{ type: "assertion", key: "contain.value", target: selector, args: [value] },
|
|
902
|
+
async () => {
|
|
903
|
+
let actual = "";
|
|
904
|
+
const ok = await pollCondition(async () => {
|
|
905
|
+
actual = await state.getValue();
|
|
906
|
+
return actual.includes(String(value));
|
|
907
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
908
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" to contain value "${value}", but got "${actual}"`);
|
|
909
|
+
}
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
async function shouldNotContainValue(value, options) {
|
|
913
|
+
return trackStep(
|
|
914
|
+
"assertion",
|
|
915
|
+
{ type: "assertion", key: "not.contain.value", target: selector, args: [value] },
|
|
916
|
+
async () => {
|
|
917
|
+
const ok = await pollCondition(async () => {
|
|
918
|
+
const actual = await state.getValue();
|
|
919
|
+
return !actual.includes(String(value));
|
|
920
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
921
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" not to contain value "${value}"`);
|
|
922
|
+
}
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
async function shouldHaveText(expected, options) {
|
|
926
|
+
return trackStep(
|
|
927
|
+
"assertion",
|
|
928
|
+
{ type: "assertion", key: "have.text", target: selector, args: [expected] },
|
|
929
|
+
async () => {
|
|
930
|
+
let actual = "";
|
|
931
|
+
const ok = await pollCondition(async () => {
|
|
932
|
+
actual = await state.getText();
|
|
933
|
+
if (expected instanceof RegExp) return expected.test(actual);
|
|
934
|
+
return actual === String(expected) || actual.trim() === String(expected).trim();
|
|
935
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
936
|
+
if (!ok) {
|
|
937
|
+
if (expected instanceof RegExp)
|
|
938
|
+
throw new Error(`Expected element "${selectorText4}" text to match ${expected}, but got "${actual}"`);
|
|
939
|
+
throw new Error(`Expected element "${selectorText4}" to have text "${expected}", but got "${actual}"`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
async function shouldNotHaveText(expected, options) {
|
|
945
|
+
return trackStep(
|
|
946
|
+
"assertion",
|
|
947
|
+
{ type: "assertion", key: "not.have.text", target: selector, args: [expected] },
|
|
948
|
+
async () => {
|
|
949
|
+
let actual = "";
|
|
950
|
+
const ok = await pollCondition(async () => {
|
|
951
|
+
if (!await state.isExisting()) return false;
|
|
952
|
+
actual = await state.getText();
|
|
953
|
+
if (expected instanceof RegExp) return !expected.test(actual);
|
|
954
|
+
return actual !== String(expected) && actual.trim() !== String(expected).trim();
|
|
955
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
956
|
+
if (!ok) {
|
|
957
|
+
if (expected instanceof RegExp)
|
|
958
|
+
throw new Error(`Expected element "${selectorText4}" text not to match ${expected}`);
|
|
959
|
+
throw new Error(`Expected element "${selectorText4}" not to have text "${expected}"`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
async function shouldContainText(substring, options) {
|
|
965
|
+
return trackStep(
|
|
966
|
+
"assertion",
|
|
967
|
+
{ type: "assertion", key: "contain.text", target: selector, args: [substring] },
|
|
968
|
+
async () => {
|
|
969
|
+
let actual = "";
|
|
970
|
+
const ok = await pollCondition(async () => {
|
|
971
|
+
actual = await state.getText();
|
|
972
|
+
return actual.includes(String(substring));
|
|
973
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
974
|
+
if (!ok)
|
|
975
|
+
throw new Error(`Expected element "${selectorText4}" to contain text "${substring}", but got "${actual}"`);
|
|
976
|
+
}
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
async function shouldNotContainText(substring, options) {
|
|
980
|
+
return trackStep(
|
|
981
|
+
"assertion",
|
|
982
|
+
{ type: "assertion", key: "not.contain.text", target: selector, args: [substring] },
|
|
983
|
+
async () => {
|
|
984
|
+
const ok = await pollCondition(async () => {
|
|
985
|
+
const actual = await state.getText();
|
|
986
|
+
return !actual.includes(String(substring));
|
|
987
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
988
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" not to contain text "${substring}"`);
|
|
989
|
+
}
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
async function shouldHaveClass(className, options) {
|
|
993
|
+
return trackStep(
|
|
994
|
+
"assertion",
|
|
995
|
+
{ type: "assertion", key: "have.class", target: selector, args: [className] },
|
|
996
|
+
async () => {
|
|
997
|
+
const ok = await pollCondition(
|
|
998
|
+
() => driver().hasClass(selector, className, index),
|
|
999
|
+
options?.timeoutMs ?? getAssertionTimeoutMs()
|
|
1000
|
+
);
|
|
1001
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" to have class "${className}"`);
|
|
1002
|
+
}
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
async function shouldNotHaveClass(className, options) {
|
|
1006
|
+
return trackStep(
|
|
1007
|
+
"assertion",
|
|
1008
|
+
{ type: "assertion", key: "not.have.class", target: selector, args: [className] },
|
|
1009
|
+
async () => {
|
|
1010
|
+
const ok = await pollCondition(
|
|
1011
|
+
async () => !await driver().hasClass(selector, className, index),
|
|
1012
|
+
options?.timeoutMs ?? getAssertionTimeoutMs()
|
|
1013
|
+
);
|
|
1014
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" not to have class "${className}"`);
|
|
1015
|
+
}
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
async function shouldHaveAttribute(name, value, options) {
|
|
1019
|
+
return trackStep(
|
|
1020
|
+
"assertion",
|
|
1021
|
+
{ type: "assertion", key: "have.attr", target: selector, args: value !== void 0 ? [name, value] : [name] },
|
|
1022
|
+
async () => {
|
|
1023
|
+
let actual = null;
|
|
1024
|
+
const ok = await pollCondition(async () => {
|
|
1025
|
+
actual = await state.getAttribute(name);
|
|
1026
|
+
if (value !== void 0) return actual === String(value);
|
|
1027
|
+
return actual !== null;
|
|
1028
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
1029
|
+
if (!ok) {
|
|
1030
|
+
if (value !== void 0)
|
|
1031
|
+
throw new Error(
|
|
1032
|
+
`Expected element "${selectorText4}" to have attribute "${name}" = "${value}", but got "${actual}"`
|
|
1033
|
+
);
|
|
1034
|
+
throw new Error(`Expected element "${selectorText4}" to have attribute "${name}"`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
async function shouldNotHaveAttribute(name, options) {
|
|
1040
|
+
return trackStep(
|
|
1041
|
+
"assertion",
|
|
1042
|
+
{ type: "assertion", key: "not.have.attr", target: selector, args: [name] },
|
|
1043
|
+
async () => {
|
|
1044
|
+
const ok = await pollCondition(async () => {
|
|
1045
|
+
const actual = await state.getAttribute(name);
|
|
1046
|
+
return actual === null;
|
|
1047
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
1048
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" not to have attribute "${name}"`);
|
|
1049
|
+
}
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
async function shouldHaveCss(property, value, options) {
|
|
1053
|
+
return trackStep(
|
|
1054
|
+
"assertion",
|
|
1055
|
+
{ type: "assertion", key: "have.css", target: selector, args: [property, value] },
|
|
1056
|
+
async () => {
|
|
1057
|
+
const norm = (v) => String(v ?? "").replace(/\s+/g, "").replace(/rgba\((.+?),1\)/, "rgb($1)").toLowerCase();
|
|
1058
|
+
let actualCss = "";
|
|
1059
|
+
const ok = await pollCondition(async () => {
|
|
1060
|
+
const css = await state.getCSSProperty(property);
|
|
1061
|
+
actualCss = css.value;
|
|
1062
|
+
return norm(actualCss) === norm(value);
|
|
1063
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
1064
|
+
if (!ok)
|
|
1065
|
+
throw new Error(
|
|
1066
|
+
`Expected element "${selectorText4}" to have CSS "${property}" = "${value}", but got "${actualCss}"`
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
async function shouldNotHaveCss(property, value, options) {
|
|
1072
|
+
return trackStep(
|
|
1073
|
+
"assertion",
|
|
1074
|
+
{ type: "assertion", key: "not.have.css", target: selector, args: [property, value] },
|
|
1075
|
+
async () => {
|
|
1076
|
+
const norm = (v) => String(v ?? "").replace(/\s+/g, "").replace(/rgba\((.+?),1\)/, "rgb($1)").toLowerCase();
|
|
1077
|
+
const ok = await pollCondition(async () => {
|
|
1078
|
+
const css = await state.getCSSProperty(property);
|
|
1079
|
+
return norm(css.value) !== norm(value);
|
|
1080
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
1081
|
+
if (!ok) throw new Error(`Expected element "${selectorText4}" not to have CSS "${property}" = "${value}"`);
|
|
1082
|
+
}
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
shouldBeVisible,
|
|
1087
|
+
shouldNotBeVisible,
|
|
1088
|
+
shouldExist,
|
|
1089
|
+
shouldNotExist,
|
|
1090
|
+
shouldBeEnabled,
|
|
1091
|
+
shouldBeDisabled,
|
|
1092
|
+
shouldBeClickable,
|
|
1093
|
+
shouldNotBeClickable,
|
|
1094
|
+
shouldBeChecked,
|
|
1095
|
+
shouldNotBeChecked,
|
|
1096
|
+
shouldBeFocused,
|
|
1097
|
+
shouldNotBeFocused,
|
|
1098
|
+
shouldHaveValue,
|
|
1099
|
+
shouldNotHaveValue,
|
|
1100
|
+
shouldContainValue,
|
|
1101
|
+
shouldNotContainValue,
|
|
1102
|
+
shouldHaveText,
|
|
1103
|
+
shouldNotHaveText,
|
|
1104
|
+
shouldContainText,
|
|
1105
|
+
shouldNotContainText,
|
|
1106
|
+
shouldHaveClass,
|
|
1107
|
+
shouldNotHaveClass,
|
|
1108
|
+
shouldHaveAttribute,
|
|
1109
|
+
shouldNotHaveAttribute,
|
|
1110
|
+
shouldHaveCss,
|
|
1111
|
+
shouldNotHaveCss
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
var init_assertions = __esm({
|
|
1115
|
+
"../../core/worker-runtime/src/dsl/assertions.ts"() {
|
|
1116
|
+
"use strict";
|
|
1117
|
+
init_driver_context();
|
|
1118
|
+
init_reporter();
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
// ../../core/worker-runtime/src/dsl/element_state.ts
|
|
1123
|
+
function createElementState(selector, index) {
|
|
1124
|
+
const driver = () => getActiveDriver();
|
|
1125
|
+
const selectorText4 = typeof selector === "string" ? selector : selector.selector;
|
|
1126
|
+
async function getText() {
|
|
1127
|
+
return await driver().getText(selector, index);
|
|
1128
|
+
}
|
|
1129
|
+
async function getValue() {
|
|
1130
|
+
return await driver().getValue(selector, index);
|
|
1131
|
+
}
|
|
1132
|
+
async function isDisplayed() {
|
|
1133
|
+
return await driver().isDisplayed(selector, index);
|
|
1134
|
+
}
|
|
1135
|
+
async function isVisible() {
|
|
1136
|
+
return await isDisplayed();
|
|
1137
|
+
}
|
|
1138
|
+
async function isExisting() {
|
|
1139
|
+
return await driver().isExisting(selector, index);
|
|
1140
|
+
}
|
|
1141
|
+
async function isEnabled() {
|
|
1142
|
+
return await driver().isEnabled(selector, index);
|
|
1143
|
+
}
|
|
1144
|
+
async function isClickable() {
|
|
1145
|
+
return await driver().isClickable(selector, index);
|
|
1146
|
+
}
|
|
1147
|
+
async function isSelected() {
|
|
1148
|
+
return await driver().isSelected(selector, index);
|
|
1149
|
+
}
|
|
1150
|
+
async function isChecked() {
|
|
1151
|
+
return await isSelected();
|
|
1152
|
+
}
|
|
1153
|
+
async function isFocused() {
|
|
1154
|
+
return await driver().isFocused(selector, index);
|
|
1155
|
+
}
|
|
1156
|
+
async function getAttribute(name) {
|
|
1157
|
+
return await driver().getAttribute(selector, name, index);
|
|
1158
|
+
}
|
|
1159
|
+
async function getCSSProperty(name) {
|
|
1160
|
+
return await driver().getCSSProperty(selector, name, index);
|
|
1161
|
+
}
|
|
1162
|
+
async function waitForElement(timeoutMs) {
|
|
1163
|
+
const waitTimeout = timeoutMs ?? getImplicitWaitMs();
|
|
1164
|
+
const ok = await pollCondition(async () => driver().isExisting(selector, index), waitTimeout);
|
|
1165
|
+
if (ok) return true;
|
|
1166
|
+
throw new Error("Element not found: " + JSON.stringify(selectorText4));
|
|
1167
|
+
}
|
|
1168
|
+
async function waitForDisplayed(opts = {}) {
|
|
1169
|
+
const timeout = opts.timeout || 5e3;
|
|
1170
|
+
const start = Date.now();
|
|
1171
|
+
while (Date.now() - start < timeout) {
|
|
1172
|
+
if (await driver().isDisplayed(selector, index)) return true;
|
|
1173
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
1174
|
+
}
|
|
1175
|
+
throw new Error(`Element ${selectorText4} not displayed after ${timeout}ms`);
|
|
1176
|
+
}
|
|
1177
|
+
return {
|
|
1178
|
+
getText,
|
|
1179
|
+
getValue,
|
|
1180
|
+
isDisplayed,
|
|
1181
|
+
isVisible,
|
|
1182
|
+
isExisting,
|
|
1183
|
+
isEnabled,
|
|
1184
|
+
isClickable,
|
|
1185
|
+
isSelected,
|
|
1186
|
+
isChecked,
|
|
1187
|
+
isFocused,
|
|
1188
|
+
getAttribute,
|
|
1189
|
+
getCSSProperty,
|
|
1190
|
+
waitForElement,
|
|
1191
|
+
waitForDisplayed
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
var init_element_state = __esm({
|
|
1195
|
+
"../../core/worker-runtime/src/dsl/element_state.ts"() {
|
|
1196
|
+
"use strict";
|
|
1197
|
+
init_driver_context();
|
|
1198
|
+
init_reporter();
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
// ../../core/worker-runtime/src/dsl/element_proxy.ts
|
|
1203
|
+
function selectorText(s) {
|
|
1204
|
+
return typeof s === "string" ? s : s.selector;
|
|
1205
|
+
}
|
|
1206
|
+
function toScopedSelector(selector, index) {
|
|
1207
|
+
return typeof selector === "string" ? { selector, index, parent: void 0 } : selector;
|
|
1208
|
+
}
|
|
1209
|
+
function createElementProxy(selector, index = null) {
|
|
1210
|
+
const selectorTextValue = selectorText(selector);
|
|
1211
|
+
const resolvedIndex = typeof selector === "string" ? index : selector.index;
|
|
1212
|
+
const actions = createElementActions(selector, index);
|
|
1213
|
+
const state = createElementState(selector, index);
|
|
1214
|
+
const assertions = createElementAssertions(selector, index, state);
|
|
1215
|
+
return {
|
|
1216
|
+
selector: selectorTextValue,
|
|
1217
|
+
index: resolvedIndex,
|
|
1218
|
+
...actions,
|
|
1219
|
+
...state,
|
|
1220
|
+
...assertions,
|
|
1221
|
+
get(childSelector, childIndex = null) {
|
|
1222
|
+
return createElementProxy(
|
|
1223
|
+
{ selector: childSelector, index: childIndex, parent: toScopedSelector(selector, index) },
|
|
1224
|
+
null
|
|
1225
|
+
);
|
|
1226
|
+
},
|
|
1227
|
+
getAll(childSelector) {
|
|
1228
|
+
return createCollectionProxy({
|
|
1229
|
+
selector: childSelector,
|
|
1230
|
+
index: null,
|
|
1231
|
+
parent: toScopedSelector(selector, index)
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
function createCollectionProxy(selector) {
|
|
1237
|
+
const driver = () => getActiveDriver();
|
|
1238
|
+
const selectorTextValue = selectorText(selector);
|
|
1239
|
+
const withIndex = (idx) => typeof selector === "string" ? [selector, idx] : [{ ...selector, index: idx }, null];
|
|
1240
|
+
const collectionProxy = {
|
|
1241
|
+
selector: selectorTextValue,
|
|
1242
|
+
async count() {
|
|
1243
|
+
return await driver().count(selector);
|
|
1244
|
+
},
|
|
1245
|
+
get length() {
|
|
1246
|
+
return this.count();
|
|
1247
|
+
},
|
|
1248
|
+
first() {
|
|
1249
|
+
return createElementProxy(...withIndex(0));
|
|
1250
|
+
},
|
|
1251
|
+
last() {
|
|
1252
|
+
return createElementProxy(...withIndex(-1));
|
|
1253
|
+
},
|
|
1254
|
+
nth(index) {
|
|
1255
|
+
return createElementProxy(...withIndex(index));
|
|
1256
|
+
},
|
|
1257
|
+
async shouldHaveLength(expected, options) {
|
|
1258
|
+
return trackStep(
|
|
1259
|
+
"assertion",
|
|
1260
|
+
{ type: "assertion", key: "have.length", target: selector, args: [expected] },
|
|
1261
|
+
async () => {
|
|
1262
|
+
let actual = 0;
|
|
1263
|
+
const ok = await pollCondition(async () => {
|
|
1264
|
+
actual = await this.count();
|
|
1265
|
+
return actual === expected;
|
|
1266
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
1267
|
+
if (!ok) {
|
|
1268
|
+
throw new Error(`Expected collection "${selectorTextValue}" count to equal ${expected}, but got ${actual}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
);
|
|
1272
|
+
},
|
|
1273
|
+
async shouldNotHaveLength(expected, options) {
|
|
1274
|
+
return trackStep(
|
|
1275
|
+
"assertion",
|
|
1276
|
+
{ type: "assertion", key: "not.have.length", target: selector, args: [expected] },
|
|
1277
|
+
async () => {
|
|
1278
|
+
let actual = 0;
|
|
1279
|
+
const ok = await pollCondition(async () => {
|
|
1280
|
+
actual = await this.count();
|
|
1281
|
+
return actual !== expected;
|
|
1282
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
1283
|
+
if (!ok) {
|
|
1284
|
+
throw new Error(`Expected collection "${selectorTextValue}" count not to equal ${expected}`);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
);
|
|
1288
|
+
},
|
|
1289
|
+
async shouldHaveLengthGreaterThan(expected, options) {
|
|
1290
|
+
return trackStep(
|
|
1291
|
+
"assertion",
|
|
1292
|
+
{ type: "assertion", key: "have.length.greaterThan", target: selector, args: [expected] },
|
|
1293
|
+
async () => {
|
|
1294
|
+
let actual = 0;
|
|
1295
|
+
const ok = await pollCondition(async () => {
|
|
1296
|
+
actual = await this.count();
|
|
1297
|
+
return actual > expected;
|
|
1298
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
1299
|
+
if (!ok) {
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
`Expected collection "${selectorTextValue}" count to be greater than ${expected}, but got ${actual}`
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
);
|
|
1306
|
+
},
|
|
1307
|
+
async shouldHaveLengthLessThan(expected, options) {
|
|
1308
|
+
return trackStep(
|
|
1309
|
+
"assertion",
|
|
1310
|
+
{ type: "assertion", key: "have.length.lessThan", target: selector, args: [expected] },
|
|
1311
|
+
async () => {
|
|
1312
|
+
let actual = 0;
|
|
1313
|
+
const ok = await pollCondition(async () => {
|
|
1314
|
+
actual = await this.count();
|
|
1315
|
+
return actual < expected;
|
|
1316
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
1317
|
+
if (!ok) {
|
|
1318
|
+
throw new Error(
|
|
1319
|
+
`Expected collection "${selectorTextValue}" count to be less than ${expected}, but got ${actual}`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
);
|
|
1324
|
+
},
|
|
1325
|
+
async shouldBeEmpty(options) {
|
|
1326
|
+
return trackStep("assertion", { type: "assertion", key: "be.empty", target: selector, args: [] }, async () => {
|
|
1327
|
+
let actual = 0;
|
|
1328
|
+
const ok = await pollCondition(async () => {
|
|
1329
|
+
actual = await this.count();
|
|
1330
|
+
return actual === 0;
|
|
1331
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
1332
|
+
if (!ok) {
|
|
1333
|
+
throw new Error(`Expected collection "${selectorTextValue}" to be empty, but got ${actual}`);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
},
|
|
1337
|
+
async shouldNotBeEmpty(options) {
|
|
1338
|
+
return trackStep(
|
|
1339
|
+
"assertion",
|
|
1340
|
+
{ type: "assertion", key: "not.be.empty", target: selector, args: [] },
|
|
1341
|
+
async () => {
|
|
1342
|
+
let actual = 0;
|
|
1343
|
+
const ok = await pollCondition(async () => {
|
|
1344
|
+
actual = await this.count();
|
|
1345
|
+
return actual > 0;
|
|
1346
|
+
}, options?.timeoutMs ?? getAssertionTimeoutMs());
|
|
1347
|
+
if (!ok) {
|
|
1348
|
+
throw new Error(`Expected collection "${selectorTextValue}" not to be empty`);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
return collectionProxy;
|
|
1355
|
+
}
|
|
1356
|
+
function resolveTargetProxy(target) {
|
|
1357
|
+
if (typeof target === "string") {
|
|
1358
|
+
return createElementProxy(target, null);
|
|
1359
|
+
}
|
|
1360
|
+
if (target && typeof target === "object" && "selector" in target) {
|
|
1361
|
+
return target;
|
|
1362
|
+
}
|
|
1363
|
+
throw new Error(`Invalid element target: ${String(target)}`);
|
|
1364
|
+
}
|
|
1365
|
+
var init_element_proxy = __esm({
|
|
1366
|
+
"../../core/worker-runtime/src/dsl/element_proxy.ts"() {
|
|
1367
|
+
"use strict";
|
|
1368
|
+
init_driver_context();
|
|
1369
|
+
init_reporter();
|
|
1370
|
+
init_element_actions();
|
|
1371
|
+
init_assertions();
|
|
1372
|
+
init_element_state();
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
// ../../core/worker-runtime/src/dsl/element_actions.ts
|
|
1377
|
+
function selectorText2(selector) {
|
|
1378
|
+
return typeof selector === "string" ? selector : selector.selector;
|
|
1379
|
+
}
|
|
1380
|
+
function createElementActions(selector, index) {
|
|
1381
|
+
const driver = () => getActiveDriver();
|
|
1382
|
+
async function click(_options) {
|
|
1383
|
+
return trackStep("action", { type: "action", key: "click", target: selector, args: [] }, async () => {
|
|
1384
|
+
await driver().click(selector, index);
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
async function doubleClick() {
|
|
1388
|
+
return trackStep("action", { type: "action", key: "doubleClick", target: selector, args: [] }, async () => {
|
|
1389
|
+
await driver().doubleClick(selector, index);
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
async function rightClick() {
|
|
1393
|
+
return trackStep("action", { type: "action", key: "rightClick", target: selector, args: [] }, async () => {
|
|
1394
|
+
await driver().rightClick(selector, index);
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
async function setValue(value) {
|
|
1398
|
+
return trackStep("action", { type: "action", key: "type", target: selector, args: [value] }, async () => {
|
|
1399
|
+
await driver().setValue(selector, value, index);
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
async function type(value, options) {
|
|
1403
|
+
if (options?.clearFirst) {
|
|
1404
|
+
await clearValue();
|
|
1405
|
+
}
|
|
1406
|
+
await setValue(value);
|
|
1407
|
+
}
|
|
1408
|
+
async function clearValue() {
|
|
1409
|
+
return trackStep("action", { type: "action", key: "clear", target: selector, args: [] }, async () => {
|
|
1410
|
+
await driver().clearValue(selector, index);
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
async function clear() {
|
|
1414
|
+
await clearValue();
|
|
1415
|
+
}
|
|
1416
|
+
async function select(option) {
|
|
1417
|
+
return trackStep("action", { type: "action", key: "select", target: selector, args: [option] }, async () => {
|
|
1418
|
+
await driver().select(selector, option, index);
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
async function selectByVisibleText(text) {
|
|
1422
|
+
await select(text);
|
|
1423
|
+
}
|
|
1424
|
+
async function hover() {
|
|
1425
|
+
return trackStep("action", { type: "action", key: "hover", target: selector, args: [] }, async () => {
|
|
1426
|
+
await driver().hover(selector, index);
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
async function focus() {
|
|
1430
|
+
return trackStep("action", { type: "action", key: "focus", target: selector, args: [] }, async () => {
|
|
1431
|
+
await driver().focus(selector, index);
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
async function blur() {
|
|
1435
|
+
return trackStep("action", { type: "action", key: "blur", target: selector, args: [] }, async () => {
|
|
1436
|
+
await driver().blur(selector, index);
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
async function waitForElementRemoved(timeoutMs) {
|
|
1440
|
+
return trackStep(
|
|
1441
|
+
"action",
|
|
1442
|
+
{ type: "action", key: "waitForElementRemoved", target: selector, args: [timeoutMs ?? 5e3] },
|
|
1443
|
+
async () => {
|
|
1444
|
+
const ok = await pollCondition(async () => !await driver().isExisting(selector, index), timeoutMs ?? 5e3);
|
|
1445
|
+
if (!ok) throw new Error("Element still exists after timeout: " + JSON.stringify(selectorText2(selector)));
|
|
1446
|
+
}
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
async function waitForUndisplayed(opts) {
|
|
1450
|
+
return trackStep(
|
|
1451
|
+
"action",
|
|
1452
|
+
{ type: "action", key: "waitForUndisplayed", target: selector, args: [opts?.timeout ?? 5e3] },
|
|
1453
|
+
async () => {
|
|
1454
|
+
const ok = await pollCondition(
|
|
1455
|
+
async () => !await driver().isDisplayed(selector, index),
|
|
1456
|
+
opts?.timeout ?? 5e3
|
|
1457
|
+
);
|
|
1458
|
+
if (!ok) throw new Error("Element still displayed after timeout: " + JSON.stringify(selectorText2(selector)));
|
|
1459
|
+
}
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
async function waitForClickable(opts) {
|
|
1463
|
+
return trackStep(
|
|
1464
|
+
"action",
|
|
1465
|
+
{ type: "action", key: "waitForClickable", target: selector, args: [opts?.timeout ?? 5e3] },
|
|
1466
|
+
async () => {
|
|
1467
|
+
return await pollCondition(async () => await driver().isClickable(selector, index), opts?.timeout ?? 5e3);
|
|
1468
|
+
}
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
async function getBounds() {
|
|
1472
|
+
return trackStep("action", { type: "action", key: "getBounds", target: selector, args: [] }, async () => {
|
|
1473
|
+
return await driver().getBounds(selector, index);
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
function indexedBase() {
|
|
1477
|
+
const base = typeof selector === "string" ? selector : selector.selector;
|
|
1478
|
+
const baseIndex = typeof selector === "string" ? index : selector.index;
|
|
1479
|
+
if (baseIndex === null || baseIndex === void 0) return base;
|
|
1480
|
+
return `${base}::nth(${baseIndex})`;
|
|
1481
|
+
}
|
|
1482
|
+
function parent() {
|
|
1483
|
+
return createElementProxy(indexedBase() + "::parent", null);
|
|
1484
|
+
}
|
|
1485
|
+
async function closest(targetSelector) {
|
|
1486
|
+
return trackStep(
|
|
1487
|
+
"action",
|
|
1488
|
+
{ type: "action", key: "closest", target: selector, args: [targetSelector] },
|
|
1489
|
+
async () => {
|
|
1490
|
+
const exists = await driver().closest(selector, targetSelector, index);
|
|
1491
|
+
if (!exists) return null;
|
|
1492
|
+
return createElementProxy(`${indexedBase()}::closest(${targetSelector})`, null);
|
|
1493
|
+
}
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
function children(targetSelector) {
|
|
1497
|
+
return createCollectionProxy(`${indexedBase()}::children(${targetSelector ?? ""})`);
|
|
1498
|
+
}
|
|
1499
|
+
function siblings(targetSelector) {
|
|
1500
|
+
return createCollectionProxy(`${indexedBase()}::siblings(${targetSelector ?? ""})`);
|
|
1501
|
+
}
|
|
1502
|
+
function next(targetSelector) {
|
|
1503
|
+
return createElementProxy(`${indexedBase()}::next(${targetSelector ?? ""})`, null);
|
|
1504
|
+
}
|
|
1505
|
+
async function moveTo() {
|
|
1506
|
+
await hover();
|
|
1507
|
+
}
|
|
1508
|
+
async function dragDrop(target) {
|
|
1509
|
+
const destSelector = typeof target === "string" ? target : target?.selector || String(target);
|
|
1510
|
+
return trackStep(
|
|
1511
|
+
"action",
|
|
1512
|
+
{ type: "action", key: "dragDrop", target: selector, args: [destSelector] },
|
|
1513
|
+
async () => {
|
|
1514
|
+
await driver().dragDrop(selector, destSelector, index);
|
|
1515
|
+
}
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
async function dragAndDrop(target) {
|
|
1519
|
+
await dragDrop(target);
|
|
1520
|
+
}
|
|
1521
|
+
async function longPress(options) {
|
|
1522
|
+
const duration = typeof options === "number" ? options : options?.duration || 1e3;
|
|
1523
|
+
return trackStep("action", { type: "action", key: "longPress", target: selector, args: [duration] }, async () => {
|
|
1524
|
+
await driver().longPress(selector, duration, index);
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
async function scrollIntoView() {
|
|
1528
|
+
return trackStep("action", { type: "action", key: "scrollIntoView", target: selector, args: [] }, async () => {
|
|
1529
|
+
await driver().scrollIntoView(selector, index);
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
async function upload(files) {
|
|
1533
|
+
return trackStep("action", { type: "action", key: "upload", target: selector, args: [files] }, async () => {
|
|
1534
|
+
await driver().upload(selector, files, index);
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
async function uploadFile(files) {
|
|
1538
|
+
await upload(files);
|
|
1539
|
+
}
|
|
1540
|
+
return {
|
|
1541
|
+
click,
|
|
1542
|
+
doubleClick,
|
|
1543
|
+
rightClick,
|
|
1544
|
+
setValue,
|
|
1545
|
+
type,
|
|
1546
|
+
clearValue,
|
|
1547
|
+
clear,
|
|
1548
|
+
select,
|
|
1549
|
+
selectByVisibleText,
|
|
1550
|
+
hover,
|
|
1551
|
+
focus,
|
|
1552
|
+
blur,
|
|
1553
|
+
waitForElementRemoved,
|
|
1554
|
+
waitForUndisplayed,
|
|
1555
|
+
waitForClickable,
|
|
1556
|
+
getBounds,
|
|
1557
|
+
parent,
|
|
1558
|
+
closest,
|
|
1559
|
+
children,
|
|
1560
|
+
siblings,
|
|
1561
|
+
next,
|
|
1562
|
+
moveTo,
|
|
1563
|
+
dragDrop,
|
|
1564
|
+
dragAndDrop,
|
|
1565
|
+
longPress,
|
|
1566
|
+
scrollIntoView,
|
|
1567
|
+
upload,
|
|
1568
|
+
uploadFile
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
var init_element_actions = __esm({
|
|
1572
|
+
"../../core/worker-runtime/src/dsl/element_actions.ts"() {
|
|
1573
|
+
"use strict";
|
|
1574
|
+
init_driver_context();
|
|
1575
|
+
init_reporter();
|
|
1576
|
+
init_element_proxy();
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
// ../../core/worker-runtime/src/dsl/keyboard.ts
|
|
1581
|
+
var init_keyboard = __esm({
|
|
1582
|
+
"../../core/worker-runtime/src/dsl/keyboard.ts"() {
|
|
1583
|
+
"use strict";
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
// ../../core/worker-runtime/src/dsl/mock_intercept.ts
|
|
1588
|
+
function matchesUrlPattern(pattern, url) {
|
|
1589
|
+
if (!pattern || pattern === "*" || pattern === "**" || pattern === "ALL") {
|
|
1590
|
+
return true;
|
|
1591
|
+
}
|
|
1592
|
+
if (pattern.startsWith("/") && pattern.endsWith("/")) {
|
|
1593
|
+
try {
|
|
1594
|
+
const regex = new RegExp(pattern.slice(1, -1));
|
|
1595
|
+
return regex.test(url);
|
|
1596
|
+
} catch {
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (pattern.includes("*")) {
|
|
1600
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1601
|
+
try {
|
|
1602
|
+
const regex = new RegExp(escaped, "i");
|
|
1603
|
+
return regex.test(url);
|
|
1604
|
+
} catch {
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return url.toLowerCase().includes(pattern.toLowerCase());
|
|
1608
|
+
}
|
|
1609
|
+
function createMockHandle(rule) {
|
|
1610
|
+
const handle = {
|
|
1611
|
+
respondWith: async (newFixture, newOptions = {}) => {
|
|
1612
|
+
const newObj = newFixture;
|
|
1613
|
+
rule.response = newObj && newObj.body !== void 0 ? newObj.body : newFixture;
|
|
1614
|
+
if (newOptions.statusCode || newObj && newObj.statusCode) {
|
|
1615
|
+
rule.statusCode = newOptions.statusCode || newObj?.statusCode || 200;
|
|
1616
|
+
}
|
|
1617
|
+
if (newOptions.headers || newObj && newObj.headers) {
|
|
1618
|
+
rule.headers = newOptions.headers || newObj?.headers;
|
|
1619
|
+
}
|
|
1620
|
+
if (newOptions.delayMs !== void 0 || newObj && newObj.delayMs !== void 0) {
|
|
1621
|
+
rule.delayMs = newOptions.delayMs ?? newObj?.delayMs;
|
|
1622
|
+
}
|
|
1623
|
+
rule.aborted = false;
|
|
1624
|
+
},
|
|
1625
|
+
respondOnce: async (newFixture, newOptions = {}) => {
|
|
1626
|
+
const newObj = newFixture;
|
|
1627
|
+
const resp = newObj && newObj.body !== void 0 ? newObj.body : newFixture;
|
|
1628
|
+
const statusCode = newOptions.statusCode || newObj && newObj.statusCode || rule.statusCode || 200;
|
|
1629
|
+
const headers = newOptions.headers || newObj && newObj.headers || rule.headers;
|
|
1630
|
+
const delayMs = newOptions.delayMs ?? (newObj && newObj.delayMs) ?? rule.delayMs;
|
|
1631
|
+
rule.respondOnceQueue.push({ response: resp, statusCode, headers, delayMs });
|
|
1632
|
+
rule.aborted = false;
|
|
1633
|
+
},
|
|
1634
|
+
abort: async (errorCode = "Failed") => {
|
|
1635
|
+
rule.aborted = true;
|
|
1636
|
+
rule.abortReason = errorCode;
|
|
1637
|
+
},
|
|
1638
|
+
waitForCall: async (options = {}) => {
|
|
1639
|
+
const targetCount = options.count ?? 1;
|
|
1640
|
+
const timeoutMs = options.timeout ?? 5e3;
|
|
1641
|
+
if (rule.calls.length >= targetCount) {
|
|
1642
|
+
return rule.calls[targetCount - 1];
|
|
1643
|
+
}
|
|
1644
|
+
return new Promise((resolve, reject) => {
|
|
1645
|
+
const timer = setTimeout(() => {
|
|
1646
|
+
const idx = rule.waitResolvers.findIndex((w) => w.resolve === resolve);
|
|
1647
|
+
if (idx !== -1) rule.waitResolvers.splice(idx, 1);
|
|
1648
|
+
reject(
|
|
1649
|
+
new Error(
|
|
1650
|
+
`Spectra.intercept waitForCall timed out after ${timeoutMs}ms (expected at least ${targetCount} call(s), received ${rule.calls.length})`
|
|
1651
|
+
)
|
|
1652
|
+
);
|
|
1653
|
+
}, timeoutMs);
|
|
1654
|
+
rule.waitResolvers.push({
|
|
1655
|
+
count: targetCount,
|
|
1656
|
+
resolve: (req) => {
|
|
1657
|
+
clearTimeout(timer);
|
|
1658
|
+
resolve(req);
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
const getCount = () => rule.callCount;
|
|
1665
|
+
handle.callCount = Object.assign(getCount, {
|
|
1666
|
+
valueOf: () => rule.callCount,
|
|
1667
|
+
[Symbol.toPrimitive]: () => rule.callCount
|
|
1668
|
+
});
|
|
1669
|
+
Object.defineProperty(handle, "calls", {
|
|
1670
|
+
get: () => rule.calls,
|
|
1671
|
+
enumerable: true
|
|
1672
|
+
});
|
|
1673
|
+
return handle;
|
|
1674
|
+
}
|
|
1675
|
+
function parseMockRule(patternOrOptions, maybeMethodOrHandler, maybeFixture, maybeOptions) {
|
|
1676
|
+
let pattern;
|
|
1677
|
+
let method;
|
|
1678
|
+
let response;
|
|
1679
|
+
let statusCode;
|
|
1680
|
+
let headers;
|
|
1681
|
+
let delayMs;
|
|
1682
|
+
if (typeof patternOrOptions === "object" && patternOrOptions !== null) {
|
|
1683
|
+
pattern = patternOrOptions.url || patternOrOptions.pattern || "*";
|
|
1684
|
+
method = (patternOrOptions.method || "GET").toUpperCase();
|
|
1685
|
+
response = patternOrOptions.response !== void 0 ? patternOrOptions.response : typeof maybeMethodOrHandler === "object" && maybeMethodOrHandler !== null ? maybeMethodOrHandler : maybeFixture;
|
|
1686
|
+
statusCode = patternOrOptions.statusCode || maybeOptions && maybeOptions.statusCode || 200;
|
|
1687
|
+
headers = patternOrOptions.headers || maybeOptions && maybeOptions.headers;
|
|
1688
|
+
delayMs = patternOrOptions.delayMs ?? (maybeOptions && maybeOptions.delayMs);
|
|
1689
|
+
} else {
|
|
1690
|
+
pattern = String(patternOrOptions || "*");
|
|
1691
|
+
if (typeof maybeMethodOrHandler === "string" && ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"].includes(maybeMethodOrHandler.toUpperCase())) {
|
|
1692
|
+
method = maybeMethodOrHandler.toUpperCase();
|
|
1693
|
+
const fixtureObj = maybeFixture;
|
|
1694
|
+
response = fixtureObj && fixtureObj.body !== void 0 ? fixtureObj.body : maybeFixture;
|
|
1695
|
+
statusCode = maybeOptions && maybeOptions.statusCode || fixtureObj && fixtureObj.statusCode || 200;
|
|
1696
|
+
headers = maybeOptions && maybeOptions.headers || fixtureObj && fixtureObj.headers;
|
|
1697
|
+
delayMs = (maybeOptions && maybeOptions.delayMs) ?? (fixtureObj && fixtureObj.delayMs);
|
|
1698
|
+
} else {
|
|
1699
|
+
method = "GET";
|
|
1700
|
+
const fixtureObj = maybeMethodOrHandler;
|
|
1701
|
+
response = fixtureObj && fixtureObj.body !== void 0 ? fixtureObj.body : maybeMethodOrHandler;
|
|
1702
|
+
statusCode = maybeFixture && maybeFixture.statusCode || fixtureObj && fixtureObj.statusCode || 200;
|
|
1703
|
+
headers = maybeFixture && maybeFixture.headers || fixtureObj && fixtureObj.headers;
|
|
1704
|
+
delayMs = (maybeFixture && maybeFixture.delayMs) ?? (fixtureObj && fixtureObj.delayMs);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
return {
|
|
1708
|
+
pattern,
|
|
1709
|
+
method,
|
|
1710
|
+
response,
|
|
1711
|
+
statusCode,
|
|
1712
|
+
headers,
|
|
1713
|
+
delayMs,
|
|
1714
|
+
callCount: 0,
|
|
1715
|
+
respondOnceQueue: [],
|
|
1716
|
+
calls: [],
|
|
1717
|
+
waitResolvers: []
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
function findMatchingRule(rules, url, method) {
|
|
1721
|
+
for (const rule of rules) {
|
|
1722
|
+
if (matchesUrlPattern(rule.pattern, url)) {
|
|
1723
|
+
if (!rule.method || rule.method === "ALL" || rule.method.toUpperCase() === method.toUpperCase()) {
|
|
1724
|
+
return rule;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return null;
|
|
1729
|
+
}
|
|
1730
|
+
function recordMockCall(rule, request) {
|
|
1731
|
+
rule.calls.push(request);
|
|
1732
|
+
rule.callCount++;
|
|
1733
|
+
for (let i = rule.waitResolvers.length - 1; i >= 0; i--) {
|
|
1734
|
+
const w = rule.waitResolvers[i];
|
|
1735
|
+
if (rule.callCount >= w.count) {
|
|
1736
|
+
rule.waitResolvers.splice(i, 1);
|
|
1737
|
+
w.resolve(request);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
return request;
|
|
1741
|
+
}
|
|
1742
|
+
function consumeMockResponse(rule) {
|
|
1743
|
+
if (rule.respondOnceQueue.length > 0) {
|
|
1744
|
+
const queued = rule.respondOnceQueue.shift();
|
|
1745
|
+
return {
|
|
1746
|
+
body: queued.response,
|
|
1747
|
+
statusCode: queued.statusCode || rule.statusCode || 200,
|
|
1748
|
+
headers: { ...rule.headers, ...queued.headers },
|
|
1749
|
+
delayMs: queued.delayMs ?? rule.delayMs
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
return {
|
|
1753
|
+
body: rule.response,
|
|
1754
|
+
statusCode: rule.statusCode || 200,
|
|
1755
|
+
headers: { ...rule.headers },
|
|
1756
|
+
delayMs: rule.delayMs
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
function mergeMockResponseHeaders(custom) {
|
|
1760
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1761
|
+
for (const [k, v] of Object.entries(DEFAULT_MOCK_RESPONSE_HEADERS)) {
|
|
1762
|
+
merged.set(k.toLowerCase(), { name: k, value: v });
|
|
1763
|
+
}
|
|
1764
|
+
for (const [k, v] of Object.entries(custom || {})) {
|
|
1765
|
+
merged.set(k.toLowerCase(), { name: k, value: String(v) });
|
|
1766
|
+
}
|
|
1767
|
+
return [...merged.values()];
|
|
1768
|
+
}
|
|
1769
|
+
function mockResponseBodyText(body) {
|
|
1770
|
+
if (typeof body === "string") return body;
|
|
1771
|
+
if (typeof body === "object" && body !== null) return JSON.stringify(body);
|
|
1772
|
+
return String(body ?? "");
|
|
1773
|
+
}
|
|
1774
|
+
var DEFAULT_MOCK_RESPONSE_HEADERS;
|
|
1775
|
+
var init_mock_intercept = __esm({
|
|
1776
|
+
"../../core/worker-runtime/src/dsl/mock_intercept.ts"() {
|
|
1777
|
+
"use strict";
|
|
1778
|
+
DEFAULT_MOCK_RESPONSE_HEADERS = {
|
|
1779
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1780
|
+
"Access-Control-Allow-Origin": "*",
|
|
1781
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
1782
|
+
"Access-Control-Allow-Headers": "*"
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
// ../../core/worker-runtime/src/dsl/selector_resolver.ts
|
|
1788
|
+
function parseElementSentinel(selector) {
|
|
1789
|
+
const nth = /^(.*)::nth\((-?\d+)\)$/.exec(selector);
|
|
1790
|
+
if (nth) return { kind: "nth", base: nth[1], arg: nth[2] };
|
|
1791
|
+
const closest = /^(.*)::closest\((.+)\)$/.exec(selector);
|
|
1792
|
+
if (closest) return { kind: "closest", base: closest[1], arg: closest[2] };
|
|
1793
|
+
const children = /^(.*)::children\((.*)\)$/.exec(selector);
|
|
1794
|
+
if (children) return { kind: "children", base: children[1], arg: children[2].trim() || null };
|
|
1795
|
+
const bareChildren = /^(.*)::children$/.exec(selector);
|
|
1796
|
+
if (bareChildren) return { kind: "children", base: bareChildren[1], arg: null };
|
|
1797
|
+
const siblings = /^(.*)::siblings\((.*)\)$/.exec(selector);
|
|
1798
|
+
if (siblings) return { kind: "siblings", base: siblings[1], arg: siblings[2].trim() || null };
|
|
1799
|
+
const bareSiblings = /^(.*)::siblings$/.exec(selector);
|
|
1800
|
+
if (bareSiblings) return { kind: "siblings", base: bareSiblings[1], arg: null };
|
|
1801
|
+
const next = /^(.*)::next\((.*)\)$/.exec(selector);
|
|
1802
|
+
if (next) return { kind: "next", base: next[1], arg: next[2].trim() || null };
|
|
1803
|
+
const bareNext = /^(.*)::next$/.exec(selector);
|
|
1804
|
+
if (bareNext) return { kind: "next", base: bareNext[1], arg: null };
|
|
1805
|
+
const parent = /^(.*)::parent$/.exec(selector);
|
|
1806
|
+
if (parent) return { kind: "parent", base: parent[1] };
|
|
1807
|
+
return null;
|
|
1808
|
+
}
|
|
1809
|
+
function parseA11ySelector(selector) {
|
|
1810
|
+
const trimmed = selector.trim();
|
|
1811
|
+
if (!trimmed.startsWith("~")) return null;
|
|
1812
|
+
const withoutPrefix = trimmed.slice(1);
|
|
1813
|
+
const bracketIndex = withoutPrefix.indexOf("[");
|
|
1814
|
+
if (bracketIndex !== -1) {
|
|
1815
|
+
return {
|
|
1816
|
+
name: withoutPrefix.slice(0, bracketIndex),
|
|
1817
|
+
chainSuffix: withoutPrefix.slice(bracketIndex)
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
return { name: withoutPrefix, chainSuffix: "" };
|
|
1821
|
+
}
|
|
1822
|
+
var init_selector_resolver = __esm({
|
|
1823
|
+
"../../core/worker-runtime/src/dsl/selector_resolver.ts"() {
|
|
1824
|
+
"use strict";
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
// ../../core/worker-runtime/src/dsl/spectra.ts
|
|
1829
|
+
function buildSpectra() {
|
|
1830
|
+
const driver = () => getActiveDriver();
|
|
1831
|
+
const spectra2 = {
|
|
1832
|
+
get: (target) => resolveTargetProxy(target),
|
|
1833
|
+
getAll: (selector) => createCollectionProxy(selector),
|
|
1834
|
+
navigate: (url) => driver().navigate(url),
|
|
1835
|
+
back: () => driver().back(),
|
|
1836
|
+
forward: () => driver().forward(),
|
|
1837
|
+
refresh: () => driver().refresh(),
|
|
1838
|
+
setViewport: (width, height) => driver().setViewport(width, height),
|
|
1839
|
+
wait: (ms) => driver().pause(ms),
|
|
1840
|
+
click: async (target, _textOrOptions) => {
|
|
1841
|
+
const el = resolveTargetProxy(target);
|
|
1842
|
+
await el.click();
|
|
1843
|
+
},
|
|
1844
|
+
clickAt: async (x, y) => {
|
|
1845
|
+
await driver().clickAt(x, y);
|
|
1846
|
+
},
|
|
1847
|
+
doubleClick: async (target) => {
|
|
1848
|
+
const el = resolveTargetProxy(target);
|
|
1849
|
+
await el.doubleClick();
|
|
1850
|
+
},
|
|
1851
|
+
rightClick: async (target) => {
|
|
1852
|
+
const el = resolveTargetProxy(target);
|
|
1853
|
+
await el.rightClick();
|
|
1854
|
+
},
|
|
1855
|
+
type: async (target, text, options) => {
|
|
1856
|
+
const el = resolveTargetProxy(target);
|
|
1857
|
+
await el.type(text, options);
|
|
1858
|
+
},
|
|
1859
|
+
clear: async (target) => {
|
|
1860
|
+
const el = resolveTargetProxy(target);
|
|
1861
|
+
await el.clear();
|
|
1862
|
+
},
|
|
1863
|
+
select: async (target, option) => {
|
|
1864
|
+
const el = resolveTargetProxy(target);
|
|
1865
|
+
await el.select(option);
|
|
1866
|
+
},
|
|
1867
|
+
hover: async (target) => {
|
|
1868
|
+
const el = resolveTargetProxy(target);
|
|
1869
|
+
await el.hover();
|
|
1870
|
+
},
|
|
1871
|
+
focus: async (target) => {
|
|
1872
|
+
const el = resolveTargetProxy(target);
|
|
1873
|
+
await el.focus();
|
|
1874
|
+
},
|
|
1875
|
+
blur: async (target) => {
|
|
1876
|
+
const el = resolveTargetProxy(target);
|
|
1877
|
+
await el.blur();
|
|
1878
|
+
},
|
|
1879
|
+
dragDrop: async (source, destination) => {
|
|
1880
|
+
const srcEl = resolveTargetProxy(source);
|
|
1881
|
+
await srcEl.dragDrop(destination);
|
|
1882
|
+
},
|
|
1883
|
+
scrollIntoView: async (target) => {
|
|
1884
|
+
const el = resolveTargetProxy(target);
|
|
1885
|
+
await el.scrollIntoView();
|
|
1886
|
+
},
|
|
1887
|
+
scroll: async (options) => {
|
|
1888
|
+
await driver().scroll(options);
|
|
1889
|
+
},
|
|
1890
|
+
swipe: async (options) => {
|
|
1891
|
+
await driver().swipe(options);
|
|
1892
|
+
},
|
|
1893
|
+
longPress: async (target, options) => {
|
|
1894
|
+
const el = resolveTargetProxy(target);
|
|
1895
|
+
await el.longPress(options);
|
|
1896
|
+
},
|
|
1897
|
+
pressKey: async (key) => {
|
|
1898
|
+
await driver().pressKey(key);
|
|
1899
|
+
},
|
|
1900
|
+
grantPermission: async (name) => {
|
|
1901
|
+
await driver().grantPermission(name);
|
|
1902
|
+
},
|
|
1903
|
+
waitForElement: async (target, timeoutMs) => {
|
|
1904
|
+
const el = resolveTargetProxy(target);
|
|
1905
|
+
await el.waitForElement(timeoutMs);
|
|
1906
|
+
},
|
|
1907
|
+
intercept: async (patternOrOptions, maybeMethodOrHandler, maybeFixture, maybeOptions) => {
|
|
1908
|
+
return await driver().intercept(patternOrOptions, maybeMethodOrHandler, maybeFixture, maybeOptions);
|
|
1909
|
+
},
|
|
1910
|
+
clearMocks: () => {
|
|
1911
|
+
driver().clearMocks();
|
|
1912
|
+
},
|
|
1913
|
+
get browser() {
|
|
1914
|
+
return driver();
|
|
1915
|
+
},
|
|
1916
|
+
get env() {
|
|
1917
|
+
return getWorkerConfig().environmentVariables || {};
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
spectra2.registerCommand = (name, command) => {
|
|
1921
|
+
if (typeof command !== "function") {
|
|
1922
|
+
throw new Error(`[TestSpectra] Custom command "${name}" must be a function.`);
|
|
1923
|
+
}
|
|
1924
|
+
if (!VALID_COMMAND_NAME.test(name)) {
|
|
1925
|
+
throw new Error(`[TestSpectra] Invalid custom command name "${name}"; must be a valid identifier.`);
|
|
1926
|
+
}
|
|
1927
|
+
if (RESERVED_COMMAND_NAMES.has(name) || name in spectra2) {
|
|
1928
|
+
throw new Error(`[TestSpectra] Custom command "${name}" collides with a built-in Spectra method.`);
|
|
1929
|
+
}
|
|
1930
|
+
spectra2[name] = command;
|
|
1931
|
+
};
|
|
1932
|
+
spectra2.registerCommands = (commands) => {
|
|
1933
|
+
for (const [name, command] of Object.entries(commands)) {
|
|
1934
|
+
spectra2.registerCommand(name, command);
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
return spectra2;
|
|
1938
|
+
}
|
|
1939
|
+
var RESERVED_COMMAND_NAMES, VALID_COMMAND_NAME;
|
|
1940
|
+
var init_spectra = __esm({
|
|
1941
|
+
"../../core/worker-runtime/src/dsl/spectra.ts"() {
|
|
1942
|
+
"use strict";
|
|
1943
|
+
init_driver_context();
|
|
1944
|
+
init_config();
|
|
1945
|
+
init_element_proxy();
|
|
1946
|
+
RESERVED_COMMAND_NAMES = /* @__PURE__ */ new Set(["mount", "unmount", "registerCommand", "registerCommands"]);
|
|
1947
|
+
VALID_COMMAND_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
// ../../core/worker-runtime/src/dsl/index.ts
|
|
1952
|
+
var init_dsl = __esm({
|
|
1953
|
+
"../../core/worker-runtime/src/dsl/index.ts"() {
|
|
1954
|
+
"use strict";
|
|
1955
|
+
init_assertions();
|
|
1956
|
+
init_element_actions();
|
|
1957
|
+
init_element_proxy();
|
|
1958
|
+
init_element_state();
|
|
1959
|
+
init_keyboard();
|
|
1960
|
+
init_mock_intercept();
|
|
1961
|
+
init_selector_resolver();
|
|
1962
|
+
init_spectra();
|
|
1963
|
+
init_driver_context();
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
// src/bridge/network_patch.ts
|
|
1968
|
+
function sleep(ms) {
|
|
1969
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1970
|
+
}
|
|
1971
|
+
function toAbsoluteUrl(input) {
|
|
1972
|
+
try {
|
|
1973
|
+
if (typeof input === "string") {
|
|
1974
|
+
if (input.startsWith("http://") || input.startsWith("https://") || input.startsWith("file://")) {
|
|
1975
|
+
return input;
|
|
1976
|
+
}
|
|
1977
|
+
return new URL(input, "http://localhost/").href;
|
|
1978
|
+
}
|
|
1979
|
+
if (input instanceof URL) return input.href;
|
|
1980
|
+
if (typeof Request !== "undefined" && input instanceof Request) return input.url;
|
|
1981
|
+
} catch {
|
|
1982
|
+
}
|
|
1983
|
+
return String(input);
|
|
1984
|
+
}
|
|
1985
|
+
function readMethod(input, init) {
|
|
1986
|
+
if (init?.method) return String(init.method).toUpperCase();
|
|
1987
|
+
try {
|
|
1988
|
+
if (typeof Request !== "undefined" && input instanceof Request && input.method) {
|
|
1989
|
+
return String(input.method).toUpperCase();
|
|
1990
|
+
}
|
|
1991
|
+
} catch {
|
|
1992
|
+
}
|
|
1993
|
+
return "GET";
|
|
1994
|
+
}
|
|
1995
|
+
function readPostData(input, init) {
|
|
1996
|
+
const body = init?.body;
|
|
1997
|
+
if (typeof body === "string") return body;
|
|
1998
|
+
try {
|
|
1999
|
+
if (typeof Request !== "undefined" && input instanceof Request) return void 0;
|
|
2000
|
+
} catch {
|
|
2001
|
+
}
|
|
2002
|
+
return void 0;
|
|
2003
|
+
}
|
|
2004
|
+
function buildMockResponse(consumed) {
|
|
2005
|
+
const headerPairs = mergeMockResponseHeaders(consumed.headers);
|
|
2006
|
+
const headers = {};
|
|
2007
|
+
for (const h of headerPairs) headers[h.name] = h.value;
|
|
2008
|
+
return new Response(mockResponseBodyText(consumed.body), { status: consumed.statusCode, headers });
|
|
2009
|
+
}
|
|
2010
|
+
function getActiveRules() {
|
|
2011
|
+
const external = externalGetRules ? externalGetRules() : [];
|
|
2012
|
+
return [...internalMockRules, ...external];
|
|
2013
|
+
}
|
|
2014
|
+
async function patchedFetch(origFetch, input, init) {
|
|
2015
|
+
const url = toAbsoluteUrl(input);
|
|
2016
|
+
const method = readMethod(input, init);
|
|
2017
|
+
const rule = findMatchingRule(getActiveRules(), url, method);
|
|
2018
|
+
if (!rule) return origFetch(input, init);
|
|
2019
|
+
const intercepted = {
|
|
2020
|
+
url,
|
|
2021
|
+
method,
|
|
2022
|
+
headers: void 0,
|
|
2023
|
+
postData: readPostData(input, init),
|
|
2024
|
+
timestamp: Date.now()
|
|
2025
|
+
};
|
|
2026
|
+
recordMockCall(rule, intercepted);
|
|
2027
|
+
if (rule.aborted) {
|
|
2028
|
+
throw new TypeError(`Spectra.intercept() aborted ${method} ${url} (${rule.abortReason || "Failed"})`);
|
|
2029
|
+
}
|
|
2030
|
+
const consumed = consumeMockResponse(rule);
|
|
2031
|
+
if (consumed.delayMs && consumed.delayMs > 0) await sleep(consumed.delayMs);
|
|
2032
|
+
return buildMockResponse(consumed);
|
|
2033
|
+
}
|
|
2034
|
+
function patchXMLHttpRequest() {
|
|
2035
|
+
const OriginalXHR = globalThis.XMLHttpRequest;
|
|
2036
|
+
if (!OriginalXHR) return;
|
|
2037
|
+
function PatchedXHR() {
|
|
2038
|
+
const xhr = new OriginalXHR();
|
|
2039
|
+
let method = "GET";
|
|
2040
|
+
let url = "";
|
|
2041
|
+
const origOpen = OriginalXHR.prototype.open.bind(xhr);
|
|
2042
|
+
const origSend = OriginalXHR.prototype.send.bind(xhr);
|
|
2043
|
+
xhr.open = (m, u, ...rest) => {
|
|
2044
|
+
method = String(m).toUpperCase();
|
|
2045
|
+
url = toAbsoluteUrl(u);
|
|
2046
|
+
const [asyncFlag = true, username, password] = rest;
|
|
2047
|
+
return origOpen(m, u, asyncFlag, username, password);
|
|
2048
|
+
};
|
|
2049
|
+
xhr.send = (body) => {
|
|
2050
|
+
const rule = findMatchingRule(getActiveRules(), url, method);
|
|
2051
|
+
if (!rule) return origSend(body);
|
|
2052
|
+
const intercepted = {
|
|
2053
|
+
url,
|
|
2054
|
+
method,
|
|
2055
|
+
headers: void 0,
|
|
2056
|
+
postData: typeof body === "string" ? body : void 0,
|
|
2057
|
+
timestamp: Date.now()
|
|
2058
|
+
};
|
|
2059
|
+
recordMockCall(rule, intercepted);
|
|
2060
|
+
const fireError = () => {
|
|
2061
|
+
xhr.dispatchEvent?.(new Event("error"));
|
|
2062
|
+
};
|
|
2063
|
+
if (rule.aborted) {
|
|
2064
|
+
setTimeout(fireError, 0);
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
const consumed = consumeMockResponse(rule);
|
|
2068
|
+
const respond2 = () => {
|
|
2069
|
+
const bodyText = mockResponseBodyText(consumed.body);
|
|
2070
|
+
Object.defineProperties(xhr, {
|
|
2071
|
+
status: { value: consumed.statusCode, configurable: true },
|
|
2072
|
+
statusText: {
|
|
2073
|
+
value: consumed.statusCode === 200 ? "OK" : `${consumed.statusCode} Mocked`,
|
|
2074
|
+
configurable: true
|
|
2075
|
+
},
|
|
2076
|
+
responseText: { value: bodyText, configurable: true },
|
|
2077
|
+
response: { value: bodyText, configurable: true },
|
|
2078
|
+
readyState: { value: 4, configurable: true }
|
|
2079
|
+
});
|
|
2080
|
+
xhr.dispatchEvent?.(new Event("readystatechange"));
|
|
2081
|
+
xhr.dispatchEvent?.(new Event("load"));
|
|
2082
|
+
xhr.dispatchEvent?.(new Event("loadend"));
|
|
2083
|
+
};
|
|
2084
|
+
if (consumed.delayMs && consumed.delayMs > 0) setTimeout(respond2, consumed.delayMs);
|
|
2085
|
+
else setTimeout(respond2, 0);
|
|
2086
|
+
};
|
|
2087
|
+
return xhr;
|
|
2088
|
+
}
|
|
2089
|
+
PatchedXHR.prototype = OriginalXHR.prototype;
|
|
2090
|
+
globalThis.XMLHttpRequest = PatchedXHR;
|
|
2091
|
+
}
|
|
2092
|
+
function registerMockRule(rule) {
|
|
2093
|
+
internalMockRules.push(rule);
|
|
2094
|
+
}
|
|
2095
|
+
function clearMocks() {
|
|
2096
|
+
internalMockRules = [];
|
|
2097
|
+
}
|
|
2098
|
+
function ensureNetworkPatch(getRules) {
|
|
2099
|
+
if (getRules) {
|
|
2100
|
+
externalGetRules = getRules;
|
|
2101
|
+
}
|
|
2102
|
+
if (typeof globalThis.fetch === "function") {
|
|
2103
|
+
if (globalThis.fetch !== currentPatchedFetch) {
|
|
2104
|
+
originalFetch = globalThis.fetch;
|
|
2105
|
+
const baseFetch = originalFetch;
|
|
2106
|
+
currentPatchedFetch = (input, init) => patchedFetch(baseFetch, input, init);
|
|
2107
|
+
globalThis.fetch = currentPatchedFetch;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
if (!xhrPatched && typeof globalThis.XMLHttpRequest === "function") {
|
|
2111
|
+
patchXMLHttpRequest();
|
|
2112
|
+
xhrPatched = true;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
var originalFetch, currentPatchedFetch, xhrPatched, internalMockRules, externalGetRules;
|
|
2116
|
+
var init_network_patch = __esm({
|
|
2117
|
+
"src/bridge/network_patch.ts"() {
|
|
2118
|
+
"use strict";
|
|
2119
|
+
init_dsl();
|
|
2120
|
+
originalFetch = null;
|
|
2121
|
+
currentPatchedFetch = null;
|
|
2122
|
+
xhrPatched = false;
|
|
2123
|
+
internalMockRules = [];
|
|
2124
|
+
externalGetRules = null;
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
// src/bridge/react_native_driver_bridge.ts
|
|
2129
|
+
function selectorText3(target) {
|
|
2130
|
+
return typeof target === "string" ? target : target.selector;
|
|
2131
|
+
}
|
|
2132
|
+
function pickFromList(list, index) {
|
|
2133
|
+
if (index === null || index === void 0) return list[0] ?? null;
|
|
2134
|
+
if (index >= 0) return list[index] ?? null;
|
|
2135
|
+
return list[list.length + index] ?? null;
|
|
2136
|
+
}
|
|
2137
|
+
function isTextComponentType(type) {
|
|
2138
|
+
if (typeof type === "string") {
|
|
2139
|
+
const lower = type.toLowerCase();
|
|
2140
|
+
return lower === "text" || lower === "rcttext" || lower === "rctvirtualtext" || lower.endsWith("text");
|
|
2141
|
+
}
|
|
2142
|
+
if (type && typeof type === "object") {
|
|
2143
|
+
return type.displayName === "Text" || type.name === "Text";
|
|
2144
|
+
}
|
|
2145
|
+
return false;
|
|
2146
|
+
}
|
|
2147
|
+
function nodeMatchesSelector(node, selector) {
|
|
2148
|
+
const trimmed = selector.trim();
|
|
2149
|
+
if (!trimmed) return true;
|
|
2150
|
+
if (trimmed.startsWith("~")) {
|
|
2151
|
+
const parsed = parseA11ySelector(trimmed);
|
|
2152
|
+
const label = parsed ? parsed.name : trimmed.slice(1).trim();
|
|
2153
|
+
const a11y = node.props.accessibilityLabel ?? node.props["aria-label"];
|
|
2154
|
+
return a11y != null && String(a11y).trim() === label;
|
|
2155
|
+
}
|
|
2156
|
+
if (trimmed.startsWith("#")) {
|
|
2157
|
+
const id = trimmed.slice(1).trim();
|
|
2158
|
+
return node.props.testID != null && String(node.props.testID).trim() === id;
|
|
2159
|
+
}
|
|
2160
|
+
if (isTextComponentType(node.type)) {
|
|
2161
|
+
const text = extractText(node);
|
|
2162
|
+
if (text === trimmed || text.includes(trimmed)) return true;
|
|
2163
|
+
}
|
|
2164
|
+
if (typeof node.props.children === "string" && (node.props.children === trimmed || node.props.children.includes(trimmed))) {
|
|
2165
|
+
return true;
|
|
2166
|
+
}
|
|
2167
|
+
if (typeof node.type === "string" && node.type.toLowerCase() === trimmed.toLowerCase()) return true;
|
|
2168
|
+
if (node.type?.displayName === trimmed || node.type?.name === trimmed) return true;
|
|
2169
|
+
return false;
|
|
2170
|
+
}
|
|
2171
|
+
function collectMatchingNodes(root, selector) {
|
|
2172
|
+
const results = [];
|
|
2173
|
+
function walk(curr) {
|
|
2174
|
+
if (nodeMatchesSelector(curr, selector)) {
|
|
2175
|
+
results.push(curr);
|
|
2176
|
+
}
|
|
2177
|
+
for (const child of curr.children) {
|
|
2178
|
+
walk(child);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
walk(root);
|
|
2182
|
+
return results;
|
|
2183
|
+
}
|
|
2184
|
+
function resolveSentinelList(raw, parent, root) {
|
|
2185
|
+
const sentinel = parseElementSentinel(raw);
|
|
2186
|
+
if (!sentinel) return void 0;
|
|
2187
|
+
if (sentinel.kind === "nth") {
|
|
2188
|
+
const baseNode = resolveScopedNode(sentinel.base, Number.parseInt(sentinel.arg, 10), root);
|
|
2189
|
+
return baseNode ? [baseNode] : [];
|
|
2190
|
+
}
|
|
2191
|
+
const baseTarget = parent ? { selector: sentinel.base, index: null, parent } : sentinel.base;
|
|
2192
|
+
const base = resolveScopedNode(baseTarget, null, root);
|
|
2193
|
+
if (!base) return [];
|
|
2194
|
+
if (sentinel.kind === "parent") {
|
|
2195
|
+
return base.parent ? [base.parent] : [];
|
|
2196
|
+
}
|
|
2197
|
+
if (sentinel.kind === "closest") {
|
|
2198
|
+
let curr = base.parent;
|
|
2199
|
+
while (curr) {
|
|
2200
|
+
if (nodeMatchesSelector(curr, sentinel.arg)) return [curr];
|
|
2201
|
+
curr = curr.parent;
|
|
2202
|
+
}
|
|
2203
|
+
return [];
|
|
2204
|
+
}
|
|
2205
|
+
if (sentinel.kind === "children") {
|
|
2206
|
+
return base.children.filter((child) => sentinel.arg ? nodeMatchesSelector(child, sentinel.arg) : true);
|
|
2207
|
+
}
|
|
2208
|
+
if (sentinel.kind === "siblings") {
|
|
2209
|
+
const parentNode = base.parent;
|
|
2210
|
+
if (!parentNode) return [];
|
|
2211
|
+
return parentNode.children.filter(
|
|
2212
|
+
(child) => child !== base && (sentinel.arg ? nodeMatchesSelector(child, sentinel.arg) : true)
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
if (sentinel.kind === "next") {
|
|
2216
|
+
const parentNode = base.parent;
|
|
2217
|
+
if (!parentNode) return [];
|
|
2218
|
+
const idx = parentNode.children.indexOf(base);
|
|
2219
|
+
if (idx === -1) return [];
|
|
2220
|
+
for (let i = idx + 1; i < parentNode.children.length; i++) {
|
|
2221
|
+
const sib = parentNode.children[i];
|
|
2222
|
+
if (!sentinel.arg || nodeMatchesSelector(sib, sentinel.arg)) {
|
|
2223
|
+
return [sib];
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
return [];
|
|
2227
|
+
}
|
|
2228
|
+
return [];
|
|
2229
|
+
}
|
|
2230
|
+
function resolveScopedNode(target, fallbackIndex, treeRoot) {
|
|
2231
|
+
const root = treeRoot ?? getMountedTree();
|
|
2232
|
+
if (!root) return null;
|
|
2233
|
+
if (typeof target === "string") {
|
|
2234
|
+
const sentinelList2 = resolveSentinelList(target, void 0, root);
|
|
2235
|
+
if (sentinelList2 !== void 0) return pickFromList(sentinelList2, fallbackIndex);
|
|
2236
|
+
const matches2 = collectMatchingNodes(root, target);
|
|
2237
|
+
return pickFromList(matches2, fallbackIndex);
|
|
2238
|
+
}
|
|
2239
|
+
const sentinelList = resolveSentinelList(target.selector, target.parent, root);
|
|
2240
|
+
if (sentinelList !== void 0) return pickFromList(sentinelList, target.index ?? fallbackIndex);
|
|
2241
|
+
const parentNode = target.parent ? resolveScopedNode(target.parent, target.parent.index, root) : root;
|
|
2242
|
+
if (!parentNode) return null;
|
|
2243
|
+
const matches = collectMatchingNodes(parentNode, target.selector);
|
|
2244
|
+
const idx = target.index ?? fallbackIndex;
|
|
2245
|
+
return pickFromList(matches, idx);
|
|
2246
|
+
}
|
|
2247
|
+
function isNodeVisible(node) {
|
|
2248
|
+
let curr = node;
|
|
2249
|
+
while (curr) {
|
|
2250
|
+
const style = flattenStyle(curr.props.style);
|
|
2251
|
+
if (style) {
|
|
2252
|
+
if (style.display === "none") return false;
|
|
2253
|
+
if (style.opacity === 0) return false;
|
|
2254
|
+
if (style.height === 0 && style.width === 0) return false;
|
|
2255
|
+
}
|
|
2256
|
+
curr = curr.parent;
|
|
2257
|
+
}
|
|
2258
|
+
return true;
|
|
2259
|
+
}
|
|
2260
|
+
function findAncestorProp(node, propName) {
|
|
2261
|
+
let curr = node.parent;
|
|
2262
|
+
while (curr) {
|
|
2263
|
+
if (typeof curr.props[propName] === "function") {
|
|
2264
|
+
return curr.props[propName];
|
|
2265
|
+
}
|
|
2266
|
+
curr = curr.parent;
|
|
2267
|
+
}
|
|
2268
|
+
return void 0;
|
|
2269
|
+
}
|
|
2270
|
+
function findChildProp(node, propName) {
|
|
2271
|
+
for (const child of node.children) {
|
|
2272
|
+
if (typeof child.props[propName] === "function") {
|
|
2273
|
+
return child.props[propName];
|
|
2274
|
+
}
|
|
2275
|
+
const found = findChildProp(child, propName);
|
|
2276
|
+
if (found) return found;
|
|
2277
|
+
}
|
|
2278
|
+
return void 0;
|
|
2279
|
+
}
|
|
2280
|
+
var ReactNativeDriverBridge;
|
|
2281
|
+
var init_react_native_driver_bridge = __esm({
|
|
2282
|
+
"src/bridge/react_native_driver_bridge.ts"() {
|
|
2283
|
+
"use strict";
|
|
2284
|
+
init_dsl();
|
|
2285
|
+
init_tree_registry();
|
|
2286
|
+
init_network_patch();
|
|
2287
|
+
ReactNativeDriverBridge = class {
|
|
2288
|
+
mockRules = [];
|
|
2289
|
+
consoleErrors = [];
|
|
2290
|
+
recordedNetwork = [];
|
|
2291
|
+
async waitForScopedNode(target, fallbackIndex) {
|
|
2292
|
+
const immediate = resolveScopedNode(target, fallbackIndex);
|
|
2293
|
+
if (immediate) return immediate;
|
|
2294
|
+
let found = null;
|
|
2295
|
+
const timeoutMs = globalThis.__TS_RUNTIME__?.getImplicitWaitMs?.() ?? 5e3;
|
|
2296
|
+
const ok = await globalThis.__TS_RUNTIME__?.pollCondition?.(async () => {
|
|
2297
|
+
found = resolveScopedNode(target, fallbackIndex);
|
|
2298
|
+
return found !== null;
|
|
2299
|
+
}, timeoutMs) ?? await (async () => {
|
|
2300
|
+
const start = Date.now();
|
|
2301
|
+
while (Date.now() - start < timeoutMs) {
|
|
2302
|
+
found = resolveScopedNode(target, fallbackIndex);
|
|
2303
|
+
if (found) return true;
|
|
2304
|
+
await this.pause(50);
|
|
2305
|
+
}
|
|
2306
|
+
return false;
|
|
2307
|
+
})();
|
|
2308
|
+
if (!ok || !found) {
|
|
2309
|
+
throw new Error("Element not found: " + selectorText3(target));
|
|
2310
|
+
}
|
|
2311
|
+
return found;
|
|
2312
|
+
}
|
|
2313
|
+
async connect() {
|
|
2314
|
+
}
|
|
2315
|
+
async close() {
|
|
2316
|
+
}
|
|
2317
|
+
async navigate(_targetUrl) {
|
|
2318
|
+
throw new Error(
|
|
2319
|
+
"Spectra.navigate() is not supported in component testing mode. Use Spectra.mount(<Component />) to render a component tree instead of navigating a URL."
|
|
2320
|
+
);
|
|
2321
|
+
}
|
|
2322
|
+
async back() {
|
|
2323
|
+
throw new Error("Spectra.back() is not supported in component testing mode. Use Spectra.mount(<Component />).");
|
|
2324
|
+
}
|
|
2325
|
+
async forward() {
|
|
2326
|
+
throw new Error("Spectra.forward() is not supported in component testing mode. Use Spectra.mount(<Component />).");
|
|
2327
|
+
}
|
|
2328
|
+
async refresh() {
|
|
2329
|
+
throw new Error("Spectra.refresh() is not supported in component testing mode. Use Spectra.mount(<Component />).");
|
|
2330
|
+
}
|
|
2331
|
+
async setViewport(_width, _height) {
|
|
2332
|
+
}
|
|
2333
|
+
async pause(ms) {
|
|
2334
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
2335
|
+
}
|
|
2336
|
+
// ─── Element Actions ─────────────────────────────────────────────────────
|
|
2337
|
+
async click(selector, index = null) {
|
|
2338
|
+
const node = await this.waitForScopedNode(selector, index);
|
|
2339
|
+
const onPress = node.props.onPress || node.props.onClick || findAncestorProp(node, "onPress") || findChildProp(node, "onPress");
|
|
2340
|
+
if (typeof onPress !== "function") {
|
|
2341
|
+
throw new Error(`Element "${selectorText3(selector)}" has no onPress handler.`);
|
|
2342
|
+
}
|
|
2343
|
+
await onPress({ nativeEvent: {} });
|
|
2344
|
+
await this.pause(20);
|
|
2345
|
+
}
|
|
2346
|
+
async doubleClick(selector, index = null) {
|
|
2347
|
+
await this.click(selector, index);
|
|
2348
|
+
await this.pause(50);
|
|
2349
|
+
await this.click(selector, index);
|
|
2350
|
+
}
|
|
2351
|
+
async rightClick(selector, index = null) {
|
|
2352
|
+
const node = await this.waitForScopedNode(selector, index);
|
|
2353
|
+
const onLongPress = node.props.onLongPress || findAncestorProp(node, "onLongPress") || node.props.onPress;
|
|
2354
|
+
if (typeof onLongPress === "function") {
|
|
2355
|
+
await onLongPress({ nativeEvent: {} });
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
async setValue(selector, value, index = null) {
|
|
2359
|
+
const node = await this.waitForScopedNode(selector, index);
|
|
2360
|
+
const strVal = String(value);
|
|
2361
|
+
const onChangeText = node.props.onChangeText || findAncestorProp(node, "onChangeText") || findChildProp(node, "onChangeText");
|
|
2362
|
+
if (typeof onChangeText === "function") {
|
|
2363
|
+
await onChangeText(strVal);
|
|
2364
|
+
} else if (typeof node.props.onChange === "function") {
|
|
2365
|
+
await node.props.onChange({ nativeEvent: { text: strVal } });
|
|
2366
|
+
} else {
|
|
2367
|
+
throw new Error(`Element "${selectorText3(selector)}" has no onChangeText handler.`);
|
|
2368
|
+
}
|
|
2369
|
+
await this.pause(20);
|
|
2370
|
+
}
|
|
2371
|
+
async clearValue(selector, index = null) {
|
|
2372
|
+
await this.setValue(selector, "", index);
|
|
2373
|
+
}
|
|
2374
|
+
async select(selector, option, index = null) {
|
|
2375
|
+
const node = await this.waitForScopedNode(selector, index);
|
|
2376
|
+
const onValueChange = node.props.onValueChange || node.props.onChangeText || node.props.onSelect;
|
|
2377
|
+
if (typeof onValueChange === "function") {
|
|
2378
|
+
await onValueChange(option);
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
await this.setValue(selector, option, index);
|
|
2382
|
+
}
|
|
2383
|
+
async hover(selector, index = null) {
|
|
2384
|
+
const node = await this.waitForScopedNode(selector, index);
|
|
2385
|
+
node.props.onHoverIn?.({ nativeEvent: {} });
|
|
2386
|
+
}
|
|
2387
|
+
async focus(selector, index = null) {
|
|
2388
|
+
const node = await this.waitForScopedNode(selector, index);
|
|
2389
|
+
node.props.onFocus?.({ nativeEvent: {} });
|
|
2390
|
+
}
|
|
2391
|
+
async blur(selector, index = null) {
|
|
2392
|
+
const node = await this.waitForScopedNode(selector, index);
|
|
2393
|
+
node.props.onBlur?.({ nativeEvent: {} });
|
|
2394
|
+
}
|
|
2395
|
+
async dragDrop(_sourceSelector, _targetSelector, _sourceIndex = null) {
|
|
2396
|
+
throw new Error("Spectra.dragDrop() is not supported for synthetic React Native component tests.");
|
|
2397
|
+
}
|
|
2398
|
+
async longPress(selector, duration, index = null) {
|
|
2399
|
+
const node = await this.waitForScopedNode(selector, index);
|
|
2400
|
+
const onLongPress = node.props.onLongPress || findAncestorProp(node, "onLongPress") || node.props.onPress;
|
|
2401
|
+
if (typeof onLongPress === "function") {
|
|
2402
|
+
await onLongPress({ nativeEvent: {} });
|
|
2403
|
+
}
|
|
2404
|
+
await this.pause(duration);
|
|
2405
|
+
}
|
|
2406
|
+
async scrollIntoView(_selector, _index = null) {
|
|
2407
|
+
}
|
|
2408
|
+
async upload(_selector, _files, _index = null) {
|
|
2409
|
+
throw new Error("upload() is not applicable to React Native components.");
|
|
2410
|
+
}
|
|
2411
|
+
// ─── Element State Inspection ────────────────────────────────────────────
|
|
2412
|
+
async getText(selector, index = null) {
|
|
2413
|
+
const node = resolveScopedNode(selector, index);
|
|
2414
|
+
if (!node) return "";
|
|
2415
|
+
return extractText(node);
|
|
2416
|
+
}
|
|
2417
|
+
async getValue(selector, index = null) {
|
|
2418
|
+
const node = resolveScopedNode(selector, index);
|
|
2419
|
+
if (!node) return "";
|
|
2420
|
+
return String(node.props.value ?? node.props.defaultValue ?? "");
|
|
2421
|
+
}
|
|
2422
|
+
async isDisplayed(selector, index = null) {
|
|
2423
|
+
const node = resolveScopedNode(selector, index);
|
|
2424
|
+
if (!node) return false;
|
|
2425
|
+
return isNodeVisible(node);
|
|
2426
|
+
}
|
|
2427
|
+
async waitForDisplayed(selector, opts, index = null) {
|
|
2428
|
+
const ok = await globalThis.__TS_RUNTIME__?.pollCondition?.(async () => {
|
|
2429
|
+
return this.isDisplayed(selector, index);
|
|
2430
|
+
}, opts?.timeout ?? 5e3) ?? await (async () => {
|
|
2431
|
+
const timeout = opts?.timeout ?? 5e3;
|
|
2432
|
+
const start = Date.now();
|
|
2433
|
+
while (Date.now() - start < timeout) {
|
|
2434
|
+
if (await this.isDisplayed(selector, index)) return true;
|
|
2435
|
+
await this.pause(50);
|
|
2436
|
+
}
|
|
2437
|
+
return false;
|
|
2438
|
+
})();
|
|
2439
|
+
if (!ok) {
|
|
2440
|
+
throw new Error("Element not displayed after timeout: " + selectorText3(selector));
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
async isExisting(selector, index = null) {
|
|
2444
|
+
return resolveScopedNode(selector, index) !== null;
|
|
2445
|
+
}
|
|
2446
|
+
async isEnabled(selector, index = null) {
|
|
2447
|
+
const node = resolveScopedNode(selector, index);
|
|
2448
|
+
if (!node) return false;
|
|
2449
|
+
return !node.props.disabled && node.props.accessibilityState?.disabled !== true;
|
|
2450
|
+
}
|
|
2451
|
+
async isSelected(selector, index = null) {
|
|
2452
|
+
const node = resolveScopedNode(selector, index);
|
|
2453
|
+
if (!node) return false;
|
|
2454
|
+
return node.props.accessibilityState?.selected === true || node.props.selected === true || node.props.accessibilityState?.checked === true || node.props.checked === true;
|
|
2455
|
+
}
|
|
2456
|
+
async isFocused(selector, index = null) {
|
|
2457
|
+
const node = resolveScopedNode(selector, index);
|
|
2458
|
+
if (!node) return false;
|
|
2459
|
+
return node.props.accessibilityState?.focused === true || node.props.focused === true || node.props.autoFocus === true;
|
|
2460
|
+
}
|
|
2461
|
+
async isClickable(selector, index = null) {
|
|
2462
|
+
const node = resolveScopedNode(selector, index);
|
|
2463
|
+
if (!node) return false;
|
|
2464
|
+
if (!isNodeVisible(node)) return false;
|
|
2465
|
+
if (node.props.disabled || node.props.accessibilityState?.disabled === true) return false;
|
|
2466
|
+
return typeof node.props.onPress === "function" || typeof findAncestorProp(node, "onPress") === "function" || typeof findChildProp(node, "onPress") === "function";
|
|
2467
|
+
}
|
|
2468
|
+
async hasClass(_selector, _className, _index = null) {
|
|
2469
|
+
return false;
|
|
2470
|
+
}
|
|
2471
|
+
async getAttribute(selector, name, index = null) {
|
|
2472
|
+
const node = resolveScopedNode(selector, index);
|
|
2473
|
+
if (!node) return null;
|
|
2474
|
+
const val = node.props[name];
|
|
2475
|
+
return val != null ? String(val) : null;
|
|
2476
|
+
}
|
|
2477
|
+
async getCSSProperty(selector, name, index = null) {
|
|
2478
|
+
const node = resolveScopedNode(selector, index);
|
|
2479
|
+
if (!node) return { value: "" };
|
|
2480
|
+
const style = flattenStyle(node.props.style);
|
|
2481
|
+
return { value: style && style[name] != null ? String(style[name]) : "" };
|
|
2482
|
+
}
|
|
2483
|
+
async count(selector) {
|
|
2484
|
+
const root = getMountedTree();
|
|
2485
|
+
if (!root) return 0;
|
|
2486
|
+
if (typeof selector === "string") {
|
|
2487
|
+
const sentinelList2 = resolveSentinelList(selector, void 0, root);
|
|
2488
|
+
if (sentinelList2 !== void 0) return sentinelList2.length;
|
|
2489
|
+
return collectMatchingNodes(root, selector).length;
|
|
2490
|
+
}
|
|
2491
|
+
const sentinelList = resolveSentinelList(selector.selector, selector.parent, root);
|
|
2492
|
+
if (sentinelList !== void 0) return sentinelList.length;
|
|
2493
|
+
const parentNode = selector.parent ? resolveScopedNode(selector.parent, selector.parent.index, root) : root;
|
|
2494
|
+
if (!parentNode) return 0;
|
|
2495
|
+
return collectMatchingNodes(parentNode, selector.selector).length;
|
|
2496
|
+
}
|
|
2497
|
+
async waitForElementRemoved(selector, timeoutMs = 5e3, index = null) {
|
|
2498
|
+
const ok = await globalThis.__TS_RUNTIME__?.pollCondition?.(async () => {
|
|
2499
|
+
return !await this.isExisting(selector, index);
|
|
2500
|
+
}, timeoutMs) ?? true;
|
|
2501
|
+
if (!ok) throw new Error("Element still exists after timeout: " + selectorText3(selector));
|
|
2502
|
+
}
|
|
2503
|
+
async waitForUndisplayed(selector, opts, index = null) {
|
|
2504
|
+
const ok = await globalThis.__TS_RUNTIME__?.pollCondition?.(async () => {
|
|
2505
|
+
return !await this.isDisplayed(selector, index);
|
|
2506
|
+
}, opts?.timeout ?? 5e3) ?? true;
|
|
2507
|
+
if (!ok) throw new Error("Element still displayed after timeout: " + selectorText3(selector));
|
|
2508
|
+
}
|
|
2509
|
+
async waitForClickable(selector, opts, index = null) {
|
|
2510
|
+
return await globalThis.__TS_RUNTIME__?.pollCondition?.(async () => {
|
|
2511
|
+
return this.isClickable(selector, index);
|
|
2512
|
+
}, opts?.timeout ?? 5e3) ?? true;
|
|
2513
|
+
}
|
|
2514
|
+
async getBounds(selector, index = null) {
|
|
2515
|
+
const node = resolveScopedNode(selector, index);
|
|
2516
|
+
if (!node) return { x: 0, y: 0, top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 };
|
|
2517
|
+
const style = flattenStyle(node.props.style) || {};
|
|
2518
|
+
const w = typeof style.width === "number" ? style.width : 0;
|
|
2519
|
+
const h = typeof style.height === "number" ? style.height : 0;
|
|
2520
|
+
return { x: 0, y: 0, top: 0, right: w, bottom: h, left: 0, width: w, height: h };
|
|
2521
|
+
}
|
|
2522
|
+
async closest(selector, targetSelector, index = null) {
|
|
2523
|
+
const node = resolveScopedNode(selector, index);
|
|
2524
|
+
if (!node) return null;
|
|
2525
|
+
let curr = node.parent;
|
|
2526
|
+
while (curr) {
|
|
2527
|
+
if (nodeMatchesSelector(curr, targetSelector)) {
|
|
2528
|
+
return typeof curr.type === "string" ? curr.type : curr.type?.displayName || curr.type?.name || "component";
|
|
2529
|
+
}
|
|
2530
|
+
curr = curr.parent;
|
|
2531
|
+
}
|
|
2532
|
+
return null;
|
|
2533
|
+
}
|
|
2534
|
+
async clickAt(_x, _y) {
|
|
2535
|
+
}
|
|
2536
|
+
async pressKey(key) {
|
|
2537
|
+
const root = getMountedTree();
|
|
2538
|
+
if (!root) return;
|
|
2539
|
+
const focused = collectMatchingNodes(root, "*").find((n) => n.props.accessibilityState?.focused);
|
|
2540
|
+
focused?.props.onKeyPress?.({ nativeEvent: { key: String(key) } });
|
|
2541
|
+
}
|
|
2542
|
+
async scroll(_options = {}) {
|
|
2543
|
+
}
|
|
2544
|
+
async swipe(_options) {
|
|
2545
|
+
}
|
|
2546
|
+
async grantPermission(_name) {
|
|
2547
|
+
}
|
|
2548
|
+
// ─── Browser/Runtime Level Bridge ────────────────────────────────────────
|
|
2549
|
+
async getUrl() {
|
|
2550
|
+
return "react-native://spectra-harness";
|
|
2551
|
+
}
|
|
2552
|
+
async getTitle() {
|
|
2553
|
+
return "React Native Component Test";
|
|
2554
|
+
}
|
|
2555
|
+
async execute(fn, ...args) {
|
|
2556
|
+
if (typeof fn === "function") {
|
|
2557
|
+
return fn(...args);
|
|
2558
|
+
}
|
|
2559
|
+
return new Function(`return (${fn});`)();
|
|
2560
|
+
}
|
|
2561
|
+
async evaluate(expr) {
|
|
2562
|
+
return new Function(`return (${expr});`)();
|
|
2563
|
+
}
|
|
2564
|
+
async waitUntil(fn, opts) {
|
|
2565
|
+
const ok = await globalThis.__TS_RUNTIME__?.pollCondition?.(fn, opts?.timeout) ?? true;
|
|
2566
|
+
if (!ok) throw new Error(opts?.timeoutMsg || "waitUntil() condition was not met in time");
|
|
2567
|
+
return ok;
|
|
2568
|
+
}
|
|
2569
|
+
async getLogs(_type) {
|
|
2570
|
+
return [];
|
|
2571
|
+
}
|
|
2572
|
+
async shouldHaveUrl(expectedUrl, _options) {
|
|
2573
|
+
const actual = await this.getUrl();
|
|
2574
|
+
if (actual !== expectedUrl) throw new Error(`Expected URL to be "${expectedUrl}", but got "${actual}"`);
|
|
2575
|
+
}
|
|
2576
|
+
async shouldContainUrl(expectedSubstr, _options) {
|
|
2577
|
+
const actual = await this.getUrl();
|
|
2578
|
+
if (!actual.includes(expectedSubstr)) {
|
|
2579
|
+
throw new Error(`Expected URL to contain "${expectedSubstr}", but got "${actual}"`);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
async shouldHaveTitle(expectedTitle, _options) {
|
|
2583
|
+
const actual = await this.getTitle();
|
|
2584
|
+
if (actual !== expectedTitle) throw new Error(`Expected title to be "${expectedTitle}", but got "${actual}"`);
|
|
2585
|
+
}
|
|
2586
|
+
async shouldContainTitle(expectedSubstr, _options) {
|
|
2587
|
+
const actual = await this.getTitle();
|
|
2588
|
+
if (!actual.includes(expectedSubstr)) {
|
|
2589
|
+
throw new Error(`Expected title to contain "${expectedSubstr}", but got "${actual}"`);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
async shouldBeLoaded(_options) {
|
|
2593
|
+
}
|
|
2594
|
+
async shouldHaveNoConsoleErrors(_options) {
|
|
2595
|
+
if (this.consoleErrors.length > 0) {
|
|
2596
|
+
throw new Error(
|
|
2597
|
+
`Expected no console errors, but found ${this.consoleErrors.length}: ${this.consoleErrors.join("; ")}`
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
async clearCookies() {
|
|
2602
|
+
}
|
|
2603
|
+
async setCookies(_setCookieHeaders, _url) {
|
|
2604
|
+
}
|
|
2605
|
+
async clearLocalStorage() {
|
|
2606
|
+
}
|
|
2607
|
+
// ─── Network Interception ────────────────────────────────────────────────
|
|
2608
|
+
async intercept(patternOrOptions, method, fixture, options) {
|
|
2609
|
+
const rule = parseMockRule(patternOrOptions, method, fixture, options);
|
|
2610
|
+
ensureNetworkPatch(() => this.mockRules);
|
|
2611
|
+
this.mockRules.push(rule);
|
|
2612
|
+
return createMockHandle(rule);
|
|
2613
|
+
}
|
|
2614
|
+
clearMocks() {
|
|
2615
|
+
this.mockRules = [];
|
|
2616
|
+
clearMocks();
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
});
|
|
2621
|
+
|
|
2622
|
+
// src/runtime/mount.ts
|
|
2623
|
+
async function mount(element) {
|
|
2624
|
+
let mountListener = getActiveMountListener();
|
|
2625
|
+
if (!mountListener) {
|
|
2626
|
+
registerMountHandlers(
|
|
2627
|
+
(el) => setMountedElement(el),
|
|
2628
|
+
() => setMountedElement(null)
|
|
2629
|
+
);
|
|
2630
|
+
mountListener = getActiveMountListener();
|
|
2631
|
+
}
|
|
2632
|
+
setMountedElement(element);
|
|
2633
|
+
mountListener(element);
|
|
2634
|
+
await new Promise((resolve) => setTimeout(resolve, 35));
|
|
2635
|
+
if (globalThis.__TS_RUNTIME__?.createElementProxy) {
|
|
2636
|
+
return globalThis.__TS_RUNTIME__.createElementProxy("~spectra-root");
|
|
2637
|
+
}
|
|
2638
|
+
return { selector: "~spectra-root" };
|
|
2639
|
+
}
|
|
2640
|
+
function unmount() {
|
|
2641
|
+
const unmountListener = getActiveUnmountListener();
|
|
2642
|
+
unmountListener?.();
|
|
2643
|
+
setMountedElement(null);
|
|
2644
|
+
}
|
|
2645
|
+
var init_mount = __esm({
|
|
2646
|
+
"src/runtime/mount.ts"() {
|
|
2647
|
+
"use strict";
|
|
2648
|
+
init_harness_root();
|
|
2649
|
+
init_tree_registry();
|
|
2650
|
+
}
|
|
2651
|
+
});
|
|
2652
|
+
|
|
2653
|
+
// src/runtime/setup.ts
|
|
2654
|
+
function expect(actual) {
|
|
2655
|
+
return {
|
|
2656
|
+
toBe: (exp) => {
|
|
2657
|
+
if (actual !== exp) throw new Error(`Expected ${actual} to be ${exp}`);
|
|
2658
|
+
},
|
|
2659
|
+
toEqual: (exp) => {
|
|
2660
|
+
if (JSON.stringify(actual) !== JSON.stringify(exp))
|
|
2661
|
+
throw new Error(`Expected ${JSON.stringify(actual)} to equal ${JSON.stringify(exp)}`);
|
|
2662
|
+
},
|
|
2663
|
+
toBeTruthy: () => {
|
|
2664
|
+
if (!actual) throw new Error(`Expected ${actual} to be truthy`);
|
|
2665
|
+
},
|
|
2666
|
+
toBeFalsy: () => {
|
|
2667
|
+
if (!actual) throw new Error(`Expected ${actual} to be falsy`);
|
|
2668
|
+
},
|
|
2669
|
+
toBeNull: () => {
|
|
2670
|
+
if (actual !== null) throw new Error(`Expected ${actual} to be null`);
|
|
2671
|
+
},
|
|
2672
|
+
toBeUndefined: () => {
|
|
2673
|
+
if (actual !== void 0) throw new Error(`Expected ${actual} to be undefined`);
|
|
2674
|
+
},
|
|
2675
|
+
toBeDefined: () => {
|
|
2676
|
+
if (actual === void 0) throw new Error(`Expected value to be defined`);
|
|
2677
|
+
},
|
|
2678
|
+
toContain: (item) => {
|
|
2679
|
+
if (Array.isArray(actual)) {
|
|
2680
|
+
if (!actual.includes(item)) throw new Error(`Expected array to contain ${item}`);
|
|
2681
|
+
} else if (typeof actual === "string") {
|
|
2682
|
+
if (!actual.includes(String(item))) throw new Error(`Expected string to contain ${item}`);
|
|
2683
|
+
}
|
|
2684
|
+
},
|
|
2685
|
+
not: {
|
|
2686
|
+
toBe: (exp) => {
|
|
2687
|
+
if (actual === exp) throw new Error(`Expected ${actual} not to be ${exp}`);
|
|
2688
|
+
},
|
|
2689
|
+
toEqual: (exp) => {
|
|
2690
|
+
if (JSON.stringify(actual) === JSON.stringify(exp))
|
|
2691
|
+
throw new Error(`Expected value not to equal ${JSON.stringify(exp)}`);
|
|
2692
|
+
},
|
|
2693
|
+
toBeTruthy: () => {
|
|
2694
|
+
if (actual) throw new Error(`Expected ${actual} not to be truthy`);
|
|
2695
|
+
},
|
|
2696
|
+
toBeFalsy: () => {
|
|
2697
|
+
if (!actual) throw new Error(`Expected ${actual} not to be falsy`);
|
|
2698
|
+
},
|
|
2699
|
+
toBeNull: () => {
|
|
2700
|
+
if (actual === null) throw new Error(`Expected value not to be null`);
|
|
2701
|
+
},
|
|
2702
|
+
toBeUndefined: () => {
|
|
2703
|
+
if (actual === void 0) throw new Error(`Expected value not to be undefined`);
|
|
2704
|
+
},
|
|
2705
|
+
toBeDefined: () => {
|
|
2706
|
+
if (actual !== void 0) throw new Error(`Expected value not to be defined`);
|
|
2707
|
+
},
|
|
2708
|
+
toContain: (item) => {
|
|
2709
|
+
if (Array.isArray(actual)) {
|
|
2710
|
+
if (actual.includes(item)) throw new Error(`Expected array not to contain ${item}`);
|
|
2711
|
+
} else if (typeof actual === "string") {
|
|
2712
|
+
if (actual.includes(String(item))) throw new Error(`Expected string not to contain ${item}`);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
var activeDriver2, spectra;
|
|
2719
|
+
var init_setup = __esm({
|
|
2720
|
+
"src/runtime/setup.ts"() {
|
|
2721
|
+
"use strict";
|
|
2722
|
+
init_react_native_driver_bridge();
|
|
2723
|
+
init_mount();
|
|
2724
|
+
init_dsl();
|
|
2725
|
+
activeDriver2 = new ReactNativeDriverBridge();
|
|
2726
|
+
setActiveDriver(activeDriver2);
|
|
2727
|
+
spectra = buildSpectra();
|
|
2728
|
+
spectra.mount = mount;
|
|
2729
|
+
spectra.unmount = unmount;
|
|
2730
|
+
globalThis.Spectra = spectra;
|
|
2731
|
+
globalThis.__SPECTRA_HARNESS_READY__ = true;
|
|
2732
|
+
spectra.expect = expect;
|
|
2733
|
+
if (typeof globalThis.expect === "undefined") {
|
|
2734
|
+
globalThis.expect = expect;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
});
|
|
2738
|
+
|
|
2739
|
+
// src/runner/library_spec_runner.ts
|
|
2740
|
+
var library_spec_runner_exports = {};
|
|
2741
|
+
__export(library_spec_runner_exports, {
|
|
2742
|
+
runLibraryComponentSpecs: () => runLibraryComponentSpecs
|
|
2743
|
+
});
|
|
2744
|
+
import fs2 from "node:fs";
|
|
2745
|
+
import { createRequire } from "node:module";
|
|
2746
|
+
import path2 from "node:path";
|
|
2747
|
+
import { fileURLToPath } from "node:url";
|
|
2748
|
+
import * as esbuild2 from "esbuild";
|
|
2749
|
+
import React3 from "react";
|
|
2750
|
+
import * as ReactJsxRuntime from "react/jsx-runtime";
|
|
2751
|
+
function getShimPath() {
|
|
2752
|
+
const candidates = [
|
|
2753
|
+
path2.resolve(__dirname, "mock/react_native_shim.js"),
|
|
2754
|
+
path2.resolve(__dirname, "../mock/react_native_shim.js"),
|
|
2755
|
+
path2.resolve(__dirname, "../src/mock/react_native_shim.ts"),
|
|
2756
|
+
path2.resolve(__dirname, "mock/react_native_shim.cjs")
|
|
2757
|
+
];
|
|
2758
|
+
for (const c of candidates) {
|
|
2759
|
+
if (fs2.existsSync(c)) return c;
|
|
2760
|
+
}
|
|
2761
|
+
return candidates[0];
|
|
2762
|
+
}
|
|
2763
|
+
function createReactNativeAliasPlugin() {
|
|
2764
|
+
const targetShim = getShimPath();
|
|
2765
|
+
return {
|
|
2766
|
+
name: "react-native-alias",
|
|
2767
|
+
setup(build3) {
|
|
2768
|
+
build3.onResolve({ filter: /^react-native($|\/)/ }, () => {
|
|
2769
|
+
return { path: targetShim };
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
async function loadPageObjects(testRoot) {
|
|
2775
|
+
const poDir = path2.join(testRoot, "page-objects");
|
|
2776
|
+
if (!fs2.existsSync(poDir)) return;
|
|
2777
|
+
const entries = fs2.readdirSync(poDir, { withFileTypes: true });
|
|
2778
|
+
for (const entry of entries) {
|
|
2779
|
+
if (!entry.isDirectory()) continue;
|
|
2780
|
+
const poName = entry.name;
|
|
2781
|
+
const poFolder = path2.join(poDir, poName);
|
|
2782
|
+
const candidates = ["mobile.ts", "mobile.js", "android.ts", "android.js", "index.ts", "index.js"];
|
|
2783
|
+
let matchedFile = null;
|
|
2784
|
+
for (const cand of candidates) {
|
|
2785
|
+
const full = path2.join(poFolder, cand);
|
|
2786
|
+
if (fs2.existsSync(full)) {
|
|
2787
|
+
matchedFile = full;
|
|
2788
|
+
break;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
if (matchedFile) {
|
|
2792
|
+
try {
|
|
2793
|
+
const bundle = await esbuild2.build({
|
|
2794
|
+
entryPoints: [matchedFile],
|
|
2795
|
+
bundle: true,
|
|
2796
|
+
format: "cjs",
|
|
2797
|
+
platform: "node",
|
|
2798
|
+
target: "es2020",
|
|
2799
|
+
external: ["@testspectra/matchers", "@testspectra/react-native", "react", "react-native"],
|
|
2800
|
+
write: false,
|
|
2801
|
+
plugins: [createReactNativeAliasPlugin()]
|
|
2802
|
+
});
|
|
2803
|
+
if (bundle.outputFiles?.[0]) {
|
|
2804
|
+
const code = bundle.outputFiles[0].text;
|
|
2805
|
+
const exportsObj = {};
|
|
2806
|
+
const moduleObj = { exports: exportsObj };
|
|
2807
|
+
const localRequire = createRequire(matchedFile);
|
|
2808
|
+
const runFn = new Function("require", "module", "exports", "globalThis", code);
|
|
2809
|
+
runFn(
|
|
2810
|
+
(id) => {
|
|
2811
|
+
if (id.startsWith("@testspectra/react-native")) return { Spectra: globalThis.Spectra };
|
|
2812
|
+
if (id.startsWith("@testspectra/matchers")) return { Spectra: globalThis.Spectra };
|
|
2813
|
+
if (id === "react") return React3;
|
|
2814
|
+
if (id === "react-native") return react_native_shim_exports;
|
|
2815
|
+
try {
|
|
2816
|
+
return localRequire(id);
|
|
2817
|
+
} catch {
|
|
2818
|
+
return {};
|
|
2819
|
+
}
|
|
2820
|
+
},
|
|
2821
|
+
moduleObj,
|
|
2822
|
+
exportsObj,
|
|
2823
|
+
globalThis
|
|
2824
|
+
);
|
|
2825
|
+
const instance = moduleObj.exports.default ?? moduleObj.exports;
|
|
2826
|
+
globalThis[poName] = instance;
|
|
2827
|
+
}
|
|
2828
|
+
} catch {
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
async function runLibraryComponentSpecs(specFiles, options = {}) {
|
|
2834
|
+
const testRoot = path2.resolve(options.testRoot ?? options.cwd ?? process.cwd());
|
|
2835
|
+
const startTime = Date.now();
|
|
2836
|
+
const emitLine = (line) => {
|
|
2837
|
+
if (options.reporter && typeof options.reporter.addLog === "function") {
|
|
2838
|
+
options.reporter.addLog({
|
|
2839
|
+
timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString(),
|
|
2840
|
+
level: "INFO",
|
|
2841
|
+
message: line
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
options.onProtocolLine?.(line);
|
|
2845
|
+
};
|
|
2846
|
+
registerMountHandlers(
|
|
2847
|
+
(el) => setMountedElement(el),
|
|
2848
|
+
() => setMountedElement(null)
|
|
2849
|
+
);
|
|
2850
|
+
await loadPageObjects(testRoot);
|
|
2851
|
+
const results = [];
|
|
2852
|
+
let passedCount = 0;
|
|
2853
|
+
let failedCount = 0;
|
|
2854
|
+
for (const specRelPath of specFiles) {
|
|
2855
|
+
const fullSpecPath = path2.isAbsolute(specRelPath) ? specRelPath : path2.join(testRoot, specRelPath);
|
|
2856
|
+
const specStartTime = Date.now();
|
|
2857
|
+
emitLine(`[TESTSPECTRA_SUITE_START] ${JSON.stringify({ file: specRelPath })}`);
|
|
2858
|
+
const registeredInFile = [];
|
|
2859
|
+
globalThis.it = (title, fn) => {
|
|
2860
|
+
registeredInFile.push({ title, fn });
|
|
2861
|
+
};
|
|
2862
|
+
let specPassed = true;
|
|
2863
|
+
let specError;
|
|
2864
|
+
try {
|
|
2865
|
+
const buildResult = await esbuild2.build({
|
|
2866
|
+
entryPoints: [fullSpecPath],
|
|
2867
|
+
bundle: true,
|
|
2868
|
+
format: "cjs",
|
|
2869
|
+
platform: "node",
|
|
2870
|
+
target: "es2020",
|
|
2871
|
+
external: ["@testspectra/matchers", "@testspectra/react-native"],
|
|
2872
|
+
write: false,
|
|
2873
|
+
jsx: "automatic",
|
|
2874
|
+
jsxImportSource: "react",
|
|
2875
|
+
plugins: [createReactNativeAliasPlugin()]
|
|
2876
|
+
});
|
|
2877
|
+
if (!buildResult.outputFiles?.[0]) {
|
|
2878
|
+
throw new Error(`Failed to bundle spec: ${specRelPath}`);
|
|
2879
|
+
}
|
|
2880
|
+
const bundleCode = buildResult.outputFiles[0].text;
|
|
2881
|
+
const exportsObj = {};
|
|
2882
|
+
const moduleObj = { exports: exportsObj };
|
|
2883
|
+
const localRequire = createRequire(fullSpecPath);
|
|
2884
|
+
const customRequire = (pkgName) => {
|
|
2885
|
+
if (pkgName === "react") return React3;
|
|
2886
|
+
if (pkgName === "react/jsx-runtime" || pkgName === "react/jsx-dev-runtime") {
|
|
2887
|
+
return ReactJsxRuntime;
|
|
2888
|
+
}
|
|
2889
|
+
if (pkgName.startsWith("@testspectra/react-native")) {
|
|
2890
|
+
return {
|
|
2891
|
+
Spectra: globalThis.Spectra,
|
|
2892
|
+
it: globalThis.it,
|
|
2893
|
+
mount: globalThis.Spectra.mount,
|
|
2894
|
+
unmount: globalThis.Spectra.unmount
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
if (pkgName.startsWith("@testspectra/matchers")) {
|
|
2898
|
+
return { Spectra: globalThis.Spectra };
|
|
2899
|
+
}
|
|
2900
|
+
if (pkgName === "react-native") {
|
|
2901
|
+
return react_native_shim_exports;
|
|
2902
|
+
}
|
|
2903
|
+
try {
|
|
2904
|
+
return localRequire(pkgName);
|
|
2905
|
+
} catch {
|
|
2906
|
+
return {};
|
|
2907
|
+
}
|
|
2908
|
+
};
|
|
2909
|
+
const runSpecCode = new Function("require", "module", "exports", "globalThis", bundleCode);
|
|
2910
|
+
runSpecCode(customRequire, moduleObj, exportsObj, globalThis);
|
|
2911
|
+
for (const { title, fn } of registeredInFile) {
|
|
2912
|
+
emitLine(`[TESTSPECTRA_TEST_START] ${title}`);
|
|
2913
|
+
globalThis.__CURRENT_TEST_TITLE__ = title;
|
|
2914
|
+
const testStartTime = Date.now();
|
|
2915
|
+
try {
|
|
2916
|
+
await fn();
|
|
2917
|
+
const duration = Date.now() - testStartTime;
|
|
2918
|
+
emitLine(`[TESTSPECTRA_TEST_PASS] ${title} (${duration}ms)`);
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
specPassed = false;
|
|
2921
|
+
specError = err?.message || String(err);
|
|
2922
|
+
const duration = Date.now() - testStartTime;
|
|
2923
|
+
emitLine(`[TESTSPECTRA_TEST_ERROR] ${specError}`);
|
|
2924
|
+
emitLine(`[TESTSPECTRA_TEST_FAIL] ${title} (${duration}ms)`);
|
|
2925
|
+
} finally {
|
|
2926
|
+
cleanup();
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
if (registeredInFile.length === 0) {
|
|
2930
|
+
emitLine(`[TESTSPECTRA_TEST_START] ${path2.basename(specRelPath)}`);
|
|
2931
|
+
emitLine(`[TESTSPECTRA_TEST_PASS] ${path2.basename(specRelPath)} (5ms)`);
|
|
2932
|
+
}
|
|
2933
|
+
} catch (err) {
|
|
2934
|
+
specPassed = false;
|
|
2935
|
+
specError = err?.message || String(err);
|
|
2936
|
+
emitLine(`[TESTSPECTRA_TEST_ERROR] Bundle/Execution error in ${specRelPath}: ${specError}`);
|
|
2937
|
+
emitLine(`[TESTSPECTRA_TEST_FAIL] ${path2.basename(specRelPath)} (1ms)`);
|
|
2938
|
+
}
|
|
2939
|
+
const durationMs = Date.now() - specStartTime;
|
|
2940
|
+
emitLine(
|
|
2941
|
+
`[TESTSPECTRA_SUITE_END] ${JSON.stringify({ file: specRelPath, passed: specPassed ? 1 : 0, failed: specPassed ? 0 : 1 })}`
|
|
2942
|
+
);
|
|
2943
|
+
if (specPassed) {
|
|
2944
|
+
passedCount++;
|
|
2945
|
+
} else {
|
|
2946
|
+
failedCount++;
|
|
2947
|
+
}
|
|
2948
|
+
results.push({
|
|
2949
|
+
specPath: specRelPath,
|
|
2950
|
+
passed: specPassed,
|
|
2951
|
+
durationMs,
|
|
2952
|
+
error: specError
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
return {
|
|
2956
|
+
status: failedCount === 0 ? "passed" : "failed",
|
|
2957
|
+
passedCount,
|
|
2958
|
+
failedCount,
|
|
2959
|
+
suitesRun: specFiles.length,
|
|
2960
|
+
durationMs: Date.now() - startTime,
|
|
2961
|
+
results
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
var __filename, __dirname;
|
|
2965
|
+
var init_library_spec_runner = __esm({
|
|
2966
|
+
"src/runner/library_spec_runner.ts"() {
|
|
2967
|
+
"use strict";
|
|
2968
|
+
init_react_native_shim();
|
|
2969
|
+
init_harness_root();
|
|
2970
|
+
init_tree_registry();
|
|
2971
|
+
init_in_app_runner();
|
|
2972
|
+
init_setup();
|
|
2973
|
+
__filename = fileURLToPath(import.meta.url);
|
|
2974
|
+
__dirname = path2.dirname(__filename);
|
|
2975
|
+
}
|
|
2976
|
+
});
|
|
2977
|
+
|
|
2978
|
+
// src/bundler/bundle_server.ts
|
|
2979
|
+
import fs from "node:fs";
|
|
2980
|
+
import http from "node:http";
|
|
2981
|
+
import path from "node:path";
|
|
2982
|
+
import * as esbuild from "esbuild";
|
|
2983
|
+
var SHARED_MOBILE_EXTERNALS = [
|
|
2984
|
+
"react",
|
|
2985
|
+
"react/jsx-runtime",
|
|
2986
|
+
"react/jsx-dev-runtime",
|
|
2987
|
+
"react/compiler-runtime",
|
|
2988
|
+
"react-native",
|
|
2989
|
+
"react-native/*"
|
|
2990
|
+
];
|
|
2991
|
+
var bundleCache = /* @__PURE__ */ new Map();
|
|
2992
|
+
var inFlightBuilds = /* @__PURE__ */ new Map();
|
|
2993
|
+
async function buildSpecBundle(resolvedPath, options = {}) {
|
|
2994
|
+
const mergedExternals = Array.from(/* @__PURE__ */ new Set([...SHARED_MOBILE_EXTERNALS, ...options.external ?? []]));
|
|
2995
|
+
const format = options.format ?? "cjs";
|
|
2996
|
+
const buildOptions = {
|
|
2997
|
+
entryPoints: [resolvedPath],
|
|
2998
|
+
bundle: true,
|
|
2999
|
+
format,
|
|
3000
|
+
platform: "neutral",
|
|
3001
|
+
target: "es2020",
|
|
3002
|
+
external: mergedExternals,
|
|
3003
|
+
jsx: "automatic",
|
|
3004
|
+
jsxImportSource: "react",
|
|
3005
|
+
sourcemap: "inline",
|
|
3006
|
+
minify: false,
|
|
3007
|
+
write: false,
|
|
3008
|
+
loader: {
|
|
3009
|
+
".png": "dataurl",
|
|
3010
|
+
".jpg": "dataurl",
|
|
3011
|
+
".jpeg": "dataurl",
|
|
3012
|
+
".gif": "dataurl",
|
|
3013
|
+
".webp": "dataurl",
|
|
3014
|
+
".svg": "text",
|
|
3015
|
+
".json": "json",
|
|
3016
|
+
".txt": "text",
|
|
3017
|
+
".csv": "text",
|
|
3018
|
+
".tsx": "tsx",
|
|
3019
|
+
".ts": "ts",
|
|
3020
|
+
".jsx": "jsx",
|
|
3021
|
+
".js": "js"
|
|
3022
|
+
},
|
|
3023
|
+
logLevel: "error",
|
|
3024
|
+
plugins: options.plugins
|
|
3025
|
+
};
|
|
3026
|
+
const result = await esbuild.build(buildOptions);
|
|
3027
|
+
if (result.errors.length > 0) {
|
|
3028
|
+
const msgs = result.errors.map((e) => e.text).join("\n");
|
|
3029
|
+
throw new Error(`esbuild compilation failed for ${resolvedPath}:
|
|
3030
|
+
${msgs}`);
|
|
3031
|
+
}
|
|
3032
|
+
if (!result.outputFiles || result.outputFiles.length === 0) {
|
|
3033
|
+
throw new Error(`esbuild produced no output files for ${resolvedPath}`);
|
|
3034
|
+
}
|
|
3035
|
+
return result.outputFiles[0].text;
|
|
3036
|
+
}
|
|
3037
|
+
async function getCachedSpecBundle(cacheKey, resolvedPath, options = {}) {
|
|
3038
|
+
const cached = bundleCache.get(cacheKey);
|
|
3039
|
+
if (cached) return cached;
|
|
3040
|
+
const existingInFlight = inFlightBuilds.get(cacheKey);
|
|
3041
|
+
if (existingInFlight) return existingInFlight;
|
|
3042
|
+
const buildPromise = buildSpecBundle(resolvedPath, options).then(
|
|
3043
|
+
(js) => {
|
|
3044
|
+
bundleCache.set(cacheKey, js);
|
|
3045
|
+
inFlightBuilds.delete(cacheKey);
|
|
3046
|
+
return js;
|
|
3047
|
+
},
|
|
3048
|
+
(err) => {
|
|
3049
|
+
inFlightBuilds.delete(cacheKey);
|
|
3050
|
+
throw err;
|
|
3051
|
+
}
|
|
3052
|
+
);
|
|
3053
|
+
inFlightBuilds.set(cacheKey, buildPromise);
|
|
3054
|
+
return buildPromise;
|
|
3055
|
+
}
|
|
3056
|
+
function respond(res, status, contentType, body) {
|
|
3057
|
+
res.writeHead(status, { "Content-Type": contentType });
|
|
3058
|
+
res.end(body);
|
|
3059
|
+
}
|
|
3060
|
+
function makeHandler(testRoot, external = [], plugins = []) {
|
|
3061
|
+
return async (req, res) => {
|
|
3062
|
+
const hostHeader = req.headers.host || "127.0.0.1";
|
|
3063
|
+
const url = new URL(req.url ?? "/", `http://${hostHeader}`);
|
|
3064
|
+
if (url.pathname === "/health") {
|
|
3065
|
+
respond(res, 200, "application/json", JSON.stringify({ status: "ok" }));
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
if (url.pathname === "/bundle") {
|
|
3069
|
+
const rawFile = url.searchParams.get("file") ?? url.searchParams.get("spec");
|
|
3070
|
+
if (!rawFile) {
|
|
3071
|
+
respond(res, 400, "text/plain", "Missing ?file= parameter");
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
const decodedPath = decodeURIComponent(rawFile);
|
|
3075
|
+
const resolvedPath = path.isAbsolute(decodedPath) ? path.normalize(decodedPath) : path.resolve(testRoot, decodedPath);
|
|
3076
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
3077
|
+
respond(res, 404, "text/plain", `File not found: ${decodedPath}`);
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
const requestedFormat = url.searchParams.get("format") || "cjs";
|
|
3081
|
+
const cacheKey = `${resolvedPath}:${requestedFormat}`;
|
|
3082
|
+
try {
|
|
3083
|
+
const js = await getCachedSpecBundle(cacheKey, resolvedPath, {
|
|
3084
|
+
format: requestedFormat,
|
|
3085
|
+
external,
|
|
3086
|
+
plugins
|
|
3087
|
+
});
|
|
3088
|
+
respond(res, 200, "application/javascript; charset=utf-8", js);
|
|
3089
|
+
} catch (err) {
|
|
3090
|
+
const message = err?.message || String(err);
|
|
3091
|
+
respond(
|
|
3092
|
+
res,
|
|
3093
|
+
500,
|
|
3094
|
+
"application/javascript; charset=utf-8",
|
|
3095
|
+
`throw new Error(${JSON.stringify(`Bundle error for ${decodedPath}: ${message}`)});`
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
respond(res, 404, "text/plain", "Not Found");
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
async function listenWithFallback(server, preferredPort, host = "127.0.0.1") {
|
|
3104
|
+
const portsToTry = [preferredPort, 8082, 9246, 0];
|
|
3105
|
+
for (const port of portsToTry) {
|
|
3106
|
+
try {
|
|
3107
|
+
await new Promise((resolve, reject) => {
|
|
3108
|
+
const onError = (err) => {
|
|
3109
|
+
server.removeListener("listening", onListening);
|
|
3110
|
+
reject(err);
|
|
3111
|
+
};
|
|
3112
|
+
const onListening = () => {
|
|
3113
|
+
server.removeListener("error", onError);
|
|
3114
|
+
resolve();
|
|
3115
|
+
};
|
|
3116
|
+
server.once("error", onError);
|
|
3117
|
+
server.once("listening", onListening);
|
|
3118
|
+
server.listen(port, host);
|
|
3119
|
+
});
|
|
3120
|
+
const addr = server.address();
|
|
3121
|
+
return addr.port;
|
|
3122
|
+
} catch (err) {
|
|
3123
|
+
if (err.code === "EADDRINUSE" && port !== 0) {
|
|
3124
|
+
continue;
|
|
3125
|
+
}
|
|
3126
|
+
throw err;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
throw new Error("Could not bind mobile bundle server to any port");
|
|
3130
|
+
}
|
|
3131
|
+
async function startMobileBundleServer(opts = {}) {
|
|
3132
|
+
const testRoot = path.resolve(opts.testRoot ?? process.cwd());
|
|
3133
|
+
const host = opts.host ?? "127.0.0.1";
|
|
3134
|
+
const handler = makeHandler(testRoot, opts.external ?? [], opts.plugins ?? []);
|
|
3135
|
+
const server = http.createServer(handler);
|
|
3136
|
+
let actualPort;
|
|
3137
|
+
if (opts.port !== void 0) {
|
|
3138
|
+
await new Promise((resolve, reject) => {
|
|
3139
|
+
const onError = (err) => {
|
|
3140
|
+
server.removeListener("listening", onListening);
|
|
3141
|
+
reject(err);
|
|
3142
|
+
};
|
|
3143
|
+
const onListening = () => {
|
|
3144
|
+
server.removeListener("error", onError);
|
|
3145
|
+
resolve();
|
|
3146
|
+
};
|
|
3147
|
+
server.once("error", onError);
|
|
3148
|
+
server.once("listening", onListening);
|
|
3149
|
+
server.listen(opts.port, host);
|
|
3150
|
+
});
|
|
3151
|
+
actualPort = server.address().port;
|
|
3152
|
+
} else {
|
|
3153
|
+
actualPort = await listenWithFallback(server, 8081, host);
|
|
3154
|
+
}
|
|
3155
|
+
const baseUrl = `http://${host}:${actualPort}`;
|
|
3156
|
+
return {
|
|
3157
|
+
url: baseUrl,
|
|
3158
|
+
port: actualPort,
|
|
3159
|
+
stop: () => new Promise((resolve) => {
|
|
3160
|
+
server.close(() => resolve());
|
|
3161
|
+
})
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
// src/runner/host_mobile_runner.ts
|
|
3166
|
+
import fs3 from "node:fs";
|
|
3167
|
+
import net from "node:net";
|
|
3168
|
+
import path3 from "node:path";
|
|
3169
|
+
import spawn from "cross-spawn";
|
|
3170
|
+
async function checkAdbDevices() {
|
|
3171
|
+
return new Promise((resolve) => {
|
|
3172
|
+
const proc = spawn("adb", ["devices"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3173
|
+
let stdout = "";
|
|
3174
|
+
let stderr = "";
|
|
3175
|
+
proc.stdout?.on("data", (d) => {
|
|
3176
|
+
stdout += d.toString();
|
|
3177
|
+
});
|
|
3178
|
+
proc.stderr?.on("data", (d) => {
|
|
3179
|
+
stderr += d.toString();
|
|
3180
|
+
});
|
|
3181
|
+
proc.on("close", (code) => {
|
|
3182
|
+
if (code !== 0) {
|
|
3183
|
+
resolve({ connected: false, devices: [], raw: stderr || stdout });
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3187
|
+
const devices = [];
|
|
3188
|
+
for (const line of lines) {
|
|
3189
|
+
if (line.startsWith("List of devices attached") || line.startsWith("* daemon")) continue;
|
|
3190
|
+
const [id, state] = line.split(/\s+/);
|
|
3191
|
+
if (id && state === "device") {
|
|
3192
|
+
devices.push(id);
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
resolve({ connected: devices.length > 0, devices, raw: stdout });
|
|
3196
|
+
});
|
|
3197
|
+
proc.on("error", (err) => {
|
|
3198
|
+
resolve({ connected: false, devices: [], raw: err.message });
|
|
3199
|
+
});
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
async function setupAdbPortForwarding(hostPort = 9245, devicePort = 9245, deviceId) {
|
|
3203
|
+
return new Promise((resolve, reject) => {
|
|
3204
|
+
const args = [];
|
|
3205
|
+
if (deviceId) {
|
|
3206
|
+
args.push("-s", deviceId);
|
|
3207
|
+
}
|
|
3208
|
+
args.push("forward", `tcp:${hostPort}`, `tcp:${devicePort}`);
|
|
3209
|
+
const proc = spawn("adb", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3210
|
+
let stderr = "";
|
|
3211
|
+
proc.stderr?.on("data", (d) => {
|
|
3212
|
+
stderr += d.toString();
|
|
3213
|
+
});
|
|
3214
|
+
proc.on("close", (code) => {
|
|
3215
|
+
if (code === 0) {
|
|
3216
|
+
resolve();
|
|
3217
|
+
} else {
|
|
3218
|
+
reject(new Error(`adb forward failed with exit code ${code}: ${stderr.trim()}`));
|
|
3219
|
+
}
|
|
3220
|
+
});
|
|
3221
|
+
proc.on("error", (err) => {
|
|
3222
|
+
reject(new Error(`Failed to execute adb forward: ${err.message}`));
|
|
3223
|
+
});
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
async function removeAdbPortForwarding(hostPort = 9245, deviceId) {
|
|
3227
|
+
return new Promise((resolve) => {
|
|
3228
|
+
const args = [];
|
|
3229
|
+
if (deviceId) {
|
|
3230
|
+
args.push("-s", deviceId);
|
|
3231
|
+
}
|
|
3232
|
+
args.push("forward", "--remove", `tcp:${hostPort}`);
|
|
3233
|
+
const proc = spawn("adb", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3234
|
+
proc.on("close", () => resolve());
|
|
3235
|
+
proc.on("error", () => resolve());
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
async function setupAdbReversePortForwarding(devicePort = 9245, hostPort = 9245, deviceId) {
|
|
3239
|
+
return new Promise((resolve) => {
|
|
3240
|
+
const args = [];
|
|
3241
|
+
if (deviceId) {
|
|
3242
|
+
args.push("-s", deviceId);
|
|
3243
|
+
}
|
|
3244
|
+
args.push("reverse", `tcp:${devicePort}`, `tcp:${hostPort}`);
|
|
3245
|
+
const proc = spawn("adb", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3246
|
+
proc.on("close", () => resolve());
|
|
3247
|
+
proc.on("error", () => resolve());
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
function findMobileSpecs(testRoot, filter) {
|
|
3251
|
+
const specs = [];
|
|
3252
|
+
const walk = (dir) => {
|
|
3253
|
+
let entries = [];
|
|
3254
|
+
try {
|
|
3255
|
+
entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
3256
|
+
} catch {
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
for (const entry of entries) {
|
|
3260
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") {
|
|
3261
|
+
continue;
|
|
3262
|
+
}
|
|
3263
|
+
const fullPath = path3.join(dir, entry.name);
|
|
3264
|
+
if (entry.isDirectory()) {
|
|
3265
|
+
walk(fullPath);
|
|
3266
|
+
} else if (entry.isFile()) {
|
|
3267
|
+
const lower = entry.name.toLowerCase();
|
|
3268
|
+
if (lower === "mobile.test.tsx" || lower.endsWith(".mobile.test.tsx") || lower === "android.test.tsx" || lower.endsWith(".android.test.tsx") || lower === "mobile.test.ts" || lower.endsWith(".mobile.test.ts") || lower === "android.test.ts" || lower.endsWith(".android.test.ts")) {
|
|
3269
|
+
const relPath = path3.relative(testRoot, fullPath);
|
|
3270
|
+
if (!filter || relPath.includes(filter) || fullPath.includes(filter)) {
|
|
3271
|
+
specs.push(relPath);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
};
|
|
3277
|
+
walk(testRoot);
|
|
3278
|
+
return specs;
|
|
3279
|
+
}
|
|
3280
|
+
async function connectClientWithRetries(port, host = "127.0.0.1", maxRetries = 10, retryDelayMs = 400) {
|
|
3281
|
+
let lastError = null;
|
|
3282
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
3283
|
+
try {
|
|
3284
|
+
const socket = await new Promise((resolve, reject) => {
|
|
3285
|
+
const s = net.createConnection({ port, host });
|
|
3286
|
+
const onConnect = () => {
|
|
3287
|
+
s.removeListener("error", onError);
|
|
3288
|
+
resolve(s);
|
|
3289
|
+
};
|
|
3290
|
+
const onError = (err) => {
|
|
3291
|
+
s.removeListener("connect", onConnect);
|
|
3292
|
+
s.destroy();
|
|
3293
|
+
reject(err);
|
|
3294
|
+
};
|
|
3295
|
+
s.once("connect", onConnect);
|
|
3296
|
+
s.once("error", onError);
|
|
3297
|
+
});
|
|
3298
|
+
return socket;
|
|
3299
|
+
} catch (err) {
|
|
3300
|
+
lastError = err;
|
|
3301
|
+
if (attempt < maxRetries) {
|
|
3302
|
+
await new Promise((r) => setTimeout(r, retryDelayMs));
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
throw lastError || new Error(`Failed to connect to harness on ${host}:${port}`);
|
|
3307
|
+
}
|
|
3308
|
+
function createHarnessServer(port, host = "127.0.0.1", timeoutMs = 15e3) {
|
|
3309
|
+
return new Promise((resolve, reject) => {
|
|
3310
|
+
let clientSocket = null;
|
|
3311
|
+
let pendingWaiter = null;
|
|
3312
|
+
const server = net.createServer((socket) => {
|
|
3313
|
+
clientSocket = socket;
|
|
3314
|
+
if (pendingWaiter) {
|
|
3315
|
+
pendingWaiter(socket);
|
|
3316
|
+
pendingWaiter = null;
|
|
3317
|
+
}
|
|
3318
|
+
});
|
|
3319
|
+
server.once("error", reject);
|
|
3320
|
+
server.listen(port, host, () => {
|
|
3321
|
+
resolve({
|
|
3322
|
+
server,
|
|
3323
|
+
getSocket: () => {
|
|
3324
|
+
if (clientSocket && !clientSocket.destroyed) {
|
|
3325
|
+
return Promise.resolve(clientSocket);
|
|
3326
|
+
}
|
|
3327
|
+
return new Promise((res, rej) => {
|
|
3328
|
+
const timer = setTimeout(() => {
|
|
3329
|
+
rej(new Error(`Timed out waiting for mobile app to connect to host port ${port}`));
|
|
3330
|
+
}, timeoutMs);
|
|
3331
|
+
pendingWaiter = (sock) => {
|
|
3332
|
+
clearTimeout(timer);
|
|
3333
|
+
res(sock);
|
|
3334
|
+
};
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
});
|
|
3338
|
+
});
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
function executeSpecOnSocket(socket, specPath, options) {
|
|
3342
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
3343
|
+
const startTime = Date.now();
|
|
3344
|
+
return new Promise((resolve) => {
|
|
3345
|
+
let buffer = "";
|
|
3346
|
+
let settled = false;
|
|
3347
|
+
let specPassed = false;
|
|
3348
|
+
let errorMessage;
|
|
3349
|
+
const emitLine = (line) => {
|
|
3350
|
+
if (options.reporter && typeof options.reporter.addLog === "function") {
|
|
3351
|
+
options.reporter.addLog({
|
|
3352
|
+
timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString(),
|
|
3353
|
+
level: "INFO",
|
|
3354
|
+
message: line
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
options.onProtocolLine?.(line);
|
|
3358
|
+
};
|
|
3359
|
+
const finish = (passed, err) => {
|
|
3360
|
+
if (settled) return;
|
|
3361
|
+
settled = true;
|
|
3362
|
+
clearTimeout(timer);
|
|
3363
|
+
socket.removeListener("data", onData);
|
|
3364
|
+
socket.removeListener("close", onClose);
|
|
3365
|
+
socket.removeListener("error", onError);
|
|
3366
|
+
const durationMs = Date.now() - startTime;
|
|
3367
|
+
resolve({
|
|
3368
|
+
specPath,
|
|
3369
|
+
passed,
|
|
3370
|
+
durationMs,
|
|
3371
|
+
error: err
|
|
3372
|
+
});
|
|
3373
|
+
};
|
|
3374
|
+
const timer = setTimeout(() => {
|
|
3375
|
+
const err = `Timeout: Spec execution exceeded ${timeoutMs}ms`;
|
|
3376
|
+
emitLine(`[TESTSPECTRA_TEST_ERROR] ${err}`);
|
|
3377
|
+
emitLine(`[TESTSPECTRA_TEST_FAIL] ${specPath} (${Date.now() - startTime}ms)`);
|
|
3378
|
+
finish(false, err);
|
|
3379
|
+
}, timeoutMs);
|
|
3380
|
+
const onData = (chunk) => {
|
|
3381
|
+
buffer += chunk.toString("utf8");
|
|
3382
|
+
const lines = buffer.split("\n");
|
|
3383
|
+
buffer = lines.pop() ?? "";
|
|
3384
|
+
for (const rawLine of lines) {
|
|
3385
|
+
const line = rawLine.trim();
|
|
3386
|
+
if (!line) continue;
|
|
3387
|
+
emitLine(line);
|
|
3388
|
+
if (line.startsWith("[TESTSPECTRA_TEST_PASS]")) {
|
|
3389
|
+
specPassed = true;
|
|
3390
|
+
} else if (line.startsWith("[TESTSPECTRA_TEST_FAIL]")) {
|
|
3391
|
+
specPassed = false;
|
|
3392
|
+
} else if (line.startsWith("[TESTSPECTRA_TEST_ERROR]")) {
|
|
3393
|
+
errorMessage = line.replace("[TESTSPECTRA_TEST_ERROR]", "").trim();
|
|
3394
|
+
} else if (line.startsWith("[TESTSPECTRA_SUITE_END]")) {
|
|
3395
|
+
const payloadStr = line.replace("[TESTSPECTRA_SUITE_END]", "").trim();
|
|
3396
|
+
try {
|
|
3397
|
+
const payload = JSON.parse(payloadStr);
|
|
3398
|
+
specPassed = payload.failed === 0 && (payload.passed > 0 || specPassed);
|
|
3399
|
+
} catch {
|
|
3400
|
+
}
|
|
3401
|
+
finish(specPassed, errorMessage);
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
};
|
|
3405
|
+
const onClose = () => {
|
|
3406
|
+
if (!settled) {
|
|
3407
|
+
const msg = errorMessage || (buffer.length === 0 ? "Connection closed immediately with 0 bytes. Ensure the React Native app is opened on the device and configured with withSpectraComponentHarness." : "Connection closed unexpectedly before test completed.");
|
|
3408
|
+
emitLine(`[TESTSPECTRA_TEST_ERROR] ${msg}`);
|
|
3409
|
+
finish(specPassed, msg);
|
|
3410
|
+
}
|
|
3411
|
+
};
|
|
3412
|
+
const onError = (err) => {
|
|
3413
|
+
if (!settled) {
|
|
3414
|
+
emitLine(`[TESTSPECTRA_TEST_ERROR] Socket error: ${err.message}`);
|
|
3415
|
+
finish(false, err.message);
|
|
3416
|
+
}
|
|
3417
|
+
};
|
|
3418
|
+
socket.on("data", onData);
|
|
3419
|
+
socket.once("close", onClose);
|
|
3420
|
+
socket.once("error", onError);
|
|
3421
|
+
const command = {
|
|
3422
|
+
action: "RUN_SPEC",
|
|
3423
|
+
specPath,
|
|
3424
|
+
timeoutMs,
|
|
3425
|
+
bundleUrl: options.bundleUrl ?? "http://127.0.0.1:8081"
|
|
3426
|
+
};
|
|
3427
|
+
socket.write(JSON.stringify(command) + "\n");
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
async function runMobileComponentTests(options = {}) {
|
|
3431
|
+
const testRoot = path3.resolve(options.testRoot ?? options.cwd ?? process.cwd());
|
|
3432
|
+
const port = options.port ?? 9245;
|
|
3433
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
3434
|
+
const startTime = Date.now();
|
|
3435
|
+
const mode = options.mode ?? "auto";
|
|
3436
|
+
let specFiles;
|
|
3437
|
+
if (options.specs && options.specs.length > 0) {
|
|
3438
|
+
specFiles = options.specs;
|
|
3439
|
+
} else if (options.spec) {
|
|
3440
|
+
specFiles = findMobileSpecs(testRoot, options.spec);
|
|
3441
|
+
if (specFiles.length === 0) {
|
|
3442
|
+
specFiles = [options.spec];
|
|
3443
|
+
}
|
|
3444
|
+
} else {
|
|
3445
|
+
specFiles = findMobileSpecs(testRoot);
|
|
3446
|
+
}
|
|
3447
|
+
const isLibraryProject = mode === "library" || mode !== "client" && mode !== "server" && !fs3.existsSync(path3.join(testRoot, "android")) && !fs3.existsSync(path3.join(testRoot, "ios"));
|
|
3448
|
+
if (isLibraryProject) {
|
|
3449
|
+
const { runLibraryComponentSpecs: runLibraryComponentSpecs2 } = await Promise.resolve().then(() => (init_library_spec_runner(), library_spec_runner_exports));
|
|
3450
|
+
return runLibraryComponentSpecs2(specFiles, options);
|
|
3451
|
+
}
|
|
3452
|
+
if (specFiles.length === 0) {
|
|
3453
|
+
if (options.reporter && typeof options.reporter.addLog === "function") {
|
|
3454
|
+
options.reporter.addLog({
|
|
3455
|
+
timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString(),
|
|
3456
|
+
level: "ERROR",
|
|
3457
|
+
message: "[TESTSPECTRA_TEST_ERROR] No mobile component specs found (*.mobile.test.tsx, *.android.test.tsx)."
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3460
|
+
return {
|
|
3461
|
+
status: "passed",
|
|
3462
|
+
passedCount: 0,
|
|
3463
|
+
failedCount: 0,
|
|
3464
|
+
suitesRun: 0,
|
|
3465
|
+
durationMs: Date.now() - startTime,
|
|
3466
|
+
results: []
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3469
|
+
if (!options.skipAdbForward) {
|
|
3470
|
+
try {
|
|
3471
|
+
await setupAdbPortForwarding(port, port, options.deviceId);
|
|
3472
|
+
} catch (err) {
|
|
3473
|
+
const errorMsg = `Failed to configure ADB port forwarding on port ${port}: ${err.message}`;
|
|
3474
|
+
if (options.reporter && typeof options.reporter.addLog === "function") {
|
|
3475
|
+
options.reporter.addLog({
|
|
3476
|
+
timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString(),
|
|
3477
|
+
level: "ERROR",
|
|
3478
|
+
message: `[TESTSPECTRA_TEST_ERROR] ${errorMsg}`
|
|
3479
|
+
});
|
|
3480
|
+
}
|
|
3481
|
+
return {
|
|
3482
|
+
status: "failed",
|
|
3483
|
+
passedCount: 0,
|
|
3484
|
+
failedCount: specFiles.length,
|
|
3485
|
+
suitesRun: specFiles.length,
|
|
3486
|
+
durationMs: Date.now() - startTime,
|
|
3487
|
+
results: specFiles.map((s) => ({
|
|
3488
|
+
specPath: s,
|
|
3489
|
+
passed: false,
|
|
3490
|
+
durationMs: 0,
|
|
3491
|
+
error: errorMsg
|
|
3492
|
+
}))
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
let serverInstance = null;
|
|
3497
|
+
let activeSocket = null;
|
|
3498
|
+
try {
|
|
3499
|
+
if (mode === "server") {
|
|
3500
|
+
await setupAdbReversePortForwarding(port, port, options.deviceId);
|
|
3501
|
+
const harnessServer = await createHarnessServer(port, "127.0.0.1", timeoutMs);
|
|
3502
|
+
serverInstance = harnessServer.server;
|
|
3503
|
+
activeSocket = await harnessServer.getSocket();
|
|
3504
|
+
} else {
|
|
3505
|
+
try {
|
|
3506
|
+
activeSocket = await connectClientWithRetries(port, "127.0.0.1", 8, 400);
|
|
3507
|
+
} catch (clientErr) {
|
|
3508
|
+
if (mode === "auto") {
|
|
3509
|
+
try {
|
|
3510
|
+
await setupAdbReversePortForwarding(port, port, options.deviceId);
|
|
3511
|
+
const harnessServer = await createHarnessServer(port, "127.0.0.1", 1e4);
|
|
3512
|
+
serverInstance = harnessServer.server;
|
|
3513
|
+
activeSocket = await harnessServer.getSocket();
|
|
3514
|
+
} catch {
|
|
3515
|
+
throw clientErr;
|
|
3516
|
+
}
|
|
3517
|
+
} else {
|
|
3518
|
+
throw clientErr;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
} catch (connErr) {
|
|
3523
|
+
const msg = `Failed to connect to React Native harness on port ${port}. Ensure the app is running with withSpectraComponentHarness. (${connErr.message})`;
|
|
3524
|
+
if (options.reporter && typeof options.reporter.addLog === "function") {
|
|
3525
|
+
options.reporter.addLog({
|
|
3526
|
+
timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString(),
|
|
3527
|
+
level: "ERROR",
|
|
3528
|
+
message: `[TESTSPECTRA_TEST_ERROR] ${msg}`
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3531
|
+
return {
|
|
3532
|
+
status: "failed",
|
|
3533
|
+
passedCount: 0,
|
|
3534
|
+
failedCount: specFiles.length,
|
|
3535
|
+
suitesRun: specFiles.length,
|
|
3536
|
+
durationMs: Date.now() - startTime,
|
|
3537
|
+
results: specFiles.map((s) => ({
|
|
3538
|
+
specPath: s,
|
|
3539
|
+
passed: false,
|
|
3540
|
+
durationMs: 0,
|
|
3541
|
+
error: msg
|
|
3542
|
+
}))
|
|
3543
|
+
};
|
|
3544
|
+
}
|
|
3545
|
+
const results = [];
|
|
3546
|
+
let passedCount = 0;
|
|
3547
|
+
let failedCount = 0;
|
|
3548
|
+
try {
|
|
3549
|
+
for (const specFile of specFiles) {
|
|
3550
|
+
if (options.reporter && typeof options.reporter.addLog === "function") {
|
|
3551
|
+
options.reporter.addLog({
|
|
3552
|
+
timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString(),
|
|
3553
|
+
level: "INFO",
|
|
3554
|
+
message: `[TESTSPECTRA_SUITE_START] ${JSON.stringify({ file: specFile })}`
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
if (!activeSocket || activeSocket.destroyed) {
|
|
3558
|
+
activeSocket = await connectClientWithRetries(port, "127.0.0.1", 5, 300);
|
|
3559
|
+
}
|
|
3560
|
+
const specResult = await executeSpecOnSocket(activeSocket, specFile, {
|
|
3561
|
+
bundleUrl: options.bundleUrl,
|
|
3562
|
+
timeoutMs,
|
|
3563
|
+
reporter: options.reporter,
|
|
3564
|
+
onProtocolLine: options.onProtocolLine
|
|
3565
|
+
});
|
|
3566
|
+
results.push(specResult);
|
|
3567
|
+
if (specResult.passed) {
|
|
3568
|
+
passedCount++;
|
|
3569
|
+
} else {
|
|
3570
|
+
failedCount++;
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
} finally {
|
|
3574
|
+
if (activeSocket && !activeSocket.destroyed) {
|
|
3575
|
+
activeSocket.destroy();
|
|
3576
|
+
}
|
|
3577
|
+
if (serverInstance) {
|
|
3578
|
+
serverInstance.close();
|
|
3579
|
+
}
|
|
3580
|
+
if (!options.skipAdbForward) {
|
|
3581
|
+
await removeAdbPortForwarding(port, options.deviceId);
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
const totalDurationMs = Date.now() - startTime;
|
|
3585
|
+
const overallStatus = failedCount === 0 && passedCount > 0 ? "passed" : "failed";
|
|
3586
|
+
return {
|
|
3587
|
+
status: overallStatus,
|
|
3588
|
+
passedCount,
|
|
3589
|
+
failedCount,
|
|
3590
|
+
suitesRun: specFiles.length,
|
|
3591
|
+
durationMs: totalDurationMs,
|
|
3592
|
+
results
|
|
3593
|
+
};
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
// src/index.ts
|
|
3597
|
+
init_library_spec_runner();
|
|
3598
|
+
init_react_native_driver_bridge();
|
|
3599
|
+
init_network_patch();
|
|
3600
|
+
init_tree_registry();
|
|
3601
|
+
init_in_app_runner();
|
|
3602
|
+
init_harness_root();
|
|
3603
|
+
|
|
3604
|
+
// src/runtime/withSpectraComponentHarness.tsx
|
|
3605
|
+
init_harness_root();
|
|
3606
|
+
import React4 from "react";
|
|
3607
|
+
function withSpectraComponentHarness(App) {
|
|
3608
|
+
const SpectraComponentHarness = (props) => {
|
|
3609
|
+
const isComponentTest = process.env.SPECTRA_COMPONENT_TEST === "1" || globalThis.__SPECTRA_RUNNER_ACTIVE__ === true || Boolean(globalThis.__SPECTRA_COMPONENT_TEST__);
|
|
3610
|
+
if (isComponentTest) {
|
|
3611
|
+
return React4.createElement(SpectraHarnessRoot);
|
|
3612
|
+
}
|
|
3613
|
+
return React4.createElement(App, props);
|
|
3614
|
+
};
|
|
3615
|
+
SpectraComponentHarness.displayName = `withSpectraComponentHarness(${App.displayName || App.name || "Component"})`;
|
|
3616
|
+
return SpectraComponentHarness;
|
|
3617
|
+
}
|
|
3618
|
+
var withSpectraComponentHarness_default = withSpectraComponentHarness;
|
|
3619
|
+
|
|
3620
|
+
// src/index.ts
|
|
3621
|
+
init_mount();
|
|
3622
|
+
init_setup();
|
|
3623
|
+
export {
|
|
3624
|
+
ReactNativeDriverBridge,
|
|
3625
|
+
SpectraHarnessRoot,
|
|
3626
|
+
activeMountListener,
|
|
3627
|
+
activeUnmountListener,
|
|
3628
|
+
afterEach,
|
|
3629
|
+
beforeEach,
|
|
3630
|
+
checkAdbDevices,
|
|
3631
|
+
cleanup,
|
|
3632
|
+
clearMocks,
|
|
3633
|
+
clearMountedTree,
|
|
3634
|
+
emit,
|
|
3635
|
+
ensureNetworkPatch,
|
|
3636
|
+
expect,
|
|
3637
|
+
extractText,
|
|
3638
|
+
findMobileSpecs,
|
|
3639
|
+
flattenStyle,
|
|
3640
|
+
getActiveMountListener,
|
|
3641
|
+
getActiveUnmountListener,
|
|
3642
|
+
getMountedTree,
|
|
3643
|
+
getSocketEmitter,
|
|
3644
|
+
it,
|
|
3645
|
+
mobileRunner,
|
|
3646
|
+
mount,
|
|
3647
|
+
registerMockRule,
|
|
3648
|
+
registerMountHandlers,
|
|
3649
|
+
registerRootNode,
|
|
3650
|
+
removeAdbPortForwarding,
|
|
3651
|
+
runLibraryComponentSpecs,
|
|
3652
|
+
runMobileComponentTests,
|
|
3653
|
+
setMountedElement,
|
|
3654
|
+
setMountedRootRef,
|
|
3655
|
+
setSocketEmitter,
|
|
3656
|
+
setupAdbPortForwarding,
|
|
3657
|
+
setupAdbReversePortForwarding,
|
|
3658
|
+
startMobileBundleServer,
|
|
3659
|
+
unmount,
|
|
3660
|
+
withSpectraComponentHarness,
|
|
3661
|
+
withSpectraComponentHarness_default as withSpectraComponentHarnessDefault
|
|
3662
|
+
};
|