/**
* Roku Integration Test Runner
*
* Orchestrates automated test sequences: deploy → wait → screenshot → keypress → log check → SG inspect
* Each step result is collected into a test report.
*/
import { RokuClient } from './roku-client.js';
import { LogClient } from './log-client.js';
import { WebDriverClient } from './webdriver-client.js';
export interface TestStep {
action: string;
// Common
label?: string;
// keypress
key?: string;
keys?: string[];
// wait
ms?: number;
delay?: number;
// element
using?: string;
value?: string;
attr?: string;
// check_log
pattern?: string;
timeout?: number;
// sg_nodes
mode?: string;
sizes?: boolean;
// deep_link
contentId?: string;
mediaType?: string;
// server_check
url?: string;
}
export interface StepResult {
step: number;
action: string;
label?: string;
success: boolean;
data?: unknown;
error?: string;
durationMs: number;
}
export interface TestReport {
testName: string;
startedAt: string;
completedAt: string;
totalDurationMs: number;
totalSteps: number;
passedSteps: number;
failedSteps: number;
results: StepResult[];
screenshots: { label: string; base64: string; contentType: string }[];
}
export class TestRunner {
private roku: RokuClient;
private log: LogClient;
private webdriver: WebDriverClient;
constructor(roku: RokuClient, log: LogClient, webdriver: WebDriverClient) {
this.roku = roku;
this.log = log;
this.webdriver = webdriver;
}
async run(testName: string, steps: TestStep[]): Promise<TestReport> {
const startTime = Date.now();
const results: StepResult[] = [];
const screenshots: { label: string; base64: string; contentType: string }[] = [];
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
const stepStart = Date.now();
let success = true;
let data: unknown = undefined;
let error: string | undefined;
try {
switch (step.action) {
case 'deploy': {
const msg = await this.roku.deploy();
data = msg;
break;
}
case 'wait': {
const waitMs = step.ms || 1000;
await new Promise(resolve => setTimeout(resolve, waitMs));
data = { waited: waitMs };
break;
}
case 'screenshot': {
const result = await this.roku.screenshot();
const base64 = result.imageData.toString('base64');
screenshots.push({
label: step.label || `step_${i}`,
base64,
contentType: result.contentType,
});
data = { size: result.imageData.length, contentType: result.contentType };
break;
}
case 'keypress': {
if (step.keys) {
await this.roku.keypressSequence(step.keys, step.delay || 500);
data = { keys: step.keys };
} else if (step.key) {
await this.roku.keypress(step.key);
data = { key: step.key };
}
break;
}
case 'launch': {
await this.roku.launch('dev');
data = 'App launched';
break;
}
case 'check_log': {
if (step.pattern) {
const matches = await this.log.waitForLog(step.pattern, step.timeout || 5000);
data = { pattern: step.pattern, matches };
} else {
data = { logs: this.log.getRecentLogs(20) };
}
break;
}
case 'sg_nodes': {
const sgResult = await this.roku.getSgNodes(step.mode || 'all', { sizes: step.sizes });
data = { nodeCount: sgResult.nodeCount, xmlLength: sgResult.xml.length };
break;
}
case 'element': {
const el = await this.webdriver.findElement(step.using || 'text', step.value || '', step.attr);
if (!el) {
success = false;
error = `Element not found: ${step.using}="${step.value}"`;
}
data = el;
break;
}
case 'focused_element': {
const focused = await this.webdriver.getFocusedElement();
data = focused;
break;
}
case 'perf': {
data = await this.roku.getPerformance();
break;
}
case 'media_player': {
data = await this.roku.getMediaPlayer();
break;
}
case 'check_resolution': {
const resCheck = await this.roku.checkResolution();
if (resCheck.issues.length > 0) {
success = false;
error = resCheck.issues.join('; ');
}
data = resCheck;
break;
}
case 'server_check': {
const serverResult = await this.roku.serverCheck(step.url);
if (!serverResult.ok) {
success = false;
error = `Server returned status ${serverResult.status}`;
}
data = serverResult;
break;
}
case 'deep_link': {
await this.roku.deepLink(step.contentId || '', step.mediaType || '');
data = { contentId: step.contentId, mediaType: step.mediaType };
break;
}
case 'check_drm': {
data = await this.roku.checkDrm();
break;
}
case 'check_raf': {
const manifest = this.roku.checkRafManifest();
const logs = this.log.getRafLogs();
data = { manifest, logs };
if (manifest.issues.length > 0) {
success = false;
error = manifest.issues.join('; ');
}
break;
}
case 'check_accessibility': {
const accResult = await this.roku.checkAccessibility();
if (accResult.issues.length > 0) {
success = false;
error = accResult.issues.join('; ');
}
data = accResult;
break;
}
default:
success = false;
error = `Unknown action: ${step.action}`;
}
} catch (err) {
success = false;
error = err instanceof Error ? err.message : String(err);
}
results.push({
step: i + 1,
action: step.action,
label: step.label,
success,
data,
error,
durationMs: Date.now() - stepStart,
});
}
const endTime = Date.now();
return {
testName,
startedAt: new Date(startTime).toISOString(),
completedAt: new Date(endTime).toISOString(),
totalDurationMs: endTime - startTime,
totalSteps: steps.length,
passedSteps: results.filter(r => r.success).length,
failedSteps: results.filter(r => !r.success).length,
results,
screenshots,
};
}
}