@limrun/api 0.48.1 → 0.48.3

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/exec-client.d.mts +67 -1
  3. package/exec-client.d.mts.map +1 -1
  4. package/exec-client.d.ts +67 -1
  5. package/exec-client.d.ts.map +1 -1
  6. package/exec-client.js +67 -1
  7. package/exec-client.js.map +1 -1
  8. package/exec-client.mjs +67 -1
  9. package/exec-client.mjs.map +1 -1
  10. package/index.d.mts +1 -1
  11. package/index.d.mts.map +1 -1
  12. package/index.d.ts +1 -1
  13. package/index.d.ts.map +1 -1
  14. package/index.js.map +1 -1
  15. package/index.mjs.map +1 -1
  16. package/instance-client.d.mts +4 -1
  17. package/instance-client.d.mts.map +1 -1
  18. package/instance-client.d.ts +4 -1
  19. package/instance-client.d.ts.map +1 -1
  20. package/instance-client.js +2 -2
  21. package/instance-client.js.map +1 -1
  22. package/instance-client.mjs +2 -2
  23. package/instance-client.mjs.map +1 -1
  24. package/package.json +1 -1
  25. package/resources/xcode-instances-helpers.d.mts +37 -2
  26. package/resources/xcode-instances-helpers.d.mts.map +1 -1
  27. package/resources/xcode-instances-helpers.d.ts +37 -2
  28. package/resources/xcode-instances-helpers.d.ts.map +1 -1
  29. package/resources/xcode-instances-helpers.js +27 -2
  30. package/resources/xcode-instances-helpers.js.map +1 -1
  31. package/resources/xcode-instances-helpers.mjs +27 -2
  32. package/resources/xcode-instances-helpers.mjs.map +1 -1
  33. package/src/exec-client.ts +127 -2
  34. package/src/index.ts +3 -0
  35. package/src/instance-client.ts +12 -4
  36. package/src/resources/xcode-instances-helpers.ts +69 -3
  37. package/src/version.ts +1 -1
  38. package/version.d.mts +1 -1
  39. package/version.d.ts +1 -1
  40. package/version.js +1 -1
  41. package/version.mjs +1 -1
@@ -48,9 +48,31 @@ export type XcodeBuildExecRequest = {
48
48
  workspace?: string;
49
49
  project?: string;
50
50
  scheme?: string;
51
- sdk?: 'iphonesimulator' | 'iphoneos' | 'watchsimulator' | 'watchos';
51
+ sdk?: 'iphonesimulator' | 'iphoneos' | 'watchsimulator' | 'watchos' | 'appletvos' | 'xros';
52
52
  configuration?: 'Debug' | 'Release';
53
53
  artifactName?: string;
54
+ /**
55
+ * xcodebuild action. 'build' (default) produces one .app; with a
56
+ * simulator attached it is installed there. 'build-for-testing' compiles
57
+ * the scheme's test targets and, with a simulator attached, runs them
58
+ * there, streaming per-case results as 'xctest' events; the exec exits
59
+ * non-zero when tests fail. Requires the iphonesimulator sdk.
60
+ */
61
+ action?: 'build' | 'build-for-testing';
62
+ /**
63
+ * Run only these tests, in xcodebuild's -only-testing format:
64
+ * Target[/Class[/method]]. Mutually exclusive with skipTesting; only
65
+ * valid with action 'build-for-testing'.
66
+ */
67
+ onlyTesting?: string[];
68
+ /** Skip these tests, in xcodebuild's -skip-testing format. */
69
+ skipTesting?: string[];
70
+ /**
71
+ * When false, 'build-for-testing' compiles the test targets but does not
72
+ * run them even when a simulator is attached. Servers without support
73
+ * ignore the field and still run the tests.
74
+ */
75
+ runTests?: boolean;
54
76
  };
55
77
  xcodegen?: {
56
78
  spec?: string;
@@ -85,6 +107,8 @@ export type XcodeBuildExecRequest = {
85
107
  */
86
108
  env?: string[];
87
109
  gitInit?: boolean;
110
+ /** How xcodebuild stdout is processed before streaming. Defaults to 'xcbeautify'. */
111
+ logProcessor?: 'xcbeautify' | 'none';
88
112
  signedUploadUrl?: string;
89
113
  /**
90
114
  * ID of the Limrun asset signedUploadUrl targets, when it was minted from
@@ -234,6 +258,12 @@ export type ExecOptions = {
234
258
  apiUrl: string;
235
259
  token: string;
236
260
  log?: (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => void;
261
+ /**
262
+ * Called for each XCTest event as it streams from a build-for-testing run:
263
+ * one per finished test case, then the terminal summary. The same data
264
+ * lands accumulated on ExecResult.xctest.
265
+ */
266
+ onXctestEvent?: (event: XctestEvent) => void;
237
267
  };
238
268
 
239
269
  export type ExecResult = {
@@ -252,6 +282,13 @@ export type ExecResult = {
252
282
  * without a playstore request, or when the server predates the feature.
253
283
  */
254
284
  playstore?: PlaystoreEvent;
285
+ /**
286
+ * XCTest results streamed by a build-for-testing run: every finished case
287
+ * plus the terminal summary. Absent for plain builds and servers that
288
+ * predate the feature. A missing summary means the run died partway; the
289
+ * cases already streamed still stand.
290
+ */
291
+ xctest?: { cases: XctestCaseEvent[]; summary?: XctestSummaryEvent };
255
292
  /**
256
293
  * True when the client gave up waiting for the build's event stream. The
257
294
  * exit code is fabricated in that case; the remote build may still be
@@ -268,6 +305,32 @@ export type ExecResult = {
268
305
  incomplete?: { reason: 'timeout' | 'stream-lost' | 'stream-closed'; message: string };
269
306
  };
270
307
 
308
+ export type XctestCaseEvent = {
309
+ type: 'case';
310
+ /**
311
+ * The test class as XCTest reports it: module-qualified for Swift
312
+ * ('AppTests.LoginTests'), bare for Objective-C.
313
+ */
314
+ testClass: string;
315
+ method: string;
316
+ passed: boolean;
317
+ durationMs: number;
318
+ /** First failure message of a failed case. */
319
+ failureMessage?: string;
320
+ };
321
+
322
+ export type XctestSummaryEvent = {
323
+ type: 'summary';
324
+ passed: number;
325
+ failed: number;
326
+ /** True only once every test in scope ran to completion. */
327
+ planFinished: boolean;
328
+ /** Crash or per-target failure detail when the run degraded. */
329
+ error?: string;
330
+ };
331
+
332
+ export type XctestEvent = XctestCaseEvent | XctestSummaryEvent;
333
+
271
334
  export type AppStoreEvent = {
272
335
  /** 'unknown' means an App Store upload event arrived but its payload was unreadable. */
273
336
  state: 'uploading' | 'processing' | 'accepted' | 'failed' | 'unknown';
@@ -389,6 +452,13 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
389
452
  private detached = false;
390
453
  private appStoreEvent: AppStoreEvent | null = null;
391
454
  private playstoreEvent: PlaystoreEvent | null = null;
455
+ private xctestCases: XctestCaseEvent[] = [];
456
+ private xctestSummary: XctestSummaryEvent | null = null;
457
+ // Highest server-assigned event id already dispatched. The server numbers
458
+ // every frame and replays the stream from the start on reconnect; skipping
459
+ // ids at or below this mark drops the replayed prefix for every event type
460
+ // at once, and stays correct if the server ever honors Last-Event-ID.
461
+ private lastSeenEventId = -1;
392
462
  private readonly options: ExecOptions;
393
463
  private readonly log: (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => void;
394
464
 
@@ -555,7 +625,18 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
555
625
  let timeoutMs = 3600 * 1000;
556
626
  if (request.command === 'xcodebuild' && request.testflight) {
557
627
  timeoutMs += (Math.max(0, request.testflight.waitTimeoutSeconds ?? 0) + 900) * 1000;
558
- } else if (request.command === 'run') {
628
+ }
629
+ if (request.command === 'xcodebuild' && request.xcodebuild?.action === 'build-for-testing') {
630
+ // The test run happens after the build: products sync plus the suite,
631
+ // which the server caps at an hour per target. Two hours cover the
632
+ // common unit-plus-UI shape; the client cannot know the target count, so
633
+ // schemes with more targets may need a liveness-based deadline instead.
634
+ // Kept even with runTests: false: servers without run suppression still
635
+ // execute the suite, and the cap is an upper bound so a pure build
636
+ // finishes early regardless.
637
+ timeoutMs += 2 * 3600 * 1000;
638
+ }
639
+ if (request.command === 'run') {
559
640
  timeoutMs = (Math.max(1, request.timeoutSeconds ?? 3600) + 60) * 1000;
560
641
  }
561
642
  let exitCode: number;
@@ -629,6 +710,14 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
629
710
  ...('additionalMetadata' in request ? request.additionalMetadata ?? {} : {}),
630
711
  ...(this.appStoreEvent ? { appstore: this.appStoreEvent } : {}),
631
712
  ...(this.playstoreEvent ? { playstore: this.playstoreEvent } : {}),
713
+ ...(this.xctestCases.length > 0 || this.xctestSummary ?
714
+ {
715
+ xctest: {
716
+ cases: this.xctestCases,
717
+ ...(this.xctestSummary ? { summary: this.xctestSummary } : {}),
718
+ },
719
+ }
720
+ : {}),
632
721
  ...(timedOut ? { timedOut } : {}),
633
722
  ...(incomplete ? { incomplete } : {}),
634
723
  };
@@ -768,6 +857,15 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
768
857
  this.streamDeadSince = 0;
769
858
  proofOfLifeThisCycle = true;
770
859
  lastStreamError = undefined;
860
+ const eventId = message.id !== undefined ? parseInt(message.id, 10) : NaN;
861
+ if (!Number.isNaN(eventId)) {
862
+ if (eventId <= this.lastSeenEventId) {
863
+ // A reconnect replays the stream from the start; this frame was
864
+ // already dispatched on an earlier connection.
865
+ return;
866
+ }
867
+ this.lastSeenEventId = eventId;
868
+ }
771
869
  const data = typeof message.data === 'string' ? message.data : String(message.data ?? '');
772
870
  const eventType = message.event;
773
871
  if (eventType === 'command') {
@@ -795,6 +893,33 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
795
893
  this.playstoreEvent = { state: 'unknown' };
796
894
  this.log('warn', `SSE playstore event has invalid data: ${data}`);
797
895
  }
896
+ } else if (eventType === 'xctest') {
897
+ let event: XctestEvent | undefined;
898
+ try {
899
+ const parsed = JSON.parse(data) as XctestEvent;
900
+ if (parsed?.type === 'case') {
901
+ this.xctestCases.push(parsed);
902
+ event = parsed;
903
+ } else if (parsed?.type === 'summary') {
904
+ this.xctestSummary = parsed;
905
+ event = parsed;
906
+ } else {
907
+ // The simulator owns the frame vocabulary and may grow it; the
908
+ // callback's contract is cases and summaries only.
909
+ this.log('debug', `SSE xctest event of unknown type ignored: ${data}`);
910
+ }
911
+ } catch {
912
+ this.log('warn', `SSE xctest event has invalid data: ${data}`);
913
+ }
914
+ if (event) {
915
+ try {
916
+ this.options.onXctestEvent?.(event);
917
+ } catch (err) {
918
+ // The consumer's callback, not the frame: the run must not be
919
+ // derailed and the log must not blame the payload.
920
+ this.log('warn', `onXctestEvent callback threw: ${String(err)}`);
921
+ }
922
+ }
798
923
  } else if (eventType === 'exitCode') {
799
924
  const exitCode = parseInt(data, 10);
800
925
  if (Number.isNaN(exitCode)) {
package/src/index.ts CHANGED
@@ -41,6 +41,9 @@ export {
41
41
  type AppStoreUploadConfig,
42
42
  type PlaystoreEvent,
43
43
  type WebhookConfig,
44
+ type XctestCaseEvent,
45
+ type XctestEvent,
46
+ type XctestSummaryEvent,
44
47
  } from './exec-client';
45
48
  export {
46
49
  type XcodeCreateClientParams,
@@ -73,7 +73,7 @@ export type InstanceClient = {
73
73
  /**
74
74
  * Fetch Android UI hierarchy from UIAutomator.
75
75
  */
76
- getElementTree: () => Promise<ElementTreeData>;
76
+ getElementTree: (options?: AndroidElementTreeOptions) => Promise<ElementTreeData>;
77
77
  /**
78
78
  * Find matching elements by Android-native selector.
79
79
  */
@@ -368,6 +368,10 @@ export type AndroidElementNode = {
368
368
  };
369
369
  };
370
370
 
371
+ export type AndroidElementTreeOptions = {
372
+ waitForIdleTimeoutMs?: number;
373
+ };
374
+
371
375
  /**
372
376
  * Configuration options for creating an Instance API client
373
377
  */
@@ -796,7 +800,7 @@ type ServerMessage =
796
800
 
797
801
  type CommandRequestMap = {
798
802
  screenshot: {};
799
- getElementTree: {};
803
+ getElementTree: AndroidElementTreeOptions;
800
804
  findElement: { selector: AndroidSelector; limit?: number };
801
805
  tap: AndroidElementTarget;
802
806
  setText: { text: string } & AndroidElementTarget;
@@ -1309,8 +1313,12 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1309
1313
  return sendRequest('screenshot', {});
1310
1314
  };
1311
1315
 
1312
- const getElementTree = async (): Promise<ElementTreeData> => {
1313
- const result = await sendRequest('getElementTree', {});
1316
+ const getElementTree = async (options?: AndroidElementTreeOptions): Promise<ElementTreeData> => {
1317
+ const result = await sendRequest(
1318
+ 'getElementTree',
1319
+ options ?? {},
1320
+ 30_000 + (options?.waitForIdleTimeoutMs ?? 0),
1321
+ );
1314
1322
  return {
1315
1323
  xml: typeof result.xml === 'string' ? result.xml : '',
1316
1324
  nodes: Array.isArray(result.nodes) ? result.nodes : [],
@@ -20,6 +20,7 @@ import {
20
20
  type ExecChildProcess,
21
21
  type ExecRequest,
22
22
  type WebhookConfig,
23
+ type XctestEvent,
23
24
  } from '../exec-client';
24
25
 
25
26
  export type { AppStoreUploadConfig, WebhookConfig } from '../exec-client';
@@ -103,7 +104,7 @@ export type XcodeProjectConfig = {
103
104
  workspace?: string;
104
105
  project?: string;
105
106
  scheme?: string;
106
- sdk?: 'iphonesimulator' | 'iphoneos' | 'watchsimulator' | 'watchos';
107
+ sdk?: 'iphonesimulator' | 'iphoneos' | 'watchsimulator' | 'watchos' | 'appletvos' | 'xros';
107
108
  /**
108
109
  * xcodebuild configuration. Omit to use limbuild's project-type default:
109
110
  * Debug for native Xcode builds and Release for React Native / Expo builds.
@@ -117,6 +118,33 @@ export type XcodeProjectConfig = {
117
118
  * discovery picks the wrong bundle.
118
119
  */
119
120
  artifactName?: string;
121
+ /**
122
+ * xcodebuild action. 'build' (default) produces one .app; with a simulator
123
+ * attached it is installed there. 'build-for-testing' compiles the scheme's
124
+ * test targets and, with a simulator attached, runs them there: per-case
125
+ * results stream through XcodeBuildOptions.onXctestEvent and accumulate on
126
+ * ExecResult.xctest, and the exec exits non-zero when tests fail. Requires
127
+ * the iphonesimulator sdk. Older limbuild servers reject the field.
128
+ */
129
+ action?: 'build' | 'build-for-testing';
130
+ /**
131
+ * Run only these tests, in xcodebuild's -only-testing format:
132
+ * Target[/Class[/method]]. A bare target runs that target unfiltered;
133
+ * targets named by no entry are skipped. Mutually exclusive with
134
+ * skipTesting; only valid with action 'build-for-testing'.
135
+ */
136
+ onlyTesting?: string[];
137
+ /**
138
+ * Skip these tests, in xcodebuild's -skip-testing format. A bare target
139
+ * skips that whole target.
140
+ */
141
+ skipTesting?: string[];
142
+ /**
143
+ * When false, 'build-for-testing' compiles the test targets but does not
144
+ * run them even when a simulator is attached. Servers without support
145
+ * ignore the field and still run the tests.
146
+ */
147
+ runTests?: boolean;
120
148
  };
121
149
 
122
150
  export type XcodeGenConfig = {
@@ -251,6 +279,8 @@ export type XcodeBuildOptions = {
251
279
  * dependency resolution, and xcodebuild.
252
280
  */
253
281
  gitInit?: boolean;
282
+ /** How xcodebuild stdout is processed before streaming. Defaults to 'xcbeautify'. */
283
+ logProcessor?: 'xcbeautify' | 'none';
254
284
  /**
255
285
  * HTTP callback fired once the build reaches a terminal state (SUCCEEDED,
256
286
  * FAILED, or CANCELLED). The payload carries the result (execId, status,
@@ -260,6 +290,12 @@ export type XcodeBuildOptions = {
260
290
  * build. Older limbuild servers silently ignore this option.
261
291
  */
262
292
  webhook?: WebhookConfig;
293
+ /**
294
+ * Called for each XCTest event streamed by a build-for-testing run: one per
295
+ * finished test case, then the terminal summary. The same data accumulates
296
+ * on ExecResult.xctest.
297
+ */
298
+ onXctestEvent?: (event: XctestEvent) => void;
263
299
  };
264
300
 
265
301
  export type SimulatorInstallState =
@@ -843,6 +879,25 @@ export class XcodeInstances extends GeneratedXcodeInstances {
843
879
  if (options?.reactNative?.devServerURL && settings?.configuration === 'Release') {
844
880
  throw new Error('reactNative.devServerURL is only supported for Debug builds');
845
881
  }
882
+ if (settings?.onlyTesting?.length && settings?.skipTesting?.length) {
883
+ throw new Error('onlyTesting and skipTesting are mutually exclusive; pass one');
884
+ }
885
+ if (
886
+ (settings?.onlyTesting?.length || settings?.skipTesting?.length) &&
887
+ settings?.action !== 'build-for-testing'
888
+ ) {
889
+ throw new Error("onlyTesting/skipTesting require action: 'build-for-testing'");
890
+ }
891
+ if (settings?.runTests === false && settings?.action !== 'build-for-testing') {
892
+ throw new Error("runTests: false requires action: 'build-for-testing'");
893
+ }
894
+ if (
895
+ settings?.action === 'build-for-testing' &&
896
+ settings.sdk !== undefined &&
897
+ settings.sdk !== 'iphonesimulator'
898
+ ) {
899
+ throw new Error("action: 'build-for-testing' requires the iphonesimulator sdk");
900
+ }
846
901
  if (options?.buildSettings) {
847
902
  validateBuildSettings(options.buildSettings);
848
903
  }
@@ -859,9 +914,20 @@ export class XcodeInstances extends GeneratedXcodeInstances {
859
914
  ...(options?.buildSettings && { buildSettings: options.buildSettings }),
860
915
  ...(options?.env?.length && { env: options.env }),
861
916
  ...(options?.gitInit !== undefined && { gitInit: options.gitInit }),
917
+ ...(options?.logProcessor && { logProcessor: options.logProcessor }),
862
918
  ...(options?.webhook && { webhook: options.webhook }),
863
919
  };
864
920
 
921
+ // One options object for both exec call sites, so a future per-exec
922
+ // option cannot be added to one branch and silently dropped from the
923
+ // other.
924
+ const execOptions = {
925
+ apiUrl,
926
+ token,
927
+ log,
928
+ ...(options?.onXctestEvent && { onXctestEvent: options.onXctestEvent }),
929
+ };
930
+
865
931
  if (options?.upload && 'assetName' in options.upload) {
866
932
  const requestPromise = mintAssetUploadUrls(
867
933
  client.assets,
@@ -875,14 +941,14 @@ export class XcodeInstances extends GeneratedXcodeInstances {
875
941
  request.additionalMetadata = { signedDownloadUrl: asset.signedDownloadUrl };
876
942
  return request;
877
943
  });
878
- return exec(requestPromise, { apiUrl, token, log });
944
+ return exec(requestPromise, execOptions);
879
945
  }
880
946
 
881
947
  if (options?.upload && 'signedUploadUrl' in options.upload) {
882
948
  request.signedUploadUrl = options.upload.signedUploadUrl;
883
949
  }
884
950
 
885
- return exec(request, { apiUrl, token, log });
951
+ return exec(request, execOptions);
886
952
  },
887
953
 
888
954
  run(commandLine: string, options?: XcodeRunOptions): ExecChildProcess {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.48.1'; // x-release-please-version
1
+ export const VERSION = '0.48.3'; // x-release-please-version
package/version.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.48.1";
1
+ export declare const VERSION = "0.48.3";
2
2
  //# sourceMappingURL=version.d.mts.map
package/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.48.1";
1
+ export declare const VERSION = "0.48.3";
2
2
  //# sourceMappingURL=version.d.ts.map
package/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = '0.48.1'; // x-release-please-version
4
+ exports.VERSION = '0.48.3'; // x-release-please-version
5
5
  //# sourceMappingURL=version.js.map
package/version.mjs CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = '0.48.1'; // x-release-please-version
1
+ export const VERSION = '0.48.3'; // x-release-please-version
2
2
  //# sourceMappingURL=version.mjs.map