@validate.qa/runner 1.0.2 → 1.0.4
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/cli.js +450 -334
- package/dist/cli.mjs +455 -339
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -1410,7 +1410,7 @@ function selectorForValue(value) {
|
|
|
1410
1410
|
if (value.startsWith("~") || value.startsWith("id="))
|
|
1411
1411
|
return value;
|
|
1412
1412
|
if (value.includes(":id/"))
|
|
1413
|
-
return `id=${value
|
|
1413
|
+
return `id=${value}`;
|
|
1414
1414
|
return `~${value}`;
|
|
1415
1415
|
}
|
|
1416
1416
|
function selectorCandidatesForStep(step) {
|
|
@@ -7557,7 +7557,7 @@ Each entry is exactly one of:
|
|
|
7557
7557
|
const systemPrompt = context.mode === "survey" ? SURVEY_SYSTEM_PROMPT : exports.SCENARIO_SYSTEM_PROMPT;
|
|
7558
7558
|
const userPrompt = formatEvidenceForPlanner(compressed, evidence, context);
|
|
7559
7559
|
logs2.push(`AI planner: ~${Math.ceil(userPrompt.length / 3.5)} input tokens`);
|
|
7560
|
-
const aiClient = options.modelOverride ? (0, ai_provider_js_1.getClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getAIClient)();
|
|
7560
|
+
const aiClient = options.aiClient ?? (options.modelOverride ? (0, ai_provider_js_1.getClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getAIClient)());
|
|
7561
7561
|
const messages = [
|
|
7562
7562
|
{ role: "system", content: systemPrompt },
|
|
7563
7563
|
{ role: "user", content: userPrompt }
|
|
@@ -7769,6 +7769,7 @@ var require_mobile_discovery_prompts = __commonJS({
|
|
|
7769
7769
|
exports.formatMobileEvidenceForPlanner = formatMobileEvidenceForPlanner;
|
|
7770
7770
|
exports.buildMobileSiteMap = buildMobileSiteMap;
|
|
7771
7771
|
exports.runMobileAIPlanner = runMobileAIPlanner;
|
|
7772
|
+
var ai_provider_js_1 = require_ai_provider();
|
|
7772
7773
|
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
7773
7774
|
exports.MOBILE_SURVEY_SYSTEM_PROMPT = `You are a feature analyst for a native mobile application (iOS or Android). You receive evidence from an AI explorer that mapped the app's screens by tapping, typing, and swiping through the live UI. Your job is to organize screens into meaningful feature groups.
|
|
7774
7775
|
|
|
@@ -8168,6 +8169,8 @@ ${constraints.join("\n")}`);
|
|
|
8168
8169
|
}
|
|
8169
8170
|
async function runMobileAIPlanner(input, options = {}) {
|
|
8170
8171
|
const planner = options.planner ?? discover_ai_planner_js_1.runAIPlanner;
|
|
8172
|
+
const shouldBuildMobileAIClient = (process.env.SERVER_URL ?? "").trim().length > 0 || !options.planner;
|
|
8173
|
+
const aiClient = options.aiClient ?? (shouldBuildMobileAIClient ? options.modelOverride ? (0, ai_provider_js_1.getMobileClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getMobileAIClient)() : void 0);
|
|
8171
8174
|
const siteMap = buildMobileSiteMap(input.graph, {
|
|
8172
8175
|
projectId: input.projectId,
|
|
8173
8176
|
appId: input.appId
|
|
@@ -8193,7 +8196,8 @@ ${constraints.join("\n")}`);
|
|
|
8193
8196
|
testBudget: input.testBudget
|
|
8194
8197
|
}, {
|
|
8195
8198
|
modelOverride: options.modelOverride,
|
|
8196
|
-
onAICall: options.onAICall
|
|
8199
|
+
onAICall: options.onAICall,
|
|
8200
|
+
...aiClient ? { aiClient } : {}
|
|
8197
8201
|
});
|
|
8198
8202
|
}
|
|
8199
8203
|
}
|
|
@@ -21743,7 +21747,7 @@ var require_util = __commonJS({
|
|
|
21743
21747
|
return path;
|
|
21744
21748
|
}
|
|
21745
21749
|
exports.normalize = normalize;
|
|
21746
|
-
function
|
|
21750
|
+
function join5(aRoot, aPath) {
|
|
21747
21751
|
if (aRoot === "") {
|
|
21748
21752
|
aRoot = ".";
|
|
21749
21753
|
}
|
|
@@ -21775,7 +21779,7 @@ var require_util = __commonJS({
|
|
|
21775
21779
|
}
|
|
21776
21780
|
return joined;
|
|
21777
21781
|
}
|
|
21778
|
-
exports.join =
|
|
21782
|
+
exports.join = join5;
|
|
21779
21783
|
exports.isAbsolute = function(aPath) {
|
|
21780
21784
|
return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
|
|
21781
21785
|
};
|
|
@@ -21948,7 +21952,7 @@ var require_util = __commonJS({
|
|
|
21948
21952
|
parsed.path = parsed.path.substring(0, index + 1);
|
|
21949
21953
|
}
|
|
21950
21954
|
}
|
|
21951
|
-
sourceURL =
|
|
21955
|
+
sourceURL = join5(urlGenerate(parsed), sourceURL);
|
|
21952
21956
|
}
|
|
21953
21957
|
return normalize(sourceURL);
|
|
21954
21958
|
}
|
|
@@ -243103,7 +243107,7 @@ var require_thread_stream = __commonJS({
|
|
|
243103
243107
|
var { version } = require_package();
|
|
243104
243108
|
var { EventEmitter: EventEmitter2 } = __require("events");
|
|
243105
243109
|
var { Worker } = __require("worker_threads");
|
|
243106
|
-
var { join:
|
|
243110
|
+
var { join: join5 } = __require("path");
|
|
243107
243111
|
var { pathToFileURL } = __require("url");
|
|
243108
243112
|
var { wait } = require_wait();
|
|
243109
243113
|
var {
|
|
@@ -243154,7 +243158,7 @@ var require_thread_stream = __commonJS({
|
|
|
243154
243158
|
function createWorker(stream, opts) {
|
|
243155
243159
|
const { filename, workerData } = opts;
|
|
243156
243160
|
const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
|
|
243157
|
-
const toExecute = bundlerOverrides["thread-stream-worker"] ||
|
|
243161
|
+
const toExecute = bundlerOverrides["thread-stream-worker"] || join5(__dirname, "lib", "worker.js");
|
|
243158
243162
|
const worker = new Worker(toExecute, {
|
|
243159
243163
|
...opts.workerOpts,
|
|
243160
243164
|
name: opts.workerOpts?.name || "thread-stream",
|
|
@@ -243622,7 +243626,7 @@ var require_transport = __commonJS({
|
|
|
243622
243626
|
var { createRequire } = __require("module");
|
|
243623
243627
|
var { existsSync: existsSync4 } = __require("fs");
|
|
243624
243628
|
var getCallers = require_caller();
|
|
243625
|
-
var { join:
|
|
243629
|
+
var { join: join5, isAbsolute, sep } = __require("path");
|
|
243626
243630
|
var { fileURLToPath } = __require("url");
|
|
243627
243631
|
var sleep = require_atomic_sleep();
|
|
243628
243632
|
var onExit = require_on_exit_leak_free();
|
|
@@ -243775,7 +243779,7 @@ var require_transport = __commonJS({
|
|
|
243775
243779
|
throw new Error("only one of target or targets can be specified");
|
|
243776
243780
|
}
|
|
243777
243781
|
if (targets) {
|
|
243778
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
243782
|
+
target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
|
|
243779
243783
|
options.targets = targets.filter((dest) => dest.target).map((dest) => {
|
|
243780
243784
|
return {
|
|
243781
243785
|
...dest,
|
|
@@ -243793,7 +243797,7 @@ var require_transport = __commonJS({
|
|
|
243793
243797
|
});
|
|
243794
243798
|
});
|
|
243795
243799
|
} else if (pipeline) {
|
|
243796
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
243800
|
+
target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
|
|
243797
243801
|
options.pipelines = [pipeline.map((dest) => {
|
|
243798
243802
|
return {
|
|
243799
243803
|
...dest,
|
|
@@ -243816,7 +243820,7 @@ var require_transport = __commonJS({
|
|
|
243816
243820
|
return origin;
|
|
243817
243821
|
}
|
|
243818
243822
|
if (origin === "pino/file") {
|
|
243819
|
-
return
|
|
243823
|
+
return join5(__dirname, "..", "file.js");
|
|
243820
243824
|
}
|
|
243821
243825
|
let fixTarget2;
|
|
243822
243826
|
for (const filePath of callers) {
|
|
@@ -244797,7 +244801,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
244797
244801
|
return circularValue;
|
|
244798
244802
|
}
|
|
244799
244803
|
let res = "";
|
|
244800
|
-
let
|
|
244804
|
+
let join5 = ",";
|
|
244801
244805
|
const originalIndentation = indentation;
|
|
244802
244806
|
if (Array.isArray(value)) {
|
|
244803
244807
|
if (value.length === 0) {
|
|
@@ -244811,7 +244815,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
244811
244815
|
indentation += spacer;
|
|
244812
244816
|
res += `
|
|
244813
244817
|
${indentation}`;
|
|
244814
|
-
|
|
244818
|
+
join5 = `,
|
|
244815
244819
|
${indentation}`;
|
|
244816
244820
|
}
|
|
244817
244821
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -244819,13 +244823,13 @@ ${indentation}`;
|
|
|
244819
244823
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
244820
244824
|
const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
244821
244825
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
244822
|
-
res +=
|
|
244826
|
+
res += join5;
|
|
244823
244827
|
}
|
|
244824
244828
|
const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
244825
244829
|
res += tmp !== void 0 ? tmp : "null";
|
|
244826
244830
|
if (value.length - 1 > maximumBreadth) {
|
|
244827
244831
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
244828
|
-
res += `${
|
|
244832
|
+
res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
244829
244833
|
}
|
|
244830
244834
|
if (spacer !== "") {
|
|
244831
244835
|
res += `
|
|
@@ -244846,7 +244850,7 @@ ${originalIndentation}`;
|
|
|
244846
244850
|
let separator = "";
|
|
244847
244851
|
if (spacer !== "") {
|
|
244848
244852
|
indentation += spacer;
|
|
244849
|
-
|
|
244853
|
+
join5 = `,
|
|
244850
244854
|
${indentation}`;
|
|
244851
244855
|
whitespace = " ";
|
|
244852
244856
|
}
|
|
@@ -244860,13 +244864,13 @@ ${indentation}`;
|
|
|
244860
244864
|
const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
|
|
244861
244865
|
if (tmp !== void 0) {
|
|
244862
244866
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
244863
|
-
separator =
|
|
244867
|
+
separator = join5;
|
|
244864
244868
|
}
|
|
244865
244869
|
}
|
|
244866
244870
|
if (keyLength > maximumBreadth) {
|
|
244867
244871
|
const removedKeys = keyLength - maximumBreadth;
|
|
244868
244872
|
res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
|
|
244869
|
-
separator =
|
|
244873
|
+
separator = join5;
|
|
244870
244874
|
}
|
|
244871
244875
|
if (spacer !== "" && separator.length > 1) {
|
|
244872
244876
|
res = `
|
|
@@ -244907,7 +244911,7 @@ ${originalIndentation}`;
|
|
|
244907
244911
|
}
|
|
244908
244912
|
const originalIndentation = indentation;
|
|
244909
244913
|
let res = "";
|
|
244910
|
-
let
|
|
244914
|
+
let join5 = ",";
|
|
244911
244915
|
if (Array.isArray(value)) {
|
|
244912
244916
|
if (value.length === 0) {
|
|
244913
244917
|
return "[]";
|
|
@@ -244920,7 +244924,7 @@ ${originalIndentation}`;
|
|
|
244920
244924
|
indentation += spacer;
|
|
244921
244925
|
res += `
|
|
244922
244926
|
${indentation}`;
|
|
244923
|
-
|
|
244927
|
+
join5 = `,
|
|
244924
244928
|
${indentation}`;
|
|
244925
244929
|
}
|
|
244926
244930
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -244928,13 +244932,13 @@ ${indentation}`;
|
|
|
244928
244932
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
244929
244933
|
const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
244930
244934
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
244931
|
-
res +=
|
|
244935
|
+
res += join5;
|
|
244932
244936
|
}
|
|
244933
244937
|
const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
244934
244938
|
res += tmp !== void 0 ? tmp : "null";
|
|
244935
244939
|
if (value.length - 1 > maximumBreadth) {
|
|
244936
244940
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
244937
|
-
res += `${
|
|
244941
|
+
res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
244938
244942
|
}
|
|
244939
244943
|
if (spacer !== "") {
|
|
244940
244944
|
res += `
|
|
@@ -244947,7 +244951,7 @@ ${originalIndentation}`;
|
|
|
244947
244951
|
let whitespace = "";
|
|
244948
244952
|
if (spacer !== "") {
|
|
244949
244953
|
indentation += spacer;
|
|
244950
|
-
|
|
244954
|
+
join5 = `,
|
|
244951
244955
|
${indentation}`;
|
|
244952
244956
|
whitespace = " ";
|
|
244953
244957
|
}
|
|
@@ -244956,7 +244960,7 @@ ${indentation}`;
|
|
|
244956
244960
|
const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
|
|
244957
244961
|
if (tmp !== void 0) {
|
|
244958
244962
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
244959
|
-
separator =
|
|
244963
|
+
separator = join5;
|
|
244960
244964
|
}
|
|
244961
244965
|
}
|
|
244962
244966
|
if (spacer !== "" && separator.length > 1) {
|
|
@@ -245014,20 +245018,20 @@ ${originalIndentation}`;
|
|
|
245014
245018
|
indentation += spacer;
|
|
245015
245019
|
let res2 = `
|
|
245016
245020
|
${indentation}`;
|
|
245017
|
-
const
|
|
245021
|
+
const join6 = `,
|
|
245018
245022
|
${indentation}`;
|
|
245019
245023
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
245020
245024
|
let i = 0;
|
|
245021
245025
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
245022
245026
|
const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
245023
245027
|
res2 += tmp2 !== void 0 ? tmp2 : "null";
|
|
245024
|
-
res2 +=
|
|
245028
|
+
res2 += join6;
|
|
245025
245029
|
}
|
|
245026
245030
|
const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
245027
245031
|
res2 += tmp !== void 0 ? tmp : "null";
|
|
245028
245032
|
if (value.length - 1 > maximumBreadth) {
|
|
245029
245033
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
245030
|
-
res2 += `${
|
|
245034
|
+
res2 += `${join6}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
245031
245035
|
}
|
|
245032
245036
|
res2 += `
|
|
245033
245037
|
${originalIndentation}`;
|
|
@@ -245043,16 +245047,16 @@ ${originalIndentation}`;
|
|
|
245043
245047
|
return '"[Object]"';
|
|
245044
245048
|
}
|
|
245045
245049
|
indentation += spacer;
|
|
245046
|
-
const
|
|
245050
|
+
const join5 = `,
|
|
245047
245051
|
${indentation}`;
|
|
245048
245052
|
let res = "";
|
|
245049
245053
|
let separator = "";
|
|
245050
245054
|
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
|
|
245051
245055
|
if (isTypedArrayWithEntries(value)) {
|
|
245052
|
-
res += stringifyTypedArray(value,
|
|
245056
|
+
res += stringifyTypedArray(value, join5, maximumBreadth);
|
|
245053
245057
|
keys = keys.slice(value.length);
|
|
245054
245058
|
maximumPropertiesToStringify -= value.length;
|
|
245055
|
-
separator =
|
|
245059
|
+
separator = join5;
|
|
245056
245060
|
}
|
|
245057
245061
|
if (deterministic) {
|
|
245058
245062
|
keys = sort(keys, comparator);
|
|
@@ -245063,13 +245067,13 @@ ${indentation}`;
|
|
|
245063
245067
|
const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
|
|
245064
245068
|
if (tmp !== void 0) {
|
|
245065
245069
|
res += `${separator}${strEscape(key2)}: ${tmp}`;
|
|
245066
|
-
separator =
|
|
245070
|
+
separator = join5;
|
|
245067
245071
|
}
|
|
245068
245072
|
}
|
|
245069
245073
|
if (keyLength > maximumBreadth) {
|
|
245070
245074
|
const removedKeys = keyLength - maximumBreadth;
|
|
245071
245075
|
res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
|
|
245072
|
-
separator =
|
|
245076
|
+
separator = join5;
|
|
245073
245077
|
}
|
|
245074
245078
|
if (separator !== "") {
|
|
245075
245079
|
res = `
|
|
@@ -285426,11 +285430,11 @@ var require_core = __commonJS({
|
|
|
285426
285430
|
function mapOuter(r, f) {
|
|
285427
285431
|
return r.matched ? f(r) : r;
|
|
285428
285432
|
}
|
|
285429
|
-
function ab(pa, pb,
|
|
285430
|
-
return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) =>
|
|
285433
|
+
function ab(pa, pb, join5) {
|
|
285434
|
+
return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join5(ma.value, vb, data, i, j)));
|
|
285431
285435
|
}
|
|
285432
|
-
function abc(pa, pb, pc,
|
|
285433
|
-
return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) =>
|
|
285436
|
+
function abc(pa, pb, pc, join5) {
|
|
285437
|
+
return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join5(ma.value, mb.value, vc, data, i, j))));
|
|
285434
285438
|
}
|
|
285435
285439
|
function action(f) {
|
|
285436
285440
|
return (data, i) => {
|
|
@@ -312624,7 +312628,7 @@ var require_utils2 = __commonJS({
|
|
|
312624
312628
|
module.exports = Utils;
|
|
312625
312629
|
Utils.prototype.makeDir = function(folder) {
|
|
312626
312630
|
const self = this;
|
|
312627
|
-
function
|
|
312631
|
+
function mkdirSync4(fpath) {
|
|
312628
312632
|
let resolvedPath = fpath.split(self.sep)[0];
|
|
312629
312633
|
fpath.split(self.sep).forEach(function(name) {
|
|
312630
312634
|
if (!name || name.substr(-1, 1) === ":") return;
|
|
@@ -312642,7 +312646,7 @@ var require_utils2 = __commonJS({
|
|
|
312642
312646
|
if (stat && stat.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
|
|
312643
312647
|
});
|
|
312644
312648
|
}
|
|
312645
|
-
|
|
312649
|
+
mkdirSync4(folder);
|
|
312646
312650
|
};
|
|
312647
312651
|
Utils.prototype.writeFileTo = function(path, content, overwrite, attr) {
|
|
312648
312652
|
const self = this;
|
|
@@ -314261,8 +314265,8 @@ var require_adm_zip = __commonJS({
|
|
|
314261
314265
|
return null;
|
|
314262
314266
|
}
|
|
314263
314267
|
function fixPath(zipPath) {
|
|
314264
|
-
const { join:
|
|
314265
|
-
return
|
|
314268
|
+
const { join: join5, normalize, sep } = pth.posix;
|
|
314269
|
+
return join5(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
|
|
314266
314270
|
}
|
|
314267
314271
|
function filenameFilter(filterfn) {
|
|
314268
314272
|
if (filterfn instanceof RegExp) {
|
|
@@ -319675,14 +319679,14 @@ var require_path_arg = __commonJS({
|
|
|
319675
319679
|
var require_find_made = __commonJS({
|
|
319676
319680
|
"../../node_modules/tar/node_modules/mkdirp/lib/find-made.js"(exports, module) {
|
|
319677
319681
|
"use strict";
|
|
319678
|
-
var { dirname } = __require("path");
|
|
319682
|
+
var { dirname: dirname2 } = __require("path");
|
|
319679
319683
|
var findMade = (opts, parent, path = void 0) => {
|
|
319680
319684
|
if (path === parent)
|
|
319681
319685
|
return Promise.resolve();
|
|
319682
319686
|
return opts.statAsync(parent).then(
|
|
319683
319687
|
(st) => st.isDirectory() ? path : void 0,
|
|
319684
319688
|
// will fail later
|
|
319685
|
-
(er) => er.code === "ENOENT" ? findMade(opts,
|
|
319689
|
+
(er) => er.code === "ENOENT" ? findMade(opts, dirname2(parent), parent) : void 0
|
|
319686
319690
|
);
|
|
319687
319691
|
};
|
|
319688
319692
|
var findMadeSync = (opts, parent, path = void 0) => {
|
|
@@ -319691,7 +319695,7 @@ var require_find_made = __commonJS({
|
|
|
319691
319695
|
try {
|
|
319692
319696
|
return opts.statSync(parent).isDirectory() ? path : void 0;
|
|
319693
319697
|
} catch (er) {
|
|
319694
|
-
return er.code === "ENOENT" ? findMadeSync(opts,
|
|
319698
|
+
return er.code === "ENOENT" ? findMadeSync(opts, dirname2(parent), parent) : void 0;
|
|
319695
319699
|
}
|
|
319696
319700
|
};
|
|
319697
319701
|
module.exports = { findMade, findMadeSync };
|
|
@@ -319702,10 +319706,10 @@ var require_find_made = __commonJS({
|
|
|
319702
319706
|
var require_mkdirp_manual = __commonJS({
|
|
319703
319707
|
"../../node_modules/tar/node_modules/mkdirp/lib/mkdirp-manual.js"(exports, module) {
|
|
319704
319708
|
"use strict";
|
|
319705
|
-
var { dirname } = __require("path");
|
|
319709
|
+
var { dirname: dirname2 } = __require("path");
|
|
319706
319710
|
var mkdirpManual = (path, opts, made) => {
|
|
319707
319711
|
opts.recursive = false;
|
|
319708
|
-
const parent =
|
|
319712
|
+
const parent = dirname2(path);
|
|
319709
319713
|
if (parent === path) {
|
|
319710
319714
|
return opts.mkdirAsync(path, opts).catch((er) => {
|
|
319711
319715
|
if (er.code !== "EISDIR")
|
|
@@ -319728,7 +319732,7 @@ var require_mkdirp_manual = __commonJS({
|
|
|
319728
319732
|
});
|
|
319729
319733
|
};
|
|
319730
319734
|
var mkdirpManualSync = (path, opts, made) => {
|
|
319731
|
-
const parent =
|
|
319735
|
+
const parent = dirname2(path);
|
|
319732
319736
|
opts.recursive = false;
|
|
319733
319737
|
if (parent === path) {
|
|
319734
319738
|
try {
|
|
@@ -319764,12 +319768,12 @@ var require_mkdirp_manual = __commonJS({
|
|
|
319764
319768
|
var require_mkdirp_native = __commonJS({
|
|
319765
319769
|
"../../node_modules/tar/node_modules/mkdirp/lib/mkdirp-native.js"(exports, module) {
|
|
319766
319770
|
"use strict";
|
|
319767
|
-
var { dirname } = __require("path");
|
|
319771
|
+
var { dirname: dirname2 } = __require("path");
|
|
319768
319772
|
var { findMade, findMadeSync } = require_find_made();
|
|
319769
319773
|
var { mkdirpManual, mkdirpManualSync } = require_mkdirp_manual();
|
|
319770
319774
|
var mkdirpNative = (path, opts) => {
|
|
319771
319775
|
opts.recursive = true;
|
|
319772
|
-
const parent =
|
|
319776
|
+
const parent = dirname2(path);
|
|
319773
319777
|
if (parent === path)
|
|
319774
319778
|
return opts.mkdirAsync(path, opts);
|
|
319775
319779
|
return findMade(opts, path).then((made) => opts.mkdirAsync(path, opts).then(() => made).catch((er) => {
|
|
@@ -319781,7 +319785,7 @@ var require_mkdirp_native = __commonJS({
|
|
|
319781
319785
|
};
|
|
319782
319786
|
var mkdirpNativeSync = (path, opts) => {
|
|
319783
319787
|
opts.recursive = true;
|
|
319784
|
-
const parent =
|
|
319788
|
+
const parent = dirname2(path);
|
|
319785
319789
|
if (parent === path)
|
|
319786
319790
|
return opts.mkdirSync(path, opts);
|
|
319787
319791
|
const made = findMadeSync(opts, path);
|
|
@@ -320186,7 +320190,7 @@ var require_path_reservations = __commonJS({
|
|
|
320186
320190
|
var assert = __require("assert");
|
|
320187
320191
|
var normalize = require_normalize_unicode();
|
|
320188
320192
|
var stripSlashes = require_strip_trailing_slashes();
|
|
320189
|
-
var { join:
|
|
320193
|
+
var { join: join5 } = __require("path");
|
|
320190
320194
|
var platform3 = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
|
|
320191
320195
|
var isWindows = platform3 === "win32";
|
|
320192
320196
|
module.exports = () => {
|
|
@@ -320195,7 +320199,7 @@ var require_path_reservations = __commonJS({
|
|
|
320195
320199
|
const getDirs = (path) => {
|
|
320196
320200
|
const dirs = path.split("/").slice(0, -1).reduce((set, path2) => {
|
|
320197
320201
|
if (set.length) {
|
|
320198
|
-
path2 =
|
|
320202
|
+
path2 = join5(set[set.length - 1], path2);
|
|
320199
320203
|
}
|
|
320200
320204
|
set.push(path2 || "/");
|
|
320201
320205
|
return set;
|
|
@@ -320263,7 +320267,7 @@ var require_path_reservations = __commonJS({
|
|
|
320263
320267
|
};
|
|
320264
320268
|
const reserve = (paths, fn) => {
|
|
320265
320269
|
paths = isWindows ? ["win32 parallelization disabled"] : paths.map((p) => {
|
|
320266
|
-
return stripSlashes(
|
|
320270
|
+
return stripSlashes(join5(normalize(p))).toLowerCase();
|
|
320267
320271
|
});
|
|
320268
320272
|
const dirs = new Set(
|
|
320269
320273
|
paths.map((path) => getDirs(path)).reduce((a, b) => a.concat(b))
|
|
@@ -324801,7 +324805,7 @@ var require_package4 = __commonJS({
|
|
|
324801
324805
|
"package.json"(exports, module) {
|
|
324802
324806
|
module.exports = {
|
|
324803
324807
|
name: "@validate.qa/runner",
|
|
324804
|
-
version: "1.0.
|
|
324808
|
+
version: "1.0.4",
|
|
324805
324809
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
324806
324810
|
bin: {
|
|
324807
324811
|
"validate-runner": "dist/cli.js"
|
|
@@ -324809,7 +324813,7 @@ var require_package4 = __commonJS({
|
|
|
324809
324813
|
scripts: {
|
|
324810
324814
|
build: "tsup",
|
|
324811
324815
|
dev: "tsx src/cli.ts",
|
|
324812
|
-
"test:mobile": "node --import tsx --test src/services/mobile/mobile-network-capture.smoke.test.ts",
|
|
324816
|
+
"test:mobile": "node --import tsx --test src/runner.mobile-ai-env.test.ts src/services/mobile/mobile-network-capture.smoke.test.ts",
|
|
324813
324817
|
prepublishOnly: "npm run build"
|
|
324814
324818
|
},
|
|
324815
324819
|
files: [
|
|
@@ -324887,9 +324891,9 @@ function getConfigDir() {
|
|
|
324887
324891
|
}
|
|
324888
324892
|
|
|
324889
324893
|
// src/runner.ts
|
|
324890
|
-
import { existsSync as
|
|
324891
|
-
import { join as
|
|
324892
|
-
import { homedir as
|
|
324894
|
+
import { existsSync as existsSync3, statSync, mkdirSync as mkdirSync3 } from "fs";
|
|
324895
|
+
import { join as join4 } from "path";
|
|
324896
|
+
import { homedir as homedir3 } from "os";
|
|
324893
324897
|
import { createHash } from "crypto";
|
|
324894
324898
|
|
|
324895
324899
|
// src/services/playwright-native.ts
|
|
@@ -325302,11 +325306,12 @@ function discoverRealIOSDevices() {
|
|
|
325302
325306
|
for (const [index, node] of iosNodes.entries()) {
|
|
325303
325307
|
const serial = typeof node.serial_num === "string" && node.serial_num.trim() ? node.serial_num.trim() : `usb-ios-device-${index + 1}`;
|
|
325304
325308
|
const name = typeof node._name === "string" && node._name.trim() ? node._name.trim() : "iOS Device";
|
|
325309
|
+
const version = runCommand("ideviceinfo", ["-u", serial, "-k", "ProductVersion"]) || "unknown";
|
|
325305
325310
|
devices.push({
|
|
325306
325311
|
id: serial,
|
|
325307
325312
|
name,
|
|
325308
325313
|
platform: "IOS",
|
|
325309
|
-
platformVersion:
|
|
325314
|
+
platformVersion: version,
|
|
325310
325315
|
state: "BOOTED",
|
|
325311
325316
|
isAvailable: true,
|
|
325312
325317
|
isPhysical: true
|
|
@@ -325381,7 +325386,7 @@ function parseAdbDevices() {
|
|
|
325381
325386
|
const output = runCommand("adb", ["devices", "-l"]);
|
|
325382
325387
|
if (!output) return [];
|
|
325383
325388
|
const lines = output.split("\n").slice(1);
|
|
325384
|
-
return lines.filter((line) => line.
|
|
325389
|
+
return lines.filter((line) => line.trim().length > 0).map((line) => {
|
|
325385
325390
|
const parts = line.trim().split(/\s+/);
|
|
325386
325391
|
const serial = parts[0];
|
|
325387
325392
|
const type = parts[1] ?? "";
|
|
@@ -325397,26 +325402,25 @@ function parseAdbDevices() {
|
|
|
325397
325402
|
device: props["device"],
|
|
325398
325403
|
transportId: props["transport_id"]
|
|
325399
325404
|
};
|
|
325400
|
-
}).filter((d) => d.type === "device");
|
|
325405
|
+
}).filter((d) => d.type === "device" || d.type === "unauthorized");
|
|
325401
325406
|
}
|
|
325402
325407
|
function getAndroidProp(serial, prop) {
|
|
325403
325408
|
return runCommand("adb", ["-s", serial, "shell", "getprop", prop]);
|
|
325404
325409
|
}
|
|
325405
325410
|
function discoverAndroidDevices() {
|
|
325406
325411
|
const adbDevices = parseAdbDevices();
|
|
325407
|
-
return adbDevices.filter((d) => {
|
|
325408
|
-
return d.type === "device";
|
|
325409
|
-
}).map((d) => {
|
|
325412
|
+
return adbDevices.filter((d) => d.type === "device" || d.type === "unauthorized").map((d) => {
|
|
325410
325413
|
const isEmulator = d.serial.startsWith("emulator-");
|
|
325414
|
+
const isAvailable = d.type === "device";
|
|
325411
325415
|
const name = d.model || d.device || (isEmulator ? "Android Emulator" : "Android Device");
|
|
325412
|
-
const platformVersion = getAndroidProp(d.serial, "ro.build.version.release") || "unknown";
|
|
325416
|
+
const platformVersion = isAvailable ? getAndroidProp(d.serial, "ro.build.version.release") || "unknown" : "unknown";
|
|
325413
325417
|
return {
|
|
325414
325418
|
id: d.serial,
|
|
325415
325419
|
name,
|
|
325416
325420
|
platform: "ANDROID",
|
|
325417
325421
|
platformVersion,
|
|
325418
325422
|
state: "BOOTED",
|
|
325419
|
-
isAvailable
|
|
325423
|
+
isAvailable,
|
|
325420
325424
|
// Mark if it's a real device vs emulator
|
|
325421
325425
|
...isEmulator ? {} : { isPhysical: true }
|
|
325422
325426
|
};
|
|
@@ -325449,10 +325453,14 @@ function discoverAllDevices(options) {
|
|
|
325449
325453
|
const androidDevices = discoverAndroidDevices();
|
|
325450
325454
|
if (options?.includeApps) {
|
|
325451
325455
|
for (const device of iosDevices) {
|
|
325452
|
-
|
|
325456
|
+
if (device.isAvailable) {
|
|
325457
|
+
device.installedApps = listIOSInstalledApps(device.id, !!device.isPhysical);
|
|
325458
|
+
}
|
|
325453
325459
|
}
|
|
325454
325460
|
for (const device of androidDevices) {
|
|
325455
|
-
|
|
325461
|
+
if (device.isAvailable) {
|
|
325462
|
+
device.installedApps = listAndroidInstalledApps(device.id);
|
|
325463
|
+
}
|
|
325456
325464
|
}
|
|
325457
325465
|
}
|
|
325458
325466
|
return [...iosDevices, ...androidDevices];
|
|
@@ -325460,11 +325468,12 @@ function discoverAllDevices(options) {
|
|
|
325460
325468
|
function getRunnerCapabilities() {
|
|
325461
325469
|
const iosDevices = discoverIOSDevices();
|
|
325462
325470
|
const androidDevices = discoverAndroidDevices();
|
|
325471
|
+
const isReadyDevice = (device) => device.isAvailable && device.state === "BOOTED";
|
|
325463
325472
|
return {
|
|
325464
325473
|
web: true,
|
|
325465
325474
|
// Always capable of web testing via Playwright
|
|
325466
|
-
ios: iosDevices.
|
|
325467
|
-
android: androidDevices.
|
|
325475
|
+
ios: iosDevices.some(isReadyDevice),
|
|
325476
|
+
android: androidDevices.some(isReadyDevice)
|
|
325468
325477
|
};
|
|
325469
325478
|
}
|
|
325470
325479
|
|
|
@@ -325866,7 +325875,7 @@ async function executeMobileTest(options) {
|
|
|
325866
325875
|
const devices = discoverAllDevices({ includeApps: true });
|
|
325867
325876
|
const platformFilter = options.target.platform;
|
|
325868
325877
|
const matchingDevices = devices.filter(
|
|
325869
|
-
(d) => d.platform === platformFilter && d.state === "BOOTED"
|
|
325878
|
+
(d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
|
|
325870
325879
|
);
|
|
325871
325880
|
if (matchingDevices.length === 0) {
|
|
325872
325881
|
throw new Error(
|
|
@@ -326030,7 +326039,7 @@ async function createMobileDiscoverySession(target) {
|
|
|
326030
326039
|
const devices = discoverAllDevices({ includeApps: true });
|
|
326031
326040
|
const platformFilter = target.platform;
|
|
326032
326041
|
const matchingDevices = devices.filter(
|
|
326033
|
-
(d) => d.platform === platformFilter && d.state === "BOOTED"
|
|
326042
|
+
(d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
|
|
326034
326043
|
);
|
|
326035
326044
|
if (matchingDevices.length === 0) {
|
|
326036
326045
|
throw new Error(
|
|
@@ -326081,6 +326090,9 @@ async function createMobileDiscoverySession(target) {
|
|
|
326081
326090
|
import { createServer } from "http";
|
|
326082
326091
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
326083
326092
|
import { randomBytes, timingSafeEqual } from "crypto";
|
|
326093
|
+
import { homedir as homedir2 } from "os";
|
|
326094
|
+
import { join as join2, dirname } from "path";
|
|
326095
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
326084
326096
|
init_dist();
|
|
326085
326097
|
|
|
326086
326098
|
// src/services/mobile/appium-actions.ts
|
|
@@ -326311,6 +326323,7 @@ var polling = false;
|
|
|
326311
326323
|
var sessionStartInFlight = null;
|
|
326312
326324
|
var sessionReaperTimer = null;
|
|
326313
326325
|
var mirrorClientsEmptyAt = null;
|
|
326326
|
+
var mirrorSessionOwner = null;
|
|
326314
326327
|
var executorBusy = false;
|
|
326315
326328
|
var rateLimitBuckets = /* @__PURE__ */ new Map();
|
|
326316
326329
|
function isRateLimited(clientIp) {
|
|
@@ -326366,30 +326379,50 @@ async function getWindowSize3() {
|
|
|
326366
326379
|
return getWindowSize2(`/session/${state.appiumSessionId}`);
|
|
326367
326380
|
}
|
|
326368
326381
|
var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
|
|
326369
|
-
|
|
326370
|
-
|
|
326382
|
+
var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
|
|
326383
|
+
var BRIDGE_PID_FILE = join2(homedir2(), ".validate.qa", "mobile-bridge.pid");
|
|
326384
|
+
function readBridgePidFile() {
|
|
326371
326385
|
try {
|
|
326372
|
-
const
|
|
326373
|
-
|
|
326374
|
-
|
|
326375
|
-
|
|
326376
|
-
|
|
326377
|
-
|
|
326378
|
-
|
|
326379
|
-
|
|
326380
|
-
|
|
326381
|
-
|
|
326382
|
-
|
|
326383
|
-
|
|
326384
|
-
|
|
326385
|
-
|
|
326386
|
-
|
|
326387
|
-
|
|
326388
|
-
|
|
326389
|
-
|
|
326390
|
-
|
|
326386
|
+
const raw = readFileSync2(BRIDGE_PID_FILE, "utf-8").trim();
|
|
326387
|
+
const pid = parseInt(raw, 10);
|
|
326388
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
326389
|
+
} catch {
|
|
326390
|
+
return null;
|
|
326391
|
+
}
|
|
326392
|
+
}
|
|
326393
|
+
function writeBridgePidFile(pid) {
|
|
326394
|
+
try {
|
|
326395
|
+
mkdirSync2(dirname(BRIDGE_PID_FILE), { recursive: true, mode: 448 });
|
|
326396
|
+
writeFileSync2(BRIDGE_PID_FILE, String(pid), { mode: 384 });
|
|
326397
|
+
} catch {
|
|
326398
|
+
}
|
|
326399
|
+
}
|
|
326400
|
+
function clearBridgePidFile() {
|
|
326401
|
+
try {
|
|
326402
|
+
unlinkSync2(BRIDGE_PID_FILE);
|
|
326403
|
+
} catch {
|
|
326404
|
+
}
|
|
326405
|
+
}
|
|
326406
|
+
function killStaleBridge() {
|
|
326407
|
+
const pid = readBridgePidFile();
|
|
326408
|
+
if (!pid) return;
|
|
326409
|
+
try {
|
|
326410
|
+
execFileSync3("kill", ["-0", String(pid)], { stdio: "ignore" });
|
|
326411
|
+
} catch {
|
|
326412
|
+
clearBridgePidFile();
|
|
326413
|
+
return;
|
|
326414
|
+
}
|
|
326415
|
+
try {
|
|
326416
|
+
const pidsOnPort = execFileSync3("lsof", ["-ti", `:${BRIDGE_PORT}`], { encoding: "utf-8" }).trim();
|
|
326417
|
+
const pidList = pidsOnPort.split("\n").filter(Boolean);
|
|
326418
|
+
if (!pidList.includes(String(pid))) {
|
|
326419
|
+
return;
|
|
326391
326420
|
}
|
|
326421
|
+
execFileSync3("kill", ["-9", String(pid)], { stdio: "ignore" });
|
|
326422
|
+
execFileSync3("sleep", ["1"], { stdio: "ignore" });
|
|
326392
326423
|
} catch {
|
|
326424
|
+
} finally {
|
|
326425
|
+
clearBridgePidFile();
|
|
326393
326426
|
}
|
|
326394
326427
|
}
|
|
326395
326428
|
function parseBody(req) {
|
|
@@ -326627,6 +326660,7 @@ async function startSession(validated, res) {
|
|
|
326627
326660
|
}
|
|
326628
326661
|
state.appiumSessionId = null;
|
|
326629
326662
|
state.platform = null;
|
|
326663
|
+
mirrorSessionOwner = null;
|
|
326630
326664
|
}
|
|
326631
326665
|
stopAllMirrorClients();
|
|
326632
326666
|
const realCaps = isPhysicalDevice(deviceId, targetPlatform) ? realDeviceCapabilities2(targetPlatform) : {};
|
|
@@ -326657,11 +326691,14 @@ async function startSession(validated, res) {
|
|
|
326657
326691
|
})
|
|
326658
326692
|
});
|
|
326659
326693
|
state.appiumSessionId = result?.value?.sessionId ?? result?.sessionId ?? null;
|
|
326660
|
-
state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
|
|
326661
326694
|
if (!state.appiumSessionId) {
|
|
326695
|
+
state.platform = null;
|
|
326696
|
+
mirrorSessionOwner = null;
|
|
326662
326697
|
sendJSON(res, 500, { error: "Appium session creation returned no session ID" });
|
|
326663
326698
|
return;
|
|
326664
326699
|
}
|
|
326700
|
+
state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
|
|
326701
|
+
mirrorSessionOwner = "recording";
|
|
326665
326702
|
if (launchMode === "DEEP_LINK" && deeplink) {
|
|
326666
326703
|
try {
|
|
326667
326704
|
const args = targetPlatform === "IOS" ? [{ url: deeplink, bundleId: appId }] : [{ url: deeplink, package: appId }];
|
|
@@ -326686,6 +326723,7 @@ async function handleSessionStop(_req, res) {
|
|
|
326686
326723
|
}
|
|
326687
326724
|
state.appiumSessionId = null;
|
|
326688
326725
|
state.platform = null;
|
|
326726
|
+
mirrorSessionOwner = null;
|
|
326689
326727
|
}
|
|
326690
326728
|
stopAllMirrorClients();
|
|
326691
326729
|
stopSessionReaper();
|
|
@@ -326766,6 +326804,7 @@ async function pollAndBroadcastFrame() {
|
|
|
326766
326804
|
if (!alive) {
|
|
326767
326805
|
state.appiumSessionId = null;
|
|
326768
326806
|
state.platform = null;
|
|
326807
|
+
mirrorSessionOwner = null;
|
|
326769
326808
|
stopAllMirrorClients();
|
|
326770
326809
|
return;
|
|
326771
326810
|
}
|
|
@@ -326836,6 +326875,7 @@ async function reapIdleSession() {
|
|
|
326836
326875
|
const sessionId = state.appiumSessionId;
|
|
326837
326876
|
state.appiumSessionId = null;
|
|
326838
326877
|
state.platform = null;
|
|
326878
|
+
mirrorSessionOwner = null;
|
|
326839
326879
|
mirrorClientsEmptyAt = null;
|
|
326840
326880
|
stopSessionReaper();
|
|
326841
326881
|
try {
|
|
@@ -327014,7 +327054,7 @@ async function startMobileBridge(options) {
|
|
|
327014
327054
|
if (!process.title.includes(BRIDGE_PROCESS_MARKER)) {
|
|
327015
327055
|
process.title = `${process.title} ${BRIDGE_PROCESS_MARKER}`;
|
|
327016
327056
|
}
|
|
327017
|
-
|
|
327057
|
+
killStaleBridge();
|
|
327018
327058
|
return new Promise((resolve, reject) => {
|
|
327019
327059
|
server2 = createServer(route);
|
|
327020
327060
|
server2.on("error", (err) => {
|
|
@@ -327030,6 +327070,7 @@ async function startMobileBridge(options) {
|
|
|
327030
327070
|
state.port = port;
|
|
327031
327071
|
state.tunnelToken = tunnelToken;
|
|
327032
327072
|
state.tunnelUrl = void 0;
|
|
327073
|
+
writeBridgePidFile(process.pid);
|
|
327033
327074
|
let tunnelUrl;
|
|
327034
327075
|
if (enableTunnel) {
|
|
327035
327076
|
try {
|
|
@@ -327097,6 +327138,7 @@ async function startMobileBridge(options) {
|
|
|
327097
327138
|
let checkInterval = null;
|
|
327098
327139
|
const timeout = setTimeout(() => {
|
|
327099
327140
|
if (checkInterval) clearInterval(checkInterval);
|
|
327141
|
+
checkInterval = null;
|
|
327100
327142
|
if (!tunnelUrl) {
|
|
327101
327143
|
reject2(new Error("ngrok tunnel failed to start"));
|
|
327102
327144
|
}
|
|
@@ -327105,6 +327147,7 @@ async function startMobileBridge(options) {
|
|
|
327105
327147
|
if (tunnelUrl) {
|
|
327106
327148
|
clearTimeout(timeout);
|
|
327107
327149
|
if (checkInterval) clearInterval(checkInterval);
|
|
327150
|
+
checkInterval = null;
|
|
327108
327151
|
resolve2();
|
|
327109
327152
|
}
|
|
327110
327153
|
}, 500);
|
|
@@ -327134,6 +327177,7 @@ async function startMobileBridge(options) {
|
|
|
327134
327177
|
async function stopMobileBridge() {
|
|
327135
327178
|
stopAllMirrorClients();
|
|
327136
327179
|
stopSessionReaper();
|
|
327180
|
+
clearBridgePidFile();
|
|
327137
327181
|
if (state.appiumSessionId) {
|
|
327138
327182
|
try {
|
|
327139
327183
|
validateSessionId(state.appiumSessionId);
|
|
@@ -327142,6 +327186,7 @@ async function stopMobileBridge() {
|
|
|
327142
327186
|
}
|
|
327143
327187
|
state.appiumSessionId = null;
|
|
327144
327188
|
state.platform = null;
|
|
327189
|
+
mirrorSessionOwner = null;
|
|
327145
327190
|
}
|
|
327146
327191
|
if (ngrokProcess) {
|
|
327147
327192
|
try {
|
|
@@ -327172,13 +327217,37 @@ function getBridgeState() {
|
|
|
327172
327217
|
tunnelUrl: state.tunnelUrl,
|
|
327173
327218
|
tunnelToken: state.tunnelToken,
|
|
327174
327219
|
hasActiveSession: !!state.appiumSessionId,
|
|
327175
|
-
recordingActive: !!state.appiumSessionId,
|
|
327220
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording",
|
|
327176
327221
|
platform: state.platform
|
|
327177
327222
|
};
|
|
327178
327223
|
}
|
|
327179
327224
|
function setExecutorBusy(busy) {
|
|
327180
327225
|
executorBusy = busy;
|
|
327181
327226
|
}
|
|
327227
|
+
function attachMirrorSession(sessionId, platform3) {
|
|
327228
|
+
validateSessionId(sessionId);
|
|
327229
|
+
if (state.appiumSessionId && state.appiumSessionId !== sessionId) {
|
|
327230
|
+
stopAllMirrorClients();
|
|
327231
|
+
}
|
|
327232
|
+
state.appiumSessionId = sessionId;
|
|
327233
|
+
state.platform = platform3;
|
|
327234
|
+
mirrorSessionOwner = "runner";
|
|
327235
|
+
mirrorNullTicks = 0;
|
|
327236
|
+
mirrorClientsEmptyAt = null;
|
|
327237
|
+
ensureSessionReaper();
|
|
327238
|
+
if (state.mirrorClients.size > 0) {
|
|
327239
|
+
ensureMirrorPolling();
|
|
327240
|
+
void pollAndBroadcastFrame();
|
|
327241
|
+
}
|
|
327242
|
+
}
|
|
327243
|
+
function detachMirrorSession(sessionId) {
|
|
327244
|
+
if (sessionId && state.appiumSessionId && state.appiumSessionId !== sessionId) return;
|
|
327245
|
+
state.appiumSessionId = null;
|
|
327246
|
+
state.platform = null;
|
|
327247
|
+
mirrorSessionOwner = null;
|
|
327248
|
+
stopAllMirrorClients();
|
|
327249
|
+
stopSessionReaper();
|
|
327250
|
+
}
|
|
327182
327251
|
|
|
327183
327252
|
// src/services/mobile/mobile-bridge-driver.ts
|
|
327184
327253
|
var import_runner_core7 = __toESM(require_dist2());
|
|
@@ -327392,7 +327461,7 @@ import { execFile, execFileSync as execFileSync4 } from "child_process";
|
|
|
327392
327461
|
import { randomUUID } from "crypto";
|
|
327393
327462
|
import { mkdtemp, rm, writeFile, readFile, readdir, mkdir, unlink } from "fs/promises";
|
|
327394
327463
|
import { tmpdir, networkInterfaces } from "os";
|
|
327395
|
-
import { join as
|
|
327464
|
+
import { join as join3 } from "path";
|
|
327396
327465
|
import { promisify } from "util";
|
|
327397
327466
|
import {
|
|
327398
327467
|
getLocal,
|
|
@@ -327404,7 +327473,7 @@ var MAX_BODY_CAPTURE_BYTES = 64 * 1024;
|
|
|
327404
327473
|
var DEVICE_CMD_TIMEOUT_MS = 15e3;
|
|
327405
327474
|
var CA_KEY_BITS = 2048;
|
|
327406
327475
|
var ANDROID_EMULATOR_HOST_LOOPBACK = "10.0.2.2";
|
|
327407
|
-
var PENDING_CLEANUP_DIR =
|
|
327476
|
+
var PENDING_CLEANUP_DIR = join3(tmpdir(), "validateqa-mobile-capture-pending");
|
|
327408
327477
|
function rawHeadersToPairs(rawHeaders) {
|
|
327409
327478
|
return rawHeaders.map(([name, value]) => ({ name, value }));
|
|
327410
327479
|
}
|
|
@@ -327688,7 +327757,7 @@ async function generateCaCert(dir, onLog) {
|
|
|
327688
327757
|
// rely on stop() to remove the cert from the device promptly after the run
|
|
327689
327758
|
// rather than on a short expiry.
|
|
327690
327759
|
});
|
|
327691
|
-
const caCertPath =
|
|
327760
|
+
const caCertPath = join3(dir, "validateqa-mobile-ca.pem");
|
|
327692
327761
|
await writeFile(caCertPath, cert, "utf-8");
|
|
327693
327762
|
return { caCertPath, caKeyPem: key, caCertPem: cert };
|
|
327694
327763
|
} catch (err) {
|
|
@@ -327794,7 +327863,7 @@ function errMsg(err) {
|
|
|
327794
327863
|
var activeCleanups = /* @__PURE__ */ new Map();
|
|
327795
327864
|
var exitHandlersInstalled = false;
|
|
327796
327865
|
function markerPath(sessionKey) {
|
|
327797
|
-
return
|
|
327866
|
+
return join3(PENDING_CLEANUP_DIR, `${sessionKey}.json`);
|
|
327798
327867
|
}
|
|
327799
327868
|
async function writePendingCleanup(sessionKey, marker) {
|
|
327800
327869
|
try {
|
|
@@ -327836,7 +327905,7 @@ async function reconcilePendingCleanups(onLog) {
|
|
|
327836
327905
|
}
|
|
327837
327906
|
for (const file of files) {
|
|
327838
327907
|
if (!file.endsWith(".json")) continue;
|
|
327839
|
-
const full =
|
|
327908
|
+
const full = join3(PENDING_CLEANUP_DIR, file);
|
|
327840
327909
|
try {
|
|
327841
327910
|
const raw = await readFile(full, "utf-8");
|
|
327842
327911
|
const marker = JSON.parse(raw);
|
|
@@ -327881,7 +327950,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
327881
327950
|
let caKeyPem = null;
|
|
327882
327951
|
let caCertPem = null;
|
|
327883
327952
|
try {
|
|
327884
|
-
tmpDir = await mkdtemp(
|
|
327953
|
+
tmpDir = await mkdtemp(join3(tmpdir(), "validateqa-mobile-ca-"));
|
|
327885
327954
|
const ca = await generateCaCert(tmpDir, onLog);
|
|
327886
327955
|
if (ca) {
|
|
327887
327956
|
caCertPath = ca.caCertPath;
|
|
@@ -327997,6 +328066,219 @@ async function startMobileNetworkCapture(options) {
|
|
|
327997
328066
|
};
|
|
327998
328067
|
}
|
|
327999
328068
|
|
|
328069
|
+
// src/services/doctor.ts
|
|
328070
|
+
import { execSync } from "child_process";
|
|
328071
|
+
import { existsSync as existsSync2 } from "fs";
|
|
328072
|
+
import { platform as platform2 } from "os";
|
|
328073
|
+
var C = {
|
|
328074
|
+
reset: "\x1B[0m",
|
|
328075
|
+
bold: "\x1B[1m",
|
|
328076
|
+
green: "\x1B[32m",
|
|
328077
|
+
red: "\x1B[31m",
|
|
328078
|
+
yellow: "\x1B[33m",
|
|
328079
|
+
cyan: "\x1B[36m",
|
|
328080
|
+
gray: "\x1B[90m"
|
|
328081
|
+
};
|
|
328082
|
+
function run(cmd) {
|
|
328083
|
+
try {
|
|
328084
|
+
return execSync(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
328085
|
+
} catch {
|
|
328086
|
+
return null;
|
|
328087
|
+
}
|
|
328088
|
+
}
|
|
328089
|
+
function check(name, fn) {
|
|
328090
|
+
try {
|
|
328091
|
+
const result = fn();
|
|
328092
|
+
return { name, ...result };
|
|
328093
|
+
} catch (err) {
|
|
328094
|
+
return { name, ok: false, detail: err instanceof Error ? err.message : "Unknown error" };
|
|
328095
|
+
}
|
|
328096
|
+
}
|
|
328097
|
+
function checkWeb() {
|
|
328098
|
+
const results = [];
|
|
328099
|
+
results.push(check("Node.js", () => {
|
|
328100
|
+
const version = process.version;
|
|
328101
|
+
const major = parseInt(version.slice(1).split(".")[0], 10);
|
|
328102
|
+
return { ok: major >= 20, detail: `${version}${major < 20 ? " (>= 20 required)" : ""}` };
|
|
328103
|
+
}));
|
|
328104
|
+
results.push(check("Playwright", () => {
|
|
328105
|
+
const version = run("npx playwright --version");
|
|
328106
|
+
if (!version) return { ok: false, detail: "Not installed. Run: npm i -D playwright" };
|
|
328107
|
+
return { ok: true, detail: version };
|
|
328108
|
+
}));
|
|
328109
|
+
return results;
|
|
328110
|
+
}
|
|
328111
|
+
function checkIOS() {
|
|
328112
|
+
const results = [];
|
|
328113
|
+
const isMac = platform2() === "darwin";
|
|
328114
|
+
results.push(check("macOS", () => ({
|
|
328115
|
+
ok: isMac,
|
|
328116
|
+
detail: isMac ? `${platform2()} detected` : "iOS testing requires macOS"
|
|
328117
|
+
})));
|
|
328118
|
+
if (!isMac) return results;
|
|
328119
|
+
results.push(check("Xcode", () => {
|
|
328120
|
+
const version = run("xcodebuild -version");
|
|
328121
|
+
if (!version) return { ok: false, detail: "Not installed. Install from App Store." };
|
|
328122
|
+
const firstLine = version.split("\n")[0];
|
|
328123
|
+
return { ok: true, detail: firstLine };
|
|
328124
|
+
}));
|
|
328125
|
+
results.push(check("Xcode Command Line Tools", () => {
|
|
328126
|
+
const path = run("xcode-select -p");
|
|
328127
|
+
return { ok: !!path, detail: path || "Not installed. Run: xcode-select --install" };
|
|
328128
|
+
}));
|
|
328129
|
+
results.push(check("Appium xcuitest driver", () => {
|
|
328130
|
+
const drivers = run("appium driver list --installed --json");
|
|
328131
|
+
if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
|
|
328132
|
+
try {
|
|
328133
|
+
const parsed = JSON.parse(drivers);
|
|
328134
|
+
const hasXcuitest = parsed?.xcuitest || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("xcuitest"));
|
|
328135
|
+
return {
|
|
328136
|
+
ok: !!hasXcuitest,
|
|
328137
|
+
detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
|
|
328138
|
+
};
|
|
328139
|
+
} catch {
|
|
328140
|
+
const hasXcuitest = drivers.toLowerCase().includes("xcuitest");
|
|
328141
|
+
return {
|
|
328142
|
+
ok: hasXcuitest,
|
|
328143
|
+
detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
|
|
328144
|
+
};
|
|
328145
|
+
}
|
|
328146
|
+
}));
|
|
328147
|
+
results.push(check("iOS Devices", () => {
|
|
328148
|
+
const json = run("xcrun simctl list devices booted --json");
|
|
328149
|
+
let bootedSimulators = 0;
|
|
328150
|
+
if (json) {
|
|
328151
|
+
try {
|
|
328152
|
+
const parsed = JSON.parse(json);
|
|
328153
|
+
const devices = Object.values(parsed.devices ?? {}).flat();
|
|
328154
|
+
bootedSimulators = devices.filter((d) => d.state === "Booted").length;
|
|
328155
|
+
} catch {
|
|
328156
|
+
}
|
|
328157
|
+
}
|
|
328158
|
+
const realDevices = discoverRealIOSDevices().length;
|
|
328159
|
+
const total = bootedSimulators + realDevices;
|
|
328160
|
+
if (total === 0) {
|
|
328161
|
+
return {
|
|
328162
|
+
ok: false,
|
|
328163
|
+
detail: "No devices found. Boot a simulator or connect a real device via USB."
|
|
328164
|
+
};
|
|
328165
|
+
}
|
|
328166
|
+
const details = [];
|
|
328167
|
+
if (bootedSimulators > 0) details.push(`${bootedSimulators} simulator(s)`);
|
|
328168
|
+
if (realDevices > 0) details.push(`${realDevices} real device(s)`);
|
|
328169
|
+
return {
|
|
328170
|
+
ok: true,
|
|
328171
|
+
detail: `${total} device(s) available: ${details.join(", ")}`
|
|
328172
|
+
};
|
|
328173
|
+
}));
|
|
328174
|
+
return results;
|
|
328175
|
+
}
|
|
328176
|
+
function checkAndroid() {
|
|
328177
|
+
const results = [];
|
|
328178
|
+
const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
328179
|
+
results.push(check("ANDROID_HOME", () => ({
|
|
328180
|
+
ok: !!androidHome && existsSync2(androidHome),
|
|
328181
|
+
detail: androidHome ? existsSync2(androidHome) ? androidHome : `${androidHome} (not found)` : "Not set. Set ANDROID_HOME to your Android SDK path."
|
|
328182
|
+
})));
|
|
328183
|
+
if (!androidHome || !existsSync2(androidHome)) return results;
|
|
328184
|
+
results.push(check("adb", () => {
|
|
328185
|
+
const version = run("adb version");
|
|
328186
|
+
if (!version) return { ok: false, detail: "Not found in PATH. Ensure Android platform-tools are installed." };
|
|
328187
|
+
const firstLine = version.split("\n")[0];
|
|
328188
|
+
return { ok: true, detail: firstLine };
|
|
328189
|
+
}));
|
|
328190
|
+
results.push(check("Appium uiautomator2 driver", () => {
|
|
328191
|
+
const drivers = run("appium driver list --installed --json");
|
|
328192
|
+
if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
|
|
328193
|
+
try {
|
|
328194
|
+
const parsed = JSON.parse(drivers);
|
|
328195
|
+
const hasUia2 = parsed?.uiautomator2 || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("uiautomator2"));
|
|
328196
|
+
return {
|
|
328197
|
+
ok: !!hasUia2,
|
|
328198
|
+
detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
|
|
328199
|
+
};
|
|
328200
|
+
} catch {
|
|
328201
|
+
const hasUia2 = drivers.toLowerCase().includes("uiautomator2");
|
|
328202
|
+
return {
|
|
328203
|
+
ok: hasUia2,
|
|
328204
|
+
detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
|
|
328205
|
+
};
|
|
328206
|
+
}
|
|
328207
|
+
}));
|
|
328208
|
+
results.push(check("Android Devices", () => {
|
|
328209
|
+
const output = run("adb devices");
|
|
328210
|
+
if (!output) return { ok: false, detail: "adb devices failed" };
|
|
328211
|
+
const lines = output.split("\n").filter((l) => l.includes(" device"));
|
|
328212
|
+
const emulators = lines.filter((l) => l.startsWith("emulator-"));
|
|
328213
|
+
const realDevices = lines.filter((l) => !l.startsWith("emulator-") && l.includes(" device"));
|
|
328214
|
+
const total = emulators.length + realDevices.length;
|
|
328215
|
+
if (total === 0) {
|
|
328216
|
+
return {
|
|
328217
|
+
ok: false,
|
|
328218
|
+
detail: "No devices found. Start an emulator or connect a real device via USB with USB debugging enabled."
|
|
328219
|
+
};
|
|
328220
|
+
}
|
|
328221
|
+
const details = [];
|
|
328222
|
+
if (emulators.length > 0) details.push(`${emulators.length} emulator(s)`);
|
|
328223
|
+
if (realDevices.length > 0) details.push(`${realDevices.length} real device(s)`);
|
|
328224
|
+
return {
|
|
328225
|
+
ok: true,
|
|
328226
|
+
detail: `${total} device(s) available: ${details.join(", ")}`
|
|
328227
|
+
};
|
|
328228
|
+
}));
|
|
328229
|
+
return results;
|
|
328230
|
+
}
|
|
328231
|
+
function checkTunnel() {
|
|
328232
|
+
const version = run("ngrok version") || run("ngrok --version");
|
|
328233
|
+
if (!version) {
|
|
328234
|
+
return [{
|
|
328235
|
+
name: "ngrok",
|
|
328236
|
+
ok: false,
|
|
328237
|
+
detail: "Not installed (only needed for --tunnel with an HTTPS dashboard). Install: brew install ngrok && ngrok config add-authtoken <token>"
|
|
328238
|
+
}];
|
|
328239
|
+
}
|
|
328240
|
+
const match = version.match(/(\d+)\.\d+\.\d+/);
|
|
328241
|
+
const major = match ? parseInt(match[1], 10) : 0;
|
|
328242
|
+
return [{
|
|
328243
|
+
name: "ngrok",
|
|
328244
|
+
ok: major >= 3,
|
|
328245
|
+
detail: major >= 3 ? `${version} (configure an authtoken with: ngrok config add-authtoken <token>)` : `${version} \u2014 ngrok v3+ is recommended (brew install ngrok)`
|
|
328246
|
+
}];
|
|
328247
|
+
}
|
|
328248
|
+
function runDoctor() {
|
|
328249
|
+
const web = checkWeb();
|
|
328250
|
+
const ios = checkIOS();
|
|
328251
|
+
const android = checkAndroid();
|
|
328252
|
+
const tunnel = checkTunnel();
|
|
328253
|
+
const allPassed = [...web, ...ios, ...android].every((c) => c.ok);
|
|
328254
|
+
return { web, ios, android, tunnel, allPassed };
|
|
328255
|
+
}
|
|
328256
|
+
function printDoctorReport(report) {
|
|
328257
|
+
console.log("");
|
|
328258
|
+
console.log(`${C.bold}${C.cyan}\u{1F3E5} validate.qa Doctor${C.reset}`);
|
|
328259
|
+
console.log(`${C.gray}${"\u2500".repeat(40)}${C.reset}`);
|
|
328260
|
+
const printSection = (title, checks) => {
|
|
328261
|
+
console.log("");
|
|
328262
|
+
console.log(` ${C.bold}${title}:${C.reset}`);
|
|
328263
|
+
for (const c of checks) {
|
|
328264
|
+
const icon = c.ok ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`;
|
|
328265
|
+
console.log(` ${icon} ${c.name}: ${c.detail}`);
|
|
328266
|
+
}
|
|
328267
|
+
};
|
|
328268
|
+
printSection("Web Testing", report.web);
|
|
328269
|
+
printSection("iOS Testing", report.ios);
|
|
328270
|
+
printSection("Android Testing", report.android);
|
|
328271
|
+
printSection("Tunnel (optional \u2014 only for --tunnel / HTTPS dashboard)", report.tunnel);
|
|
328272
|
+
console.log("");
|
|
328273
|
+
if (report.allPassed) {
|
|
328274
|
+
console.log(` ${C.green}\u2705 All checks passed - ready for web + mobile testing${C.reset}`);
|
|
328275
|
+
} else {
|
|
328276
|
+
const failCount = [...report.web, ...report.ios, ...report.android].filter((c) => !c.ok).length;
|
|
328277
|
+
console.log(` ${C.yellow}\u26A0 ${failCount} check(s) need attention${C.reset}`);
|
|
328278
|
+
}
|
|
328279
|
+
console.log("");
|
|
328280
|
+
}
|
|
328281
|
+
|
|
328000
328282
|
// src/runner.ts
|
|
328001
328283
|
init_dist();
|
|
328002
328284
|
var import_runner_core11 = __toESM(require_dist2());
|
|
@@ -328086,7 +328368,7 @@ var browserPool = new import_runner_core6.BrowserPool({
|
|
|
328086
328368
|
maxContextsPerBrowser: envInt("MAX_CONTEXTS_PER_BROWSER", DEFAULT_MAX_CONTEXTS_PER_BROWSER),
|
|
328087
328369
|
retireAfterContexts: 30
|
|
328088
328370
|
});
|
|
328089
|
-
var
|
|
328371
|
+
var C2 = {
|
|
328090
328372
|
reset: "\x1B[0m",
|
|
328091
328373
|
bold: "\x1B[1m",
|
|
328092
328374
|
green: "\x1B[32m",
|
|
@@ -328096,24 +328378,24 @@ var C = {
|
|
|
328096
328378
|
gray: "\x1B[90m"
|
|
328097
328379
|
};
|
|
328098
328380
|
var log = {
|
|
328099
|
-
info: (msg) => console.log(`${
|
|
328100
|
-
ok: (msg) => console.log(`${
|
|
328101
|
-
fail: (msg) => console.log(`${
|
|
328102
|
-
warn: (msg) => console.log(`${
|
|
328103
|
-
dim: (msg) => console.log(`${
|
|
328381
|
+
info: (msg) => console.log(`${C2.cyan}\u2139${C2.reset} ${msg}`),
|
|
328382
|
+
ok: (msg) => console.log(`${C2.green}\u2713${C2.reset} ${msg}`),
|
|
328383
|
+
fail: (msg) => console.log(`${C2.red}\u2717${C2.reset} ${msg}`),
|
|
328384
|
+
warn: (msg) => console.log(`${C2.yellow}\u26A0${C2.reset} ${msg}`),
|
|
328385
|
+
dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
|
|
328104
328386
|
};
|
|
328105
328387
|
var AUTH_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
328106
328388
|
function getRunsDir() {
|
|
328107
|
-
const dir = process.env.RUNS_DIR ||
|
|
328108
|
-
if (!
|
|
328389
|
+
const dir = process.env.RUNS_DIR || join4(homedir3(), ".validate.qa", "runs");
|
|
328390
|
+
if (!existsSync3(dir)) mkdirSync3(dir, { recursive: true });
|
|
328109
328391
|
return dir;
|
|
328110
328392
|
}
|
|
328111
328393
|
function authStatePath(projectId) {
|
|
328112
|
-
return
|
|
328394
|
+
return join4(getRunsDir(), `auth-state-${projectId || "default"}.json`);
|
|
328113
328395
|
}
|
|
328114
328396
|
function getValidAuthState(projectId) {
|
|
328115
328397
|
const path = authStatePath(projectId);
|
|
328116
|
-
if (!
|
|
328398
|
+
if (!existsSync3(path)) return void 0;
|
|
328117
328399
|
const age = Date.now() - statSync(path).mtimeMs;
|
|
328118
328400
|
return age < AUTH_MAX_AGE_MS ? path : void 0;
|
|
328119
328401
|
}
|
|
@@ -329056,9 +329338,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
329056
329338
|
(l) => l.includes("Phase 1: Script-driven") || l.includes("Phase 2: Escalating") || l.includes("Phase 2: Script + MCP") || l.includes("Phase 1 succeeded") || l.includes("Phase 2 iteration") || l.includes("\u{1F916} Verdict:") || l.includes("\u{1F916} finish_fix:") || l.includes("native verification PASSED") || l.includes("native verification FAILED") || l.includes("Native run PASSED") || l.includes("Native run FAILED") || l.includes("APP_BUG") || l.includes("CONFIG_ISSUE") || l.includes("needs_browser") || l.includes("--- Attempt") || l.includes("Proven commands") || l.includes("Proven fix:") || l.includes("Same error repeated") || l.includes("aborting heal")
|
|
329057
329339
|
);
|
|
329058
329340
|
if (phaseLines.length > 0) {
|
|
329059
|
-
console.log(`${
|
|
329341
|
+
console.log(`${C2.gray}\u2500\u2500 Heal summary \u2500\u2500${C2.reset}`);
|
|
329060
329342
|
for (const line of phaseLines) console.log(line);
|
|
329061
|
-
console.log(`${
|
|
329343
|
+
console.log(`${C2.gray}\u2500\u2500 End summary \u2500\u2500\u2500${C2.reset}`);
|
|
329062
329344
|
}
|
|
329063
329345
|
}
|
|
329064
329346
|
log[result2.passed ? "ok" : "fail"](
|
|
@@ -329597,11 +329879,18 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
329597
329879
|
}
|
|
329598
329880
|
try {
|
|
329599
329881
|
if (isNativeMobile) {
|
|
329600
|
-
const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : "ANDROID";
|
|
329601
329882
|
const mobileTarget = ctx.target;
|
|
329602
329883
|
if (!mobileTarget || !mobileTarget.appId) {
|
|
329603
329884
|
throw new Error("Native mobile discovery requires a target with an appId (bundleId/appPackage).");
|
|
329604
329885
|
}
|
|
329886
|
+
const targetPlatform = mobileTarget.platform;
|
|
329887
|
+
const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : mobileMedium === "ANDROID_NATIVE" ? "ANDROID" : targetPlatform === "IOS" || targetPlatform === "ANDROID" ? targetPlatform : null;
|
|
329888
|
+
if (!platform3) {
|
|
329889
|
+
throw new Error("Native mobile discovery requires medium IOS_NATIVE/ANDROID_NATIVE or target.platform IOS/ANDROID.");
|
|
329890
|
+
}
|
|
329891
|
+
if (mobileMedium === "IOS_NATIVE" && targetPlatform === "ANDROID" || mobileMedium === "ANDROID_NATIVE" && targetPlatform === "IOS") {
|
|
329892
|
+
throw new Error("Native mobile discovery medium does not match target.platform.");
|
|
329893
|
+
}
|
|
329605
329894
|
if (getBridgeState().hasActiveSession) {
|
|
329606
329895
|
throw new Error("CONFIG_ISSUE: the shared mobile device is busy with an active recording session; mobile discovery will retry once it ends.");
|
|
329607
329896
|
}
|
|
@@ -329620,6 +329909,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
329620
329909
|
deeplink: mobileTarget.deeplink
|
|
329621
329910
|
});
|
|
329622
329911
|
mobileOnLog(`Appium session created: ${session.sessionId} on device ${session.deviceId}`);
|
|
329912
|
+
attachMirrorSession(session.sessionId, platform3);
|
|
329623
329913
|
const driver = new MobileBridgeDriver({
|
|
329624
329914
|
sessionId: session.sessionId,
|
|
329625
329915
|
platform: platform3,
|
|
@@ -329660,6 +329950,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
329660
329950
|
}
|
|
329661
329951
|
}
|
|
329662
329952
|
if (session) {
|
|
329953
|
+
detachMirrorSession(session.sessionId);
|
|
329663
329954
|
try {
|
|
329664
329955
|
await session.stop();
|
|
329665
329956
|
} catch {
|
|
@@ -330030,9 +330321,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330030
330321
|
} else {
|
|
330031
330322
|
log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
|
|
330032
330323
|
if (result.logs) {
|
|
330033
|
-
console.log(`${
|
|
330324
|
+
console.log(`${C2.gray}\u2500\u2500 Runner logs \u2500\u2500${C2.reset}`);
|
|
330034
330325
|
console.log(result.logs);
|
|
330035
|
-
console.log(`${
|
|
330326
|
+
console.log(`${C2.gray}\u2500\u2500 End logs \u2500\u2500\u2500\u2500\u2500${C2.reset}`);
|
|
330036
330327
|
}
|
|
330037
330328
|
}
|
|
330038
330329
|
} catch (err) {
|
|
@@ -330054,20 +330345,24 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330054
330345
|
}
|
|
330055
330346
|
async function startRunner(opts) {
|
|
330056
330347
|
const { serverUrl, authToken, projectId, pollIntervalMs = 2e3, runnerMode = "playwright-native", enableMobile = false, enableMobileTunnel = false } = opts;
|
|
330348
|
+
process.env.SERVER_URL = serverUrl;
|
|
330349
|
+
process.env.AUTH_TOKEN = authToken;
|
|
330350
|
+
process.env.RUNNER_TOKEN = authToken;
|
|
330351
|
+
if (projectId) process.env.PROJECT_ID = projectId;
|
|
330057
330352
|
const headers = buildHeaders(authToken, projectId);
|
|
330058
330353
|
console.log("");
|
|
330059
|
-
console.log(`${
|
|
330060
|
-
console.log(`${
|
|
330354
|
+
console.log(`${C2.bold}${C2.cyan}\u{1F399}\uFE0F validate.qa Local Runner${C2.reset}`);
|
|
330355
|
+
console.log(`${C2.gray}${"\u2500".repeat(40)}${C2.reset}`);
|
|
330061
330356
|
log.info(`Server: ${serverUrl}`);
|
|
330062
330357
|
log.info(`Project: ${projectId}`);
|
|
330063
330358
|
log.info(`Mode: \u{1F3AD} Playwright Native${enableMobile ? " + \u{1F4F1} Mobile" : ""}${enableMobile && enableMobileTunnel ? " \u{1F310} Tunnel" : ""}`);
|
|
330064
330359
|
log.info(`Interval: ${pollIntervalMs}ms`);
|
|
330065
330360
|
if (process.env.DISCOVERY_PROMPT_V3 === "true") {
|
|
330066
|
-
log.ok("Discovery: V3
|
|
330361
|
+
log.ok("Discovery prompt: V3 (per-turn rebuild) \u2014 DISCOVERY_PROMPT_V3=true");
|
|
330067
330362
|
} else if (process.env.DISCOVERY_PROMPT_V3 === "false") {
|
|
330068
|
-
log.ok("Discovery: V2.5
|
|
330363
|
+
log.ok("Discovery prompt: V2.5 (full transcript + control brief) \u2014 DISCOVERY_PROMPT_V3=false");
|
|
330069
330364
|
} else {
|
|
330070
|
-
log.ok("Discovery:
|
|
330365
|
+
log.ok("Discovery prompt: V3 (per-turn rebuild, default) \u2014 set DISCOVERY_PROMPT_V3=false for V2.5");
|
|
330071
330366
|
}
|
|
330072
330367
|
if ((0, import_runner_core5.hasUsableChromeProfile)()) {
|
|
330073
330368
|
log.ok(`Browser: Chrome with user profile (cookies/logins persist)`);
|
|
@@ -330079,8 +330374,8 @@ async function startRunner(opts) {
|
|
|
330079
330374
|
const devices = discoverAllDevices();
|
|
330080
330375
|
const capabilities = {
|
|
330081
330376
|
web: true,
|
|
330082
|
-
ios: devices.some((d) => d.platform === "IOS"),
|
|
330083
|
-
android: devices.some((d) => d.platform === "ANDROID")
|
|
330377
|
+
ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
|
|
330378
|
+
android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
|
|
330084
330379
|
};
|
|
330085
330380
|
log.info(`\u{1F4F1} Mobile: iOS ${capabilities.ios ? "\u2713" : "\u2717"} | Android ${capabilities.android ? "\u2713" : "\u2717"} | ${devices.length} device(s)`);
|
|
330086
330381
|
try {
|
|
@@ -330141,6 +330436,33 @@ async function startRunner(opts) {
|
|
|
330141
330436
|
const startedAt = Date.now();
|
|
330142
330437
|
const MAX_BACKOFF_MS = 6e4;
|
|
330143
330438
|
let consecutivePollErrors = 0;
|
|
330439
|
+
let cachedDoctor = null;
|
|
330440
|
+
const DOCTOR_TTL_MS = 5 * 60 * 1e3;
|
|
330441
|
+
let doctorRefreshInFlight = false;
|
|
330442
|
+
const refreshDoctorInBackground = () => {
|
|
330443
|
+
if (doctorRefreshInFlight) return;
|
|
330444
|
+
doctorRefreshInFlight = true;
|
|
330445
|
+
setImmediate(() => {
|
|
330446
|
+
try {
|
|
330447
|
+
const report = runDoctor();
|
|
330448
|
+
cachedDoctor = { report, at: Date.now() };
|
|
330449
|
+
} catch (err) {
|
|
330450
|
+
log.dim(` [doctor] refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
330451
|
+
} finally {
|
|
330452
|
+
doctorRefreshInFlight = false;
|
|
330453
|
+
}
|
|
330454
|
+
});
|
|
330455
|
+
};
|
|
330456
|
+
const getCachedDoctor = () => {
|
|
330457
|
+
if (!enableMobile) return void 0;
|
|
330458
|
+
if (!cachedDoctor) {
|
|
330459
|
+
refreshDoctorInBackground();
|
|
330460
|
+
return void 0;
|
|
330461
|
+
}
|
|
330462
|
+
const now = Date.now();
|
|
330463
|
+
if (now - cachedDoctor.at >= DOCTOR_TTL_MS) refreshDoctorInBackground();
|
|
330464
|
+
return cachedDoctor.report;
|
|
330465
|
+
};
|
|
330144
330466
|
log.info(`Concurrency: ${RESOURCE_GOVERNOR_OPTIONS.adaptive ? `adaptive (${RESOURCE_GOVERNOR_OPTIONS.maxUnits} weighted units, hard cap ${MAX_CONCURRENT})` : `static (hard cap ${MAX_CONCURRENT})`}`);
|
|
330145
330467
|
const poolSize = envInt("BROWSER_POOL_SIZE", DEFAULT_BROWSER_POOL_SIZE);
|
|
330146
330468
|
if (poolSize > 0) {
|
|
@@ -330249,7 +330571,11 @@ async function startRunner(opts) {
|
|
|
330249
330571
|
const sendHeartbeat = async () => {
|
|
330250
330572
|
try {
|
|
330251
330573
|
const devices = enableMobile ? discoverAllDevices() : [];
|
|
330252
|
-
const capabilities = enableMobile ? {
|
|
330574
|
+
const capabilities = enableMobile ? {
|
|
330575
|
+
web: true,
|
|
330576
|
+
ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
|
|
330577
|
+
android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
|
|
330578
|
+
} : { web: true, ios: false, android: false };
|
|
330253
330579
|
const bridge = enableMobile ? getBridgeState() : void 0;
|
|
330254
330580
|
const resourceStatus = (0, import_runner_core11.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
|
|
330255
330581
|
const processInventory = await (0, import_runner_core11.getBrowserProcessInventory)();
|
|
@@ -330280,7 +330606,10 @@ async function startRunner(opts) {
|
|
|
330280
330606
|
resourceGovernor: resourceStatus.governor,
|
|
330281
330607
|
processInventory,
|
|
330282
330608
|
aiQueue: (0, import_runner_core11.getRunnerAIQueueStats)(),
|
|
330283
|
-
liveBrowsers: import_runner_core10.liveBrowserRegistry.snapshotForHeartbeat()
|
|
330609
|
+
liveBrowsers: import_runner_core10.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
330610
|
+
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
330611
|
+
// Cached & cheap — see getCachedDoctor above.
|
|
330612
|
+
doctor: getCachedDoctor()
|
|
330284
330613
|
}),
|
|
330285
330614
|
signal: AbortSignal.timeout(1e4)
|
|
330286
330615
|
});
|
|
@@ -330480,219 +330809,6 @@ async function startRunner(opts) {
|
|
|
330480
330809
|
}
|
|
330481
330810
|
}
|
|
330482
330811
|
|
|
330483
|
-
// src/services/doctor.ts
|
|
330484
|
-
import { execSync } from "child_process";
|
|
330485
|
-
import { existsSync as existsSync3 } from "fs";
|
|
330486
|
-
import { platform as platform2 } from "os";
|
|
330487
|
-
var C2 = {
|
|
330488
|
-
reset: "\x1B[0m",
|
|
330489
|
-
bold: "\x1B[1m",
|
|
330490
|
-
green: "\x1B[32m",
|
|
330491
|
-
red: "\x1B[31m",
|
|
330492
|
-
yellow: "\x1B[33m",
|
|
330493
|
-
cyan: "\x1B[36m",
|
|
330494
|
-
gray: "\x1B[90m"
|
|
330495
|
-
};
|
|
330496
|
-
function run(cmd) {
|
|
330497
|
-
try {
|
|
330498
|
-
return execSync(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
330499
|
-
} catch {
|
|
330500
|
-
return null;
|
|
330501
|
-
}
|
|
330502
|
-
}
|
|
330503
|
-
function check(name, fn) {
|
|
330504
|
-
try {
|
|
330505
|
-
const result = fn();
|
|
330506
|
-
return { name, ...result };
|
|
330507
|
-
} catch (err) {
|
|
330508
|
-
return { name, ok: false, detail: err instanceof Error ? err.message : "Unknown error" };
|
|
330509
|
-
}
|
|
330510
|
-
}
|
|
330511
|
-
function checkWeb() {
|
|
330512
|
-
const results = [];
|
|
330513
|
-
results.push(check("Node.js", () => {
|
|
330514
|
-
const version = process.version;
|
|
330515
|
-
const major = parseInt(version.slice(1).split(".")[0], 10);
|
|
330516
|
-
return { ok: major >= 20, detail: `${version}${major < 20 ? " (>= 20 required)" : ""}` };
|
|
330517
|
-
}));
|
|
330518
|
-
results.push(check("Playwright", () => {
|
|
330519
|
-
const version = run("npx playwright --version");
|
|
330520
|
-
if (!version) return { ok: false, detail: "Not installed. Run: npm i -D playwright" };
|
|
330521
|
-
return { ok: true, detail: version };
|
|
330522
|
-
}));
|
|
330523
|
-
return results;
|
|
330524
|
-
}
|
|
330525
|
-
function checkIOS() {
|
|
330526
|
-
const results = [];
|
|
330527
|
-
const isMac = platform2() === "darwin";
|
|
330528
|
-
results.push(check("macOS", () => ({
|
|
330529
|
-
ok: isMac,
|
|
330530
|
-
detail: isMac ? `${platform2()} detected` : "iOS testing requires macOS"
|
|
330531
|
-
})));
|
|
330532
|
-
if (!isMac) return results;
|
|
330533
|
-
results.push(check("Xcode", () => {
|
|
330534
|
-
const version = run("xcodebuild -version");
|
|
330535
|
-
if (!version) return { ok: false, detail: "Not installed. Install from App Store." };
|
|
330536
|
-
const firstLine = version.split("\n")[0];
|
|
330537
|
-
return { ok: true, detail: firstLine };
|
|
330538
|
-
}));
|
|
330539
|
-
results.push(check("Xcode Command Line Tools", () => {
|
|
330540
|
-
const path = run("xcode-select -p");
|
|
330541
|
-
return { ok: !!path, detail: path || "Not installed. Run: xcode-select --install" };
|
|
330542
|
-
}));
|
|
330543
|
-
results.push(check("Appium xcuitest driver", () => {
|
|
330544
|
-
const drivers = run("appium driver list --installed --json");
|
|
330545
|
-
if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
|
|
330546
|
-
try {
|
|
330547
|
-
const parsed = JSON.parse(drivers);
|
|
330548
|
-
const hasXcuitest = parsed?.xcuitest || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("xcuitest"));
|
|
330549
|
-
return {
|
|
330550
|
-
ok: !!hasXcuitest,
|
|
330551
|
-
detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
|
|
330552
|
-
};
|
|
330553
|
-
} catch {
|
|
330554
|
-
const hasXcuitest = drivers.toLowerCase().includes("xcuitest");
|
|
330555
|
-
return {
|
|
330556
|
-
ok: hasXcuitest,
|
|
330557
|
-
detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
|
|
330558
|
-
};
|
|
330559
|
-
}
|
|
330560
|
-
}));
|
|
330561
|
-
results.push(check("iOS Devices", () => {
|
|
330562
|
-
const json = run("xcrun simctl list devices booted --json");
|
|
330563
|
-
let bootedSimulators = 0;
|
|
330564
|
-
if (json) {
|
|
330565
|
-
try {
|
|
330566
|
-
const parsed = JSON.parse(json);
|
|
330567
|
-
const devices = Object.values(parsed.devices ?? {}).flat();
|
|
330568
|
-
bootedSimulators = devices.filter((d) => d.state === "Booted").length;
|
|
330569
|
-
} catch {
|
|
330570
|
-
}
|
|
330571
|
-
}
|
|
330572
|
-
const realDevices = discoverRealIOSDevices().length;
|
|
330573
|
-
const total = bootedSimulators + realDevices;
|
|
330574
|
-
if (total === 0) {
|
|
330575
|
-
return {
|
|
330576
|
-
ok: false,
|
|
330577
|
-
detail: "No devices found. Boot a simulator or connect a real device via USB."
|
|
330578
|
-
};
|
|
330579
|
-
}
|
|
330580
|
-
const details = [];
|
|
330581
|
-
if (bootedSimulators > 0) details.push(`${bootedSimulators} simulator(s)`);
|
|
330582
|
-
if (realDevices > 0) details.push(`${realDevices} real device(s)`);
|
|
330583
|
-
return {
|
|
330584
|
-
ok: true,
|
|
330585
|
-
detail: `${total} device(s) available: ${details.join(", ")}`
|
|
330586
|
-
};
|
|
330587
|
-
}));
|
|
330588
|
-
return results;
|
|
330589
|
-
}
|
|
330590
|
-
function checkAndroid() {
|
|
330591
|
-
const results = [];
|
|
330592
|
-
const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
330593
|
-
results.push(check("ANDROID_HOME", () => ({
|
|
330594
|
-
ok: !!androidHome && existsSync3(androidHome),
|
|
330595
|
-
detail: androidHome ? existsSync3(androidHome) ? androidHome : `${androidHome} (not found)` : "Not set. Set ANDROID_HOME to your Android SDK path."
|
|
330596
|
-
})));
|
|
330597
|
-
if (!androidHome || !existsSync3(androidHome)) return results;
|
|
330598
|
-
results.push(check("adb", () => {
|
|
330599
|
-
const version = run("adb version");
|
|
330600
|
-
if (!version) return { ok: false, detail: "Not found in PATH. Ensure Android platform-tools are installed." };
|
|
330601
|
-
const firstLine = version.split("\n")[0];
|
|
330602
|
-
return { ok: true, detail: firstLine };
|
|
330603
|
-
}));
|
|
330604
|
-
results.push(check("Appium uiautomator2 driver", () => {
|
|
330605
|
-
const drivers = run("appium driver list --installed --json");
|
|
330606
|
-
if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
|
|
330607
|
-
try {
|
|
330608
|
-
const parsed = JSON.parse(drivers);
|
|
330609
|
-
const hasUia2 = parsed?.uiautomator2 || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("uiautomator2"));
|
|
330610
|
-
return {
|
|
330611
|
-
ok: !!hasUia2,
|
|
330612
|
-
detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
|
|
330613
|
-
};
|
|
330614
|
-
} catch {
|
|
330615
|
-
const hasUia2 = drivers.toLowerCase().includes("uiautomator2");
|
|
330616
|
-
return {
|
|
330617
|
-
ok: hasUia2,
|
|
330618
|
-
detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
|
|
330619
|
-
};
|
|
330620
|
-
}
|
|
330621
|
-
}));
|
|
330622
|
-
results.push(check("Android Devices", () => {
|
|
330623
|
-
const output = run("adb devices");
|
|
330624
|
-
if (!output) return { ok: false, detail: "adb devices failed" };
|
|
330625
|
-
const lines = output.split("\n").filter((l) => l.includes(" device"));
|
|
330626
|
-
const emulators = lines.filter((l) => l.startsWith("emulator-"));
|
|
330627
|
-
const realDevices = lines.filter((l) => !l.startsWith("emulator-") && l.includes(" device"));
|
|
330628
|
-
const total = emulators.length + realDevices.length;
|
|
330629
|
-
if (total === 0) {
|
|
330630
|
-
return {
|
|
330631
|
-
ok: false,
|
|
330632
|
-
detail: "No devices found. Start an emulator or connect a real device via USB with USB debugging enabled."
|
|
330633
|
-
};
|
|
330634
|
-
}
|
|
330635
|
-
const details = [];
|
|
330636
|
-
if (emulators.length > 0) details.push(`${emulators.length} emulator(s)`);
|
|
330637
|
-
if (realDevices.length > 0) details.push(`${realDevices.length} real device(s)`);
|
|
330638
|
-
return {
|
|
330639
|
-
ok: true,
|
|
330640
|
-
detail: `${total} device(s) available: ${details.join(", ")}`
|
|
330641
|
-
};
|
|
330642
|
-
}));
|
|
330643
|
-
return results;
|
|
330644
|
-
}
|
|
330645
|
-
function checkTunnel() {
|
|
330646
|
-
const version = run("ngrok version") || run("ngrok --version");
|
|
330647
|
-
if (!version) {
|
|
330648
|
-
return [{
|
|
330649
|
-
name: "ngrok",
|
|
330650
|
-
ok: false,
|
|
330651
|
-
detail: "Not installed (only needed for --tunnel with an HTTPS dashboard). Install: brew install ngrok && ngrok config add-authtoken <token>"
|
|
330652
|
-
}];
|
|
330653
|
-
}
|
|
330654
|
-
const match = version.match(/(\d+)\.\d+\.\d+/);
|
|
330655
|
-
const major = match ? parseInt(match[1], 10) : 0;
|
|
330656
|
-
return [{
|
|
330657
|
-
name: "ngrok",
|
|
330658
|
-
ok: major >= 3,
|
|
330659
|
-
detail: major >= 3 ? `${version} (configure an authtoken with: ngrok config add-authtoken <token>)` : `${version} \u2014 ngrok v3+ is recommended (brew install ngrok)`
|
|
330660
|
-
}];
|
|
330661
|
-
}
|
|
330662
|
-
function runDoctor() {
|
|
330663
|
-
const web = checkWeb();
|
|
330664
|
-
const ios = checkIOS();
|
|
330665
|
-
const android = checkAndroid();
|
|
330666
|
-
const tunnel = checkTunnel();
|
|
330667
|
-
const allPassed = [...web, ...ios, ...android].every((c) => c.ok);
|
|
330668
|
-
return { web, ios, android, tunnel, allPassed };
|
|
330669
|
-
}
|
|
330670
|
-
function printDoctorReport(report) {
|
|
330671
|
-
console.log("");
|
|
330672
|
-
console.log(`${C2.bold}${C2.cyan}\u{1F3E5} validate.qa Doctor${C2.reset}`);
|
|
330673
|
-
console.log(`${C2.gray}${"\u2500".repeat(40)}${C2.reset}`);
|
|
330674
|
-
const printSection = (title, checks) => {
|
|
330675
|
-
console.log("");
|
|
330676
|
-
console.log(` ${C2.bold}${title}:${C2.reset}`);
|
|
330677
|
-
for (const c of checks) {
|
|
330678
|
-
const icon = c.ok ? `${C2.green}\u2713${C2.reset}` : `${C2.red}\u2717${C2.reset}`;
|
|
330679
|
-
console.log(` ${icon} ${c.name}: ${c.detail}`);
|
|
330680
|
-
}
|
|
330681
|
-
};
|
|
330682
|
-
printSection("Web Testing", report.web);
|
|
330683
|
-
printSection("iOS Testing", report.ios);
|
|
330684
|
-
printSection("Android Testing", report.android);
|
|
330685
|
-
printSection("Tunnel (optional \u2014 only for --tunnel / HTTPS dashboard)", report.tunnel);
|
|
330686
|
-
console.log("");
|
|
330687
|
-
if (report.allPassed) {
|
|
330688
|
-
console.log(` ${C2.green}\u2705 All checks passed - ready for web + mobile testing${C2.reset}`);
|
|
330689
|
-
} else {
|
|
330690
|
-
const failCount = [...report.web, ...report.ios, ...report.android].filter((c) => !c.ok).length;
|
|
330691
|
-
console.log(` ${C2.yellow}\u26A0 ${failCount} check(s) need attention${C2.reset}`);
|
|
330692
|
-
}
|
|
330693
|
-
console.log("");
|
|
330694
|
-
}
|
|
330695
|
-
|
|
330696
330812
|
// src/cli.ts
|
|
330697
330813
|
var pkg = require_package4();
|
|
330698
330814
|
var program = new Command();
|