Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-app/tests/copyable_block.rs

//! **The copy path, asserted on APPLIED OUTPUT** — the clipboard payload egui actually
//! emits, and the shape of the widget tree that makes a selection survivable.
//!
//! Filed as `kopiera-1` / `kopiera-2` (stinky 2026-08-20, measured live by the owner):
//! Ctrl+C in dwarves-ui's build panel pasted the PREVIOUS clipboard contents. Root
//! cause, read out of egui 0.35.0's own source: a selection spanning `N` separate
//! `ui.label`s is held by the context-wide `LabelSelectionState`, and
//! `label_text_selection.rs:220-224` deselects the moment either cursor end leaves the
//! visible rect ("let's just deselect"). `text_to_copy` is then empty and `copy_text`
//! is never called — so the clipboard keeps what it had, which reads as "copied the
//! wrong text" rather than "copied nothing".
//!
//! Three of these tests would pass on the BROKEN shape too if they only asserted
//! "something rendered" (LAW 2 — the identity-value trap). So each one is written
//! against a payload or a count that the N-labels shape provably cannot produce, and
//! `the_N_LABELS_shape_is_what_this_discriminates_against` is the RED control that
//! proves exactly that: it renders the old shape and asserts the new assertions fail
//! on it.
//!
//! ```bash
//! cargo test -p facett-app --test copyable_block
//! ```

use egui_kittest::Harness;
use egui_kittest::kittest::{By, Queryable};
use facett_app::widgets::{COPY_LABEL, CopyableBlock};

/// A block long enough that it CANNOT fit the area — the defect only exists when part
/// of the text is off-screen, so a short block would be a green that proves nothing.
fn serial_log(n: usize) -> Vec<String> {
    (1..=n).map(|i| format!("rad {i:04} — installer: mastering the medium")).collect()
}

/// Pump frames until egui emits a clipboard command, and return its payload.
///
/// Reading `output()` once after a click is not enough: the click is consumed on one
/// pass and the command is emitted on the pass that runs the button's handler.
fn clipboard_after(h: &mut Harness<'_>) -> Option<String> {
    for _ in 0..8 {
        h.step();
        let found = h.output().platform_output.commands.iter().find_map(|c| match c {
            egui::OutputCommand::CopyText(s) => Some(s.clone()),
            _ => None,
        });
        if found.is_some() {
            return found;
        }
    }
    None
}

/// **ONE widget carries EVERY line.** The whole point: one galley, one selection, one
/// `TextEditState` that a scroll cannot reach into. On the `N`-labels shape no single
/// accessibility node ever carries the joined text, so this count is 0 there.
#[test]
fn the_block_is_one_text_widget_carrying_every_line() {
    let text = CopyableBlock::join(serial_log(200));
    let t = text.clone();
    let mut h = Harness::builder()
        .with_size(egui::vec2(600.0, 300.0))
        .build_ui(move |ui| {
            CopyableBlock::new("serial", &t).max_height(220.0).follow(true).show(ui);
        });
    h.run();

    let carriers = h.query_all(By::new().value(&text)).count();
    assert_eq!(
        carriers, 1,
        "expected exactly ONE widget holding all 200 lines; found {carriers}. \
         Zero means the block went back to N labels (the deselect-on-scroll defect); \
         more than one means the block is drawn twice"
    );
}

/// **The button's payload is the WHOLE block, byte for byte.**
///
/// Not "a copy happened" — the exact string. `assert!(copied.is_some())` would pass if
/// the button copied only the first line, which is the failure mode a selection-based
/// copy actually has.
#[test]
fn the_copy_button_hands_over_the_whole_block_byte_for_byte() {
    let text = CopyableBlock::join(serial_log(200));
    let t = text.clone();
    let mut h = Harness::builder()
        .with_size(egui::vec2(600.0, 300.0))
        .build_ui(move |ui| {
            CopyableBlock::new("serial", &t).max_height(220.0).follow(true).show(ui);
        });
    h.run();
    h.get_by_label(COPY_LABEL).click();

    let got = clipboard_after(&mut h)
        .expect("no CopyText command — the copy button never reached the clipboard");
    assert_eq!(got, text, "the clipboard payload is not the block");
    // And say out loud that the OFF-SCREEN tail is in there. The visible head alone
    // would satisfy `assert_eq` against a truncated `text` if the fixture ever shrank.
    assert!(got.contains("rad 0200"), "the last line — the one below the fold — was not copied");
    assert!(got.contains("rad 0001"), "the first line was not copied");
}

/// **An empty block does not offer a button that would copy nothing.** Copying "" over
/// a full clipboard is the same silent damage by another route.
#[test]
fn an_empty_block_disarms_the_button() {
    let mut h = Harness::builder()
        .with_size(egui::vec2(400.0, 200.0))
        .build_ui(move |ui| {
            CopyableBlock::new("tom", "").show(ui);
        });
    h.run();
    h.get_by_label(COPY_LABEL).click();
    assert_eq!(
        clipboard_after(&mut h),
        None,
        "an empty block wrote to the clipboard — that overwrites whatever the operator had"
    );
}

/// **Two blocks on one screen are two independent blocks.** `build_scene.rs`'s scroll
/// area had no `id_salt` at all, so the offset bled between pane kinds (kopiera-3).
/// Distinct salts must produce distinct widget ids.
#[test]
fn two_blocks_do_not_share_state() {
    let a = CopyableBlock::join(serial_log(40));
    let b = CopyableBlock::join(serial_log(60));
    let (ta, tb) = (a.clone(), b.clone());
    let mut h = Harness::builder()
        .with_size(egui::vec2(600.0, 500.0))
        .build_ui(move |ui| {
            CopyableBlock::new("serial", &ta).max_height(120.0).show(ui);
            CopyableBlock::new("logg", &tb).max_height(120.0).show(ui);
        });
    h.run();
    assert_eq!(h.query_all(By::new().value(&a)).count(), 1, "block A lost its own text");
    assert_eq!(h.query_all(By::new().value(&b)).count(), 1, "block B lost its own text");
}

/// **THE RED CONTROL.** Render the shape this widget replaces — `N` labels in a
/// bounded `ScrollArea` — and assert that the two assertions above genuinely FAIL on
/// it. Without this, `the_block_is_one_text_widget_carrying_every_line` could be
/// passing for some unrelated reason and nobody would know.
#[test]
fn the_n_labels_shape_is_what_this_discriminates_against() {
    let text = CopyableBlock::join(serial_log(200));
    let lines = serial_log(200);
    let t = text.clone();
    let mut h = Harness::builder()
        .with_size(egui::vec2(600.0, 300.0))
        .build_ui(move |ui| {
            egui::ScrollArea::vertical().max_height(220.0).stick_to_bottom(true).show(ui, |ui| {
                for l in &lines {
                    ui.label(egui::RichText::new(l).monospace().small());
                }
            });
        });
    h.run();

    assert_eq!(
        h.query_all(By::new().value(&t)).count(),
        0,
        "the N-labels shape produced a whole-text node — then the count assertion above \
         proves nothing and this whole guard is hollow"
    );
    assert_eq!(
        h.query_all(By::new().label(COPY_LABEL)).count(),
        0,
        "the N-labels shape has no copy button — if this finds one the control is wrong"
    );
}