Skip to content

Commit da54214

Browse files
committed
test(native): capture ios qa logs with screenshots
1 parent 9f7b97b commit da54214

4 files changed

Lines changed: 247 additions & 108 deletions

File tree

docs/release/native-runtime-qa.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ npm run release:qa:capture -- \
124124
--matrix runtime \
125125
--row ios-line-charts \
126126
--platform ios \
127-
--output docs/release/artifacts/ios-line-charts-runtime.png
127+
--output docs/release/artifacts/ios-line-charts-runtime.png \
128+
--ios-log-output docs/release/artifacts/ios-line-charts-runtime.log
128129
```
129130

130131
Android accepts the same matrix row metadata:
@@ -138,7 +139,7 @@ npm run release:qa:capture -- \
138139
--android-log-output docs/release/artifacts/android-bar-charts-runtime.log
139140
```
140141

141-
Use `--device <simulator-udid-or-adb-serial>` for a specific target, `--dry-run` to print the native commands, and `--no-launch` when the row is already open and only the current screen should be captured. On Android, `--android-log-output <path>` clears logcat before launch and captures trailing logcat after the screenshot, so the row evidence can include both visual and runtime-log artifacts. After manual checks pass, attach the captured files to the structured matrix with `npm run release:qa:record -- --matrix runtime --row <row-id> --status pass --evidence <artifact> --evidence <log-artifact> --reviewed-by <name> --device "<device/os>" --build-surface "<build>" --notes "<checks passed>"`.
142+
Use `--device <simulator-udid-or-adb-serial>` for a specific target, `--dry-run` to print the native commands, and `--no-launch` when the row is already open and only the current screen should be captured. On iOS simulators, `--ios-log-output <path>` captures a compact `log show` window after launch and screenshot; override the default `process == "ChartKitShowcase"` predicate with `--ios-log-predicate` when needed. On Android, `--android-log-output <path>` clears logcat before launch and captures trailing logcat after the screenshot. After manual checks pass, attach the captured files to the structured matrix with `npm run release:qa:record -- --matrix runtime --row <row-id> --status pass --evidence <artifact> --evidence <log-artifact> --reviewed-by <name> --device "<device/os>" --build-surface "<build>" --notes "<checks passed>"`.
142143

143144
## Required Pages
144145

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
export const defaultAndroidPackage = "io.chartkit.showcase";
2+
3+
const validPlatforms = new Set(["android", "ios"]);
4+
5+
export const captureNativeQaUsage = `Usage:
6+
node scripts/capture-native-qa-screenshot.mjs --matrix <runtime|accessibility|performance> --row <row-id> --platform <ios|android> [options]
7+
8+
Options:
9+
--android-log-output <path> Also clear/capture Android logcat to this repo-relative path.
10+
--android-log-lines <number> Number of trailing logcat lines to capture. Defaults to 400.
11+
--device <id> iOS simulator UDID or Android adb serial. Defaults to booted/default device.
12+
--dry-run Print launch and screenshot commands without executing them.
13+
--ios-log-output <path> Also capture iOS simulator logs to this repo-relative path.
14+
--ios-log-last <duration> iOS simulator log window. Defaults to 2m.
15+
--ios-log-predicate <pred> iOS simulator log predicate. Defaults to process == "ChartKitShowcase".
16+
--no-launch Capture current screen without opening the row deep link first.
17+
--output <path> Repo-relative screenshot path. Defaults to docs/release/artifacts/<row-id>-screenshot.png.
18+
--package <id> Android package id. Defaults to io.chartkit.showcase.
19+
--wait-ms <number> Delay between launch and capture. Defaults to 1500.
20+
--help Show this help.
21+
`;
22+
23+
export const parseCaptureNativeQaArgs = (argv) => {
24+
const options = {
25+
androidPackage: defaultAndroidPackage,
26+
androidLogLines: 400,
27+
iosLogLast: "2m",
28+
iosLogPredicate: 'process == "ChartKitShowcase"',
29+
launch: true,
30+
waitMs: 1500
31+
};
32+
33+
for (let index = 0; index < argv.length; index += 1) {
34+
const arg = argv[index];
35+
const readValue = () => {
36+
const value = argv[index + 1];
37+
38+
if (!value || value.startsWith("--")) {
39+
throw new Error(`${arg} requires a value`);
40+
}
41+
42+
index += 1;
43+
return value;
44+
};
45+
46+
if (arg === "--android-log-lines") {
47+
options.androidLogLines = Number(readValue());
48+
} else if (arg === "--android-log-output") {
49+
options.androidLogOutput = readValue();
50+
} else if (arg === "--device") {
51+
options.device = readValue();
52+
} else if (arg === "--dry-run") {
53+
options.dryRun = true;
54+
} else if (arg === "--help" || arg === "-h") {
55+
options.help = true;
56+
} else if (arg === "--ios-log-last") {
57+
options.iosLogLast = readValue();
58+
} else if (arg === "--ios-log-output") {
59+
options.iosLogOutput = readValue();
60+
} else if (arg === "--ios-log-predicate") {
61+
options.iosLogPredicate = readValue();
62+
} else if (arg === "--matrix") {
63+
options.matrixName = readValue();
64+
} else if (arg === "--no-launch") {
65+
options.launch = false;
66+
} else if (arg === "--output") {
67+
options.output = readValue();
68+
} else if (arg === "--package") {
69+
options.androidPackage = readValue();
70+
} else if (arg === "--platform") {
71+
options.platform = readValue();
72+
} else if (arg === "--row") {
73+
options.rowId = readValue();
74+
} else if (arg === "--wait-ms") {
75+
options.waitMs = Number(readValue());
76+
} else {
77+
throw new Error(`Unknown argument: ${arg}`);
78+
}
79+
}
80+
81+
if (options.help) return options;
82+
if (!options.matrixName) throw new Error("--matrix is required");
83+
if (!options.rowId) throw new Error("--row is required");
84+
if (!validPlatforms.has(options.platform)) {
85+
throw new Error("--platform must be ios or android");
86+
}
87+
if (!Number.isFinite(options.waitMs) || options.waitMs < 0) {
88+
throw new Error("--wait-ms must be a non-negative number");
89+
}
90+
91+
return options;
92+
};

scripts/capture-native-qa-screenshot.mjs

Lines changed: 83 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -4,110 +4,14 @@ import process from "node:process";
44
import { spawnSync } from "node:child_process";
55
import { fileURLToPath } from "node:url";
66

7+
import {
8+
captureNativeQaUsage,
9+
defaultAndroidPackage,
10+
parseCaptureNativeQaArgs
11+
} from "./capture-native-qa-options.mjs";
712
import { listNativeQaRows } from "./record-native-qa-evidence.mjs";
813

914
const defaultRepoRoot = process.cwd();
10-
const defaultAndroidPackage = "io.chartkit.showcase";
11-
const validPlatforms = new Set(["android", "ios"]);
12-
13-
const usage = `Usage:
14-
node scripts/capture-native-qa-screenshot.mjs --matrix <runtime|accessibility|performance> --row <row-id> --platform <ios|android> [options]
15-
16-
Options:
17-
--android-log-output <path> Also clear/capture Android logcat to this repo-relative path.
18-
--android-log-lines <number> Number of trailing logcat lines to capture. Defaults to 400.
19-
--device <id> iOS simulator UDID or Android adb serial. Defaults to booted/default device.
20-
--dry-run Print launch and screenshot commands without executing them.
21-
--no-launch Capture current screen without opening the row deep link first.
22-
--output <path> Repo-relative screenshot path. Defaults to docs/release/artifacts/<row-id>-screenshot.png.
23-
--package <id> Android package id. Defaults to io.chartkit.showcase.
24-
--wait-ms <number> Delay between launch and capture. Defaults to 1500.
25-
--help Show this help.
26-
`;
27-
28-
const parseArgs = (argv) => {
29-
const options = {
30-
androidPackage: defaultAndroidPackage,
31-
androidLogLines: 400,
32-
launch: true,
33-
waitMs: 1500
34-
};
35-
36-
for (let index = 0; index < argv.length; index += 1) {
37-
const arg = argv[index];
38-
const readValue = () => {
39-
const value = argv[index + 1];
40-
41-
if (!value || value.startsWith("--")) {
42-
throw new Error(`${arg} requires a value`);
43-
}
44-
45-
index += 1;
46-
return value;
47-
};
48-
49-
if (arg === "--android-log-lines") {
50-
options.androidLogLines = Number(readValue());
51-
} else if (arg === "--android-log-output") {
52-
options.androidLogOutput = readValue();
53-
} else if (arg === "--device") {
54-
options.device = readValue();
55-
} else if (arg === "--dry-run") {
56-
options.dryRun = true;
57-
} else if (arg === "--help" || arg === "-h") {
58-
options.help = true;
59-
} else if (arg === "--matrix") {
60-
options.matrixName = readValue();
61-
} else if (arg === "--no-launch") {
62-
options.launch = false;
63-
} else if (arg === "--output") {
64-
options.output = readValue();
65-
} else if (arg === "--package") {
66-
options.androidPackage = readValue();
67-
} else if (arg === "--platform") {
68-
options.platform = readValue();
69-
} else if (arg === "--row") {
70-
options.rowId = readValue();
71-
} else if (arg === "--wait-ms") {
72-
options.waitMs = Number(readValue());
73-
} else {
74-
throw new Error(`Unknown argument: ${arg}`);
75-
}
76-
}
77-
78-
if (options.help) {
79-
return options;
80-
}
81-
82-
if (!options.matrixName) {
83-
throw new Error("--matrix is required");
84-
}
85-
86-
if (!options.rowId) {
87-
throw new Error("--row is required");
88-
}
89-
90-
if (!validPlatforms.has(options.platform)) {
91-
throw new Error("--platform must be ios or android");
92-
}
93-
94-
if (options.androidLogOutput && options.platform !== "android") {
95-
throw new Error("--android-log-output can only be used with Android");
96-
}
97-
98-
if (
99-
!Number.isInteger(options.androidLogLines) ||
100-
options.androidLogLines <= 0
101-
) {
102-
throw new Error("--android-log-lines must be a positive integer");
103-
}
104-
105-
if (!Number.isFinite(options.waitMs) || options.waitMs < 0) {
106-
throw new Error("--wait-ms must be a non-negative number");
107-
}
108-
109-
return options;
110-
};
11115

11216
const shellQuote = (value) => {
11317
const text = String(value);
@@ -139,15 +43,34 @@ const shouldWaitAfterCommand = ({ args, command }) =>
13943
const validateCaptureOptions = ({
14044
androidLogLines,
14145
androidLogOutput,
46+
iosLogLast,
47+
iosLogOutput,
48+
iosLogPredicate,
14249
platform
14350
}) => {
51+
if (platform !== "android" && platform !== "ios") {
52+
throw new Error("--platform must be ios or android");
53+
}
54+
14455
if (androidLogOutput && platform !== "android") {
14556
throw new Error("--android-log-output can only be used with Android");
14657
}
14758

59+
if (iosLogOutput && platform !== "ios") {
60+
throw new Error("--ios-log-output can only be used with iOS");
61+
}
62+
14863
if (!Number.isInteger(androidLogLines) || androidLogLines <= 0) {
14964
throw new Error("--android-log-lines must be a positive integer");
15065
}
66+
67+
if (iosLogOutput && !iosLogLast) {
68+
throw new Error("--ios-log-last must be a non-empty duration");
69+
}
70+
71+
if (iosLogOutput && !iosLogPredicate) {
72+
throw new Error("--ios-log-predicate must be non-empty");
73+
}
15174
};
15275

15376
const runCommand = ({ args, command, encoding = "utf8" }) => {
@@ -181,7 +104,15 @@ const runCommand = ({ args, command, encoding = "utf8" }) => {
181104

182105
const getIosDeviceTarget = (device) => device ?? "booted";
183106

184-
const buildIosCommands = ({ device, launch, launchUrl, outputPath }) => {
107+
const buildIosCommands = ({
108+
device,
109+
iosLogLast,
110+
iosLogOutputPath,
111+
iosLogPredicate,
112+
launch,
113+
launchUrl,
114+
outputPath
115+
}) => {
185116
const target = getIosDeviceTarget(device);
186117
const commands = [];
187118

@@ -197,6 +128,28 @@ const buildIosCommands = ({ device, launch, launchUrl, outputPath }) => {
197128
command: "xcrun"
198129
});
199130

131+
if (iosLogOutputPath) {
132+
commands.push({
133+
args: [
134+
"simctl",
135+
"spawn",
136+
target,
137+
"log",
138+
"show",
139+
"--style",
140+
"compact",
141+
"--last",
142+
iosLogLast,
143+
"--predicate",
144+
iosLogPredicate
145+
],
146+
command: "xcrun",
147+
encoding: "utf8",
148+
outputPath: iosLogOutputPath,
149+
writesStdoutToFile: true
150+
});
151+
}
152+
200153
return commands;
201154
};
202155

@@ -288,6 +241,9 @@ export const createNativeQaScreenshotPlan = async ({
288241
androidLogOutput,
289242
androidPackage = defaultAndroidPackage,
290243
device,
244+
iosLogLast = "2m",
245+
iosLogOutput,
246+
iosLogPredicate = 'process == "ChartKitShowcase"',
291247
launch = true,
292248
matrixName,
293249
output,
@@ -296,19 +252,32 @@ export const createNativeQaScreenshotPlan = async ({
296252
rowId,
297253
waitMs = 1500
298254
}) => {
299-
validateCaptureOptions({ androidLogLines, androidLogOutput, platform });
255+
validateCaptureOptions({
256+
androidLogLines,
257+
androidLogOutput,
258+
iosLogLast,
259+
iosLogOutput,
260+
iosLogPredicate,
261+
platform
262+
});
300263

301264
const row = await findRow({ matrixName, repoRoot, rowId });
302265
const outputPath = output ?? defaultOutputForRow(rowId);
303266
const absoluteOutputPath = path.resolve(repoRoot, outputPath);
304267
const absoluteAndroidLogOutputPath = androidLogOutput
305268
? path.resolve(repoRoot, androidLogOutput)
306269
: undefined;
270+
const absoluteIosLogOutputPath = iosLogOutput
271+
? path.resolve(repoRoot, iosLogOutput)
272+
: undefined;
307273
const commandOptions = {
308274
androidLogLines,
309275
androidLogOutputPath: absoluteAndroidLogOutputPath,
310276
androidPackage,
311277
device,
278+
iosLogLast,
279+
iosLogOutputPath: absoluteIosLogOutputPath,
280+
iosLogPredicate,
312281
launch,
313282
launchUrl: row.launchUrl,
314283
outputPath: absoluteOutputPath
@@ -320,16 +289,19 @@ export const createNativeQaScreenshotPlan = async ({
320289

321290
return {
322291
absoluteAndroidLogOutputPath,
292+
absoluteIosLogOutputPath,
323293
absoluteOutputPath,
324294
androidLogOutput,
325295
commands,
296+
iosLogOutput,
326297
launchUrl: row.launchUrl,
327298
outputPath,
328299
recordCommand: [
329300
`npm run release:qa:record -- --matrix ${matrixName} --row ${rowId}`,
330301
"--status partial",
331302
`--evidence ${outputPath}`,
332-
androidLogOutput ? `--evidence ${androidLogOutput}` : ""
303+
androidLogOutput ? `--evidence ${androidLogOutput}` : "",
304+
iosLogOutput ? `--evidence ${iosLogOutput}` : ""
333305
]
334306
.filter(Boolean)
335307
.join(" "),
@@ -355,6 +327,11 @@ export const captureNativeQaScreenshot = async ({
355327
recursive: true
356328
});
357329
}
330+
if (plan.absoluteIosLogOutputPath) {
331+
await mkdir(path.dirname(plan.absoluteIosLogOutputPath), {
332+
recursive: true
333+
});
334+
}
358335

359336
for (const command of plan.commands) {
360337
if (command.writesStdoutToFile) {
@@ -380,10 +357,10 @@ export const captureNativeQaScreenshot = async ({
380357
};
381358

382359
const main = async () => {
383-
const options = parseArgs(process.argv.slice(2));
360+
const options = parseCaptureNativeQaArgs(process.argv.slice(2));
384361

385362
if (options.help) {
386-
console.log(usage.trim());
363+
console.log(captureNativeQaUsage.trim());
387364
return;
388365
}
389366

0 commit comments

Comments
 (0)