import type { ITestResult, ITestMetadata, IPlanLine, ISnapshotData, IErrorBlock, ITestEvent } from './protocol.types.js'; import { PROTOCOL_MARKERS, PROTOCOL_VERSION } from './protocol.types.js'; /** * ProtocolEmitter generates Protocol V2 messages * This class is used by tapbundle to emit test results in the new protocol format */ export class ProtocolEmitter { /** * Emit protocol version header */ public emitProtocolHeader(): string { return `${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.PROTOCOL_PREFIX}${PROTOCOL_VERSION}${PROTOCOL_MARKERS.END}`; } /** * Emit TAP version line */ public emitTapVersion(version: number = 13): string { return `TAP version ${version}`; } /** * Emit test plan */ public emitPlan(plan: IPlanLine): string { if (plan.skipAll) { return `1..0 # Skipped: ${plan.skipAll}`; } return `${plan.start}..${plan.end}`; } /** * Emit a test result */ public emitTest(result: ITestResult): string[] { const lines: string[] = []; // Build the basic TAP line let tapLine = result.ok ? 'ok' : 'not ok'; tapLine += ` ${result.testNumber}`; tapLine += ` - ${result.description}`; // Add directive if present if (result.directive) { tapLine += ` # ${result.directive.type.toUpperCase()}`; if (result.directive.reason) { tapLine += ` ${result.directive.reason}`; } } // Add inline metadata for simple cases if (result.metadata && this.shouldUseInlineMetadata(result.metadata)) { const metaStr = this.createInlineMetadata(result.metadata); if (metaStr) { tapLine += ` ${metaStr}`; } } lines.push(tapLine); // Add block metadata for complex cases if (result.metadata && !this.shouldUseInlineMetadata(result.metadata)) { lines.push(...this.createBlockMetadata(result.metadata, result.testNumber)); } return lines; } /** * Emit a comment line */ public emitComment(comment: string): string { return `# ${comment}`; } /** * Emit bailout */ public emitBailout(reason: string): string { return `Bail out! ${reason}`; } /** * Emit snapshot data */ public emitSnapshot(snapshot: ISnapshotData): string[] { const lines: string[] = []; lines.push(`${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.SNAPSHOT_PREFIX}${snapshot.name}${PROTOCOL_MARKERS.END}`); if (snapshot.format === 'json') { lines.push(JSON.stringify(snapshot.content, null, 2)); } else { lines.push(String(snapshot.content)); } lines.push(`${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.SNAPSHOT_END}${PROTOCOL_MARKERS.END}`); return lines; } /** * Emit error block */ public emitError(error: IErrorBlock): string[] { const lines: string[] = []; lines.push(`${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.ERROR_PREFIX}${PROTOCOL_MARKERS.END}`); lines.push(JSON.stringify(error, null, 2)); lines.push(`${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.ERROR_END}${PROTOCOL_MARKERS.END}`); return lines; } /** * Emit test event */ public emitEvent(event: ITestEvent): string { const eventJson = JSON.stringify(event); return `${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.EVENT_PREFIX}${eventJson}${PROTOCOL_MARKERS.END}`; } /** * Check if metadata should be inline */ private shouldUseInlineMetadata(metadata: ITestMetadata): boolean { // Use inline for simple metadata (time, retry, simple skip/todo) const hasComplexData = metadata.error || metadata.custom || (metadata.tags && metadata.tags.length > 0) || metadata.file || metadata.line; return !hasComplexData; } /** * Create inline metadata string */ private createInlineMetadata(metadata: ITestMetadata): string { const parts: string[] = []; if (metadata.time !== undefined) { parts.push(`time:${metadata.time}`); } if (metadata.retry !== undefined) { parts.push(`retry:${metadata.retry}`); } if (metadata.skip) { return `${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.SKIP_PREFIX}${metadata.skip}${PROTOCOL_MARKERS.END}`; } if (metadata.todo) { return `${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.TODO_PREFIX}${metadata.todo}${PROTOCOL_MARKERS.END}`; } if (parts.length > 0) { return `${PROTOCOL_MARKERS.START}${parts.join(',')}${PROTOCOL_MARKERS.END}`; } return ''; } /** * Create block metadata lines */ private createBlockMetadata(metadata: ITestMetadata, testNumber?: number): string[] { const lines: string[] = []; // Create a clean metadata object without skip/todo (handled inline) const blockMeta = { ...metadata }; delete blockMeta.skip; delete blockMeta.todo; // Emit metadata block const metaJson = JSON.stringify(blockMeta); lines.push(`${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.META_PREFIX}${metaJson}${PROTOCOL_MARKERS.END}`); // Emit separate error block if present if (metadata.error) { lines.push(...this.emitError({ testNumber, error: metadata.error })); } return lines; } }