mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
ff59309415
* test(android-e2e): record rotation state and logcat rotation decisions on a failed step The Android smoke has failed on the post-alert canary since 2026-09-03, and the failed-step screenshot from run 34021894996 shows why the reads miss: the device is in landscape at that point, with the canary below the fold, although `orientation portrait` had taken effect (the fixture confirmed it and every tap before the alert landed at x=540). Nothing we keep says what rotated it. A failed step now also writes failed-step-N-device.txt with the two rotation settings, the display's rotation lines, and WindowManager's rotation decisions from logcat, read through adb so they stand even when the CLI path failed. * test(android-e2e): keep the rotation evidence to WindowManager decisions and display rotation fields * test(e2e): own failed-step evidence in one collector, bound the device probes, test it Review follow-up on the rotation evidence. The collectors move out of the harness closure into failed-step-evidence.ts (fallow complexity), where the platform hook runs alongside the screenshot and snapshot and is bounded as a group (15s) so it can never delay them; a hook that throws, answers nothing, or never answers records nothing for the device file and leaves the CLI evidence in place. The Android probes get a 5s per-command bound, and logcat lines are capped in count and length. Deterministic tests cover the file contents, the hook failure and timeout cases, and the harness naming every evidence file, device file included, in failed-step.txt.
134 lines
4.6 KiB
TypeScript
134 lines
4.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import type { CliJsonResult } from './cli-json.ts';
|
|
import { collectFailedStepEvidence } from './live-device-e2e/failed-step-evidence.ts';
|
|
import { createLiveDeviceContext, createLiveDeviceHarness } from './live-device-e2e/runtime.ts';
|
|
|
|
function fakeCli(artifacts: { screenshot?: boolean; snapshot?: boolean } = {}) {
|
|
const calls: string[][] = [];
|
|
const runCli = async (args: string[]): Promise<CliJsonResult> => {
|
|
calls.push(args);
|
|
if (args[0] === 'screenshot') {
|
|
if (artifacts.screenshot === false) return { status: 1, stdout: '', stderr: 'no screen' };
|
|
fs.writeFileSync(args[1]!, 'png');
|
|
return { status: 0, stdout: '', stderr: '', json: { success: true } };
|
|
}
|
|
if (args[0] === 'snapshot') {
|
|
if (artifacts.snapshot === false) return { status: 1, stdout: '', stderr: 'no tree' };
|
|
return { status: 0, stdout: '', stderr: '', json: { success: true, data: { nodes: [] } } };
|
|
}
|
|
return { status: 1, stdout: '', stderr: 'step failed', json: { success: false } };
|
|
};
|
|
return { calls, runCli };
|
|
}
|
|
|
|
function tempStem(): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'failed-step-evidence-'));
|
|
return path.join(dir, 'failed-step-3');
|
|
}
|
|
|
|
test('device facts land in their own file next to the screenshot and snapshot', async () => {
|
|
const stem = tempStem();
|
|
const cli = fakeCli();
|
|
|
|
const evidence = await collectFailedStepEvidence({
|
|
stem,
|
|
runCli: cli.runCli,
|
|
deviceEvidence: async () => '## user_rotation\n1\n',
|
|
});
|
|
|
|
assert.deepEqual(evidence, {
|
|
screenshotPath: `${stem}.png`,
|
|
snapshotPath: `${stem}-snapshot.json`,
|
|
devicePath: `${stem}-device.txt`,
|
|
});
|
|
assert.equal(fs.readFileSync(`${stem}-device.txt`, 'utf8'), '## user_rotation\n1\n');
|
|
assert.deepEqual(JSON.parse(fs.readFileSync(`${stem}-snapshot.json`, 'utf8')), {
|
|
success: true,
|
|
data: { nodes: [] },
|
|
});
|
|
assert.deepEqual(
|
|
cli.calls.map((args) => args[0]),
|
|
['screenshot', 'snapshot'],
|
|
);
|
|
});
|
|
|
|
test('a device hook that throws or returns nothing still leaves the CLI evidence in place', async () => {
|
|
for (const deviceEvidence of [
|
|
async () => {
|
|
throw new Error('adb unavailable');
|
|
},
|
|
async () => undefined,
|
|
]) {
|
|
const stem = tempStem();
|
|
const evidence = await collectFailedStepEvidence({
|
|
stem,
|
|
runCli: fakeCli().runCli,
|
|
deviceEvidence,
|
|
});
|
|
|
|
assert.deepEqual(evidence, {
|
|
screenshotPath: `${stem}.png`,
|
|
snapshotPath: `${stem}-snapshot.json`,
|
|
});
|
|
assert.equal(fs.existsSync(`${stem}-device.txt`), false);
|
|
}
|
|
});
|
|
|
|
test('a device hook that never answers is bounded and never delays the CLI evidence', async () => {
|
|
const stem = tempStem();
|
|
const startedAt = Date.now();
|
|
|
|
const evidence = await collectFailedStepEvidence({
|
|
stem,
|
|
runCli: fakeCli().runCli,
|
|
deviceEvidence: () => new Promise<string>(() => undefined),
|
|
deviceEvidenceTimeoutMs: 50,
|
|
});
|
|
|
|
assert.deepEqual(evidence, {
|
|
screenshotPath: `${stem}.png`,
|
|
snapshotPath: `${stem}-snapshot.json`,
|
|
});
|
|
assert.ok(Date.now() - startedAt < 1_000);
|
|
});
|
|
|
|
test('a failed step names its evidence files, including the device file, in failed-step.txt', async () => {
|
|
const artifactRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'failed-step-harness-'));
|
|
const cli = fakeCli();
|
|
const harness = createLiveDeviceHarness<
|
|
ReturnType<typeof createLiveDeviceContext<string>>,
|
|
string
|
|
>({
|
|
behaviorsForScenario: () => [],
|
|
commandsForScenario: () => [],
|
|
commonFlags: (_context, args) => [...args, '--json'],
|
|
runCli: cli.runCli,
|
|
deviceEvidence: async () => 'accelerometer_rotation=1\n',
|
|
writeCoverageReport: () => undefined,
|
|
});
|
|
const context = createLiveDeviceContext<string>({ artifactRoot, session: 'evidence' });
|
|
|
|
await assert.rejects(
|
|
harness.runStep(context, 'read the canary', ['get', 'text', 'id="canary"']),
|
|
(error: Error) => {
|
|
assert.match(error.message, /step: read the canary/);
|
|
assert.match(error.message, /device: .*failed-step-1-device\.txt/);
|
|
assert.match(error.message, /screenshot: .*failed-step-1\.png/);
|
|
assert.match(error.message, /snapshot: .*failed-step-1-snapshot\.json/);
|
|
return true;
|
|
},
|
|
);
|
|
|
|
const report = fs.readFileSync(path.join(context.artifactDir, 'failed-step.txt'), 'utf8');
|
|
assert.match(report, /device: .*failed-step-1-device\.txt/);
|
|
assert.equal(
|
|
fs.readFileSync(path.join(context.artifactDir, 'failed-step-1-device.txt'), 'utf8'),
|
|
'accelerometer_rotation=1\n',
|
|
);
|
|
});
|