tui-test-rs 0.1.0-beta.5

In-process terminal automation, inspection, assertions, and recording
Documentation
use super::*;
use crate::profile::Profile;
use crate::render::svg::RenderState;
use crate::terminal::alacritty::AlacrittyEmu;
use crate::terminal::emu::Emulator;

pub(super) const HOSTILE: &str = "</script><script>globalThis.reportPwned=true</script>\n| ``` ` & <img src=x onerror=alert(1)> /* REPORT_JS */ null /* REPORT_DATA */";

fn capture(history: &mut ScreenHistory, emu: &AlacrittyEmu, time: u64) -> u64 {
    history.capture(
        emu.viewable_rows(),
        emu.size().0,
        emu.title(),
        emu.cursor(),
        emu.cursor_visible(),
        Emulator::cursor_shape(emu),
        time,
        RenderState::capture(emu),
    )
}

pub(super) fn fixture() -> (FailureReport, FailureObservation) {
    let mut emu = AlacrittyEmu::new(20, 4, &Profile::default());
    let mut history = ScreenHistory::new(10);
    emu.process(b"\x1b[?25lREADY");
    capture(&mut history, &emu, 10);
    history.pin_current();
    capture(&mut history, &emu, 12);
    emu.process(b"\x1b[2J\x1b[HWORKING");
    capture(&mut history, &emu, 20);
    emu.resize(24, 5);
    emu.process("\x1b]4;1;#112233\x07\x1b[2J\x1b[H\x1b[31mA\x1b[0m\u{4f60}e\u{301}".as_bytes());
    emu.process(b"\x1b]2;</script><img src=x onerror=alert(1)>\x07");
    let sequence = capture(&mut history, &emu, 30);
    capture(&mut history, &emu, 40);
    history.pin_current();
    let observation = FailureObservation {
        rows: emu.viewable_rows(),
        cols: emu.size().0,
        title: emu.title(),
        cursor: None,
        cursor_position: emu.cursor(),
        cursor_visible: emu.cursor_visible(),
        cursor_shape: Emulator::cursor_shape(&emu),
        render_state: RenderState::capture(&emu),
        screen_sequence: sequence,
        output_revision: 3,
        captured_ms: 40,
        last_visual_change_ms: 30,
        history,
        process: ProcessDiagnostics {
            pid: Some(123),
            state: "running".into(),
            exit_code: None,
            status_error: None,
            cancelled: false,
            ready: true,
            command_running: false,
            last_command_exit: Some(0),
        },
        runtime: RuntimeDiagnostics {
            session_name: Some("deployment-wizard".into()),
            shell: Some("pwsh".into()),
            timeouts: Some(crate::api::EffectiveTimeouts {
                text: 5000,
                idle: 5000,
                command: 30000,
                exit: 30000,
                ready: 30000,
            }),
            tui_test_version: "fixture".into(),
            backend: "alacritty".into(),
            target_os: "test".into(),
            target_arch: "test".into(),
        },
    };
    let mut details = FailureReport::new(
        "expect.text",
        Some(20),
        FailureReason::LocatorNoMatch,
        HOSTILE,
    );
    details.operation.failed_screen_sequence = sequence;
    details.operation.started_screen_sequence = 2;
    details.operation.elapsed_ms = 20;
    details.terminal = Some(observation.terminal());
    details.runtime = Some(observation.runtime.clone());
    details.process = Some(observation.process.clone());
    details.context.insert("test".into(), HOSTILE.into());
    details.comparison = Some(ComparisonDiagnostics {
        kind: "text".into(),
        expected: Some(HOSTILE.into()),
        actual: Some("A\u{4f60}e\u{301}".into()),
    });
    for (id, name, screen, result, assertion, started_ms, ended_ms) in [
        (1, "evicted check", 999, "ok", true, 0, 1),
        (2, "ready", 1, "ok", true, 2, 10),
        (3, "ready again", 1, "ok", true, 10, 12),
        (4, "submit", 2, "ok", false, 12, 20),
        (5, "expect.text", sequence, "assertion", true, 20, 40),
    ] {
        details.recent_operations.push(OperationEvent {
            sequence: id,
            name: name.into(),
            started_ms,
            ended_ms,
            result: result.into(),
            screen_before: if id == 5 { 2 } else { 0 },
            screen_at_return: screen,
            safe_summary: name.into(),
            input: None,
            is_assertion: assertion,
            expectation: match id {
                2 | 3 => Some(OperationExpectation::Locator {
                    query: Box::new(crate::api::LocatorQuery::text("READY")),
                    outcome: LocatorExpectation::Visible,
                }),
                5 => {
                    let mut query = crate::api::LocatorQuery::text("A");
                    query.style.foreground = Some("2".into());
                    Some(OperationExpectation::Locator {
                        query: Box::new(query),
                        outcome: LocatorExpectation::Visible,
                    })
                }
                _ => None,
            },
        });
    }
    details.locator = Some(LocatorDiagnostics {
        stages_truncated: false,
        evaluation_error: None,
        search_scope: "full".into(),
        viewport_origin_y: 7,
        final_candidate_count: 0,
        selected: Vec::new(),
        failure_stage: Some(0),
        failure_reason: Some(LocatorFailureReason::StyleFilterRemovedAll),
        stages: vec![LocatorStageDiagnostics {
            expression_path: "root".into(),
            evaluations: 1,
            failure_reason: Some(LocatorFailureReason::StyleFilterRemovedAll),
            stage_index: 0,
            mode: LocatorStageMode::Text,
            selector: Some(crate::api::LocatorQuery::text("A").selector),
            direction: crate::api::LocatorDirection::Within,
            requested_occurrence: crate::api::MatchOccurrence::Any,
            effective_occurrence: crate::api::MatchOccurrence::Any,
            occurrence_source: OccurrenceSource::Explicit,
            input_candidate_count: 0,
            raw_candidate_count: 1,
            style_candidate_count: 0,
            selected_count: 0,
            candidates: Vec::new(),
            candidates_truncated: false,
            mismatches_truncated: false,
            mismatches: vec![CellMismatch {
                location: crate::api::TextPosition { column: 0, row: 7 },
                grapheme: "A".into(),
                property: "foreground".into(),
                operator: "equals".into(),
                expected: "2".into(),
                actual: "1".into(),
                resolved: Some("#112233".into()),
                reason: "color mismatch".into(),
            }],
        }],
    });
    details.finish_signature();
    (details, observation)
}