@tamagui/native-ci 2.1.0-1780964419737 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/detox.mjs CHANGED
@@ -70,6 +70,43 @@ function buildDetoxArgs(options) {
70
70
  return args;
71
71
  }
72
72
  const DETOX_TIMEOUT_MS = 60 * 60 * 1e3;
73
+ const MAX_CONNECT_FLAKE_RETRIES = 1;
74
+ const CONNECT_FLAKE_SIGNATURES = [/Exceeded timeout of \d+ ms for a hook/, /\btimed out after \d+ms/, /could not connect to Detox/, /launch recovery deadline exceeded/, /skipping launchApp:/, /skipping reload:/];
75
+ const ANSI_PATTERN = /\[[0-9;]*m/g;
76
+ function classifyDetoxFailures(rawOutput) {
77
+ const output = rawOutput.replace(ANSI_PATTERN, "");
78
+ const delimiter = /(PASS|FAIL) (e2e\/[^\s):]+\.test\.ts)/g;
79
+ const marks = [];
80
+ for (const m of output.matchAll(delimiter)) {
81
+ marks.push({
82
+ kind: m[1],
83
+ file: m[2],
84
+ index: m.index ?? 0
85
+ });
86
+ }
87
+ const failedFiles = [];
88
+ const flakeFiles = [];
89
+ const realFiles = [];
90
+ for (let i = 0; i < marks.length; i++) {
91
+ const mark = marks[i];
92
+ if (mark.kind !== "FAIL") continue;
93
+ const blockEnd = i + 1 < marks.length ? marks[i + 1].index : output.length;
94
+ const block = output.slice(mark.index, blockEnd);
95
+ if (failedFiles.includes(mark.file)) continue;
96
+ failedFiles.push(mark.file);
97
+ const isFlake = CONNECT_FLAKE_SIGNATURES.some(sig => sig.test(block));
98
+ if (isFlake) {
99
+ flakeFiles.push(mark.file);
100
+ } else {
101
+ realFiles.push(mark.file);
102
+ }
103
+ }
104
+ return {
105
+ failedFiles,
106
+ flakeFiles,
107
+ realFiles
108
+ };
109
+ }
73
110
  async function resetDetoxLockFile() {
74
111
  console.info("Resetting Detox lock file...");
75
112
  const proc = Bun.spawn(["npx", "detox", "reset-lock-file"], {
@@ -78,9 +115,7 @@ async function resetDetoxLockFile() {
78
115
  });
79
116
  await proc.exited;
80
117
  }
81
- async function runDetoxTests(options) {
82
- await resetDetoxLockFile();
83
- const detoxArgs = buildDetoxArgs(options);
118
+ async function spawnDetoxOnce(detoxArgs) {
84
119
  console.info("\n--- Running Detox tests ---");
85
120
  console.info(`Using fixed Detox server port: ${DETOX_SERVER_PORT}`);
86
121
  console.info(`Command: npx ${detoxArgs.join(" ")}`);
@@ -89,9 +124,26 @@ async function runDetoxTests(options) {
89
124
  ...process.env,
90
125
  DETOX_SERVER_PORT: String(DETOX_SERVER_PORT)
91
126
  },
92
- stdout: "inherit",
93
- stderr: "inherit"
127
+ stdout: "pipe",
128
+ stderr: "pipe"
94
129
  });
130
+ const decoder = new TextDecoder();
131
+ let output = "";
132
+ const tee = async (stream, sink) => {
133
+ const reader = stream.getReader();
134
+ while (true) {
135
+ const {
136
+ done,
137
+ value
138
+ } = await reader.read();
139
+ if (done) break;
140
+ sink.write(value);
141
+ output += decoder.decode(value, {
142
+ stream: true
143
+ });
144
+ }
145
+ };
146
+ const pumps = Promise.all([tee(proc.stdout, process.stdout), tee(proc.stderr, process.stderr)]);
95
147
  const timeoutPromise = new Promise(resolve => {
96
148
  setTimeout(() => resolve("timeout"), DETOX_TIMEOUT_MS);
97
149
  });
@@ -99,16 +151,66 @@ async function runDetoxTests(options) {
99
151
  if (result === "timeout") {
100
152
  console.info("\nDetox timed out, killing process...");
101
153
  proc.kill("SIGKILL");
102
- return 1;
154
+ return {
155
+ exitCode: 1,
156
+ output
157
+ };
103
158
  }
104
- const exitCode = result;
159
+ await pumps;
160
+ return {
161
+ exitCode: result,
162
+ output
163
+ };
164
+ }
165
+ async function runDetoxTests(options) {
166
+ await resetDetoxLockFile();
167
+ const detoxArgs = buildDetoxArgs(options);
168
+ const {
169
+ exitCode,
170
+ output
171
+ } = await spawnDetoxOnce(detoxArgs);
105
172
  if (exitCode === 0) {
106
173
  console.info("\nAll tests passed!");
107
- } else {
108
- console.info(`
174
+ return 0;
175
+ }
176
+ console.info(`
109
177
  Tests failed with exit code: ${exitCode}`);
178
+ const {
179
+ flakeFiles,
180
+ realFiles
181
+ } = classifyDetoxFailures(output);
182
+ if (flakeFiles.length === 0) {
183
+ return exitCode;
184
+ }
185
+ if (realFiles.length > 0) {
186
+ console.info(`
187
+ Real test failure(s) present (${realFiles.join(", ")}); not retrying the connect-flaked file(s).`);
188
+ return exitCode;
189
+ }
190
+ for (let attempt = 1; attempt <= MAX_CONNECT_FLAKE_RETRIES; attempt++) {
191
+ console.info(`
192
+ ::warning::Detox launch-connect flake detected (beforeAll could not connect to Detox). Retry ${attempt}/${MAX_CONNECT_FLAKE_RETRIES} of: ${flakeFiles.join(", ")}`);
193
+ await resetDetoxLockFile();
194
+ const retryArgs = buildDetoxArgs({
195
+ ...options,
196
+ testFiles: flakeFiles
197
+ });
198
+ const retry = await spawnDetoxOnce(retryArgs);
199
+ if (retry.exitCode === 0) {
200
+ console.info(`
201
+ \u2713 Recovered ${flakeFiles.length} connect-flaked file(s) on retry ${attempt}.`);
202
+ return 0;
203
+ }
204
+ const retryClass = classifyDetoxFailures(retry.output);
205
+ if (retryClass.realFiles.length > 0 || retryClass.flakeFiles.length === 0) {
206
+ console.info("\nRetry surfaced a non-flake failure; surfacing it.");
207
+ return retry.exitCode;
208
+ }
209
+ flakeFiles.length = 0;
210
+ flakeFiles.push(...retryClass.flakeFiles);
110
211
  }
111
- return exitCode;
212
+ console.info("\nConnect-flake retries exhausted; tests still failing.");
213
+ return 1;
112
214
  }
113
- export { buildDetoxArgs, parseDetoxArgs, resetDetoxLockFile, runDetoxTests };
215
+ export { buildDetoxArgs, classifyDetoxFailures, parseDetoxArgs, resetDetoxLockFile, runDetoxTests };
114
216
  //# sourceMappingURL=detox.mjs.map
@@ -1 +1 @@
1
- {"version":3,"names":["parseArgs","DETOX_SERVER_PORT","parseDetoxArgs","platform","defaultConfig","values","positionals","options","config","type","default","process","cwd","headless","retries","workers","allowPositionals","retriesNum","Number","parseInt","isNaN","console","error","exit","workersNum","projectRoot","recordLogs","testFiles","length","buildDetoxArgs","args","String","push","DETOX_TIMEOUT_MS","resetDetoxLockFile","info","proc","Bun","spawn","stdout","stderr","exited","runDetoxTests","detoxArgs","join","env","timeoutPromise","Promise","resolve","setTimeout","result","race","kill","exitCode"],"sources":["../src/detox.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["parseArgs","DETOX_SERVER_PORT","parseDetoxArgs","platform","defaultConfig","values","positionals","options","config","type","default","process","cwd","headless","retries","workers","allowPositionals","retriesNum","Number","parseInt","isNaN","console","error","exit","workersNum","projectRoot","recordLogs","testFiles","length","buildDetoxArgs","args","String","push","DETOX_TIMEOUT_MS","MAX_CONNECT_FLAKE_RETRIES","CONNECT_FLAKE_SIGNATURES","ANSI_PATTERN","classifyDetoxFailures","rawOutput","output","replace","delimiter","marks","m","matchAll","kind","file","index","failedFiles","flakeFiles","realFiles","i","mark","blockEnd","block","slice","includes","isFlake","some","sig","test","resetDetoxLockFile","info","proc","Bun","spawn","stdout","stderr","exited","spawnDetoxOnce","detoxArgs","join","env","decoder","TextDecoder","tee","stream","sink","reader","getReader","done","value","read","write","decode","pumps","Promise","all","timeoutPromise","resolve","setTimeout","result","race","kill","exitCode","runDetoxTests","attempt","retryArgs","retry","retryClass"],"sources":["../src/detox.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamagui/native-ci",
3
- "version": "2.1.0-1780964419737",
3
+ "version": "2.2.0",
4
4
  "description": "Native CI/CD helpers for React Native apps with Expo - fingerprinting, caching, and build optimization",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
  "@expo/fingerprint": "~0.16.6"
43
43
  },
44
44
  "devDependencies": {
45
- "@tamagui/build": "2.1.0-1780964419737",
45
+ "@tamagui/build": "2.2.0",
46
46
  "@types/bun": "^1.1.0",
47
47
  "@types/node": "^22.1.0"
48
48
  },
package/src/detox.ts CHANGED
@@ -107,6 +107,84 @@ export function buildDetoxArgs(options: DetoxRunnerOptions): string[] {
107
107
  // workflow job timeout is 90min, leaving room for setup + this run.
108
108
  const DETOX_TIMEOUT_MS = 60 * 60 * 1000
109
109
 
110
+ // Connect-flake retry: a single beforeAll launchApp that can't connect to Detox on a
111
+ // degraded runner fails the whole spec file (all its tests fail off the one hook), and
112
+ // nothing retries beforeAll (jest.retryTimes only re-runs individual tests; detox
113
+ // --retries is held at 0 to avoid blanket whole-file retry wall-time). So we retry the
114
+ // failed files ONCE, but ONLY when every failed file's failure carries the launch/connect
115
+ // flake signature - never on a real assertion failure. This is safe by construction: a
116
+ // real failure fails again on retry, so a misclassification can only cost one extra file
117
+ // run, never mask a genuine bug. See memory project_detox_app_connect_runaway.
118
+ const MAX_CONNECT_FLAKE_RETRIES = 1
119
+
120
+ // Signatures that mark a spec file's failure as the launch/connect flake rather than a
121
+ // real test failure. Matched against the ANSI-stripped per-file FAIL block.
122
+ // - "for a hook" - jest hook (beforeAll/beforeEach) timeout; real test timeouts say
123
+ // "for a test", so this never matches an assertion failure.
124
+ // - "timed out after <n>ms" - lowercase message thrown by our withTimeout() wrapper,
125
+ // used ONLY by safeLaunchApp/safeReloadApp/safeTerminate; detox's own waitFor emits
126
+ // capital "Timed out after <n>ms waiting for", so the lowercase form is launch-only.
127
+ // - the safeLaunchApp/safeReloadApp breaker messages.
128
+ const CONNECT_FLAKE_SIGNATURES: RegExp[] = [
129
+ /Exceeded timeout of \d+ ms for a hook/,
130
+ /\btimed out after \d+ms/,
131
+ /could not connect to Detox/,
132
+ /launch recovery deadline exceeded/,
133
+ /skipping launchApp:/,
134
+ /skipping reload:/,
135
+ ]
136
+
137
+ const ANSI_PATTERN = /\[[0-9;]*m/g
138
+
139
+ interface DetoxFailureClassification {
140
+ /** every spec file with a FAIL line */
141
+ failedFiles: string[]
142
+ /** failed files whose failure matches a connect-flake signature (retry candidates) */
143
+ flakeFiles: string[]
144
+ /** failed files whose failure looks real (assertion/logic) - never retried */
145
+ realFiles: string[]
146
+ }
147
+
148
+ /**
149
+ * Classify a detox/jest run's combined output into connect-flake vs real failures,
150
+ * per spec file. jest prints each file's `FAIL e2e/X.test.ts` line followed by that
151
+ * file's failure detail (the ● blocks) before the next PASS/FAIL line, so the text
152
+ * between one FAIL line and the next delimiter is that file's failure block.
153
+ *
154
+ * Exported for unit testing against real CI logs.
155
+ */
156
+ export function classifyDetoxFailures(rawOutput: string): DetoxFailureClassification {
157
+ const output = rawOutput.replace(ANSI_PATTERN, '')
158
+ // delimiter = any "PASS e2e/..." or "FAIL e2e/..." occurrence (not anchored: real CI
159
+ // logs may carry a leading timestamp/prefix on the line).
160
+ const delimiter = /(PASS|FAIL) (e2e\/[^\s):]+\.test\.ts)/g
161
+ const marks: { kind: string; file: string; index: number }[] = []
162
+ for (const m of output.matchAll(delimiter)) {
163
+ marks.push({ kind: m[1], file: m[2], index: m.index ?? 0 })
164
+ }
165
+
166
+ const failedFiles: string[] = []
167
+ const flakeFiles: string[] = []
168
+ const realFiles: string[] = []
169
+
170
+ for (let i = 0; i < marks.length; i++) {
171
+ const mark = marks[i]
172
+ if (mark.kind !== 'FAIL') continue
173
+ const blockEnd = i + 1 < marks.length ? marks[i + 1].index : output.length
174
+ const block = output.slice(mark.index, blockEnd)
175
+ if (failedFiles.includes(mark.file)) continue
176
+ failedFiles.push(mark.file)
177
+ const isFlake = CONNECT_FLAKE_SIGNATURES.some((sig) => sig.test(block))
178
+ if (isFlake) {
179
+ flakeFiles.push(mark.file)
180
+ } else {
181
+ realFiles.push(mark.file)
182
+ }
183
+ }
184
+
185
+ return { failedFiles, flakeFiles, realFiles }
186
+ }
187
+
110
188
  /**
111
189
  * Reset Detox lock file to prevent ECOMPROMISED errors in CI
112
190
  * See: https://github.com/wix/Detox/issues/4210
@@ -121,29 +199,46 @@ export async function resetDetoxLockFile(): Promise<void> {
121
199
  }
122
200
 
123
201
  /**
124
- * Run Detox tests with the given options
202
+ * Spawn one `npx detox test ...` invocation, streaming its output live to the CI log
203
+ * while also capturing it (ANSI included) so we can classify failures afterwards.
125
204
  *
126
- * @returns Exit code from Detox
205
+ * @returns the process exit code and the full combined stdout+stderr text.
127
206
  */
128
- export async function runDetoxTests(options: DetoxRunnerOptions): Promise<number> {
129
- // Reset lock file to prevent ECOMPROMISED errors in CI
130
- // This clears stale locks from previous runs that can cause "Unable to update lock within the stale threshold" errors
131
- await resetDetoxLockFile()
132
-
133
- const detoxArgs = buildDetoxArgs(options)
134
-
207
+ async function spawnDetoxOnce(
208
+ detoxArgs: string[]
209
+ ): Promise<{ exitCode: number; output: string }> {
135
210
  console.info('\n--- Running Detox tests ---')
136
211
  console.info(`Using fixed Detox server port: ${DETOX_SERVER_PORT}`)
137
212
  console.info(`Command: npx ${detoxArgs.join(' ')}`)
138
213
 
139
- // Use Bun.spawn with timeout to prevent hanging on zombie processes
214
+ // pipe (not inherit) so we can capture output for flake classification; tee each
215
+ // chunk straight back to our stdout/stderr so the live CI log is unchanged.
140
216
  const proc = Bun.spawn(['npx', ...detoxArgs], {
141
217
  env: { ...process.env, DETOX_SERVER_PORT: String(DETOX_SERVER_PORT) },
142
- stdout: 'inherit',
143
- stderr: 'inherit',
218
+ stdout: 'pipe',
219
+ stderr: 'pipe',
144
220
  })
145
221
 
146
- // Race between process completion and timeout
222
+ const decoder = new TextDecoder()
223
+ let output = ''
224
+ const tee = async (
225
+ stream: ReadableStream<Uint8Array>,
226
+ sink: NodeJS.WriteStream
227
+ ): Promise<void> => {
228
+ const reader = stream.getReader()
229
+ while (true) {
230
+ const { done, value } = await reader.read()
231
+ if (done) break
232
+ sink.write(value)
233
+ output += decoder.decode(value, { stream: true })
234
+ }
235
+ }
236
+
237
+ const pumps = Promise.all([
238
+ tee(proc.stdout, process.stdout),
239
+ tee(proc.stderr, process.stderr),
240
+ ])
241
+
147
242
  const timeoutPromise = new Promise<'timeout'>((resolve) => {
148
243
  setTimeout(() => resolve('timeout'), DETOX_TIMEOUT_MS)
149
244
  })
@@ -153,16 +248,83 @@ export async function runDetoxTests(options: DetoxRunnerOptions): Promise<number
153
248
  if (result === 'timeout') {
154
249
  console.info('\nDetox timed out, killing process...')
155
250
  proc.kill('SIGKILL')
156
- return 1
251
+ return { exitCode: 1, output }
157
252
  }
158
253
 
159
- const exitCode = result
254
+ // drain any buffered output the process emitted right before exit
255
+ await pumps
256
+ return { exitCode: result, output }
257
+ }
258
+
259
+ /**
260
+ * Run Detox tests with the given options.
261
+ *
262
+ * On failure, retries the failed spec files ONCE when (and only when) every failed
263
+ * file's failure carries the launch/connect flake signature - this recovers the
264
+ * beforeAll-launch flake that jest.retryTimes can't, without reintroducing blanket
265
+ * whole-file retries. Real failures are never retried (and would fail again anyway).
266
+ *
267
+ * @returns Exit code from Detox
268
+ */
269
+ export async function runDetoxTests(options: DetoxRunnerOptions): Promise<number> {
270
+ // Reset lock file to prevent ECOMPROMISED errors in CI
271
+ // This clears stale locks from previous runs that can cause "Unable to update lock within the stale threshold" errors
272
+ await resetDetoxLockFile()
273
+
274
+ const detoxArgs = buildDetoxArgs(options)
275
+ const { exitCode, output } = await spawnDetoxOnce(detoxArgs)
160
276
 
161
277
  if (exitCode === 0) {
162
278
  console.info('\nAll tests passed!')
163
- } else {
164
- console.info(`\nTests failed with exit code: ${exitCode}`)
279
+ return 0
280
+ }
281
+ console.info(`\nTests failed with exit code: ${exitCode}`)
282
+
283
+ const { flakeFiles, realFiles } = classifyDetoxFailures(output)
284
+
285
+ // nothing to retry: either we couldn't attribute the failure to specific files, or a
286
+ // real (non-flake) failure is present - in both cases surface the original result.
287
+ if (flakeFiles.length === 0) {
288
+ return exitCode
289
+ }
290
+ if (realFiles.length > 0) {
291
+ console.info(
292
+ `\nReal test failure(s) present (${realFiles.join(', ')}); not retrying the ` +
293
+ `connect-flaked file(s).`
294
+ )
295
+ return exitCode
296
+ }
297
+
298
+ for (let attempt = 1; attempt <= MAX_CONNECT_FLAKE_RETRIES; attempt++) {
299
+ console.info(
300
+ `\n::warning::Detox launch-connect flake detected (beforeAll could not connect to ` +
301
+ `Detox). Retry ${attempt}/${MAX_CONNECT_FLAKE_RETRIES} of: ${flakeFiles.join(', ')}`
302
+ )
303
+ // fresh lock file before the retry so a stale lock from the wedged run can't
304
+ // immediately re-trip the same connect failure.
305
+ await resetDetoxLockFile()
306
+
307
+ const retryArgs = buildDetoxArgs({ ...options, testFiles: flakeFiles })
308
+ const retry = await spawnDetoxOnce(retryArgs)
309
+ if (retry.exitCode === 0) {
310
+ console.info(
311
+ `\n✓ Recovered ${flakeFiles.length} connect-flaked file(s) on retry ${attempt}.`
312
+ )
313
+ return 0
314
+ }
315
+
316
+ // if the retry surfaced a real failure (the flake cleared but a genuine bug is
317
+ // underneath), stop retrying and surface it.
318
+ const retryClass = classifyDetoxFailures(retry.output)
319
+ if (retryClass.realFiles.length > 0 || retryClass.flakeFiles.length === 0) {
320
+ console.info('\nRetry surfaced a non-flake failure; surfacing it.')
321
+ return retry.exitCode
322
+ }
323
+ // still a pure connect-flake - loop will retry again if budget remains.
324
+ flakeFiles.length = 0
325
+ flakeFiles.push(...retryClass.flakeFiles)
165
326
  }
166
327
 
167
- return exitCode
328
+ console.info('\nConnect-flake retries exhausted; tests still failing.')
329
+ return 1
168
330
  }
package/types/detox.d.ts CHANGED
@@ -36,15 +36,38 @@ export declare function parseDetoxArgs(platform: Platform): {
36
36
  * Build Detox CLI command arguments
37
37
  */
38
38
  export declare function buildDetoxArgs(options: DetoxRunnerOptions): string[];
39
+ interface DetoxFailureClassification {
40
+ /** every spec file with a FAIL line */
41
+ failedFiles: string[];
42
+ /** failed files whose failure matches a connect-flake signature (retry candidates) */
43
+ flakeFiles: string[];
44
+ /** failed files whose failure looks real (assertion/logic) - never retried */
45
+ realFiles: string[];
46
+ }
47
+ /**
48
+ * Classify a detox/jest run's combined output into connect-flake vs real failures,
49
+ * per spec file. jest prints each file's `FAIL e2e/X.test.ts` line followed by that
50
+ * file's failure detail (the ● blocks) before the next PASS/FAIL line, so the text
51
+ * between one FAIL line and the next delimiter is that file's failure block.
52
+ *
53
+ * Exported for unit testing against real CI logs.
54
+ */
55
+ export declare function classifyDetoxFailures(rawOutput: string): DetoxFailureClassification;
39
56
  /**
40
57
  * Reset Detox lock file to prevent ECOMPROMISED errors in CI
41
58
  * See: https://github.com/wix/Detox/issues/4210
42
59
  */
43
60
  export declare function resetDetoxLockFile(): Promise<void>;
44
61
  /**
45
- * Run Detox tests with the given options
62
+ * Run Detox tests with the given options.
63
+ *
64
+ * On failure, retries the failed spec files ONCE when (and only when) every failed
65
+ * file's failure carries the launch/connect flake signature - this recovers the
66
+ * beforeAll-launch flake that jest.retryTimes can't, without reintroducing blanket
67
+ * whole-file retries. Real failures are never retried (and would fail again anyway).
46
68
  *
47
69
  * @returns Exit code from Detox
48
70
  */
49
71
  export declare function runDetoxTests(options: DetoxRunnerOptions): Promise<number>;
72
+ export {};
50
73
  //# sourceMappingURL=detox.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"detox.d.ts","sourceRoot":"","sources":["../src/detox.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAG3C,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAA;IAClB,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ;;;;;;;;EAsChD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,EAAE,CA6BpE;AAOD;;;GAGG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAOxD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAwChF"}
1
+ {"version":3,"file":"detox.d.ts","sourceRoot":"","sources":["../src/detox.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAG3C,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAA;IAClB,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ;;;;;;;;EAsChD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,EAAE,CA6BpE;AAoCD,UAAU,0BAA0B;IAClC,uCAAuC;IACvC,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,sFAAsF;IACtF,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,0BAA0B,CA8BnF;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAOxD;AA4DD;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6DhF"}