Skip to content

Commit 9f7b97b

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

3 files changed

Lines changed: 170 additions & 19 deletions

File tree

docs/release/native-runtime-qa.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,11 @@ npm run release:qa:capture -- \
134134
--matrix runtime \
135135
--row android-bar-charts \
136136
--platform android \
137-
--output docs/release/artifacts/android-bar-charts-runtime.png
137+
--output docs/release/artifacts/android-bar-charts-runtime.png \
138+
--android-log-output docs/release/artifacts/android-bar-charts-runtime.log
138139
```
139140

140-
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. After manual checks pass, attach the captured file to the structured matrix with `npm run release:qa:record -- --matrix runtime --row <row-id> --status pass --evidence <artifact> --reviewed-by <name> --device "<device/os>" --build-surface "<build>" --notes "<checks passed>"`.
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>"`.
141142

142143
## Required Pages
143144

scripts/capture-native-qa-screenshot.mjs

Lines changed: 102 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const usage = `Usage:
1414
node scripts/capture-native-qa-screenshot.mjs --matrix <runtime|accessibility|performance> --row <row-id> --platform <ios|android> [options]
1515
1616
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.
1719
--device <id> iOS simulator UDID or Android adb serial. Defaults to booted/default device.
1820
--dry-run Print launch and screenshot commands without executing them.
1921
--no-launch Capture current screen without opening the row deep link first.
@@ -26,6 +28,7 @@ Options:
2628
const parseArgs = (argv) => {
2729
const options = {
2830
androidPackage: defaultAndroidPackage,
31+
androidLogLines: 400,
2932
launch: true,
3033
waitMs: 1500
3134
};
@@ -43,7 +46,11 @@ const parseArgs = (argv) => {
4346
return value;
4447
};
4548

46-
if (arg === "--device") {
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") {
4754
options.device = readValue();
4855
} else if (arg === "--dry-run") {
4956
options.dryRun = true;
@@ -84,6 +91,17 @@ const parseArgs = (argv) => {
8491
throw new Error("--platform must be ios or android");
8592
}
8693

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+
87105
if (!Number.isFinite(options.waitMs) || options.waitMs < 0) {
88106
throw new Error("--wait-ms must be a non-negative number");
89107
}
@@ -114,6 +132,24 @@ const defaultOutputForRow = (rowId) =>
114132

115133
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
116134

135+
const shouldWaitAfterCommand = ({ args, command }) =>
136+
(command === "xcrun" && args[1] === "openurl") ||
137+
(command === "adb" && args.includes("start"));
138+
139+
const validateCaptureOptions = ({
140+
androidLogLines,
141+
androidLogOutput,
142+
platform
143+
}) => {
144+
if (androidLogOutput && platform !== "android") {
145+
throw new Error("--android-log-output can only be used with Android");
146+
}
147+
148+
if (!Number.isInteger(androidLogLines) || androidLogLines <= 0) {
149+
throw new Error("--android-log-lines must be a positive integer");
150+
}
151+
};
152+
117153
const runCommand = ({ args, command, encoding = "utf8" }) => {
118154
const result = spawnSync(command, args, {
119155
encoding,
@@ -127,13 +163,9 @@ const runCommand = ({ args, command, encoding = "utf8" }) => {
127163

128164
if (result.status !== 0) {
129165
const stderr =
130-
encoding === "buffer"
131-
? result.stderr?.toString("utf8")
132-
: result.stderr;
166+
encoding === "buffer" ? result.stderr?.toString("utf8") : result.stderr;
133167
const stdout =
134-
encoding === "buffer"
135-
? result.stdout?.toString("utf8")
136-
: result.stdout;
168+
encoding === "buffer" ? result.stdout?.toString("utf8") : result.stdout;
137169
const detail = (stderr || stdout || "").trim();
138170
const suffix = detail ? `\n\n${detail}` : "";
139171

@@ -182,13 +214,22 @@ const buildAndroidLaunchArgs = ({ androidPackage, device, launchUrl }) => [
182214
];
183215

184216
const buildAndroidCommands = ({
217+
androidLogLines,
218+
androidLogOutputPath,
185219
androidPackage,
186220
device,
187221
launch,
188222
launchUrl
189223
}) => {
190224
const commands = [];
191225

226+
if (androidLogOutputPath) {
227+
commands.push({
228+
args: [...(device ? ["-s", device] : []), "shell", "logcat", "-c"],
229+
command: "adb"
230+
});
231+
}
232+
192233
if (launch) {
193234
commands.push({
194235
args: buildAndroidLaunchArgs({ androidPackage, device, launchUrl }),
@@ -202,6 +243,22 @@ const buildAndroidCommands = ({
202243
writesStdoutToFile: true
203244
});
204245

246+
if (androidLogOutputPath) {
247+
commands.push({
248+
args: [
249+
...(device ? ["-s", device] : []),
250+
"logcat",
251+
"-d",
252+
"-t",
253+
String(androidLogLines)
254+
],
255+
command: "adb",
256+
encoding: "utf8",
257+
outputPath: androidLogOutputPath,
258+
writesStdoutToFile: true
259+
});
260+
}
261+
205262
return commands;
206263
};
207264

@@ -227,6 +284,8 @@ const findRow = async ({ matrixName, repoRoot, rowId }) => {
227284
};
228285

229286
export const createNativeQaScreenshotPlan = async ({
287+
androidLogLines = 400,
288+
androidLogOutput,
230289
androidPackage = defaultAndroidPackage,
231290
device,
232291
launch = true,
@@ -237,10 +296,17 @@ export const createNativeQaScreenshotPlan = async ({
237296
rowId,
238297
waitMs = 1500
239298
}) => {
299+
validateCaptureOptions({ androidLogLines, androidLogOutput, platform });
300+
240301
const row = await findRow({ matrixName, repoRoot, rowId });
241302
const outputPath = output ?? defaultOutputForRow(rowId);
242303
const absoluteOutputPath = path.resolve(repoRoot, outputPath);
304+
const absoluteAndroidLogOutputPath = androidLogOutput
305+
? path.resolve(repoRoot, androidLogOutput)
306+
: undefined;
243307
const commandOptions = {
308+
androidLogLines,
309+
androidLogOutputPath: absoluteAndroidLogOutputPath,
244310
androidPackage,
245311
device,
246312
launch,
@@ -253,11 +319,20 @@ export const createNativeQaScreenshotPlan = async ({
253319
: buildAndroidCommands(commandOptions);
254320

255321
return {
322+
absoluteAndroidLogOutputPath,
256323
absoluteOutputPath,
324+
androidLogOutput,
257325
commands,
258326
launchUrl: row.launchUrl,
259327
outputPath,
260-
recordCommand: `npm run release:qa:record -- --matrix ${matrixName} --row ${rowId} --status partial --evidence ${outputPath}`,
328+
recordCommand: [
329+
`npm run release:qa:record -- --matrix ${matrixName} --row ${rowId}`,
330+
"--status partial",
331+
`--evidence ${outputPath}`,
332+
androidLogOutput ? `--evidence ${androidLogOutput}` : ""
333+
]
334+
.filter(Boolean)
335+
.join(" "),
261336
row,
262337
waitMs
263338
};
@@ -275,22 +350,28 @@ export const captureNativeQaScreenshot = async ({
275350
}
276351

277352
await mkdir(path.dirname(plan.absoluteOutputPath), { recursive: true });
353+
if (plan.absoluteAndroidLogOutputPath) {
354+
await mkdir(path.dirname(plan.absoluteAndroidLogOutputPath), {
355+
recursive: true
356+
});
357+
}
278358

279-
for (const [index, command] of plan.commands.entries()) {
280-
const isLastCommand = index === plan.commands.length - 1;
281-
282-
if (isLastCommand && command.writesStdoutToFile) {
283-
const screenshot = runner({
359+
for (const command of plan.commands) {
360+
if (command.writesStdoutToFile) {
361+
const commandOutput = runner({
284362
args: command.args,
285363
command: command.command,
286-
encoding: "buffer"
364+
encoding: command.encoding ?? "buffer"
287365
});
288-
await writeFile(plan.absoluteOutputPath, screenshot);
366+
await writeFile(
367+
command.outputPath ?? plan.absoluteOutputPath,
368+
commandOutput
369+
);
289370
} else {
290371
runner(command);
291372
}
292373

293-
if (!isLastCommand && plan.waitMs > 0) {
374+
if (shouldWaitAfterCommand(command) && plan.waitMs > 0) {
294375
await sleep(plan.waitMs);
295376
}
296377
}
@@ -310,7 +391,11 @@ const main = async () => {
310391

311392
for (const command of result.commands) {
312393
const suffix = command.writesStdoutToFile
313-
? ` > ${shellQuote(result.outputPath)}`
394+
? ` > ${shellQuote(
395+
command.outputPath
396+
? path.relative(defaultRepoRoot, command.outputPath)
397+
: result.outputPath
398+
)}`
314399
: "";
315400
console.log(`$ ${commandText(command.command, command.args)}${suffix}`);
316401
}

scripts/capture-native-qa-screenshot.test.mjs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,58 @@ describe("native QA screenshot capture", () => {
133133
expect(screenshot).toBe("png-bytes");
134134
});
135135

136+
it("can capture Android screenshot and logcat evidence together", async () => {
137+
const tempRepo = await createTempRepo();
138+
const calls = [];
139+
const result = await captureNativeQaScreenshot({
140+
androidLogLines: 250,
141+
androidLogOutput: "docs/release/artifacts/android-line.log",
142+
matrixName: "runtime",
143+
output: "docs/release/artifacts/android-line.png",
144+
platform: "android",
145+
repoRoot: tempRepo,
146+
rowId: "android-line-charts",
147+
runner: (command) => {
148+
calls.push(command);
149+
150+
return command.encoding === "buffer"
151+
? Buffer.from("png-bytes")
152+
: "logcat lines";
153+
},
154+
waitMs: 0
155+
});
156+
const screenshot = await readFile(
157+
join(tempRepo, "docs/release/artifacts/android-line.png"),
158+
"utf8"
159+
);
160+
const logcat = await readFile(
161+
join(tempRepo, "docs/release/artifacts/android-line.log"),
162+
"utf8"
163+
);
164+
165+
expect(result.recordCommand).toBe(
166+
"npm run release:qa:record -- --matrix runtime --row android-line-charts --status partial --evidence docs/release/artifacts/android-line.png --evidence docs/release/artifacts/android-line.log"
167+
);
168+
expect(calls.map((call) => call.args)).toEqual([
169+
["shell", "logcat", "-c"],
170+
[
171+
"shell",
172+
"am",
173+
"start",
174+
"-W",
175+
"-a",
176+
"android.intent.action.VIEW",
177+
"-d",
178+
"'chartkitshowcase://showcase?view=charts&page=line-area'",
179+
"io.chartkit.showcase"
180+
],
181+
["exec-out", "screencap", "-p"],
182+
["logcat", "-d", "-t", "250"]
183+
]);
184+
expect(screenshot).toBe("png-bytes");
185+
expect(logcat).toBe("logcat lines");
186+
});
187+
136188
it("can skip launching and capture the current Android screen", async () => {
137189
const plan = await createNativeQaScreenshotPlan({
138190
launch: false,
@@ -161,4 +213,17 @@ describe("native QA screenshot capture", () => {
161213
})
162214
).rejects.toThrow("does not have a showcase deep link");
163215
});
216+
217+
it("rejects Android log output for iOS captures", async () => {
218+
await expect(
219+
captureNativeQaScreenshot({
220+
androidLogOutput: "docs/release/artifacts/ios-line.log",
221+
dryRun: true,
222+
matrixName: "runtime",
223+
platform: "ios",
224+
repoRoot,
225+
rowId: "ios-line-charts"
226+
})
227+
).rejects.toThrow("--android-log-output can only be used with Android");
228+
});
164229
});

0 commit comments

Comments
 (0)