@testrigor/testrigor-cli 1.0.1 → 2.0.1

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.
@@ -15,21 +15,25 @@ const util = tslib_1.__importStar(require("util"));
15
15
  const path = tslib_1.__importStar(require("path"));
16
16
  const os = tslib_1.__importStar(require("os"));
17
17
  const fs_1 = tslib_1.__importDefault(require("@supercharge/fs"));
18
+ const utils_1 = require("./utils");
18
19
  const execFile = util.promisify(childProcess.execFile);
19
20
  const PROJECT_ROOT = path.resolve(__dirname, "..", "..");
20
- const APPIUM_HOME = path.join(PROJECT_ROOT, ".appium");
21
21
  const DEFAULT_PORT = 4723;
22
22
  exports.DEFAULT_PORT = DEFAULT_PORT;
23
23
  const DEFAULT_BASE_PATH = "/wd/hub";
24
24
  exports.DEFAULT_BASE_PATH = DEFAULT_BASE_PATH;
25
- const PID_DIR = path.join(os.homedir(), ".testrigor");
25
+ const PID_DIR = (0, utils_1.getHomeDir)();
26
26
  exports.PID_DIR = PID_DIR;
27
27
  const PID_FILE = path.join(PID_DIR, "appium.pid");
28
28
  exports.PID_FILE = PID_FILE;
29
29
  const LOG_FILE = path.join(os.tmpdir(), "testrigor-appium.log");
30
+ // Appium writes its drivers here (hundreds of MB). It must live under the
31
+ // user's home, not the install directory: a global install is often
32
+ // root-owned, and `npm update -g` would wipe the drivers on every upgrade.
33
+ const APPIUM_HOME = process.env.APPIUM_HOME || path.join((0, utils_1.getHomeDir)(), "appium-home");
30
34
  const REQUIRED_DRIVERS = [
31
35
  { name: "uiautomator2", version: "4.2.3" },
32
- { name: "xcuitest", version: "9.2.4" },
36
+ { name: "xcuitest", version: "10.43.1" },
33
37
  ];
34
38
  exports.REQUIRED_DRIVERS = REQUIRED_DRIVERS;
35
39
  function getAppiumBin() {
@@ -42,16 +46,34 @@ function getAppiumEnv() {
42
46
  CHROMELABS_URL: "https://googlechromelabs.github.io",
43
47
  };
44
48
  }
49
+ /**
50
+ * Appium refuses to start unless APPIUM_HOME already exists, so create it
51
+ * before any invocation and surface a permission problem as itself rather
52
+ * than as a generic "appium is broken".
53
+ */
54
+ async function ensureAppiumHome() {
55
+ try {
56
+ await fs_1.default.ensureDir(APPIUM_HOME);
57
+ }
58
+ catch (error) {
59
+ throw new Error(`Cannot create the mobile server directory at ${APPIUM_HOME}: ` +
60
+ `${error instanceof Error ? error.message : String(error)}\n` +
61
+ "Check that you own that directory, or point APPIUM_HOME at a writable one.");
62
+ }
63
+ }
45
64
  async function checkAppiumInstalled() {
46
65
  const appiumBin = getAppiumBin();
47
66
  if (!(await fs_1.default.exists(appiumBin))) {
48
- throw new Error(`Appium binary not found at ${appiumBin}. Try reinstalling testrigor-cli.`);
67
+ throw new Error(`Appium binary not found at ${appiumBin}. ` +
68
+ "Reinstall the CLI with: npm install -g @testrigor/testrigor-cli");
49
69
  }
70
+ await ensureAppiumHome();
50
71
  try {
51
72
  await execFile(appiumBin, ["--version"], { env: getAppiumEnv() });
52
73
  }
53
- catch {
54
- throw new Error(`Appium binary at ${appiumBin} is not working. Try reinstalling testrigor-cli.`);
74
+ catch (error) {
75
+ const detail = error instanceof Error ? error.message : String(error);
76
+ throw new Error(`Appium binary at ${appiumBin} failed to run (APPIUM_HOME=${APPIUM_HOME}).\n${detail}`);
55
77
  }
56
78
  }
57
79
  async function getDriverStatus(driverName, requiredVersion) {
@@ -92,10 +114,7 @@ async function ensureDriver(driverName, requiredVersion) {
92
114
  }
93
115
  catch (error) {
94
116
  const raw = error instanceof Error ? error.message : String(error);
95
- return raw
96
- .split("\n")[0]
97
- .replace(/^Command failed:.*?Error:\s*/, "")
98
- .trim();
117
+ return raw.replace(/^Command failed:.*?Error:\s*/, "").trim();
99
118
  }
100
119
  }
101
120
  function isProcessRunning(pid) {
@@ -123,7 +142,36 @@ async function getRunningAppiumPid() {
123
142
  await fs_1.default.removeFile(PID_FILE);
124
143
  return null;
125
144
  }
126
- async function healthCheck(port, retries = 15, interval = 2000) {
145
+ /**
146
+ * Last lines of the Appium log, so a server that dies on startup reports the
147
+ * reason instead of just a health-check timeout. The log is shared across
148
+ * invocations, so only bytes written after `fromOffset` are considered.
149
+ */
150
+ function readLogTail(fromOffset, lines = 20) {
151
+ let fd;
152
+ try {
153
+ fd = fs.openSync(LOG_FILE, "r");
154
+ const size = fs.fstatSync(fd).size;
155
+ if (size <= fromOffset)
156
+ return "";
157
+ const maxTailBytes = 64 * 1024;
158
+ const start = Math.max(fromOffset, size - maxTailBytes);
159
+ const buffer = Buffer.alloc(size - start);
160
+ fs.readSync(fd, buffer, 0, buffer.length, start);
161
+ const content = buffer.toString("utf8").trimEnd();
162
+ if (!content)
163
+ return "";
164
+ return content.split("\n").slice(-lines).join("\n");
165
+ }
166
+ catch {
167
+ return "";
168
+ }
169
+ finally {
170
+ if (fd !== undefined)
171
+ fs.closeSync(fd);
172
+ }
173
+ }
174
+ async function healthCheck(port, logOffset, retries = 15, interval = 2000) {
127
175
  const url = `http://127.0.0.1:${port}/wd/hub/status`;
128
176
  let lastError = "";
129
177
  for (let i = 0; i < retries; i++) {
@@ -139,7 +187,11 @@ async function healthCheck(port, retries = 15, interval = 2000) {
139
187
  }
140
188
  await new Promise((r) => setTimeout(r, interval));
141
189
  }
142
- throw new Error(`Mobile server not ready after ${retries} attempts: ${lastError}`);
190
+ const tail = readLogTail(logOffset);
191
+ throw new Error(`Mobile server not ready after ${retries} attempts: ${lastError}` +
192
+ (tail
193
+ ? `\nLast lines of ${LOG_FILE}:\n${tail}`
194
+ : `\nSee ${LOG_FILE} for details.`));
143
195
  }
144
196
  async function startAppium(port = DEFAULT_PORT, logLevel = "debug") {
145
197
  const existingPid = await getRunningAppiumPid();
@@ -147,7 +199,9 @@ async function startAppium(port = DEFAULT_PORT, logLevel = "debug") {
147
199
  return { pid: existingPid, alreadyRunning: true };
148
200
  }
149
201
  const appiumBin = getAppiumBin();
202
+ await ensureAppiumHome();
150
203
  const logStream = fs.openSync(LOG_FILE, "a");
204
+ const logOffset = fs.fstatSync(logStream).size;
151
205
  const appiumProcess = childProcess.spawn(appiumBin, [
152
206
  "--port",
153
207
  String(port),
@@ -170,7 +224,7 @@ async function startAppium(port = DEFAULT_PORT, logLevel = "debug") {
170
224
  await fs_1.default.ensureDir(PID_DIR);
171
225
  await fs_1.default.writeFile(PID_FILE, String(pid));
172
226
  try {
173
- await healthCheck(port);
227
+ await healthCheck(port, logOffset);
174
228
  }
175
229
  catch (err) {
176
230
  killSafe(pid);
@@ -4,12 +4,11 @@ exports.uploadClientCertificate = uploadClientCertificate;
4
4
  const tslib_1 = require("tslib");
5
5
  const fs = tslib_1.__importStar(require("fs"));
6
6
  const path = tslib_1.__importStar(require("path"));
7
- const node_fetch_commonjs_1 = tslib_1.__importDefault(require("node-fetch-commonjs"));
8
7
  const type_guards_1 = require("./type-guards");
9
8
  async function uploadClientCertificate(api2BaseUrl, testSuiteId, token, localFilePath, password) {
10
9
  const fileName = path.basename(localFilePath);
11
10
  const certBase64 = fs.readFileSync(localFilePath).toString("base64");
12
- const response = await (0, node_fetch_commonjs_1.default)(`${api2BaseUrl}/apps/${testSuiteId}/test_data/global_variables/client_certificate`, {
11
+ const response = await fetch(`${api2BaseUrl}/apps/${testSuiteId}/test_data/global_variables/client_certificate`, {
13
12
  method: "POST",
14
13
  body: JSON.stringify({ fileName, certBase64, password }),
15
14
  headers: {
@@ -1,11 +1,5 @@
1
- import FormData from "form-data";
2
1
  import { ChromeExtensionInput } from "../types";
3
- export interface ChromeExtensionFormDataResult {
4
- form: FormData;
5
- cleanup: () => void;
6
- }
7
2
  /**
8
- * Converts ChromeExtensionInput[] -> ChromeExtensionOverridesRequest -> FormData
9
- * Returns both the FormData and a cleanup function to close streams.
3
+ * Builds the multipart body for the chrome extension overrides of a retest.
10
4
  */
11
- export declare function buildChromeExtensionFormData(chromeExtensions: ChromeExtensionInput[]): ChromeExtensionFormDataResult;
5
+ export declare function buildChromeExtensionFormData(chromeExtensions: ChromeExtensionInput[]): Promise<FormData>;
@@ -2,48 +2,25 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildChromeExtensionFormData = buildChromeExtensionFormData;
4
4
  const tslib_1 = require("tslib");
5
- const form_data_1 = tslib_1.__importDefault(require("form-data"));
6
- const fs_1 = tslib_1.__importDefault(require("@supercharge/fs"));
5
+ const fs_1 = require("fs");
7
6
  const path = tslib_1.__importStar(require("path"));
8
- function toChromeExtensionOverrideItem(input) {
7
+ async function readChromeExtension(input) {
9
8
  try {
10
- return {
11
- name: input.name,
12
- file: fs_1.default.createReadStream(input.file),
13
- };
9
+ return await (0, fs_1.openAsBlob)(input.file);
14
10
  }
15
11
  catch (error) {
16
12
  throw new Error(`Failed to read chrome extension file "${input.file}": ${error instanceof Error ? error.message : String(error)}`);
17
13
  }
18
14
  }
19
- function toChromeExtensionOverridesRequest(extensionInputs) {
20
- const chromeExtensions = extensionInputs.map(toChromeExtensionOverrideItem);
21
- const streams = chromeExtensions.map((item) => item.file);
22
- return {
23
- request: { chromeExtensions },
24
- streams,
25
- };
26
- }
27
15
  /**
28
- * Converts ChromeExtensionInput[] -> ChromeExtensionOverridesRequest -> FormData
29
- * Returns both the FormData and a cleanup function to close streams.
16
+ * Builds the multipart body for the chrome extension overrides of a retest.
30
17
  */
31
- function buildChromeExtensionFormData(chromeExtensions) {
32
- const { request, streams } = toChromeExtensionOverridesRequest(chromeExtensions);
33
- const form = new form_data_1.default();
34
- request.chromeExtensions.forEach((ext, index) => {
18
+ async function buildChromeExtensionFormData(chromeExtensions) {
19
+ const files = await Promise.all(chromeExtensions.map(readChromeExtension));
20
+ const form = new FormData();
21
+ chromeExtensions.forEach((ext, index) => {
35
22
  form.append(`chromeExtensions[${index}].name`, ext.name);
36
- const fileName = path.basename(chromeExtensions[index].file);
37
- form.append(`chromeExtensions[${index}].file`, ext.file, {
38
- filename: fileName,
39
- });
23
+ form.append(`chromeExtensions[${index}].file`, files[index], path.basename(ext.file));
40
24
  });
41
- const cleanup = () => {
42
- streams.forEach((stream) => {
43
- if (!stream.destroyed) {
44
- stream.destroy();
45
- }
46
- });
47
- };
48
- return { form, cleanup };
25
+ return form;
49
26
  }
package/dist/util/trtc.js CHANGED
@@ -6,9 +6,9 @@ exports.download = download;
6
6
  const tslib_1 = require("tslib");
7
7
  const fs_1 = tslib_1.__importDefault(require("@supercharge/fs"));
8
8
  const fs = tslib_1.__importStar(require("fs"));
9
- const node_fetch_commonjs_1 = tslib_1.__importDefault(require("node-fetch-commonjs"));
10
9
  const os = tslib_1.__importStar(require("os"));
11
10
  const path = tslib_1.__importStar(require("path"));
11
+ const stream_1 = require("stream");
12
12
  const tar = tslib_1.__importStar(require("tar-stream"));
13
13
  const unzip = tslib_1.__importStar(require("unzip-stream"));
14
14
  const zlib = tslib_1.__importStar(require("zlib"));
@@ -155,14 +155,14 @@ async function downloadOnce(dir) {
155
155
  };
156
156
  armStallTimer();
157
157
  try {
158
- const response = await (0, node_fetch_commonjs_1.default)(getUrl(), { signal: controller.signal });
158
+ const response = await fetch(getUrl(), { signal: controller.signal });
159
159
  if (!response.ok) {
160
160
  throw new Error(`unexpected response ${response.statusText}`);
161
161
  }
162
- const stream = response.body;
163
- if (stream == null) {
162
+ if (response.body == null) {
164
163
  throw new Error("unexpected null response body");
165
164
  }
165
+ const stream = stream_1.Readable.fromWeb(response.body);
166
166
  const extraction = os.platform() === WIN32_PLATFORM
167
167
  ? extractZip(dir, stream)
168
168
  : extractTar(dir, stream);
@@ -2,15 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.uploadVariableFile = uploadVariableFile;
4
4
  const tslib_1 = require("tslib");
5
- const fs_1 = tslib_1.__importDefault(require("@supercharge/fs"));
5
+ const fs_1 = require("fs");
6
6
  const path = tslib_1.__importStar(require("path"));
7
- const node_fetch_commonjs_1 = tslib_1.__importDefault(require("node-fetch-commonjs"));
8
7
  const utils_1 = require("./utils");
9
8
  const type_guards_1 = require("./type-guards");
10
9
  async function uploadVariableFile(api2BaseUrl, testSuiteId, token, localFilePath) {
11
10
  const fileName = path.basename(localFilePath);
12
- const fileSize = fs_1.default.statSync(localFilePath).size;
13
- const presignedResp = await (0, node_fetch_commonjs_1.default)(`${api2BaseUrl}/apps/${testSuiteId}/test_data/global_variables/file/pre_signed_url`, {
11
+ const presignedResp = await fetch(`${api2BaseUrl}/apps/${testSuiteId}/test_data/global_variables/file/pre_signed_url`, {
14
12
  method: "POST",
15
13
  body: JSON.stringify({ fileName }),
16
14
  headers: {
@@ -28,13 +26,9 @@ async function uploadVariableFile(api2BaseUrl, testSuiteId, token, localFilePath
28
26
  !presignedJson.data) {
29
27
  throw new Error(`Failed to request presigned URL for variable file '${fileName}': ${presignedJson.message}`);
30
28
  }
31
- const fileStream = fs_1.default.createReadStream(localFilePath);
32
- const putResp = await (0, node_fetch_commonjs_1.default)(presignedJson.data.url, {
29
+ const putResp = await fetch(presignedJson.data.url, {
33
30
  method: "PUT",
34
- body: fileStream,
35
- headers: {
36
- "Content-length": `${fileSize}`,
37
- },
31
+ body: await (0, fs_1.openAsBlob)(localFilePath),
38
32
  });
39
33
  if (!putResp.ok) {
40
34
  throw new Error(`Failed to upload variable file '${fileName}' to S3: ${putResp.statusText}`);