Files
photocraft/docs/integration-testing/per-pr-regressions.md
T

493 KiB
Raw Blame History

Per-PR regression execution appendix

Each automated case uses the synthetic fixture/setup and assertions in the named checked-in regression, linked at the reviewed head. The action is the command below. Expected: at least one matching test executes, every assertion passes, and the behavior in the case label is retained. A zero-match run is BLOCKED, never PASSED. After a moved-module port, locate the same test in the frozen integration tree and retain its fixture and assertions. Manual combined cases are in the main test plan.

All cases initially NOT RUN. Record Actual, Evidence and Tester in the empty fields; the status vocabulary is PASSED / FAILED / BLOCKED / NOT RUN / SKIPPED. Missing fixtures, unavailable adapters and unsupported platforms are BLOCKED or SKIPPED with a reason, rather than silently passing.

Reviewed-case execution rules

Run every listed test separately on the final frozen integration SHA, recording its final-tree source path, full discovered name, actual count and outcome. Historical reviewed-head links establish provenance only. Zero-match, ignored-only or early-return-without-required-capability runs are BLOCKED, not PASSED. Locate preserved equivalent assertions when a test moved; never substitute an unrelated guard. Automated and native/manual subcases have separate results. Use owned temporary settings/outputs; never migrate actual user settings, publish a release, bypass security or force a driver crash.

PR-3455: Undo and redo action definition edits from the Actions panel

Concrete setup and expected assertion examples

recording_and_appending_steps_can_be_undone_without_restarting_recording setup/action excerpt:

        let mut s = Session::new();
        s.execute("file.new", json!({"width": 8, "height": 8})).unwrap();
        s.execute("actions.record", json!({"name": "A"})).unwrap();
        s.execute("layer.new.layer", json!({})).unwrap();
        s.execute("actions.stop", json!({})).unwrap();
        s.execute("actions.record", json!({"action": "A"})).unwrap();
        s.execute("select.all", json!({})).unwrap();
        s.execute("actions.stop", json!({})).unwrap();
        let doc = s.active().unwrap().doc.clone();
        assert_eq!(s.actions.list[0].steps.len(), 2);
        s.execute("actions.undo", json!({})).unwrap();
        assert_eq!(s.actions.list[0].steps.len(), 1);

Expected assertions from that fixture:

        assert_eq!(s.actions.list[0].steps.len(), 2);
        s.execute("actions.undo", json!({})).unwrap();
        assert_eq!(s.actions.list[0].steps.len(), 1);
        assert!(s.actions.recording.is_none());
        assert_eq!(s.actions.list[0].steps.len(), 1);
        assert!(s.actions.recording.is_none());
        s.execute("actions.redo", json!({})).unwrap();
        assert_eq!(s.actions.list[0].steps.len(), 2);
        assert!(s.actions.recording.is_none());
        s.execute("actions.redo", json!({})).unwrap();
        assert_eq!(s.actions.list[0].steps.len(), 2);
        assert!(s.actions.recording.is_none());

definition_edits_undo_redo_without_touching_document_or_journal setup/action excerpt:

        let mut s = Session::new();
        s.execute("file.new", json!({"width": 8, "height": 8})).unwrap();
        s.actions.list.push(Action {
            name: "A".into(),
            steps: vec![("layer.new.layer".into(), json!({})), ("select.all".into(), json!({}))],
            disabled: Default::default(),
        });
        s.actions.list.push(Action { name: "Caller".into(), steps: vec![("actions.play".into(), json!({"action": "A"}))], disabled: Default::default() });
        s.edit_prefs(|p| {
            p.shortcuts.insert("actions.play:A".into(), "F6".into());
        });
        let before = s.actions.list.clone();

Expected assertions from that fixture:

        assert_eq!(s.actions.list, before);
        assert_eq!(s.prefs().shortcuts.get("actions.play:A").map(String::as_str), Some("F6"));
        assert!(!s.prefs().shortcuts.contains_key("actions.play:B"));
        for _ in 0..3 {
        assert_eq!(s.prefs().shortcuts.get("actions.play:A").map(String::as_str), Some("F6"));
        assert!(!s.prefs().shortcuts.contains_key("actions.play:B"));
        for _ in 0..3 {
            s.execute("actions.redo", json!({})).unwrap();
        assert!(!s.prefs().shortcuts.contains_key("actions.play:B"));
        for _ in 0..3 {
            s.execute("actions.redo", json!({})).unwrap();
        }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3748: control: suggest near matches for unknown command ids and methods (#3674)

Concrete setup and expected assertion examples

unknown_names_suggest_the_right_method_or_command setup/action excerpt:

        let mut h = Headless::new();
        let err = |h: &mut Headless, m: &str, p: Value| h.handle(m, p).unwrap_err().to_string();
        let e = err(&mut h, "engine.execute", json!({"command": "engine.commands"}));
        assert!(e.contains("did you mean `command.list`") && e.contains("protocol method"), "{e}");
        let e = err(&mut h, "engine.execute", json!({"command": "doc.inspect"}));
        assert!(e.contains("`doc.inspect` is a protocol method"), "{e}");
        let e = err(&mut h, "engine.execute", json!({"command": "edit.undoo"}));
        assert!(e.contains("did you mean `edit.undo`"), "{e}");
        let e = err(&mut h, "doc.inspct", Value::Null);
        assert!(e.contains("did you mean `doc.inspect`") && e.contains("(try `methods`)"), "{e}");
        let e = err(&mut h, "edit.undo", Value::Null);
        assert!(e.contains("engine.execute {\"command\": \"edit.undo\"}"), "{e}");

Expected assertions from that fixture:

        assert!(e.contains("did you mean `command.list`") && e.contains("protocol method"), "{e}");
        let e = err(&mut h, "engine.execute", json!({"command": "doc.inspect"}));
        assert!(e.contains("`doc.inspect` is a protocol method"), "{e}");
        let e = err(&mut h, "engine.execute", json!({"command": "edit.undoo"}));
        assert!(e.contains("`doc.inspect` is a protocol method"), "{e}");
        let e = err(&mut h, "engine.execute", json!({"command": "edit.undoo"}));
        assert!(e.contains("did you mean `edit.undo`"), "{e}");
        let e = err(&mut h, "doc.inspct", Value::Null);
        assert!(e.contains("did you mean `edit.undo`"), "{e}");
        let e = err(&mut h, "doc.inspct", Value::Null);
        assert!(e.contains("did you mean `doc.inspect`") && e.contains("(try `methods`)"), "{e}");
        let e = err(&mut h, "edit.undo", Value::Null);

errors_are_tool_errors_not_crashes setup/action excerpt:

    let client = connect(PhotocraftMcp::headless()).await;
    let r = call(&client, "command_run", json!({"id": "no.such.command"})).await;
    assert_eq!(r.is_error, Some(true));
    assert!(text(&r).contains("unknown command"));
    // #3674: the error names the nearest registered id.
    let r = call(&client, "command_run", json!({"id": "engine.commands"})).await;
    assert_eq!(r.is_error, Some(true));
    assert!(text(&r).contains("did you mean `command.list`"), "{}", text(&r));
    let r = call(&client, "doc_inspect", json!({"index": 0})).await;
    assert_eq!(r.is_error, Some(true));
    let r = call(&client, "doc_open", json!({"path": "/definitely/missing.png"})).await;
    assert_eq!(r.is_error, Some(true));

Expected assertions from that fixture:

    assert_eq!(r.is_error, Some(true));
    assert!(text(&r).contains("unknown command"));
    // #3674: the error names the nearest registered id.
    let r = call(&client, "command_run", json!({"id": "engine.commands"})).await;
    assert!(text(&r).contains("unknown command"));
    // #3674: the error names the nearest registered id.
    let r = call(&client, "command_run", json!({"id": "engine.commands"})).await;
    assert_eq!(r.is_error, Some(true));
    assert_eq!(r.is_error, Some(true));
    assert!(text(&r).contains("did you mean `command.list`"), "{}", text(&r));
    let r = call(&client, "doc_inspect", json!({"index": 0})).await;
    assert_eq!(r.is_error, Some(true));

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4993: Prevent control-channel engine commands from editing behind modal dialogs

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: engine execute cannot change document behind adjustment dialog. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

engine_execute_cannot_change_document_behind_adjustment_dialog setup/action excerpt:

        let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        let ctx = egui::Context::default();
        app.run("file.new", json!({"width": 60, "height": 40, "background": "#3366cc"})).unwrap();
        let opened = call(&mut app, &ctx, "ui.menu.invoke", json!({"id": "image.adjustments.hueSaturation"}));
        assert_eq!(opened["ok"], true, "{opened}");
        let dialog = opened["result"]["dialog"].as_u64().unwrap();
        let revision = app.session.active().unwrap().revision;
        let fields = call(&mut app, &ctx, "ui.inspect", json!({}))["result"]["dialogs"][0]["fields"].clone();

        for method in ["engine.execute", "ui.menu.invoke"] {
            let blocked = call(&mut app, &ctx, method, json!({"command": "image.adjustments.invert"}));
            assert_eq!(blocked["ok"], false, "{method}: {blocked}");

Expected assertions from that fixture:

        assert_eq!(opened["ok"], true, "{opened}");
        let dialog = opened["result"]["dialog"].as_u64().unwrap();
        let revision = app.session.active().unwrap().revision;
        let fields = call(&mut app, &ctx, "ui.inspect", json!({}))["result"]["dialogs"][0]["fields"].clone();
            assert_eq!(blocked["ok"], false, "{method}: {blocked}");
            assert_eq!(app.session.active().unwrap().revision, revision, "{method} changed the document");
            let snapshot = call(&mut app, &ctx, "ui.inspect", json!({}));
            assert_eq!(snapshot["result"]["dialogs"][0]["id"], dialog);
            assert_eq!(app.session.active().unwrap().revision, revision, "{method} changed the document");
            let snapshot = call(&mut app, &ctx, "ui.inspect", json!({}));
            assert_eq!(snapshot["result"]["dialogs"][0]["id"], dialog);
            assert_eq!(snapshot["result"]["dialogs"][0]["fields"], fields);

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5090: Actions: add editable PhotoCraft Essentials starter presets

Concrete setup and expected assertion examples

generated_defaults_are_persisted_without_unsaved_user_work setup/action excerpt:

        let b = Bridge::default();
        let (s, warnings) = session(b.clone());
        assert!(warnings.is_empty());
        assert_eq!(s.actions.list.len(), 5);
        assert!(!b.unsaved());
        let batch = b.begin().unwrap();
        let actions: serde_json::Value = serde_json::from_slice(batch.get(ACTIONS_FILE).unwrap().as_deref().unwrap()).unwrap();
        assert_eq!(actions["defaults_version"], 1);
        assert_eq!(actions["actions"].as_array().unwrap().len(), 5);
        assert!(!b.unsaved(), "generated defaults in flight can be recreated on reload");
        let restored = Bridge::default();
        for (name, bytes) in &batch {

Expected assertions from that fixture:

        assert!(warnings.is_empty());
        assert_eq!(s.actions.list.len(), 5);
        assert!(!b.unsaved());
        let batch = b.begin().unwrap();
        assert_eq!(s.actions.list.len(), 5);
        assert!(!b.unsaved());
        let batch = b.begin().unwrap();
        let actions: serde_json::Value = serde_json::from_slice(batch.get(ACTIONS_FILE).unwrap().as_deref().unwrap()).unwrap();
        assert!(!b.unsaved());
        let batch = b.begin().unwrap();
        let actions: serde_json::Value = serde_json::from_slice(batch.get(ACTIONS_FILE).unwrap().as_deref().unwrap()).unwrap();
        assert_eq!(actions["defaults_version"], 1);

denied_storage_marks_default_edits_unsaved_and_retry_saves_them setup/action excerpt:

        for command in ["actions.delete", "actions.rename"] {
            let b = Bridge::default();
            b.unavailable("SecurityError".into());
            let (mut s, _) = session(b.clone());
            assert!(!b.unsaved());
            s.execute(command, json!({"action": "Auto Tone", "name": "My Tone"})).unwrap();
            assert!(b.unsaved(), "editing generated defaults is user work: {command}");
            assert!(b.begin().is_none());
            assert!(b.error().unwrap().contains("SecurityError"));
            b.retry();
            let batch = b.begin().unwrap();
            assert!(b.unsaved(), "the edit remains unsaved while its transaction runs");

Expected assertions from that fixture:

            assert!(!b.unsaved());
            s.execute(command, json!({"action": "Auto Tone", "name": "My Tone"})).unwrap();
            assert!(b.unsaved(), "editing generated defaults is user work: {command}");
            assert!(b.begin().is_none());
            assert!(b.unsaved(), "editing generated defaults is user work: {command}");
            assert!(b.begin().is_none());
            assert!(b.error().unwrap().contains("SecurityError"));
            b.retry();
            assert!(b.begin().is_none());
            assert!(b.error().unwrap().contains("SecurityError"));
            b.retry();
            let batch = b.begin().unwrap();

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5176: Play CopyCat, AJ and Line and Color action sets; keep patterns across restarts; Shift Edge scale

  • Fixture/setup: Synthetic ATN mappings, smart-object Edit Contents action, temporary pattern stores and matting hard/soft edges.

  • Action: run each command separately:

    • cargo test -p photocraft-engine --locked every_mapped_command_exports_and_imports_back -- --nocapture
    • cargo test -p photocraft-engine --locked save_in_edit_contents_updates_the_smart_object -- --nocapture
    • cargo test -p photocraft-engine --locked patterns_survive_a_restart -- --nocapture
    • cargo test -p photocraft-engine --locked corrupt_pattern_files_are_skipped_and_kept -- --nocapture
    • cargo test -p photocraft-engine --locked large_patterns_load_on_use -- --nocapture
    • cargo test -p photocraft-algo --locked matting::tests -- --nocapture
  • Expected: Mappings round-trip parameters; Save in Edit Contents updates parent smart content. Pattern names/pixels and rename/delete survive restart; corrupt assets are warned about and preserved; large patterns load full pixels lazily. Hard edge ±100% shifts one pixel, ±50% gives half coverage within 0.02; feather 4/+100% reaches four pixels.

  • Separate coverage / prerequisites: Named CopyCat/AJ/Line and Color playback needs licensed fixture identities and required document/pattern setup; missing action sets leave that subcase BLOCKED. Mapping tests alone do not prove full set playback. Also run all new legacy/new-store migration, interruption, corruption, duplicate-ID and deletion-authority regressions.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-6492: Fail control startup promptly when its port is occupied

  • Fixture/setup: Process-isolated occupied loopback listener and successful prebound control-server fixtures in apps/photocraft tests.

  • Action: run each command separately:

    • cargo test -p photocraft --locked an_occupied_control_port_exits_with_status_2 -- --nocapture
    • cargo test -p photocraft --locked a_prebound_listener_authenticates_and_dispatches_requests -- --nocapture
  • Expected: Occupied port exits with status 2 before window/GPU initialization; successful prebound control authenticates and dispatches requests. Preserve timing, cleanup and child-exit assertions.

  • Separate coverage / prerequisites: Use only the test-owned listener and process.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-7742: Actions: a fractional setting an integer field cannot hold is refused, not rounded

Concrete setup and expected assertion examples

whole_number_settings_for_integer_fields_still_round_trip setup/action excerpt:

    for (id, params) in [
        ("filter.pixelate.crystallize", json!({"cellSize": 5})),
        ("filter.distort.ripple", json!({"amount": 81, "size": "small"})),
        ("edit.stroke", json!({"width": 2, "color": "#d03060", "location": "inside"})),
    ] {
        let mut s = Session::new();
        s.actions.list.push(Action::new("A", vec![(id.into(), params.clone())]));
        let r = s.execute("actions.save", json!({"action": "A"})).unwrap();
        assert_eq!(r["skipped"], json!([]), "{id}");
        let imported = import(&photocraft_paint::tile::b64_decode(r["data"].as_str().unwrap()).unwrap()).unwrap();
        let (back_id, back) = &imported.actions[0].steps[0];
        assert_eq!(back_id, id);

Expected assertions from that fixture:

        assert_eq!(r["skipped"], json!([]), "{id}");
        let imported = import(&photocraft_paint::tile::b64_decode(r["data"].as_str().unwrap()).unwrap()).unwrap();
        let (back_id, back) = &imported.actions[0].steps[0];
        assert_eq!(back_id, id);
        assert_eq!(back_id, id);
        for (key, want) in params.as_object().unwrap() {
            let got = back.get(key);
            match want.as_f64() {
                Some(n) => assert_eq!(got.and_then(Value::as_f64), Some(n), "{id}: {key} is {got:?}"),
                None => assert_eq!(got, Some(want), "{id}: {key} is {got:?}"),
            }
        }

a_fractional_setting_an_integer_field_cannot_hold_is_refused_not_rounded setup/action excerpt:

    for (id, params) in [("filter.pixelate.crystallize", json!({"cellSize": 5.4})), ("filter.distort.ripple", json!({"amount": 81.4, "size": "small"}))] {
        let mut s = Session::new();
        s.actions.list.push(Action::new("A", vec![(id.into(), params)]));
        let e = s.execute("actions.save", json!({"action": "A"})).unwrap_err().to_string();
        assert!(e.contains(id), "{id}: {e}");
        let r = s.execute("actions.save", json!({"action": "A", "skipUnsupported": true})).unwrap();
        assert_eq!(r["skipped"], json!([{"action": "A", "step": 0, "id": id}]), "{id}");
    }
}

Expected assertions from that fixture:

        assert!(e.contains(id), "{id}: {e}");
        let r = s.execute("actions.save", json!({"action": "A", "skipUnsupported": true})).unwrap();
        assert_eq!(r["skipped"], json!([{"action": "A", "step": 0, "id": id}]), "{id}");
    }
        assert_eq!(r["skipped"], json!([{"action": "A", "step": 0, "id": id}]), "{id}");
    }
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-8905: fix(plugins): bound a run's total plug-in memory, not just each instance

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: parallel bands fit the total memory budget; one band at a time matches parallel bands. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

parallel_bands_fit_the_total_memory_budget setup/action excerpt:

    let d = Limits::default();
    assert_eq!(d.parallel_bands(16), 4);
    assert!(d.parallel_bands(64) * d.max_memory_bytes <= d.max_total_memory_bytes);
    assert_eq!(d.parallel_bands(2), 2, "fewer workers than the budget allows");
    let small = Limits { max_memory_bytes: 64 << 20, ..Limits::default() };
    assert_eq!(small.parallel_bands(16), 16, "small instances keep every worker busy");
    let tight = Limits { max_total_memory_bytes: 1 << 20, ..Limits::default() };
    assert_eq!(tight.parallel_bands(16), 1, "at least one band runs");
    assert_eq!(d.parallel_bands(0), 1);
}

Expected assertions from that fixture:

    assert_eq!(d.parallel_bands(16), 4);
    assert!(d.parallel_bands(64) * d.max_memory_bytes <= d.max_total_memory_bytes);
    assert_eq!(d.parallel_bands(2), 2, "fewer workers than the budget allows");
    let small = Limits { max_memory_bytes: 64 << 20, ..Limits::default() };
    assert!(d.parallel_bands(64) * d.max_memory_bytes <= d.max_total_memory_bytes);
    assert_eq!(d.parallel_bands(2), 2, "fewer workers than the budget allows");
    let small = Limits { max_memory_bytes: 64 << 20, ..Limits::default() };
    assert_eq!(small.parallel_bands(16), 16, "small instances keep every worker busy");
    assert_eq!(d.parallel_bands(2), 2, "fewer workers than the budget allows");
    let small = Limits { max_memory_bytes: 64 << 20, ..Limits::default() };
    assert_eq!(small.parallel_bands(16), 16, "small instances keep every worker busy");
    let tight = Limits { max_total_memory_bytes: 1 << 20, ..Limits::default() };

one_band_at_a_time_matches_parallel_bands setup/action excerpt:

    let pool = rayon::ThreadPoolBuilder::new().num_threads(8).build().unwrap();
    let s = surface(PixelFormat::RGBA8, 600, 300);
    let canvas = Rect::new(0, 0, 600, 300);
    let run = |limits: Limits| {
        let p = Plugin::load(INVERT, limits).unwrap();
        pool.install(|| p.apply(&s, canvas, None, &json!({}))).unwrap()
    };
    let parallel = run(Limits { band_bytes: 1, ..Limits::default() });
    let serial = run(Limits { band_bytes: 1, max_total_memory_bytes: 512 << 20, ..Limits::default() });
    assert_eq!(serial, parallel);
    assert_ne!(serial, s, "the filter ran");
}

Expected assertions from that fixture:

    assert_eq!(serial, parallel);
    assert_ne!(serial, s, "the filter ran");
}
    assert_ne!(serial, s, "the filter ran");
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3874: fix(layer-style): support Blend If Alt-split handle feathering and throttle live preview

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: blend if alt split and drag behavior. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

blend_if_alt_split_and_drag_behavior setup/action excerpt:

        // Case 1: Alt-drag on unsplit black handle [0, 0, 255, 255] splits the right half
        let mut mode = BlendDrag::Pair { pair: 0, origin_v: 0 };
        let q = [0, 0, 255, 255];
        let q1 = step_blend_drag(&mut mode, &q, 45, true);
        assert_eq!(q1, [0, 45, 255, 255], "Alt-drag right must split black triangle into [0, 45]");

        // Case 2: Moving the split right half further to 70
        let q2 = step_blend_drag(&mut mode, &q1, 70, false);
        assert_eq!(q2, [0, 70, 255, 255]);

        // Case 3: Dragging the split half back to 0 merges the handle
        let q3 = step_blend_drag(&mut mode, &q2, 0, false);

Expected assertions from that fixture:

        assert_eq!(q1, [0, 45, 255, 255], "Alt-drag right must split black triangle into [0, 45]");

        // Case 2: Moving the split right half further to 70
        let q2 = step_blend_drag(&mut mode, &q1, 70, false);
        assert_eq!(q2, [0, 70, 255, 255]);

        // Case 3: Dragging the split half back to 0 merges the handle
        let q3 = step_blend_drag(&mut mode, &q2, 0, false);
        assert_eq!(q3, [0, 0, 255, 255], "Dragging back to origin reunites the triangle");

        // Case 4: Alt-drag on unsplit white handle [0, 0, 255, 255] splits the left half
        let mut mode_w = BlendDrag::Pair { pair: 2, origin_v: 255 };

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3967: Add move and delete actions to Smart Filter rows

Concrete setup and expected assertion examples

row_menu_moves_and_deletes_the_clicked_filter_with_one_undo_step setup/action excerpt:

    let _language = crate::i18n::language_scope(crate::i18n::Lang::EN);
    for depth in [8, 16, 32] {
        for ppp in [1.0, 2.0] {
            let (mut h, layer) = harness_at(depth, ppp);
            let before = filters(h.state(), layer);
            let before_pixels = pixels(h.state(), layer);
            let source = photocraft_engine::smart_cmds::contents_of(&h.state().session.active().unwrap().doc, layer).unwrap();
            let st = h.state().session.active().unwrap();
            let active = st.active_layer;
            let selected = st.selected_layers.clone();
            let history = st.history.past_len();
            let other = st.doc.layer(active.unwrap()).unwrap().clone();

Expected assertions from that fixture:

            assert!(h.query_by_label("Move Up").is_some(), "actual filter row has Move Up");
            h.get_by_label("Move Up").click();
            h.run_steps(4);
            assert_eq!(filters(h.state(), layer).iter().map(|f| f.command.as_str()).collect::<Vec<_>>(), ["filter.blur.gaussianBlur", "filter.other.offset"]);
            assert_eq!(filters(h.state(), layer).iter().map(|f| f.command.as_str()).collect::<Vec<_>>(), ["filter.blur.gaussianBlur", "filter.other.offset"]);
            assert!(text_position(&h, "Offset").y < text_position(&h, "Gaussian Blur").y);
            assert_ne!(pixels(h.state(), layer), before_pixels);
            assert_eq!(h.state().session.active().unwrap().history.past_len(), history + 1);
            assert!(text_position(&h, "Offset").y < text_position(&h, "Gaussian Blur").y);
            assert_ne!(pixels(h.state(), layer), before_pixels);
            assert_eq!(h.state().session.active().unwrap().history.past_len(), history + 1);
            assert_eq!(h.state().session.active().unwrap().active_layer, active);

two_filter_row_context_offers_reorder_and_delete setup/action excerpt:

    let _language = crate::i18n::language_scope(crate::i18n::Lang::EN);
    let (mut h, layer) = harness();
    let top = text_position(&h, "Gaussian Blur");
    let bottom = text_position(&h, "Offset");
    assert!(top.y < bottom.y, "index1 is visually above index0");
    let before = h.state().session.active().unwrap().doc.clone();
    let history = h.state().session.active().unwrap().history.past_len();
    click(&mut h, bottom, PointerButton::Secondary);
    assert!(std::sync::Arc::ptr_eq(&before, &h.state().session.active().unwrap().doc));
    assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    let missing = ["Move Up", "Move Down", "Delete Smart Filter"].into_iter().filter(|label| h.query_by_label(label).is_none()).collect::<Vec<_>>();
    assert!(missing.is_empty(), "filter row lacks context choices {missing:?}; stack has {} filters", smart(h.state(), layer).smart_filters.len());

Expected assertions from that fixture:

    assert!(top.y < bottom.y, "index1 is visually above index0");
    let before = h.state().session.active().unwrap().doc.clone();
    let history = h.state().session.active().unwrap().history.past_len();
    click(&mut h, bottom, PointerButton::Secondary);
    assert!(std::sync::Arc::ptr_eq(&before, &h.state().session.active().unwrap().doc));
    assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    let missing = ["Move Up", "Move Down", "Delete Smart Filter"].into_iter().filter(|label| h.query_by_label(label).is_none()).collect::<Vec<_>>();
    assert!(missing.is_empty(), "filter row lacks context choices {missing:?}; stack has {} filters", smart(h.state(), layer).smart_filters.len());
    assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    let missing = ["Move Up", "Move Down", "Delete Smart Filter"].into_iter().filter(|label| h.query_by_label(label).is_none()).collect::<Vec<_>>();
    assert!(missing.is_empty(), "filter row lacks context choices {missing:?}; stack has {} filters", smart(h.state(), layer).smart_filters.len());
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4023: i18n(es): add 548 missing rows, fix five filter and video terms

  • Fixture/setup: Select Spanish UI; open a synthetic document and the filter/video menus affected by this PR.
  • Action: Inspect Interpret/Replace Footage, Tilt-Shift, Mezzotint and Smart Sharpen; exercise labels containing parameter placeholders.
  • Expected: All affected labels use the intended reviewed Spanish terminology; placeholders retain their values and no missing-key fallback appears.
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester:

PR-4305: Filter > Other > Frequency Separation (#1714)

Concrete setup and expected assertion examples

low_and_high_composite_back_to_the_source_at_every_depth setup/action excerpt:

        for (depth, mode, tol) in [(8, "rgb", 1.0 / 255.0), (16, "rgb", 1.0 / 65535.0), (32, "rgb", 1e-5), (8, "gray", 1.0 / 255.0), (32, "gray", 1e-5)] {
            let tol = tol + 1e-6;
            let mut s = session(depth, mode);
            let source = active(&s);
            let before = composite(doc(&s));
            s.execute(ID, json!({"radius": 3})).unwrap_or_else(|e| panic!("{depth} {mode}: {e}"));
            let d = doc(&s);
            let (_, [low, high]) = group_of(d, source);
            // The group alone rebuilds the source: hide the source so it can't show through.
            let rebuilt = composite(&without(d, source));
            let err = max_diff(&before, &rebuilt);
            assert!(err <= tol, "{depth} {mode}: Low + High is off by {err} (tolerance {tol})");

Expected assertions from that fixture:

            assert!(err <= tol, "{depth} {mode}: Low + High is off by {err} (tolerance {tol})");
            assert!(max_diff(&composite(d), &rebuilt) <= 1e-6, "{depth} {mode}: the group covers the source");
            // Low alone is visibly smoother than the source; High carries what it lost.
            let low_only = composite(&without(&without(d, source), high));
            assert!(max_diff(&composite(d), &rebuilt) <= 1e-6, "{depth} {mode}: the group covers the source");
            // Low alone is visibly smoother than the source; High carries what it lost.
            let low_only = composite(&without(&without(d, source), high));
            assert!(max_diff(&before, &low_only) > 0.1, "{depth} {mode}: Low is blurred");
            assert!(max_diff(&before, &low_only) > 0.1, "{depth} {mode}: Low is blurred");
            let h = d.layer(high).unwrap().surface().unwrap();
            assert!((0..W).any(|x| (h.rgba(x, 5)[0] - 0.5).abs() > 0.05), "{depth} {mode}: High holds the detail");
            assert_ne!(pixels(d, low), pixels(d, source), "{depth} {mode}");

high_must_be_on_linear_light setup/action excerpt:

        let mut s = session(16, "rgb");
        let source = active(&s);
        let before = composite(doc(&s));
        s.execute(ID, json!({})).unwrap();
        let (_, [_, high]) = group_of(doc(&s), source);
        let mut d = doc(&s).clone();
        d.layer_mut(high).unwrap().blend = BlendMode::Normal;
        assert!(max_diff(&before, &composite(&d)) > 0.1);
    }

Expected assertions from that fixture:

        assert!(max_diff(&before, &composite(&d)) > 0.1);
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4711: Treat Dissolve as a per-pixel choice

Concrete setup and expected assertion examples

clipped_dissolve_is_binary_and_preserves_the_base_shape setup/action excerpt:

    let mut d = doc_white(40, 12);
    d.layers.push(solid_layer("base", Rect::new(0, 0, 20, 12), [0.0, 0.0, 1.0, 1.0]));
    let mut clip = solid_layer("clip", d.bounds(), [1.0, 0.0, 0.0, 1.0]);
    clip.clipped = true;
    clip.blend = BlendMode::Dissolve;
    clip.opacity = 0.6;
    clip.fill_opacity = 0.5;
    d.layers.push(clip);
    let mut red = 0;
    let mut blue = 0;
    for y in 0..12 {
        for x in 0..20 {

Expected assertions from that fixture:

                assert!(close4(p, [0.0, 0.0, 1.0, 1.0]), "Dissolve must not interpolate: {p:?} at ({x},{y})");
                blue += 1;
            }
        }
        assert!(close4(px(&d, 30, y), [1.0; 4]), "outside the base the clip is invisible");
    }
    assert!(red > 0 && blue > 0, "Dissolve must expose both source and base: {red}/{blue}");
    assert!((red as f32 / (red + blue) as f32 - 0.3).abs() < 0.1, "coverage should follow 60% opacity × 50% fill: {red}/{blue}");
    assert!(red > 0 && blue > 0, "Dissolve must expose both source and base: {red}/{blue}");
    assert!((red as f32 / (red + blue) as f32 - 0.3).abs() < 0.1, "coverage should follow 60% opacity × 50% fill: {red}/{blue}");
}

clipped_dissolve_matches_whole_render_at_offset_regions_and_depths setup/action excerpt:

    for fmt in [PixelFormat::RGBA8, PixelFormat::RGBA16, PixelFormat::RGBA32F] {
        let mut d = Document::new("coverage", Size::new(32, 18), ColorMode::Rgb, fmt.sample);
        let bounds = Rect::new(-3, -2, 32, 18);
        let mut base = Layer::raster("base", fmt);
        base.surface_mut().unwrap().fill_rect(bounds, &[0.0, 0.0, 1.0, 0.5]);
        d.layers.push(base);
        let mut clip = Layer::raster("clip", fmt);
        clip.surface_mut().unwrap().fill_rect(bounds, &[1.0, 0.0, 0.0, 0.75]);
        clip.clipped = true;
        clip.blend = BlendMode::Dissolve;
        clip.opacity = 0.6;
        clip.fill_opacity = 0.5;

Expected assertions from that fixture:

                    assert!(close4(part.get(x, y), whole.get(x, y)), "{fmt:?}: region mismatch at ({x},{y})");
                }
            }
        }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4992: Preserve Dissolve coverage when compositing clipped layers

Concrete setup and expected assertion examples

clipped_dissolve_is_binary_and_preserves_the_base_shape setup/action excerpt:

    let mut d = doc_white(40, 12);
    d.layers.push(solid_layer("base", Rect::new(0, 0, 20, 12), [0.0, 0.0, 1.0, 1.0]));
    let mut clip = solid_layer("clip", d.bounds(), [1.0, 0.0, 0.0, 1.0]);
    clip.clipped = true;
    clip.blend = BlendMode::Dissolve;
    clip.opacity = 0.6;
    clip.fill_opacity = 0.5;
    d.layers.push(clip);
    let mut red = 0;
    let mut blue = 0;
    for y in 0..12 {
        for x in 0..20 {

Expected assertions from that fixture:

                assert!(close4(p, [0.0, 0.0, 1.0, 1.0]), "Dissolve must not interpolate: {p:?} at ({x},{y})");
                blue += 1;
            }
        }
        assert!(close4(px(&d, 30, y), [1.0; 4]), "outside the base the clip is invisible");
    }
    assert!(red > 0 && blue > 0, "Dissolve must expose both source and base: {red}/{blue}");
    assert!((red as f32 / (red + blue) as f32 - 0.3).abs() < 0.1, "coverage should follow 60% opacity × 50% fill: {red}/{blue}");
    assert!(red > 0 && blue > 0, "Dissolve must expose both source and base: {red}/{blue}");
    assert!((red as f32 / (red + blue) as f32 - 0.3).abs() < 0.1, "coverage should follow 60% opacity × 50% fill: {red}/{blue}");
}

clipped_dissolve_matches_whole_render_at_offset_regions_and_depths setup/action excerpt:

    for fmt in [PixelFormat::RGBA8, PixelFormat::RGBA16, PixelFormat::RGBA32F] {
        let mut d = Document::new("coverage", Size::new(32, 18), ColorMode::Rgb, fmt.sample);
        let bounds = Rect::new(-3, -2, 32, 18);
        let mut base = Layer::raster("base", fmt);
        base.surface_mut().unwrap().fill_rect(bounds, &[0.0, 0.0, 1.0, 0.5]);
        d.layers.push(base);
        let mut clip = Layer::raster("clip", fmt);
        clip.surface_mut().unwrap().fill_rect(bounds, &[1.0, 0.0, 0.0, 0.75]);
        clip.clipped = true;
        clip.blend = BlendMode::Dissolve;
        clip.opacity = 0.6;
        clip.fill_opacity = 0.5;

Expected assertions from that fixture:

                    assert!(close4(part.get(x, y), whole.get(x, y)), "{fmt:?}: region mismatch at ({x},{y})");
                }
            }
        }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-7153: Smart Filters: double-clicking a Liquify filter reopens it in the Liquify workspace

  • Fixture/setup: Synthetic smart object with Liquify and Gaussian Blur filters; record original parameters, pixels and history.

  • Action: run each command separately:

    • cargo test -p photocraft-ui-egui --locked smart_filter_edit_after_the_stack_changed_updates_nothing -- --nocapture
  • Expected: Double-click opens existing Liquify in its workspace with stored settings; commit updates intended filter rather than appending; Undo/Redo restores it; Cancel preserves parameters/pixels/history. Reordered stale stack refuses without history and says Reopen its dialog.

  • Separate coverage / prerequisites: Automated stale-stack guard and native happy path/Cancel/undo are separate required subcases. Use supported controls for stale-target reproduction.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

Concrete setup and expected assertion examples

rough_pastels_and_angled_strokes_respond_to_every_slider setup/action excerpt:

        // #879: these smeared or drowned the image in texture, and some sliders did nothing.
        use crate::GalleryFilter as G;
        let b = Rect::new(0, 0, 56, 40);
        let s = pattern(SampleType::F32);
        let run = |e: &GalleryEffect, tile: i32| {
            let fp = FilterParams::FilterGallery { effects: vec![e.clone()] };
            apply_tiled(&s, &fp, b, b, None, tile, Some(b)).read_region(b)
        };
        for f in [G::RoughPastels, G::AngledStrokes, G::Cutout] {
            let base = run(&GalleryEffect::new(f), 256);
            for prm in f.params() {
                let crate::GalleryParamKind::Range { min, max, .. } = prm.kind else { continue };

Expected assertions from that fixture:

                    assert_ne!(a, base, "{} {} = {v} changed nothing", f.key(), prm.key);
                    let t = run(&e, 16);
                    let worst = a.iter().zip(&t).map(|(x, y)| (x - y).abs()).fold(0.0, f32::max);
                    assert!(worst < 1e-3, "{} {} = {v} differs across tilings by {worst}", f.key(), prm.key);
                    assert!(worst < 1e-3, "{} {} = {v} differs across tilings by {worst}", f.key(), prm.key);
                }
            }
        }

cutout_makes_flat_shapes_with_clean_edges setup/action excerpt:

        // It used to posterize every pixel after a blur: noise survived as speckle, edges were
        // ragged and smooth gradients banded instead of becoming flat paper.
        let noisy: Vec<Vec<f32>> = (0..32).map(|y| (0..64).map(|x| if x < 32 { 0.2 } else { 0.8 } + (hash01(x, y, 0, 7) - 0.5) * 0.16).collect()).collect();
        let out = cutout_on(&noisy, &[]);
        for (x0, x1) in [(2, 28), (36, 62)] {
            let vals: Vec<f32> = (2..30).flat_map(|y| out[y][x0..x1].to_vec()).collect();
            let spread = vals.iter().fold(f32::MIN, |a, &v| a.max(v)) - vals.iter().fold(f32::MAX, |a, &v| a.min(v));
            assert!(spread < 1e-4, "a noisy area should be one flat tone, spread {spread}");
        }
        // The cut is an anti-aliased pixel or two, not a blur.
        for y in 0..32 {
            let (lo, hi) = (out[y][16], out[y][48]);

Expected assertions from that fixture:

            assert!(spread < 1e-4, "a noisy area should be one flat tone, spread {spread}");
        }
        // The cut is an anti-aliased pixel or two, not a blur.
        for y in 0..32 {
            assert!(soft <= 2, "row {y}: {soft} edge pixels");
        }
        // A ramp becomes `numberOfLevels` flat steps (the palette is the image's own colours, so
        // one may go to the wide flat margins and merge as noise).
            assert!(tones.len() <= levels as usize && tones.len() + 1 >= levels as usize, "levels {levels}: tones {tones:?}");
            assert!(tones.windows(2).all(|t| t[0] < t[1]), "levels {levels}: tones {tones:?}");
        }
        // Edge Simplicity drops specks.

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-8039: Compose: Extend Softer outer glows past Size (#7899)

Concrete setup and expected assertion examples

softer_outer_glow_keeps_a_fading_tail_past_its_size setup/action excerpt:

        let mut shape = Map::new(220, 220, 0.0);
        for y in 0..shape.h {
            for x in 20..100 {
                shape.v[y * shape.w + x] = 1.0;
            }
        }
        let glow = Glow {
            common: photocraft_doc::FxCommon::new(BlendMode::Screen, 1.0),
            paint: FxPaint::Color(photocraft_color::Color::WHITE),
            technique: GlowTechnique::Softer,
            spread: 0.0,
            size: 60.0,

Expected assertions from that fixture:

        assert!(at(50) > 0.024, "the softer shoulder should stay visible: {}", at(50));
        assert!(at(60) > 1.0 / 255.0, "the glow must retain a visible tail at its Size: {}", at(60));
        assert!(at(62) > 0.5 / 255.0, "the glow must extend beyond its Size: {}", at(62));
        assert!(at(66) < 1e-6, "the added tail should fade to zero: {}", at(66));
        assert!(at(60) > 1.0 / 255.0, "the glow must retain a visible tail at its Size: {}", at(60));
        assert!(at(62) > 0.5 / 255.0, "the glow must extend beyond its Size: {}", at(62));
        assert!(at(66) < 1e-6, "the added tail should fade to zero: {}", at(66));
    }
        assert!(at(62) > 0.5 / 255.0, "the glow must extend beyond its Size: {}", at(62));
        assert!(at(66) < 1e-6, "the added tail should fade to zero: {}", at(66));
    }

softer_outer_glow_margin_includes_its_tail_support setup/action excerpt:

        let glow = Glow {
            common: photocraft_doc::FxCommon::new(BlendMode::Screen, 1.0),
            paint: FxPaint::Color(photocraft_color::Color::WHITE),
            technique: GlowTechnique::Softer,
            spread: 0.0,
            size: 60.0,
            contour: Contour::Linear,
            anti_alias: false,
            range: 0.5,
            jitter: 0.0,
            noise: 0.0,
            source: GlowSource::Edge,

Expected assertions from that fixture:

        assert_eq!(margin(&layer), 68);
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:
Concrete setup and expected assertion examples

oklab_round_trips_and_marks_the_gamut setup/action excerpt:

        let white = oklab([1.0, 1.0, 1.0]);
        assert!((white[0] - 1.0).abs() < 1e-3 && white[1].abs() < 1e-3 && white[2].abs() < 1e-3, "{white:?}");
        assert!(oklab([0.0, 0.0, 0.0])[0].abs() < 1e-6);
        for p in [[0.2, 0.5, 0.9], [0.95, 0.1, 0.3], [0.5, 0.5, 0.5], [0.0, 1.0, 0.0]] {
            let back = srgb(oklab(p)).unwrap_or([-1.0; 3]);
            assert!(dist2(back, p) < 1e-6, "{p:?} came back as {back:?}");
        }
        // Very light and very colourful: no sRGB colour is that.
        assert_eq!(srgb([0.95, 0.3, 0.0]), None);
    }

Expected assertions from that fixture:

        assert!((white[0] - 1.0).abs() < 1e-3 && white[1].abs() < 1e-3 && white[2].abs() < 1e-3, "{white:?}");
        assert!(oklab([0.0, 0.0, 0.0])[0].abs() < 1e-6);
        for p in [[0.2, 0.5, 0.9], [0.95, 0.1, 0.3], [0.5, 0.5, 0.5], [0.0, 1.0, 0.0]] {
            let back = srgb(oklab(p)).unwrap_or([-1.0; 3]);
        assert!(oklab([0.0, 0.0, 0.0])[0].abs() < 1e-6);
        for p in [[0.2, 0.5, 0.9], [0.95, 0.1, 0.3], [0.5, 0.5, 0.5], [0.0, 1.0, 0.0]] {
            let back = srgb(oklab(p)).unwrap_or([-1.0; 3]);
            assert!(dist2(back, p) < 1e-6, "{p:?} came back as {back:?}");
            assert!(dist2(back, p) < 1e-6, "{p:?} came back as {back:?}");
        }
        // Very light and very colourful: no sRGB colour is that.
        assert_eq!(srgb([0.95, 0.3, 0.0]), None);

out_of_gamut_colours_lose_strength_not_hue setup/action excerpt:

        let (l, a, b) = (0.95, 0.3, 0.1);
        let c = in_gamut(l, a, b);
        let lab = oklab(c);
        assert!((lab[0] - l).abs() < 0.01, "lightness kept: {lab:?}");
        let chroma = (lab[1] * lab[1] + lab[2] * lab[2]).sqrt();
        assert!(chroma > 0.01 && chroma < (a * a + b * b).sqrt(), "weaker but still coloured: {chroma}");
        assert!((lab[2] / lab[1] - b / a).abs() < 0.05, "same hue: {lab:?}");
        // In gamut it is just the conversion.
        assert_eq!(in_gamut(0.6, 0.02, -0.03), srgb([0.6, 0.02, -0.03]).unwrap_or_default());
    }

Expected assertions from that fixture:

        assert!((lab[0] - l).abs() < 0.01, "lightness kept: {lab:?}");
        let chroma = (lab[1] * lab[1] + lab[2] * lab[2]).sqrt();
        assert!(chroma > 0.01 && chroma < (a * a + b * b).sqrt(), "weaker but still coloured: {chroma}");
        assert!((lab[2] / lab[1] - b / a).abs() < 0.05, "same hue: {lab:?}");
        assert!(chroma > 0.01 && chroma < (a * a + b * b).sqrt(), "weaker but still coloured: {chroma}");
        assert!((lab[2] / lab[1] - b / a).abs() < 0.05, "same hue: {lab:?}");
        // In gamut it is just the conversion.
        assert_eq!(in_gamut(0.6, 0.02, -0.03), srgb([0.6, 0.02, -0.03]).unwrap_or_default());
        assert!((lab[2] / lab[1] - b / a).abs() < 0.05, "same hue: {lab:?}");
        // In gamut it is just the conversion.
        assert_eq!(in_gamut(0.6, 0.02, -0.03), srgb([0.6, 0.02, -0.03]).unwrap_or_default());
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:
  • Fixture/setup: Synthetic label maps in cutout/regions.rs.

  • Action: run each command separately:

    • cargo test -p photocraft-algo --locked components_are_four_connected_and_numbered_in_raster_order -- --nocapture
    • cargo test -p photocraft-algo --locked small_regions_merge_into_the_neighbour_they_share_most_border_with -- --nocapture
    • cargo test -p photocraft-algo --locked slivers_merge_though_they_are_large -- --nocapture
    • cargo test -p photocraft-algo --locked local_colour_drifts_within_a_shape_but_never_crosses_a_cut -- --nocapture
  • Expected: Four-connected components have deterministic raster numbering; small regions choose the shared-border neighbour; narrow slivers merge despite area; colour variation never crosses a cut.

  • Separate coverage / prerequisites: The earlier Oklab test is supporting coverage, not the regions-stage oracle.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

  • Fixture/setup: Two-region boundaries, islands, antialiased edges, squares and open chains in cutout/edges.rs and curves.rs.

  • Action: run each command separately:

    • cargo test -p photocraft-algo --locked the_edge_between_two_regions_is_one_chain_between_junctions -- --nocapture
    • cargo test -p photocraft-algo --locked an_island_is_one_closed_chain -- --nocapture
    • cargo test -p photocraft-algo --locked an_anti_aliased_edge_is_placed_inside_its_pixel -- --nocapture
    • cargo test -p photocraft-algo --locked a_square_keeps_its_four_corners_and_straight_sides -- --nocapture
    • cargo test -p photocraft-algo --locked an_open_chain_ends_where_it_started_at_any_fidelity -- --nocapture
    • cargo test -p photocraft-algo --locked fitting_keeps_small_islands_and_their_winding -- --nocapture
    • cargo test -p photocraft-algo --locked cutout_makes_flat_shapes_with_clean_edges -- --nocapture
  • Expected: Shared boundaries are single chains; islands close; subpixel edge placement, square corners, open endpoints and winding survive fitting. Complete Cutout yields the asserted clean flat shapes.

  • Separate coverage / prerequisites: Run after the complete #8191 → #8192 → #8193 → #7903 chain.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-8903: Box-filter minified patterns in the CPU and GPU samplers (#8831)

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: minified pattern is filtered not aliased; pattern fill layers. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

minified_pattern_is_filtered_not_aliased setup/action excerpt:

        // 1-px dark / light stripes (mean 0.5), shown at 25 %: every pixel covers 4 tile pixels.
        // The 0.125 px phase puts each pixel centre on a light tile pixel's centre, so a single
        // bilinear tap would return pure white.
        let mut s = Surface::new(PixelFormat::RGBA8);
        s.fill_rect(Rect::new(0, 0, 8, 8), &[1.0, 1.0, 1.0, 1.0]);
        for x in (0..8).step_by(2) {
            s.fill_rect(Rect::new(x, 0, x + 1, 8), &[0.0, 0.0, 0.0, 1.0]);
        }
        let t = Tile::new(&Pattern::new("stripes", s, 8, 8)).unwrap();
        let place = Placement::new(Rect::EMPTY, false, (0.125, 0.0), 0.25, 0.0);
        let (u, _) = place.map(0.5, 0.5);
        assert_eq!(t.sample(u, 0.5)[0], 1.0, "the single tap lands on a light stripe");

Expected assertions from that fixture:

        assert_eq!(t.sample(u, 0.5)[0], 1.0, "the single tap lands on a light stripe");
        for p in render(&t, &place, Rect::new(0, 0, 8, 1)) {
            assert!((p[0] - 0.5).abs() < 0.05, "{p:?}");
        }
            assert!((p[0] - 0.5).abs() < 0.05, "{p:?}");
        }
        // 100 % stays exact.
        let one = Placement::new(Rect::EMPTY, false, (0.0, 0.0), 1.0, 0.0);
        assert_eq!((row[0][0], row[1][0]), (0.0, 1.0));
    }

pattern_fill_layers setup/action excerpt:

    let Some(mut g) = gpu() else { return };
    let pat = checker_pattern();
    for (link, scale, angle, masked) in [(true, 1.0, 0.0, false), (false, 0.7, 25.0, true), (true, 1.6, -40.0, false), (true, 0.3, 10.0, false)] {
        let mut d = base_doc(70, 50);
        d.patterns.push(pat.clone());
        let fill = Fill::Pattern { name: pat.name.clone(), id: pat.id.clone(), scale, angle, link, phase: (3.0, -2.0) };
        let mut l = Layer::new("pat", LayerContent::Fill(fill));
        l.opacity = 0.85;
        l.blend = BlendMode::Overlay;
        if masked {
            l.mask = Some(mask(Rect::new(5, 5, 60, 45), 61, 0.0));
        }

Expected assertions from that fixture:


The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9472: fix(io): import a Displace smart filter that names its map file without embedding it (#9162)

Concrete setup and expected assertion examples

displace_psd_filter_with_only_a_map_path_renders_after_replace_contents setup/action excerpt:

    use photocraft_psd::descriptor::{Descriptor, Id, UnicodeString, Value};
    let dir = std::env::temp_dir().join(format!("pcraft-9162-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let png = |name: &str, size: (u32, u32), c: [f32; 4]| {
        let d = Document::with_background(
            "m",
            photocraft_geom::Size::new(size.0, size.1),
            photocraft_color::ColorMode::Rgb,
            photocraft_color::SampleType::U8,
            photocraft_color::Color::rgba(c[0], c[1], c[2], c[3]),
        );
        let path = dir.join(name);

Expected assertions from that fixture:

        assert_eq!(out["warnings"], json!([]), "depth {depth}");
        let cache = smart(&s2.active().unwrap().doc).1.cache.unwrap();
        // 10 % of 128 px at full map contrast: the design (x 8..80) moves 12.8 px left.
        let alpha = |x: i32| cache.pixel(x, 30)[3];
        assert!(alpha(2) > 0.99 && alpha(75) < 0.01, "depth {depth}: {} {}", alpha(2), alpha(75));
    }
    std::fs::remove_dir_all(&dir).ok();
}

a_map_named_only_by_its_path_imports_as_map_path setup/action excerpt:

        let with_file = |file: Value| {
            let mut i = item(Vec::new());
            let mut f = get_desc(&i, "Fltr").unwrap().clone();
            f.items.retain(|(k, _)| !k.is(MAP_KEY));
            set(&mut f, "DspF", file);
            set(&mut i, "Fltr", Value::Descriptor(f));
            i
        };
        let src = with_file(pth("C:\\maps\\repro-map.psd"));
        let f = filter_from_item(&src);
        assert_eq!(f.command, COMMAND);
        assert_eq!(f.params["mapPath"], json!("C:\\maps\\repro-map.psd"));

Expected assertions from that fixture:

        assert_eq!(f.command, COMMAND);
        assert_eq!(f.params["mapPath"], json!("C:\\maps\\repro-map.psd"));
        assert!(embedded_map(&f.params).is_none());
        assert_eq!(item_for_filter(&f).unwrap(), src, "written back as it was");
        assert_eq!(f.params["mapPath"], json!("C:\\maps\\repro-map.psd"));
        assert!(embedded_map(&f.params).is_none());
        assert_eq!(item_for_filter(&f).unwrap(), src, "written back as it was");
        let mut moved = f.clone();
        assert!(embedded_map(&f.params).is_none());
        assert_eq!(item_for_filter(&f).unwrap(), src, "written back as it was");
        let mut moved = f.clone();
        moved.params["mapPath"] = json!("/elsewhere/map.png");

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9509: fix: an edit at full coverage keeps its result over huge 32-bit values

Concrete setup and expected assertion examples

full_coverage_adjustment_keeps_its_result_over_huge_hdr_values setup/action excerpt:

    for v in [1.0e6_f32, 1.0e8, -1.0e6] {
        let mut d = Document::new("hdr", Size::new(2, 2), ColorMode::Rgb, SampleType::F32);
        let mut base = Layer::raster("b", PixelFormat::RGBA32F);
        base.surface_mut().unwrap().fill_rect(Rect::new(0, 0, 2, 2), &[v, v, v, 1.0]);
        let look = Adjustment::ColorLookup { name: "half".into(), lut: Some(std::sync::Arc::new(vec![0.5; 24])), size: 2, tetrahedral: false, dither: false };
        d.layers = vec![base, Layer::new("look", LayerContent::Adjustment(look))];
        let got = px(&d, 0, 0);
        for c in &got[..3] {
            assert!((c - 0.5).abs() < 1e-6, "{v}: {got:?}");
        }
    }
}

Expected assertions from that fixture:

            assert!((c - 0.5).abs() < 1e-6, "{v}: {got:?}");
        }
    }
}

reused_render_matches_previous_allocating_stack setup/action excerpt:

    for depth in [SampleType::U8, SampleType::U16, SampleType::F32] {
        let mut doc = Document::new("equivalence", Size::new(513, 7), ColorMode::Rgb, depth);
        let rect = doc.bounds();
        for (name, color) in [("bottom", [0.2, 0.4, 0.8, 1.0]), ("top", [1.5, -0.25, 0.6, 0.5])] {
            let mut layer = Layer::raster(name, doc.pixel_format());
            layer.surface_mut().unwrap().fill_rect(rect, &color);
            doc.layers.push(layer);
        }
        let mut levels = Adjustment::identity_levels();
        if let Adjustment::Levels { master, .. } = &mut levels {
            master.gamma = 1.4;
        }

Expected assertions from that fixture:


The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9512: fix(compose, gpu): adjustment layers apply Fill inside the special eight blend modes

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: adjustment fill changes the special blend modes; special eight fill on adjustment layers matches cpu. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

adjustment_fill_changes_the_special_blend_modes setup/action excerpt:

    for (mode, want) in [(BlendMode::ColorDodge, [80, 160, 240]), (BlendMode::LinearBurn, [13, 77, 141])] {
        let mut d = Document::with_background("fill", Size::new(1, 1), ColorMode::Rgb, SampleType::U8, Color::WHITE);
        let mut base = Layer::raster("b", PixelFormat::RGBA8);
        base.surface_mut().unwrap().fill_rect(Rect::new(0, 0, 1, 1), &[64.0 / 255.0, 128.0 / 255.0, 192.0 / 255.0, 1.0]);
        let look = Adjustment::ColorLookup { name: "half".into(), lut: Some(std::sync::Arc::new(vec![0.5; 24])), size: 2, tetrahedral: false, dither: false };
        let mut adj = Layer::new("look", LayerContent::Adjustment(look));
        adj.blend = mode;
        adj.fill_opacity = 0.4;
        d.layers.push(base);
        d.layers.push(adj);
        let got = px(&d, 0, 0);
        let got: Vec<i32> = got[..3].iter().map(|v| (v * 255.0).round() as i32).collect();

Expected assertions from that fixture:

        assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() <= 1), "{mode:?}: {got:?} vs Photoshop {want:?}");
    }
}

special_eight_fill_on_adjustment_layers_matches_cpu setup/action excerpt:

    let Some(mut g) = gpu() else { return };
    let special = [
        BlendMode::ColorBurn,
        BlendMode::LinearBurn,
        BlendMode::ColorDodge,
        BlendMode::LinearDodge,
        BlendMode::VividLight,
        BlendMode::LinearLight,
        BlendMode::HardMix,
        BlendMode::Difference,
    ];
    for mode in special {

Expected assertions from that fixture:


The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9584: fix(compose, gpu): Blend If hard points match Photoshop at 16 bits and for Gray

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: blend if 16 bit cutoffs follow photoshops steps; blend if gray follows photoshops weights and rounding. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

blend_if_16_bit_cutoffs_follow_photoshops_steps setup/action excerpt:

    let shown = |v16: u16, black: u8, white: u8| {
        let v = f32::from(v16) / 65535.0;
        let mut d = Document::new("t", Size::new(1, 1), ColorMode::Rgb, SampleType::U16);
        let layer16 = |name: &str, c: [f32; 4]| {
            let mut l = Layer::raster(name, PixelFormat::RGBA16);
            l.surface_mut().unwrap().fill_rect(Rect::new(0, 0, 1, 1), &c);
            l
        };
        d.layers.push(layer16("under", [v, v, v, 1.0]));
        let mut l = layer16("top", [1.0, 1.0, 1.0, 1.0]);
        l.blend_if.set(3, [FULL, range([black, black], [white, white])]);
        d.layers.push(l);

Expected assertions from that fixture:

        assert!(shown(at(step), c, 255), "black {c}: step {step} shows");
        assert!(!shown(at(step - 1), c, 255), "black {c}: step {} hides", step - 1);
        assert!(shown(at(step), 0, c), "white {c}: step {step} shows");
        assert!(!shown(at(step + 1), 0, c), "white {c}: step {} hides", step + 1);
        assert!(!shown(at(step - 1), c, 255), "black {c}: step {} hides", step - 1);
        assert!(shown(at(step), 0, c), "white {c}: step {step} shows");
        assert!(!shown(at(step + 1), 0, c), "white {c}: step {} hides", step + 1);
    }
        assert!(shown(at(step), 0, c), "white {c}: step {step} shows");
        assert!(!shown(at(step + 1), 0, c), "white {c}: step {} hides", step + 1);
    }
    assert!(shown(32895, 128, 255) && !shown(32894, 128, 255), "the reporter's RGB16 boundary");

blend_if_gray_follows_photoshops_weights_and_rounding setup/action excerpt:

    let shown = |rgb: [u8; 3], black: u8, white: u8| {
        let mut d = Document::new("t", Size::new(1, 1), ColorMode::Rgb, SampleType::U8);
        let c = rgb.map(|v| f32::from(v) / 255.0);
        d.layers.push(solid_layer("under", Rect::new(0, 0, 1, 1), [c[0], c[1], c[2], 1.0]));
        let mut l = solid_layer("top", Rect::new(0, 0, 1, 1), [1.0, 1.0, 1.0, 1.0]);
        l.blend_if.set(0, [FULL, range([black, black], [white, white])]);
        d.layers.push(l);
        px(&d, 0, 0)[0] > 0.999 && px(&d, 0, 0)[2] > 0.999
    };
    // (colour hidden by a black point at `cut`, the next colour shown), single channels.
    for (cut, below, at) in [
        (20, [65, 0, 0], [66, 0, 0]),

Expected assertions from that fixture:

        assert!(!shown(below, cut, 255), "{below:?} hidden by black {cut}");
        assert!(shown(at, cut, 255), "{at:?} shown by black {cut}");
    }
    // The reporter's mixed colours, and the white point's half-down rounding.
        assert!(shown(at, cut, 255), "{at:?} shown by black {cut}");
    }
    // The reporter's mixed colours, and the white point's half-down rounding.
    assert!(shown([123, 126, 151], 128, 255), "127.85 is 128");
    assert!(shown([123, 126, 151], 128, 255), "127.85 is 128");
    assert!(shown([120, 129, 149], 0, 128), "128.5 is 128");
    assert!(!shown([121, 129, 149], 0, 128), "128.8 is 129");
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9589: fix(compose, gpu): an adjustment layer's This Layer range judges its result before Opacity

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: adjustment blend if judges the result before opacity; blend if. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

adjustment_blend_if_judges_the_result_before_opacity setup/action excerpt:

    for (black, white, want) in [(150u8, 255u8, [115, 128, 140]), (0, 150, [64, 128, 192])] {
        let mut d = Document::with_background("bi", Size::new(1, 1), ColorMode::Rgb, SampleType::U8, Color::WHITE);
        d.layers.push(solid_layer("base", Rect::new(0, 0, 1, 1), [64.0 / 255.0, 128.0 / 255.0, 192.0 / 255.0, 1.0]));
        let mut inv = Layer::new("invert", LayerContent::Adjustment(Adjustment::Invert));
        inv.opacity = 0.4;
        inv.blend_if.set(1, [range([black, black], [white, white]), FULL]);
        d.layers.push(inv);
        let got: Vec<i32> = px(&d, 0, 0)[..3].iter().map(|v| (v * 255.0).round() as i32).collect();
        assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() <= 1), "black {black} white {white}: {got:?} vs Photoshop {want:?}");
    }
}

Expected assertions from that fixture:

        assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() <= 1), "black {black} white {white}: {got:?} vs Photoshop {want:?}");
    }
}

blend_if setup/action excerpt:

    use photocraft_doc::BlendRange;
    let Some(mut g) = gpu() else { return };
    let r = |b0: u8, b1: u8, w0: u8, w1: u8| BlendRange { black: [b0, b1], white: [w0, w1] };
    // (name, entry, [this layer, underlying], has a hard point)
    let cases: [(&str, usize, [BlendRange; 2], bool); 7] = [
        ("gray this hard", 0, [r(90, 90, 255, 255), BlendRange::FULL], true),
        ("gray under split", 0, [BlendRange::FULL, r(30, 140, 150, 230)], false),
        ("red both", 1, [r(0, 60, 200, 200), r(20, 20, 180, 250)], true),
        ("red both split", 1, [r(0, 60, 190, 230), r(10, 50, 180, 250)], false),
        ("green white split", 2, [r(0, 0, 100, 220), BlendRange::FULL], false),
        ("blue under hard", 3, [BlendRange::FULL, r(128, 128, 255, 255)], true),
        ("everything hidden", 0, [r(255, 255, 0, 0), BlendRange::FULL], true),

Expected assertions from that fixture:

            assert!(hits.prefix_hits > 0, "{depth:?} {name}: the edit above reuses the lower stack");

            // Masked, with channel restrictions.
            let mut m = l.clone();

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9595: fix(compose, gpu): a Pass Through group with Blend If renders isolated

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: pass through group with blend if is isolated; pass through group with blend if is isolated. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

pass_through_group_with_blend_if_is_isolated setup/action excerpt:

    for (gated, want) in [(true, [64, 128, 192]), (false, [191, 127, 63])] {
        let mut d = Document::with_background("pt", Size::new(1, 1), ColorMode::Rgb, SampleType::U8, Color::WHITE);
        d.layers.push(solid_layer("base", Rect::new(0, 0, 1, 1), [64.0 / 255.0, 128.0 / 255.0, 192.0 / 255.0, 1.0]));
        let mut g = Layer::group("group", vec![Layer::new("invert", LayerContent::Adjustment(Adjustment::Invert))]);
        g.blend = BlendMode::PassThrough;
        if gated {
            g.blend_if.set(1, [FULL, range([0, 0], [110, 110])]);
        }
        d.layers.push(g);
        let got: Vec<i32> = px(&d, 0, 0)[..3].iter().map(|v| (v * 255.0).round() as i32).collect();
        assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() <= 1), "Blend If {gated}: {got:?} vs Photoshop {want:?}");
    }

Expected assertions from that fixture:

        assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() <= 1), "Blend If {gated}: {got:?} vs Photoshop {want:?}");
    }
}

pass_through_group_with_blend_if_is_isolated setup/action excerpt:

    use photocraft_doc::BlendRange;
    let Some(mut g) = gpu() else { return };
    for gated in [true, false] {
        let mut d = base_doc(64, 48);
        d.layers[0] = noise_layer("bg", PixelFormat::RGBA8, Rect::from_xywh(0, 0, 64, 48), 5, 1.0);
        let mut grp = Layer::group("group", vec![Layer::new("inv", LayerContent::Adjustment(Adjustment::Invert))]);
        grp.blend = BlendMode::PassThrough;
        if gated {
            grp.blend_if.set(1, [BlendRange::FULL, BlendRange { black: [0, 0], white: [110, 110] }]);
        }
        d.layers.push(grp);
        check(&mut g, &d, &format!("pass-through group, Blend If {gated}"));

Expected assertions from that fixture:


The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9651: perf(algo): row-parallel local_sharpen with fast-path protect_detail (#5426)

Concrete setup and expected assertion examples

matches_scalar_sharpen_bit_for_bit_across_layouts_edges_and_hdr setup/action excerpt:

    for (w, h) in [(1, 1), (3, 7), (37, 29)] {
        for ch in 1..=5 {
            for alpha in std::iter::once(None).chain((0..ch).map(Some)) {
                let data: Vec<f32> = (0..w * h * ch)
                    .map(|i| if Some(i % ch) == alpha { [0.0, 1e-8, 0.25, 0.75, 1.0][(i / ch) % 5] } else { ((i * 73 + 19) % 113) as f32 / 17.0 - 2.0 })
                    .collect();
                for win in [(0, 0, w, h), (w / 2, h / 2, w, h), (0, 0, 1, 1), (0, 0, 0, h)] {
                    for amount in [0.0, 0.5, 1.0, 2.5] {
                        for protect_detail in [false, true] {
                            let expected = scalar_local_sharpen(&data, w, h, ch, alpha, win, amount, protect_detail);
                            let actual = local_sharpen(&data, w, h, ch, alpha, win, amount, protect_detail);
                            assert_eq!(actual.len(), expected.len());

Expected assertions from that fixture:

                            assert_eq!(actual.len(), expected.len());
                            for (i, (a, b)) in actual.iter().zip(&expected).enumerate() {
                                assert_eq!(
                                    a.to_bits(),
                                assert_eq!(
                                    a.to_bits(),
                                    b.to_bits(),
                                    "{w}x{h} ch={ch} alpha={alpha:?} win={win:?} amount={amount} protect={protect_detail} sample={i}"

empty_channel_layout_remains_empty setup/action excerpt:

    assert!(local_sharpen(&[], 5, 5, 0, None, (0, 0, 5, 5), 1.0, true).is_empty());
}

Expected assertions from that fixture:

    assert!(local_sharpen(&[], 5, 5, 0, None, (0, 0, 5, 5), 1.0, true).is_empty());
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9703: Fix adjustment layer name clicks targeting masks

Concrete setup and expected assertion examples

a_new_adjustment_or_fill_layer_shows_its_settings_until_the_mask_thumbnail_is_clicked setup/action excerpt:

    for (command, settings) in [("layer.newAdjustmentLayer.curves", Some("Reset to defaults")), ("layer.newFillLayer.solidColor", None)] {
        let mut s = photocraft_engine::Session::new();
        s.execute("file.new", json!({"width": 200, "height": 100})).unwrap();
        s.execute("layer.new.layer", json!({})).unwrap();
        let mut h = harness(s, 0, 1.0, 290.0);
        let ctx = h.ctx.clone();
        let (req, _rx) = crate::control::ControlRequest::new("ui.set", json!({"dock": {"collapsed": ["color", "history", "navigator"]}}));
        crate::control::handle(h.state_mut(), &ctx, &req);
        h.state_mut().run(command, json!({})).unwrap();
        h.run_steps(6);
        let id = h.state().session.active().unwrap().active_layer.unwrap().0;
        assert!(h.state().ui.mask_target, "{command}: the new mask is targeted");

Expected assertions from that fixture:

        assert!(h.state().ui.mask_target, "{command}: the new mask is targeted");
        assert_eq!(crate::canvas::paint_target(h.state()), json!("mask"), "{command}: painting edits the mask");
        assert!(h.query_all_by_label("Density").next().is_none(), "{command}: Properties shows the layer, not its mask");
        if let Some(label) = settings {
        assert_eq!(crate::canvas::paint_target(h.state()), json!("mask"), "{command}: painting edits the mask");
        assert!(h.query_all_by_label("Density").next().is_none(), "{command}: Properties shows the layer, not its mask");
        if let Some(label) = settings {
            assert!(h.query_all_by_label(label).next().is_some(), "{command}: its settings show");
        assert!(h.query_all_by_label("Density").next().is_none(), "{command}: Properties shows the layer, not its mask");
        if let Some(label) = settings {
            assert!(h.query_all_by_label(label).next().is_some(), "{command}: its settings show");
        }

adjustment_names_target_contents_instead_of_masks setup/action excerpt:

    for command in ["layer.newAdjustmentLayer.hueSaturation", "layer.newAdjustmentLayer.curves"] {
        for ppp in [1.0, 2.0] {
            let (mut s, _, other) = session();
            s.execute(command, json!({})).unwrap();
            let id = s.active().unwrap().active_layer.unwrap().0;
            let path = json!({"subpaths": [{"knots": [[20, 10], [120, 10], [120, 90], [20, 90]]}]});
            s.execute("layer.vectorMask.add", json!({"layer": id, "path": path})).unwrap();
            let mut h = harness(s, 0, ppp, 360.0);
            for kind in [MaskKind::Pixel, MaskKind::Vector] {
                let at = mask_rect(&h, id, kind).center();
                click_with(&mut h, at, Modifiers::NONE);
                assert_eq!(h.state().ui.mask_target, kind == MaskKind::Pixel);

Expected assertions from that fixture:

                assert_eq!(h.state().ui.mask_target, kind == MaskKind::Pixel);
                assert_eq!(h.state().ui.vector_mask_target, kind == MaskKind::Vector);
                let name = crate::layer_row_ui::recorded(&h.ctx).into_iter().find(|r| r.layer == id).unwrap().name.unwrap().center();
                click_with(&mut h, name, Modifiers::NONE);
                assert_eq!(h.state().ui.vector_mask_target, kind == MaskKind::Vector);
                let name = crate::layer_row_ui::recorded(&h.ctx).into_iter().find(|r| r.layer == id).unwrap().name.unwrap().center();
                click_with(&mut h, name, Modifiers::NONE);
                assert!(!h.state().ui.mask_target && !h.state().ui.vector_mask_target, "{command} @{ppp}x after {kind:?}");
                assert!(!h.state().ui.mask_target && !h.state().ui.vector_mask_target, "{command} @{ppp}x after {kind:?}");
                let thumb = crate::layer_row_ui::recorded(&h.ctx).into_iter().find(|r| r.layer == id).unwrap().thumbnail.unwrap();
                assert!(near(super::brackets(&h.ctx).unwrap(), thumb.expand(3.0)), "the content thumbnail is selected");
                assert_eq!(h.state().session.active().unwrap().active_layer.unwrap().0, id);

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-1991: feat(actions): import and export reusable Actions JSON files

Concrete setup and expected assertion examples

json_actions_round_trip_preserves_metadata setup/action excerpt:

        let mut s = Session::new();
        let mut action = Action::new("Draw", vec![("layer.new.layer".into(), json!({}))]);
        action.disabled.insert(0);
        action.dialogs.insert(0);
        action.set = Some("Painting".into());
        s.actions.list.push(action.clone());
        let exported = s.execute("actions.export", json!({})).unwrap();
        s.actions.list.clear();
        let result = s.execute("actions.import", json!({"data": exported["data"]})).unwrap();
        assert_eq!(result["imported"], 1);
        assert_eq!(s.actions.list, [action]);
        let rev = s.actions.rev;

Expected assertions from that fixture:

        assert_eq!(result["imported"], 1);
        assert_eq!(s.actions.list, [action]);
        let rev = s.actions.rev;
        assert_eq!(s.execute("actions.import", json!({"data": exported["data"]})).unwrap()["imported"], 0);
        assert_eq!(s.actions.list, [action]);
        let rev = s.actions.rev;
        assert_eq!(s.execute("actions.import", json!({"data": exported["data"]})).unwrap()["imported"], 0);
        assert_eq!(s.actions.rev, rev);
        assert_eq!(s.execute("actions.import", json!({"data": exported["data"]})).unwrap()["imported"], 0);
        assert_eq!(s.actions.rev, rev);
    }

reimport_after_changing_button_color_keeps_names_unique setup/action excerpt:

        let mut s = Session::new();
        s.actions.list.push(Action::new("Draw", vec![]));
        let data = s.execute("actions.export", json!({})).unwrap()["data"].clone();
        s.execute("actions.setColor", json!({"action": "Draw", "color": "red"})).unwrap();
        let params = json!({"data": data});
        assert_eq!(s.execute("actions.import", params.clone()).unwrap()["imported"], 1);
        assert_eq!(s.actions.list[0].name, "Draw");
        assert_eq!(s.actions.list[0].color, "red");
        assert_eq!(s.actions.list[1].name, "Draw (Imported 2)");
        assert!(s.actions.list[1].color.is_empty());
        let before = s.actions.clone();
        assert_eq!(s.execute("actions.import", params).unwrap()["imported"], 0);

Expected assertions from that fixture:

        assert_eq!(s.execute("actions.import", params.clone()).unwrap()["imported"], 1);
        assert_eq!(s.actions.list[0].name, "Draw");
        assert_eq!(s.actions.list[0].color, "red");
        assert_eq!(s.actions.list[1].name, "Draw (Imported 2)");
        assert_eq!(s.actions.list[0].name, "Draw");
        assert_eq!(s.actions.list[0].color, "red");
        assert_eq!(s.actions.list[1].name, "Draw (Imported 2)");
        assert!(s.actions.list[1].color.is_empty());
        assert_eq!(s.actions.list[0].color, "red");
        assert_eq!(s.actions.list[1].name, "Draw (Imported 2)");
        assert!(s.actions.list[1].color.is_empty());
        let before = s.actions.clone();

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3142: Read applied Sony RAW metadata and crop ILCE-6000 images

Concrete setup and expected assertion examples

measured_image_areas_crop_the_borders_without_touching_the_rest setup/action excerpt:

        let mut s = nef("NIKON D800E", 7424, 4924);
        apply(&mut s);
        assert_eq!(s.crop, Rect::new(8, 6, 7360, 4912));
        // Only the image area is measured: levels, colour and tone stay untouched.
        assert!(s.color.calibrations.is_empty() && s.tone_curve.is_empty());
        assert_eq!(s.white, [16383.0; 3]);

        let mut s = sensor("SONY", "ILCE-7", 6048, 4024);
        apply(&mut s);
        assert_eq!(s.crop, Rect::new(12, 12, 6000, 4000));

        let mut s = sensor("SONY", "ILCE-6000", 6048, 4024);

Expected assertions from that fixture:

        assert_eq!(s.crop, Rect::new(8, 6, 7360, 4912));
        // Only the image area is measured: levels, colour and tone stay untouched.
        assert!(s.color.calibrations.is_empty() && s.tone_curve.is_empty());
        assert_eq!(s.white, [16383.0; 3]);
        assert!(s.color.calibrations.is_empty() && s.tone_curve.is_empty());
        assert_eq!(s.white, [16383.0; 3]);

        let mut s = sensor("SONY", "ILCE-7", 6048, 4024);
        assert_eq!(s.white, [16383.0; 3]);

        let mut s = sensor("SONY", "ILCE-7", 6048, 4024);
        apply(&mut s);

encrypted_applied_white_balance_and_black_reach_the_developer setup/action excerpt:

        for be in [false, true] {
            let bytes = fixture(be, false, [512, 513, 514, 515], [3072, 1024, 1024, 2048]);
            let s = decode(&bytes, &Limits::default()).unwrap();
            assert_eq!(s.format, RawFormat::Arw);
            assert_eq!(s.camera_wb, Some([3.0, 1.0, 2.0]));
            assert_eq!(s.black.values, vec![512.0, 513.0, 514.0, 515.0]);
            let dev = develop_sensor(&s, &DevelopOptions::default()).unwrap();
            assert_eq!(dev.info.wb_multipliers, [3.0, 1.0, 2.0]);
            assert!(!dev.warnings.iter().any(|w| w.contains("grey world") || w.contains("assumed 0")));
            let explicit = develop_sensor(&s, &DevelopOptions { white_balance: WhiteBalance::Multipliers([3.0, 1.0, 2.0]), ..Default::default() }).unwrap();
            assert_eq!(dev.rgb, explicit.rgb);
        }

Expected assertions from that fixture:

            assert_eq!(s.format, RawFormat::Arw);
            assert_eq!(s.camera_wb, Some([3.0, 1.0, 2.0]));
            assert_eq!(s.black.values, vec![512.0, 513.0, 514.0, 515.0]);
            let dev = develop_sensor(&s, &DevelopOptions::default()).unwrap();
            assert_eq!(s.camera_wb, Some([3.0, 1.0, 2.0]));
            assert_eq!(s.black.values, vec![512.0, 513.0, 514.0, 515.0]);
            let dev = develop_sensor(&s, &DevelopOptions::default()).unwrap();
            assert_eq!(dev.info.wb_multipliers, [3.0, 1.0, 2.0]);
            assert_eq!(s.black.values, vec![512.0, 513.0, 514.0, 515.0]);
            let dev = develop_sensor(&s, &DevelopOptions::default()).unwrap();
            assert_eq!(dev.info.wb_multipliers, [3.0, 1.0, 2.0]);
            assert!(!dev.warnings.iter().any(|w| w.contains("grey world") || w.contains("assumed 0")));

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3387: feat(codecs): add lossless JPEG XL export for 8/16-bit images

  • Fixture/setup: Native 7×5 Gray/GrayA/RGB/RGBA U8/U16 fixtures plus 2/513/8193-pixel-wide alpha samples in codecs/tests/jxl_export.rs.

  • Action: run each command separately:

    • cargo test -p photocraft-codecs --locked jpeg_xl_decodes_losslessly_with_independent_decoder -- --nocapture
    • cargo test -p photocraft-codecs --locked jpeg_xl_preserves_sixteen_bit_alpha_across_dimension_header_sizes -- --nocapture
    • cargo test -p photocraft-codecs --locked jpeg_xl_encoding_is_deterministic_and_converts_float_to_integer -- --nocapture
    • cargo test -p photocraft-codecs --locked jpeg_xl_is_export_only_and_detects_both_signatures -- --nocapture
    • cargo test -p photocraft-codecs --locked jpeg_xl_rejects_one_pixel_images_without_panicking -- --nocapture
  • Expected: Independent jxl_oxide decoding equals original integer samples exactly, including alpha and GrayA widening. Dimensions/channels and deterministic output are correct; capabilities remain export-only; 1×1 refuses without panic.

  • Separate coverage / prerequisites: These native independent-decoder checks cannot be established by wasm or malformed-header rejection alone.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-3464: fix(vector): target selected saved paths and own Pen gestures

Concrete setup and expected assertion examples

appended_work_and_saved_components_preserve_existing_geometry_and_undo setup/action excerpt:

    for name in ["work", "Saved drawing"] {
        let mut session = session();
        let first = json!({"subpaths": [{"closed": false, "knots": [[20, 20], [80, 20]]}]});
        let second = json!({"subpaths": [{"closed": false, "knots": [[30, 60], [90, 60]]}]});
        session.execute("path.set", json!({"name": name, "path": first})).unwrap();
        let before = session.execute("path.info", json!({"name": name})).unwrap();
        session.execute("path.set", json!({"name": name, "path": second, "op": "combine"})).unwrap();
        let after = session.execute("path.info", json!({"name": name})).unwrap();
        assert_eq!(after["path"]["subpaths"].as_array().unwrap().len(), 2);
        assert_eq!(after["path"]["subpaths"][0], before["path"]["subpaths"][0]);
        session.execute("edit.undo", json!({})).unwrap();
        assert_eq!(session.execute("path.info", json!({"name": name})).unwrap(), before);

Expected assertions from that fixture:

        assert_eq!(after["path"]["subpaths"].as_array().unwrap().len(), 2);
        assert_eq!(after["path"]["subpaths"][0], before["path"]["subpaths"][0]);
        session.execute("edit.undo", json!({})).unwrap();
        assert_eq!(session.execute("path.info", json!({"name": name})).unwrap(), before);
        assert_eq!(after["path"]["subpaths"][0], before["path"]["subpaths"][0]);
        session.execute("edit.undo", json!({})).unwrap();
        assert_eq!(session.execute("path.info", json!({"name": name})).unwrap(), before);
        session.execute("edit.redo", json!({})).unwrap();
        assert_eq!(session.execute("path.info", json!({"name": name})).unwrap(), before);
        session.execute("edit.redo", json!({})).unwrap();
        assert_eq!(session.execute("path.info", json!({"name": name})).unwrap(), after);
    }

appending_valid_components_cannot_exceed_total_path_budget setup/action excerpt:

    let mut session = session();
    let subpath = json!({"closed": false, "knots": []});
    session.execute("path.set", json!({"path": {"subpaths": vec![subpath.clone(); 2_049]}})).unwrap();
    let before = session.active().unwrap().doc.work_path.clone();
    let history = session.active().unwrap().history.entries().len();
    let error = session.execute("path.set", json!({"path": {"subpaths": vec![subpath; 2_048]}, "op": "combine"})).unwrap_err();
    assert!(error.to_string().contains("4096"));
    assert_eq!(session.active().unwrap().doc.work_path, before);
    assert_eq!(session.active().unwrap().history.entries().len(), history);
}

Expected assertions from that fixture:

    assert!(error.to_string().contains("4096"));
    assert_eq!(session.active().unwrap().doc.work_path, before);
    assert_eq!(session.active().unwrap().history.entries().len(), history);
}
    assert_eq!(session.active().unwrap().doc.work_path, before);
    assert_eq!(session.active().unwrap().history.entries().len(), history);
}
    assert_eq!(session.active().unwrap().history.entries().len(), history);
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3519: feat(export): optionally trim transparent margins from layer files

Concrete setup and expected assertion examples

layers_to_files_can_trim_transparent_margins_without_changing_the_document setup/action excerpt:

    let dir = tmp("layers-trim");
    for depth in [8, 16, 32] {
        let mut s = session(12, 11, depth);
        s.execute("layer.new.layer", json!({"name": "Ink"})).unwrap();
        s.execute("select.rect", json!({"x": 3, "y": 2, "width": 4, "height": 5})).unwrap();
        s.execute("edit.fill", json!({"color": "#ff0000"})).unwrap();
        s.execute("select.deselect", json!({})).unwrap();

        let before = doc(&s).clone();
        let regular = s.execute("file.export.layersToFiles", json!({"dir": dir, "prefix": format!("full-{depth}"), "format": "png"})).unwrap();
        let full = std::fs::read(regular["files"][0].as_str().unwrap()).unwrap();
        let full = photocraft_io::import("full.png", &full).unwrap().document;

Expected assertions from that fixture:

        assert_eq!((full.size.width, full.size.height), (12, 11), "default is unchanged");

        let trimmed = s.execute("file.export.layersToFiles", json!({"dir": dir, "prefix": format!("trim-{depth}"), "format": "png", "trim": true})).unwrap();
        let image = std::fs::read(trimmed["files"][0].as_str().unwrap()).unwrap();
        assert_eq!((cropped.size.width, cropped.size.height), (4, 5), "depth {depth}");
        let px = photocraft_compose::render(&cropped, Rect::new(0, 0, 1, 1)).px[0];
        assert!(px[0] > 0.95 && px[1] < 0.05 && px[2] < 0.05, "depth {depth}: {px:?}");
        assert_eq!(*doc(&s), before, "export must not crop the original document");
        assert!(px[0] > 0.95 && px[1] < 0.05 && px[2] < 0.05, "depth {depth}: {px:?}");
        assert_eq!(*doc(&s), before, "export must not crop the original document");
    }
}

streamed_layer_export_respects_trim setup/action excerpt:

    let mut s = session(12, 11, 8);
    s.execute("layer.new.layer", json!({"name": "Ink"})).unwrap();
    s.execute("select.rect", json!({"x": 3, "y": 2, "width": 4, "height": 5})).unwrap();
    s.execute("edit.fill", json!({"color": "#ff0000"})).unwrap();
    s.execute("select.deselect", json!({})).unwrap();
    let mut encoded = Vec::new();
    let names = super::export_layers_with(&s, &json!({"trim": true, "format": "png"}), |name, bytes| {
        encoded.push((name.to_string(), bytes.to_vec()));
        Ok(())
    })
    .unwrap();
    assert_eq!(names.len(), encoded.len());

Expected assertions from that fixture:

    assert_eq!(names.len(), encoded.len());
    let cropped = photocraft_io::import(&encoded[0].0, &encoded[0].1).unwrap().document;
    assert_eq!((cropped.size.width, cropped.size.height), (4, 5));
}
    assert_eq!((cropped.size.width, cropped.size.height), (4, 5));
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5870: fix(format): reject duplicate native layer identities

Concrete setup and expected assertion examples

duplicate_extreme_layer_ids_are_not_silently_remapped setup/action excerpt:

    let bytes = save_to_bytes(&sample(), &SaveOptions::default()).unwrap();
    let damaged = rewrite_manifest(&bytes, |m| {
        m["document"]["layers"][0]["id"] = u64::MAX.into();
        m["document"]["layers"][2]["id"] = u64::MAX.into();
    });
    for preserve_ids in [true, false] {
        let error = load_from_bytes_with(&damaged, &LoadOptions { preserve_ids, ..Default::default() }).unwrap_err();
        assert_duplicate(error, u64::MAX);
    }
}

Expected assertions from that fixture:


unique_nested_layer_ids_preserve_comp_targets_in_both_load_modes setup/action excerpt:

    let doc = sample();
    let bytes = save_to_bytes(&doc, &SaveOptions::default()).unwrap();
    let original_ids: HashSet<_> = doc.walk().into_iter().map(|(_, _, l)| l.id).collect();
    for preserve_ids in [true, false] {
        let loaded = load_from_bytes_with(&bytes, &LoadOptions { preserve_ids, ..Default::default() }).unwrap();
        let walk = loaded.walk();
        let ids: HashSet<_> = walk.iter().map(|(_, _, l)| l.id).collect();
        assert_eq!(ids.len(), walk.len());
        for ((_, _, layer), state) in walk.iter().zip(&loaded.layer_comps[0].states) {
            assert_eq!(state.layer, layer.id);
            assert_eq!(loaded.layer(state.layer), Some(*layer));
        }

Expected assertions from that fixture:

        assert_eq!(ids.len(), walk.len());
        for ((_, _, layer), state) in walk.iter().zip(&loaded.layer_comps[0].states) {
            assert_eq!(state.layer, layer.id);
            assert_eq!(loaded.layer(state.layer), Some(*layer));
            assert_eq!(state.layer, layer.id);
            assert_eq!(loaded.layer(state.layer), Some(*layer));
        }
        if preserve_ids {
            assert_eq!(loaded.layer(state.layer), Some(*layer));
        }
        if preserve_ids {
            assert_eq!(loaded, doc);

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6383: Windows releases ship a symbols zip: the release build's PDBs for profilers (#688)

  • Fixture/setup: Portable/executable artifact and symbols ZIP from the SAME immutable final-SHA build run, per claimed architecture.

  • Action: run each command separately:

    • Perform the explicit artifact inspection below; mark BLOCKED if fixtures are absent.
  • Expected: ZIP has photocraft.pdb, photocraft_cli.pdb and symbols.txt. Each PDB GUID/age matches its same-run executable; debugger/profiler resolves a PhotoCraft function. Portable/MSI launches without optional symbols.

  • Separate coverage / prerequisites: Inspect PE CodeView/PDB identity, record run/job/artifact IDs and hashes. Same SHA rebuilt is not the same PDB identity. Source lines are not required because release omits line tables. Missing packaging artifacts/identity evidence is BLOCKED; do not publish a release just for fixtures.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-8892: ui: larger Align icons and overflow button; Save for Web keeps its width on an invalid Matte

  • Fixture/setup: Open a layered synthetic document in Move tool and Save for Web; capture initial dialog geometry.
  • Action: Inspect alignment and overflow controls; enter an invalid Matte value and compare dialog width before/after.
  • Expected: Alignment and overflow controls keep the reviewed larger dimensions, and invalid Matte input does not change Save for Web width.
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester:

PR-8897: fix(io): read linked smart objects for PSD export as regular files only

  • Fixture/setup: Unix test-owned FIFO linked by synthetic 8×6 document; bounded-reader synthetic fixtures.

  • Action: run each command separately:

    • cargo test -p photocraft-io --locked a_linked_fifo_is_not_read -- --nocapture
    • cargo test -p photocraft-format --locked reads_stop_at_the_total_limit -- --nocapture
    • cargo test -p photocraft-format --locked an_exact_hint_is_not_over_allocated -- --nocapture
  • Expected: FIFO export completes within 30 seconds without a writer, exports cached pixels and warns of fallback/not-regular-file. Exact-limit read succeeds, over-limit/never-ending reads fail FileTooLarge; size hints do not overallocate.

  • Separate coverage / prerequisites: Use only owned temporary FIFO, never a real device. Non-Unix exclusion is BLOCKED for FIFO qualification, not PASSED.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-8986: Path tools: edit the saved path selected in the Paths panel (#8478)

Concrete setup and expected assertion examples

direct_selection_drags_an_anchor_of_the_selected_saved_path setup/action excerpt:

    let mut app = app(Tool::DirectSelection);
    with_saved_path(&mut app);
    let (before, n) = (saved(&app), steps(&app));
    drag(&mut app, [120.5, 120.5], [130.5, 135.5], Modifiers::NONE);
    let after = saved(&app);
    assert_eq!(after.subpaths[0].knots[2].anchor, Point::new(130.0, 135.0), "the dragged anchor moved");
    assert_eq!(after.subpaths[0].knots[0].anchor, Point::new(40.0, 40.0), "the others stay");
    assert_eq!(steps(&app), n + 1, "one history step");
    assert_eq!((app.ui.direct_selection.target, app.ui.direct_selection.anchors.clone()), (Some(PathRef::Saved(0)), vec![[0, 2]]));
    assert!(app.session.active().unwrap().doc.work_path.is_none(), "no work path was made");
    assert!(app.session.undo());
    assert_eq!(saved(&app), before);

Expected assertions from that fixture:

    assert_eq!(after.subpaths[0].knots[2].anchor, Point::new(130.0, 135.0), "the dragged anchor moved");
    assert_eq!(after.subpaths[0].knots[0].anchor, Point::new(40.0, 40.0), "the others stay");
    assert_eq!(steps(&app), n + 1, "one history step");
    assert_eq!((app.ui.direct_selection.target, app.ui.direct_selection.anchors.clone()), (Some(PathRef::Saved(0)), vec![[0, 2]]));
    assert_eq!(after.subpaths[0].knots[0].anchor, Point::new(40.0, 40.0), "the others stay");
    assert_eq!(steps(&app), n + 1, "one history step");
    assert_eq!((app.ui.direct_selection.target, app.ui.direct_selection.anchors.clone()), (Some(PathRef::Saved(0)), vec![[0, 2]]));
    assert!(app.session.active().unwrap().doc.work_path.is_none(), "no work path was made");
    assert_eq!(steps(&app), n + 1, "one history step");
    assert_eq!((app.ui.direct_selection.target, app.ui.direct_selection.anchors.clone()), (Some(PathRef::Saved(0)), vec![[0, 2]]));
    assert!(app.session.active().unwrap().doc.work_path.is_none(), "no work path was made");
    assert!(app.session.undo());

direct_selection_marquee_selects_and_arrow_params_name_the_saved_path setup/action excerpt:

    let mut app = app(Tool::DirectSelection);
    with_saved_path(&mut app);
    drag(&mut app, [30.0, 30.0], [50.0, 50.0], Modifiers::NONE);
    assert_eq!((app.ui.direct_selection.target, app.ui.direct_selection.anchors.clone()), (Some(PathRef::Saved(0)), vec![[0, 0]]));
    assert_eq!(PathRef::Saved(0).params(&app), json!({"name": NAME}));
    // A path that is gone gets no name, which the engine refuses instead of editing another one.
    assert_eq!(PathRef::Saved(7).params(&app), json!({"name": ""}));
    assert!(app.run("path.moveAnchors", json!({"name": "", "anchors": [[0, 0]], "move": [1, 1]})).is_err());
}

Expected assertions from that fixture:

    assert_eq!((app.ui.direct_selection.target, app.ui.direct_selection.anchors.clone()), (Some(PathRef::Saved(0)), vec![[0, 0]]));
    assert_eq!(PathRef::Saved(0).params(&app), json!({"name": NAME}));
    // A path that is gone gets no name, which the engine refuses instead of editing another one.
    assert_eq!(PathRef::Saved(7).params(&app), json!({"name": ""}));
    assert_eq!(PathRef::Saved(0).params(&app), json!({"name": NAME}));
    // A path that is gone gets no name, which the engine refuses instead of editing another one.
    assert_eq!(PathRef::Saved(7).params(&app), json!({"name": ""}));
    assert!(app.run("path.moveAnchors", json!({"name": "", "anchors": [[0, 0]], "move": [1, 1]})).is_err());
    assert_eq!(PathRef::Saved(7).params(&app), json!({"name": ""}));
    assert!(app.run("path.moveAnchors", json!({"name": "", "anchors": [[0, 0]], "move": [1, 1]})).is_err());
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9339: fix(export): embed requested sRGB profile for untagged sources (#9325)

Concrete setup and expected assertion examples

nearest_neighbor_doubles_each_pixel_into_an_exact_block setup/action excerpt:

        use photocraft_doc::SampleType;
        for depth in [SampleType::U8, SampleType::U16] {
            let doc = checker_doc(depth);
            let before = doc.clone();
            let mut f = Map::new();
            f.insert("format".into(), json!("png"));
            f.insert("scale".into(), json!(200));
            f.insert("resample".into(), json!("nearest"));
            let out = default_export_document(&doc, &f, None).unwrap();
            assert_eq!((out.size.width, out.size.height), (10, 6));
            assert_eq!(out.depth, depth);
            let surf = out.layers[0].surface().unwrap();

Expected assertions from that fixture:

            assert_eq!((out.size.width, out.size.height), (10, 6));
            assert_eq!(out.depth, depth);
            let surf = out.layers[0].surface().unwrap();
            for y in 0..6 {
            assert_eq!(out.depth, depth);
            let surf = out.layers[0].surface().unwrap();
            for y in 0..6 {
                for x in 0..10 {
                    assert_eq!(surf.rgba(x, y), checker(x / 2, y / 2), "{depth:?} pixel {x},{y}");
                }
            }
            assert_eq!(doc, before, "{depth:?}: export leaves the source document unchanged");

resample_choices_are_checked setup/action excerpt:

        let doc = checker_doc(photocraft_doc::SampleType::U8);
        let mut f = Map::new();
        f.insert("scale".into(), json!(300));
        for (method, _) in RESAMPLE {
            f.insert("resample".into(), json!(method));
            let out = default_export_document(&doc, &f, None).unwrap();
            assert_eq!((out.size.width, out.size.height), (15, 9), "{method}");
        }
        for bad in [json!("lanczos"), json!("NEAREST"), json!(""), json!(1), json!({"m": "nearest"})] {
            f.insert("resample".into(), bad.clone());
            let err = default_export_document(&doc, &f, None).unwrap_err();
            assert!(err.contains("resample must be"), "{bad}: {err}");

Expected assertions from that fixture:

            assert_eq!((out.size.width, out.size.height), (15, 9), "{method}");
        }
        for bad in [json!("lanczos"), json!("NEAREST"), json!(""), json!(1), json!({"m": "nearest"})] {
            f.insert("resample".into(), bad.clone());
            assert!(err.contains("resample must be"), "{bad}: {err}");
            assert!(default_export_document(&doc, &f, Some(512)).is_err(), "{bad}: the preview reports it too");
        }
        f.remove("resample");
            assert!(default_export_document(&doc, &f, Some(512)).is_err(), "{bad}: the preview reports it too");
        }
        f.remove("resample");
        assert_eq!(resample_method(&f, None).unwrap(), "bicubicAutomatic");

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9342: fix(io, engine): Embed Color Profile writes sRGB for an untagged document (#9325)

Concrete setup and expected assertion examples

exif_srgb_jpeg_round_trip setup/action excerpt:

        let mut s = Session::new();
        s.execute("edit.colorSettings", json!({"workingRgb": "display-p3"})).unwrap();
        // Embed Color Profile off: an untagged document is otherwise tagged sRGB on export (#9325).
        let mut opts = photocraft_io::ExportOptions::default();
        opts.encode.embed_icc = false;
        let r = photocraft_io::export(&untagged_camera_file(Some(1)), "x.jpg", &opts).unwrap();
        let back = photocraft_io::import("x.jpg", &r.bytes).unwrap().document;
        assert!(back.icc_profile.is_none(), "no ICC profile in the file");
        let (_, r) = s.open_document(back, None);
        assert_eq!((r["action"].as_str(), r["exif"].as_bool()), (Some("kept"), Some(true)));
        assert!(desc(&s.active().unwrap().doc).unwrap().starts_with("sRGB"));
    }

Expected assertions from that fixture:

        assert!(back.icc_profile.is_none(), "no ICC profile in the file");
        let (_, r) = s.open_document(back, None);
        assert_eq!((r["action"].as_str(), r["exif"].as_bool()), (Some("kept"), Some(true)));
        assert!(desc(&s.active().unwrap().doc).unwrap().starts_with("sRGB"));
        assert_eq!((r["action"].as_str(), r["exif"].as_bool()), (Some("kept"), Some(true)));
        assert!(desc(&s.active().unwrap().doc).unwrap().starts_with("sRGB"));
    }
        assert!(desc(&s.active().unwrap().doc).unwrap().starts_with("sRGB"));
    }

embed_color_profile_tags_an_untagged_document_srgb setup/action excerpt:

    let mut s = session(8);
    assert!(s.active().unwrap().doc.icc_profile.is_none(), "a new document is untagged");
    let dir = tmp("assumed-icc");
    let profile = |path: &str| decode(path).icc.map(|b| photocraft_cms::Profile::parse(&b).unwrap().description);
    for (format, ext) in [("jpeg", "jpg"), ("png24", "png")] {
        let on = format!("{dir}/on.{ext}");
        s.execute("file.export.saveForWebLegacy", json!({"format": format, "embedIcc": true, "path": on})).unwrap();
        assert_eq!(profile(&on).as_deref(), Some(photocraft_cms::Builtin::Srgb.description()), "{format}");
        let off = format!("{dir}/off.{ext}");
        s.execute("file.export.saveForWebLegacy", json!({"format": format, "embedIcc": false, "path": off})).unwrap();
        assert_eq!(profile(&off), None, "{format}");
    }

Expected assertions from that fixture:

    assert!(s.active().unwrap().doc.icc_profile.is_none(), "a new document is untagged");
    let dir = tmp("assumed-icc");
    let profile = |path: &str| decode(path).icc.map(|b| photocraft_cms::Profile::parse(&b).unwrap().description);
    for (format, ext) in [("jpeg", "jpg"), ("png24", "png")] {
        assert_eq!(profile(&on).as_deref(), Some(photocraft_cms::Builtin::Srgb.description()), "{format}");
        let off = format!("{dir}/off.{ext}");
        s.execute("file.export.saveForWebLegacy", json!({"format": format, "embedIcc": false, "path": off})).unwrap();
        assert_eq!(profile(&off), None, "{format}");
        assert_eq!(profile(&off), None, "{format}");
    }
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9401: fix(format): a save on macOS keeps the file's creation time (#9388)

Concrete setup and expected assertion examples

write_keeps_the_replaced_files_creation_time setup/action excerpt:

        use std::os::macos::fs::FileTimesExt;
        let (inside, _, workspace) = roots("created");
        let path = inside.join("doc.psd");
        workspace.write("doc.psd", b"original").unwrap();
        let born = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_600_000_000);
        std::fs::OpenOptions::new().write(true).open(&path).unwrap().set_times(std::fs::FileTimes::new().set_created(born)).unwrap();
        workspace.write("doc.psd", b"replacement").unwrap();
        assert_eq!(std::fs::read(&path).unwrap(), b"replacement");
        assert_eq!(std::fs::metadata(&path).unwrap().created().unwrap(), born, "the creation time survives the automation write");
    }

Expected assertions from that fixture:

        assert_eq!(std::fs::read(&path).unwrap(), b"replacement");
        assert_eq!(std::fs::metadata(&path).unwrap().created().unwrap(), born, "the creation time survives the automation write");
    }
        assert_eq!(std::fs::metadata(&path).unwrap().created().unwrap(), born, "the creation time survives the automation write");
    }

keeps_the_original_creation_time setup/action excerpt:

    use std::os::macos::fs::FileTimesExt;
    let d = TempDir::new("created");
    let p = d.0.join("doc.psd");
    std::fs::write(&p, ORIGINAL).unwrap();
    // A creation time well in the past, so a reset to "now" can't pass for it.
    let born = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_600_000_000);
    std::fs::OpenOptions::new().write(true).open(&p).unwrap().set_times(std::fs::FileTimes::new().set_created(born)).unwrap();
    assert_eq!(std::fs::metadata(&p).unwrap().created().unwrap(), born);
    atomic_write(&p, b"new").unwrap();
    assert_eq!(std::fs::read(&p).unwrap(), b"new");
    assert_eq!(std::fs::metadata(&p).unwrap().created().unwrap(), born, "the creation time survives the save");
    assert_no_temp(&d.0);

Expected assertions from that fixture:

    assert_eq!(std::fs::metadata(&p).unwrap().created().unwrap(), born);
    atomic_write(&p, b"new").unwrap();
    assert_eq!(std::fs::read(&p).unwrap(), b"new");
    assert_eq!(std::fs::metadata(&p).unwrap().created().unwrap(), born, "the creation time survives the save");
    assert_eq!(std::fs::read(&p).unwrap(), b"new");
    assert_eq!(std::fs::metadata(&p).unwrap().created().unwrap(), born, "the creation time survives the save");
    assert_no_temp(&d.0);
}
    assert_eq!(std::fs::metadata(&p).unwrap().created().unwrap(), born, "the creation time survives the save");
    assert_no_temp(&d.0);
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9416: Affinity reader: contiguous curve records and a cheaper object tag index

  • Fixture/setup: Synthetic repeated tags and fixed-record stream in affinity/src/tests.rs; licensed curve document with known topology/reference.

  • Action: run each command separately:

    • cargo test -p photocraft-affinity --locked lookup_keeps_first_match_for_every_object_size -- --nocapture
    • cargo test -p photocraft-affinity --locked a_fixed_size_array_is_one_buffer_and_one_value_per_record -- --nocapture
  • Expected: Tag lookup preserves first occurrence for sizes 0–59 and positional-tag exclusion; five 18-byte records occupy one buffer and report five elements. Full curve import preserves finite control points, nodes and topology.

  • Separate coverage / prerequisites: Also run full affinity suite and applicable pinned corpus tests. Record curve fixture hash and compare imported geometry/appearance; if no end-to-end contiguous-curve fixture exists, mark that subcase BLOCKED. Storage tests alone do not establish render fidelity.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-9590: fix(io): PSD layer-effect contours keep at most 16 points

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: psd contours keep at most 16 points and their peaks; a point on a tiny straight span is not a bend. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

psd_contours_keep_at_most_16_points_and_their_peaks setup/action excerpt:

        let points: Vec<CurvePoint> = (0..29)
            .map(|i| {
                let x = i as f32 / 28.0;
                // Rings at x = 0.25 and 0.75, small wiggles between.
                let y = (1.0 - ((x - 0.25).abs() * 8.0)).max(0.0).max(1.0 - (x - 0.75).abs() * 8.0) + if i % 2 == 0 { 0.0 } else { 0.01 };
                CurvePoint { input: x, output: y.clamp(0.0, 1.0) }
            })
            .collect();
        let c = Contour::Custom { name: "Two rings".into(), points: points.clone(), corners: vec![false; 29] };
        let out = crv(&contour_value(&c));
        assert_eq!(out.len(), 16);
        assert_eq!(out.first().copied(), Some((0.0, f64::from(points[0].output) * 255.0)));

Expected assertions from that fixture:

        assert_eq!(out.len(), 16);
        assert_eq!(out.first().copied(), Some((0.0, f64::from(points[0].output) * 255.0)));
        assert_eq!(out.last().map(|p| p.0), Some(255.0));
        let peak = |x: f32| out.iter().any(|p| ((p.0 / 255.0) as f32 - x).abs() < 0.03 && p.1 > 240.0);
        assert_eq!(out.first().copied(), Some((0.0, f64::from(points[0].output) * 255.0)));
        assert_eq!(out.last().map(|p| p.0), Some(255.0));
        let peak = |x: f32| out.iter().any(|p| ((p.0 / 255.0) as f32 - x).abs() < 0.03 && p.1 > 240.0);
        assert!(peak(0.25) && peak(0.75), "both rings kept: {out:?}");
        assert_eq!(out.last().map(|p| p.0), Some(255.0));
        let peak = |x: f32| out.iter().any(|p| ((p.0 / 255.0) as f32 - x).abs() < 0.03 && p.1 > 240.0);
        assert!(peak(0.25) && peak(0.75), "both rings kept: {out:?}");
        assert!(out.windows(2).all(|w| w[0].0 < w[1].0), "in order: {out:?}");

a_point_on_a_tiny_straight_span_is_not_a_bend setup/action excerpt:

        let p = |input: f32, output: f32| CurvePoint { input, output };
        // Three distinct inputs a few ulps apart near 0.5, on a straight rise from 0 to 1.
        let (a, b) = (0.5_f32, f32::from_bits(0.5_f32.to_bits() + 2));
        let mid = f32::from_bits(0.5_f32.to_bits() + 1);
        let mut points = vec![p(0.0, 0.0), p(0.2, 0.0)];
        points.extend([p(a, 0.0), p(mid, 0.5), p(b, 1.0)]);
        // Fourteen gentle bends after it, each one a real (if small) peak.
        points.extend((0..14).map(|k| p(0.55 + k as f32 * 0.03, if k % 2 == 0 { 1.0 } else { 0.9 })));
        points.push(p(1.0, 1.0));
        let kept = psd_contour_points(&points);
        assert_eq!(kept.len(), 16);
        assert!(!kept.contains(&3), "the point on the tiny straight span was kept over a real bend: {kept:?}");

Expected assertions from that fixture:

        assert_eq!(kept.len(), 16);
        assert!(!kept.contains(&3), "the point on the tiny straight span was kept over a real bend: {kept:?}");
    }
        assert!(!kept.contains(&3), "the point on the tiny straight span was kept over a real bend: {kept:?}");
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4763: refactor(liquify): reuse native brush resizing and anchored HUD feedback

  • Fixture/setup: Open Liquify on a synthetic grid; place the brush center at a known point and note the initial size.
  • Action: Use the native brush-resize gesture, vary size and restore it; inspect the HUD anchor while dragging.
  • Expected: Liquify reuses the native resize behavior and the HUD stays anchored to the gesture; no image edit is committed by resizing.
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester:

PR-4985: feat(ui): add Shift+right-click brush mode picker

Concrete setup and expected assertion examples

shift_right_mode_picker_selects_journals_and_clear_erases setup/action excerpt:

    use egui_kittest::kittest::Queryable;
    let mut h = harness(Some("erase"));
    let history = h.state().session.active().unwrap().history.past_len();
    let journal = h.state().session.journal.len();
    mode_right_click(&mut h, Modifiers::SHIFT);
    assert!(h.state().ui.brush_mode_menu.is_some());
    assert!(h.state().ui.brush_picker.is_none() && h.state().drag.is_none());
    assert_eq!(h.state().session.journal.len(), journal);
    assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    h.get_by_label("Multiply").click();
    h.run_steps(2);
    assert!(h.state().ui.brush_mode_menu.is_none());

Expected assertions from that fixture:

    assert!(h.state().ui.brush_mode_menu.is_some());
    assert!(h.state().ui.brush_picker.is_none() && h.state().drag.is_none());
    assert_eq!(h.state().session.journal.len(), journal);
    assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    assert!(h.state().ui.brush_picker.is_none() && h.state().drag.is_none());
    assert_eq!(h.state().session.journal.len(), journal);
    assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    h.get_by_label("Multiply").click();
    assert_eq!(h.state().session.journal.len(), journal);
    assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    h.get_by_label("Multiply").click();
    h.run_steps(2);

mode_picker_outside_dismissal_does_not_paint_or_erase setup/action excerpt:

    for button in [PointerButton::Primary, PointerButton::Secondary] {
        let mut h = harness(Some("erase"));
        drag(&mut h, PointerButton::Primary);
        let journal = h.state().session.journal.len();
        let history = h.state().session.active().unwrap().history.past_len();
        mode_right_click(&mut h, Modifiers::SHIFT);
        // A full outside drag must only dismiss, including its release click.
        drag(&mut h, button);
        assert!(h.state().ui.brush_mode_menu.is_none());
        assert_eq!(h.state().session.journal.len(), journal, "dismissal must not paint/erase with {button:?}");
        assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    }

Expected assertions from that fixture:

        assert!(h.state().ui.brush_mode_menu.is_none());
        assert_eq!(h.state().session.journal.len(), journal, "dismissal must not paint/erase with {button:?}");
        assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    }
        assert_eq!(h.state().session.journal.len(), journal, "dismissal must not paint/erase with {button:?}");
        assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    }
}
        assert_eq!(h.state().session.active().unwrap().history.past_len(), history);
    }
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5082: fix(mixer-brush): Alt-click samples the foreground colour like the Brush (#4873)

  • Fixture/setup: Red/green canvas with blue foreground from alt_click_tests.rs; engine 80×40 transparent canvas with blue stroke and red foreground.

  • Action: run each command separately:

    • cargo test -p photocraft-ui-egui --locked alt_click_with_the_mixer_brush_loads_the_reservoir_not_the_foreground -- --nocapture
    • cargo test -p photocraft-engine --locked mixer_load_fills_the_reservoir_from_the_canvas_and_keeps_the_foreground -- --nocapture
  • Expected: Alt-click loads Current Brush Load/reservoir, preserving foreground and document revision and painting nothing. Dry stroke uses sampled blue until foreground changes; green foreground then paints green. Transparent sampling loads nothing; invalid coordinates preserve all state.

  • Separate coverage / prerequisites: Historical PR title is stale: this case follows reviewed reservoir behavior, not foreground replacement. Repeat Alt/Option-click natively as a separate NOT RUN subcase.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-5165: Fix: brush section selection

Concrete setup and expected assertion examples

selecting_a_controls_section_keeps_its_enabled_flag_and_brush_history setup/action excerpt:

    use egui_kittest::kittest::Queryable;

    for (section, name) in [(1, "Shape Dynamics"), (2, "Scattering"), (3, "Texture")] {
        let mut h = harness(0, 0);
        let before = h.state().session.tools.brush.clone();
        let journal = h.state().session.journal.len();

        h.get_by_label(name).click();
        h.run_steps(3);

        assert_eq!(h.state().ui.brush_section, section, "{name} settings are visible");
        assert_eq!(h.state().session.tools.brush, before, "{name} remains disabled");

Expected assertions from that fixture:

        assert_eq!(h.state().ui.brush_section, section, "{name} settings are visible");
        assert_eq!(h.state().session.tools.brush, before, "{name} remains disabled");
        assert_eq!(h.state().session.journal.len(), journal, "selecting {name} is not a brush edit");
    }
        assert_eq!(h.state().session.tools.brush, before, "{name} remains disabled");
        assert_eq!(h.state().session.journal.len(), journal, "selecting {name} is not a brush edit");
    }
}
        assert_eq!(h.state().session.journal.len(), journal, "selecting {name} is not a brush edit");
    }
}

controls_section_checkbox_toggles_without_selecting_its_settings setup/action excerpt:

    use egui_kittest::kittest::Queryable;

    let mut h = harness(0, 0);
    let row = h.get_by_label("Scattering").rect();
    let checkbox = egui::pos2(row.left() + 12.0, row.center().y);
    let journal = h.state().session.journal.len();

    for enabled in [true, false] {
        h.hover_at(checkbox);
        h.run_steps(1);
        h.drag_at(checkbox);
        h.run_steps(1);

Expected assertions from that fixture:

        assert_eq!(h.state().session.tools.brush.scattering.enabled, enabled);
        assert_eq!(h.state().ui.brush_section, 0, "the checkbox does not select Scattering");
        assert_eq!(last_journal(h.state()).unwrap().1["brush"], json!({"scattering": {"enabled": enabled}}));
    }
        assert_eq!(h.state().ui.brush_section, 0, "the checkbox does not select Scattering");
        assert_eq!(last_journal(h.state()).unwrap().1["brush"], json!({"scattering": {"enabled": enabled}}));
    }
    assert_eq!(h.state().session.journal.len(), journal + 2);
        assert_eq!(last_journal(h.state()).unwrap().1["brush"], json!({"scattering": {"enabled": enabled}}));
    }
    assert_eq!(h.state().session.journal.len(), journal + 2);
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5453: Shapes: independent fill and stroke paints (solid, gradient, pattern), part 1

  • Fixture/setup: Synthetic distinct paints and imported/clipboard patterns in shape_paint_fidelity.rs, shape_patterns.rs and shape_pattern_clipboard.rs across their depth/model matrix.

  • Action: run each command separately:

    • cargo test -p photocraft-engine --locked exact_paints_roundtrip_through_create_edit_and_undo -- --nocapture
    • cargo test -p photocraft-engine --locked solid_alpha_and_library_patterns_render_and_embed_in_the_same_history_step -- --nocapture
    • cargo test -p photocraft-engine --locked patterned_shapes_embed_render_and_undo_in_one_command_at_every_depth -- --nocapture
    • cargo test -p photocraft-engine --locked patterned_fill_and_stroke_clipboards_keep_cow_tiles_after_source_closes -- --nocapture
    • cargo test -p photocraft-engine --locked unresolved_and_conflicting_pattern_clipboards_fail_atomically -- --nocapture
    • cargo test -p photocraft-vector --locked pattern_shape_fill_and_stroke_render_at_every_depth_and_model -- --nocapture
    • cargo test -p photocraft-gpu --locked patterned_shape_fill_and_stroke_clip_and_effects_match_cpu -- --nocapture
  • Expected: Distinct solid/gradient/pattern fill and stroke survive create/edit/undo. Pattern embedding shares the history transaction; copied paints retain pixels after source closes; unresolved/conflicting clipboard patterns refuse atomically. GPU matches CPU at the existing tolerance.

  • Separate coverage / prerequisites: Verified adapter required for GPU comparison; cache-budget checks are additional reliability coverage, not a replacement.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-5939: fix(paint): scale blur sigma with brush size (#5892)

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: blur scales with brush size and diffuses pixels visibly. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

blur_scales_with_brush_size_and_diffuses_pixels_visibly setup/action excerpt:

    for depth in [8u64, 16] {
        let run = |size: u32| {
            let mut s = session(120, 40, depth, "rgb");
            paint_layer(&mut s, |x, _| if x < 60 { [0.0, 0.0, 0.0, 1.0] } else { [1.0, 1.0, 1.0, 1.0] });
            s.execute("paint.blur", json!({"points": [[60, 20]], "size": size, "hardness": 100, "strength": 100})).unwrap();
            (0..120).map(|x| rgba(&s, x, 20)[0]).collect::<Vec<_>>()
        };
        let p20 = run(20);
        let p60 = run(60);
        // Size 20 (sigma = 3.0, radius 10, footprint [50, 70]) leaves x=45 and x=75 untouched.
        assert_eq!(p20[45], 0.0, "size 20 footprint does not reach x=45");
        assert_eq!(p20[75], 1.0, "size 20 footprint does not reach x=75");

Expected assertions from that fixture:

        assert_eq!(p20[45], 0.0, "size 20 footprint does not reach x=45");
        assert_eq!(p20[75], 1.0, "size 20 footprint does not reach x=75");
        // Size 60 (sigma = 9.0, radius 30, footprint [30, 90]) diffuses light and dark across a wider band.
        assert!(p60[45] > 0.0, "size 60 diffuses light past x=45: {}", p60[45]);
        assert_eq!(p20[75], 1.0, "size 20 footprint does not reach x=75");
        // Size 60 (sigma = 9.0, radius 30, footprint [30, 90]) diffuses light and dark across a wider band.
        assert!(p60[45] > 0.0, "size 60 diffuses light past x=45: {}", p60[45]);
        assert!(p60[75] < 1.0, "size 60 diffuses dark past x=75: {}", p60[75]);
        assert!(p60[45] > 0.0, "size 60 diffuses light past x=45: {}", p60[45]);
        assert!(p60[75] < 1.0, "size 60 diffuses dark past x=75: {}", p60[75]);
        // Near the step edge (x=55), size 60 diffuses more than size 20 due to larger sigma.
        assert!(p60[55] > p20[55], "larger sigma diffuses deeper near edge: p60={} p20={}", p60[55], p20[55]);

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6139: Fix painting workflows, File Info and layer selection

Concrete setup and expected assertion examples

a_refresh_outside_command_execution_surfaces_the_denial_instead_of_rendering_unfiltered setup/action excerpt:

    // link-watch and transform paths re-render through Session::edit, not Session::execute or
    // jobs::run; the gate must reach them and the denial must not silently pass pixels through
    // into the process-wide stack cache.
    for lookup in [false, true] {
        let f = Fixture::new();
        let (doc, layer) = f.saved_filter(lookup);
        let mut h = f.headless();
        h.open(Path::new("stored.pcraft")).unwrap();
        let before = h.session.active().unwrap().doc.clone();
        // Session::edit is the outside-execute mutation path (an edit closure the UI or a
        // linked-file change runs); the stored filter must be denied there too.
        assert_denied(h.session.edit("Re-render", |d, _| match smart_cmds::refresh(d, layer) {

Expected assertions from that fixture:

        assert!(Arc::ptr_eq(&before, &h.session.active().unwrap().doc));
        // A trusted session (no gate) still renders the same document.
        let mut trusted = Session::new();
        trusted.add_document(doc, None);
        assert!(smart_cmds::refresh(&mut d, layer).is_ok());
    }
}

behind_brush_and_pencil_preserve_foreground_and_round_trip_history setup/action excerpt:

    for cmd in ["paint.stroke", "paint.pencil"] {
        let mut s = session(40, 20);
        s.execute(cmd, json!({"points": [[10, 10]], "size": 6, "color": "#ff0000", "hardness": 1.0})).unwrap();
        let before = surface(&s);
        s.execute(cmd, json!({"points": [[2, 10], [38, 10]], "size": 6, "color": "#0000ff", "mode": "behind", "hardness": 1.0})).unwrap();
        assert_eq!(rgba(&s, 10, 10), [1.0, 0.0, 0.0, 1.0], "{cmd}: existing opaque paint");
        assert_eq!(rgba(&s, 25, 10), [0.0, 0.0, 1.0, 1.0], "{cmd}: transparent pixels");
        let painted = surface(&s);
        s.execute("edit.undo", json!({})).unwrap();
        assert!(same_pixels(&surface(&s), &before, Rect::new(0, 0, 40, 20)));
        s.execute("edit.redo", json!({})).unwrap();
        assert!(same_pixels(&surface(&s), &painted, Rect::new(0, 0, 40, 20)));

Expected assertions from that fixture:

        assert_eq!(rgba(&s, 10, 10), [1.0, 0.0, 0.0, 1.0], "{cmd}: existing opaque paint");
        assert_eq!(rgba(&s, 25, 10), [0.0, 0.0, 1.0, 1.0], "{cmd}: transparent pixels");
        let painted = surface(&s);
        s.execute("edit.undo", json!({})).unwrap();
        assert_eq!(rgba(&s, 25, 10), [0.0, 0.0, 1.0, 1.0], "{cmd}: transparent pixels");
        let painted = surface(&s);
        s.execute("edit.undo", json!({})).unwrap();
        assert!(same_pixels(&surface(&s), &before, Rect::new(0, 0, 40, 20)));
        assert!(same_pixels(&surface(&s), &before, Rect::new(0, 0, 40, 20)));
        s.execute("edit.redo", json!({})).unwrap();
        assert!(same_pixels(&surface(&s), &painted, Rect::new(0, 0, 40, 20)));
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-7848: fix(retouch): preserve Smudge pickup with live-pixel stroke commits

Concrete setup and expected assertion examples

pressure_shape_and_smoothing_match_replay setup/action excerpt:

    let mut failures = Vec::new();
    for cmd in ["paint.smudge", "paint.blur", "paint.sharpen", "paint.dodge", "paint.burn", "paint.sponge"] {
        for case in ["pressure", "locked", "ellipse", "smoothing", "scattering"] {
            for depth in [8, 16, 32] {
                let mut s = setup(depth, "rgb");
                match case {
                    "locked" => {
                        s.edit("lock alpha", |doc, active| {
                            doc.layer_mut(active.unwrap()).unwrap().locks.transparency = true;
                            Ok(())
                        })
                        .unwrap();

Expected assertions from that fixture:

                assert_eq!(result["fromLive"], true);
                let got = pixels(&s);
                let count = got.iter().zip(&want).filter(|(a, b)| a != b).count();
                let max = got.iter().zip(&want).map(|(a, b)| (a - b).abs()).fold(0.0f32, f32::max);
    assert!(failures.is_empty(), "{}", failures.join("\n"));
}

native_modes_selection_and_lock_match_replay setup/action excerpt:

    for mode in ["rgb", "gray", "cmyk", "lab"] {
        for depth in [8, 16, 32] {
            let mut s = setup(depth, mode);
            s.edit("lock alpha", |doc, active| {
                doc.layer_mut(active.unwrap()).unwrap().locks.transparency = true;
                Ok(())
            })
            .unwrap();
            s.execute("select.rect", json!({"x":30,"y":20,"width":110,"height":60})).unwrap();
            let points = [[-10.0, 30.0], [80.0, 50.0], [160.0, 80.0], [60.0, 60.0]];
            for cmd in ["paint.blur", "paint.sharpen", "paint.dodge", "paint.burn", "paint.sponge", "paint.smudge"] {
                let params = json!({"points":points,"size":30,"hardness":40,"strength":70,"opacity":37});

Expected assertions from that fixture:

                assert_eq!(result["fromLive"], true);
                assert_eq!(result["damage"], expected_damage);
                assert_eq!(pixels(&s), want, "{mode} {depth} {cmd}");
                s.execute("edit.undo", json!({})).unwrap();
                assert_eq!(result["damage"], expected_damage);
                assert_eq!(pixels(&s), want, "{mode} {depth} {cmd}");
                s.execute("edit.undo", json!({})).unwrap();
            }
                assert_eq!(pixels(&s), want, "{mode} {depth} {cmd}");
                s.execute("edit.undo", json!({})).unwrap();
            }
        }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9623: Pencil: keep a 1 px freehand line connected where it turns

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: pencil 1px freehand line never breaks where it turns. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

pencil_1px_freehand_line_never_breaks_where_it_turns setup/action excerpt:

    // #8040: each segment of a freehand stroke samples its own line, so where the stroke turned
    // the last pixel of one segment and the first of the next could sit two pixels apart.
    let b = BrushSettings { aliased: true, hardness: 1.0, size: 1.0, ..brush() };
    let mut seed = 0x2545_f491_4f6c_dd1du64;
    let mut rnd = || {
        seed ^= seed << 13;
        seed ^= seed >> 7;
        seed ^= seed << 17;
        (seed >> 11) as f64 / (1u64 << 53) as f64
    };
    for _ in 0..300 {
        let mut pts = vec![StrokePoint::new(20.0 + rnd() * 8.0, 20.0 + rnd() * 8.0, 1.0)];

Expected assertions from that fixture:

        assert_eq!(reached.len(), painted.len(), "the line breaks along {path:?}");
    }
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3554: fix(packaging): launch bundled CLI directly from AppImage

  • Fixture/setup: Run the checked-in AppRun CLI test; it builds stand-in binaries under a path with spaces.
  • Action: bash packaging/linux/apprun-cli-test.sh
  • Expected: Argument boundaries, MCP stdio round trip, exit status 37 and unchanged GUI entry behavior all pass the script assertions.
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester:

PR-4742: fix(macos): bundle photocraft-cli and resolve it from droplet scripts

  • Fixture/setup: Frozen-SHA macOS bundle containing executable Contents/MacOS/photocraft-cli; synthetic droplet and image paths with spaces.

  • Action: run each command separately:

    • cargo test -p photocraft-engine --locked droplets_are_written_and_run -- --nocapture
  • Expected: Explicit PHOTOCRAFT_CLI wins, then supported /Applications and ~/Applications bundles, then PATH. Arguments remain separate; the intended action runs without standalone CLI installation; ordinary signature verification succeeds.

  • Separate coverage / prerequisites: Use an authorized test installation; do not replace user's app. Installed-bundle resolution is BLOCKED if required bundle is absent. Finder dropping onto a plain shell shim is not promised; app wrapper is separate.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-7743: packaging: the portable zip holds the files, not a folder with the files in it

  • Fixture/setup: Obtain an existing portable Windows ZIP built from the frozen integration; record its artifact identity.
  • Action: List root archive entries and extract into a new empty directory; launch the app from the extracted root.
  • Expected: Application files are directly at ZIP root without a redundant wrapping folder; extracted layout launches correctly.
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester:

PR-8985: Color panel and Color Picker: clip the round cursor to the colour field

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: field marker stays inside the field; color field cursor stays inside the field. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

field_marker_stays_inside_the_field setup/action excerpt:

        // Black in the default "h" mode: the marker is at the bottom-left corner.
        let h = picker("#000000");
        let mut field = None;
        let mut circles = Vec::new();
        for clipped in &h.output().shapes {
            match &clipped.shape {
                egui::Shape::Mesh(m) if m.vertices.len() == 33 * 33 => {
                    // The component slider is a 33 x 33 mesh too, drawn after the field.
                    field.get_or_insert(m.calc_bounds());
                }
                egui::Shape::Circle(c) => circles.push((c.center, c.radius, clipped.clip_rect)),
                _ => {}

Expected assertions from that fixture:

        assert!(!circles.is_empty(), "the marker is drawn");
        for (center, radius, clip) in circles {
            let visible = Rect::from_center_size(center, vec2(2.0 * radius, 2.0 * radius)).intersect(clip);
            assert!(field.contains_rect(visible), "marker {visible:?} leaves the field {field:?}");
            assert!(field.contains_rect(visible), "marker {visible:?} leaves the field {field:?}");
        }
    }

color_field_cursor_stays_inside_the_field setup/action excerpt:

        let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        // Black is s = 0, v = 0: the bottom-left corner.
        app.session.tools.foreground = [0.0, 0.0, 0.0, 1.0];
        let h = field_harness(app);
        let mut field = None;
        let mut circles = Vec::new();
        for clipped in &h.output().shapes {
            match &clipped.shape {
                // The saturation/brightness field is the 17 x 17 mesh; the hue strip is a 25 x 25 grid.
                egui::Shape::Mesh(m) if m.vertices.len() == 17 * 17 => field = Some(m.calc_bounds()),
                egui::Shape::Circle(c) => circles.push((c.center, c.radius, clipped.clip_rect)),
                _ => {}

Expected assertions from that fixture:

        assert!(rings.iter().any(|(_, radius, _)| *radius == 5.0), "inner ring sits at the field's bottom-left corner");
        assert!(rings.iter().any(|(_, radius, _)| *radius == 6.5), "outer ring sits at the field's bottom-left corner");
        for (center, radius, clip) in rings {
            let visible = egui::Rect::from_center_size(center, vec2(2.0 * radius, 2.0 * radius)).intersect(clip);
        assert!(rings.iter().any(|(_, radius, _)| *radius == 6.5), "outer ring sits at the field's bottom-left corner");
        for (center, radius, clip) in rings {
            let visible = egui::Rect::from_center_size(center, vec2(2.0 * radius, 2.0 * radius)).intersect(clip);
            assert!(field.contains_rect(visible), "cursor ring {visible:?} leaves the field {field:?}");
            assert!(field.contains_rect(visible), "cursor ring {visible:?} leaves the field {field:?}");
        }
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9019: fix(xtask): reject unknown performance benchmark selections

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: unknown bench is rejected before updating the baseline. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

unknown_bench_is_rejected_before_updating_the_baseline setup/action excerpt:

        let root = std::env::temp_dir().join(format!("photocraft-perf-unknown-bench-{}", std::process::id()));
        std::fs::create_dir(&root).unwrap();
        std::fs::create_dir(root.join("perf")).unwrap();
        std::fs::write(root.join(BUDGETS), "[[bench]]\nname = \"known\"\npackage = \"unused\"\nrequires = \"missing.rs\"\n").unwrap();
        let baseline = "{\"schema\":1,\"sentinel\":true}";
        std::fs::write(root.join(BASELINE), baseline).unwrap();
        let results = [
            run(&root, &["--bench", "unknown", "--reuse", "--update-baseline"]),
            run(&root, &["--bench", "known", "--bench", "unknown", "--quick", "--reuse", "--update-baseline"]),
        ];
        let after = std::fs::read_to_string(root.join(BASELINE)).unwrap();
        std::fs::remove_dir_all(root).unwrap();

Expected assertions from that fixture:

            assert_eq!(result, Err(format!("perf: unknown bench `unknown` (see {BUDGETS})")));
        }
        assert_eq!(after, baseline, "an unknown bench must not update the baseline");
    }
        assert_eq!(after, baseline, "an unknown bench must not update the baseline");
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9110: test: GPU tests honour WGPU_BACKEND, so they run on Windows with an Intel GPU (#4)

  • Fixture/setup: Windows Intel host with functioning DX12; record OS, GPU and driver; frozen integration build.

  • Action: run each command separately:

    • cargo test -p photocraft-gpu --locked injected_fault_skips_the_gpu -- --nocapture
    • cargo test -p photocraft-ui-egui --locked layout_document_composites_on_the_gpu_like_the_cpu -- --nocapture
  • Expected: Every process honors WGPU_BACKEND=dx12. Example completes real composition; fault test renders before injecting loss and returns expected lost-device error; layout test executes CPU/GPU assertions.

  • Separate coverage / prerequisites: Run cargo run --release -p photocraft-gpu --locked --example bench_stack_reuse -- --quick with WGPU_BACKEND=dx12. Record adapter/vendor/device/backend/driver and proof each comparison executed. Silent early returns are BLOCKED. Do not load the known-crashing Intel Vulkan backend.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-9436: Startup: a failed launch says why in a dialog instead of closing silently (#7643)

  • Fixture/setup: startup_report.rs terminal/automation/display combinations and synthetic renderer/error/log text.

  • Action: run each command separately:

    • cargo test -p photocraft --locked a_desktop_launch_asks_the_user_and_a_script_does_not -- --nocapture
    • cargo test -p photocraft --locked the_text_names_the_error_renderer_version_and_log -- --nocapture
    • cargo test -p photocraft --locked a_fallback_that_was_not_the_preference_is_explained -- --nocapture
    • cargo test -p photocraft --locked path_names_the_attached_log_file -- --nocapture
  • Expected: Desktop launch with display and without terminal/control asks via dialog; terminal/headless/automation do not hang. Text contains error, attempted renderer, version, actual log path and applicable fallback reason; no invented log path.

  • Separate coverage / prerequisites: Native dialog subcase uses only an existing safe failure reproduction in temporary settings. If unavailable, mark BLOCKED; never force a GPU driver crash.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-2300: Scroll the viewport while outlining with Polygonal Lasso

Concrete setup and expected assertion examples

polygon_edge_scroll_continues_with_button_up_and_keeps_vertices_fixed setup/action excerpt:

    for (dx, dy, flip, rotation) in [
        (-1.0, 0.0, false, 0.0),
        (1.0, 0.0, false, 0.0),
        (0.0, -1.0, false, 0.0),
        (0.0, 1.0, false, 0.0),
        (-1.0, -1.0, false, 0.0),
        (1.0, -1.0, false, 0.0),
        (-1.0, 1.0, false, 0.0),
        (1.0, 1.0, false, 0.0),
        (1.0, 0.0, true, 0.0),
        (1.0, 0.0, false, 90.0),
    ] {

Expected assertions from that fixture:

        assert!(movement.dot(direction) > 0.1, "edge ({dx},{dy}), flip={flip}, rotation={rotation}: {movement:?}");
        assert!((movement.x * direction.y - movement.y * direction.x).abs() < 0.001, "scroll follows the displayed axes");
        assert!((next[0] - first[0]).hypot(next[1] - first[1]) > 0.1, "stationary pointer keeps scrolling");
        assert_eq!(h.state().ui.polygon, points, "view motion never adds or moves vertices");
        assert!((movement.x * direction.y - movement.y * direction.x).abs() < 0.001, "scroll follows the displayed axes");
        assert!((next[0] - first[0]).hypot(next[1] - first[1]) > 0.1, "stationary pointer keeps scrolling");
        assert_eq!(h.state().ui.polygon, points, "view motion never adds or moves vertices");
        assert_eq!(steps(&h), before);
        assert!((next[0] - first[0]).hypot(next[1] - first[1]) > 0.1, "stationary pointer keeps scrolling");
        assert_eq!(h.state().ui.polygon, points, "view motion never adds or moves vertices");
        assert_eq!(steps(&h), before);
        assert!(selection(&h).is_none());

polygon_edge_scroll_places_the_next_vertex_in_the_panned_view_and_commits_once setup/action excerpt:

    let mut h = zoomed_polygon();
    let before = steps(&h);
    let original = ViewXform::active(h.state()).unwrap();
    h.event(egui::Event::PointerMoved(original.rect.right_center() - vec2(4.0, 0.0)));
    h.run_steps(10);
    let panned = ViewXform::active(h.state()).unwrap();
    assert!(panned.center[0] > original.center[0], "the zoomed image pans toward the right edge");
    let at = panned.rect.center() + vec2(60.0, 0.0);
    let expected = panned.to_doc(at);
    h.event(egui::Event::PointerMoved(at));
    h.run_steps(20);
    button(&mut h, at, true, Modifiers::NONE);

Expected assertions from that fixture:

    assert!(panned.center[0] > original.center[0], "the zoomed image pans toward the right edge");
    let at = panned.rect.center() + vec2(60.0, 0.0);
    let expected = panned.to_doc(at);
    h.event(egui::Event::PointerMoved(at));
    assert_eq!(h.state().ui.polygon[1], expected, "the next click uses the updated view transform");
    h.run_steps(30);
    click(&mut h, expected[0] as f32, 190.0, Modifiers::NONE);
    h.key_press(egui::Key::Enter);
    assert!(h.state().ui.polygon.is_empty());
    assert!(selection(&h).is_some_and(|r| near(r, 170, 130, expected[0].ceil() as i32, 190)), "{:?}", selection(&h));
    assert_eq!(steps(&h), before + 1);
    h.event(egui::Event::PointerMoved(original.rect.right_center()));

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5010: Layer Style: Photoshop's layout (three columns, OK/Cancel/New Style/Preview on the right, one-line rows) [depends on #4938]

Concrete setup and expected assertion examples

dragging_the_title_bar_moves_the_dialog setup/action excerpt:

        use egui_kittest::{Harness, kittest::Queryable};

        let app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        let mut harness = Harness::builder().with_size(egui::vec2(1400.0, 1100.0)).build_ui_state(|ui, app| show(app, ui.ctx()), app);
        PhotocraftApp::setup_context(&harness.ctx, crate::theme::ThemeKind::ALL[0]);
        harness.state_mut().ui.open_dialog(DialogKind::LayerStyle, serde_json::Map::new());
        harness.run_steps(3);
        let before = harness.get_by_label("Layer Style").rect();

        // Grab the title text itself: it must move the dialog, not select the text.
        let from = before.center();
        harness.hover_at(from);

Expected assertions from that fixture:

        assert!((moved - egui::vec2(-120.0, 80.0)).length() < 1.0, "dialog moved by {moved:?}");
        assert_eq!(harness.state().ui.dialogs.len(), 1, "dragging must not close the dialog");
    }
        assert_eq!(harness.state().ui.dialogs.len(), 1, "dragging must not close the dialog");
    }

layer_style_keeps_one_size_on_every_page setup/action excerpt:

        let mut h = layer_style_on_bevel(egui::vec2(1400.0, 900.0));
        let size = frame(&h).size();
        let dialog = h.state().ui.dialogs[0].id;
        for page in ["blendingOptions", "styles"] {
            h.state_mut().ui.dialog_mut(dialog).expect("still open").fields.insert("selected".into(), json!(page));
            h.run_steps(3);
            assert_eq!(frame(&h).size(), size, "{page}");
        }
        let grip = egui::Id::new(("dialog", dialog)).with("resize").with("__resize_corner");
        assert!(h.ctx.read_response(grip).is_none(), "no resize grip");
    }

Expected assertions from that fixture:

            assert_eq!(frame(&h).size(), size, "{page}");
        }
        let grip = egui::Id::new(("dialog", dialog)).with("resize").with("__resize_corner");
        assert!(h.ctx.read_response(grip).is_none(), "no resize grip");
        assert!(h.ctx.read_response(grip).is_none(), "no resize grip");
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5493: Import Photoshop custom shape sets into the Shapes panel

  • Fixture/setup: Synthetic multi-shape CSH, duplicate names, inner holes and malformed second shape; empty temporary library/preferences.

  • Action: run each command separately:

    • cargo test -p photocraft-engine --locked csh_import_makes_an_original_shape_available_without_a_document -- --nocapture
    • cargo test -p photocraft-engine --locked csh_invalid_second_shape_does_not_partially_import_a_set -- --nocapture
    • cargo test -p photocraft-engine --locked csh_multiple_shapes_with_matching_names_remain_individually_available -- --nocapture
    • cargo test -p photocraft-engine --locked csh_repeat_import_names_are_unique_and_survive_preferences_reload -- --nocapture
    • cargo test -p photocraft-engine --locked csh_same_winding_inner_subpath_stays_a_hole_after_place_and_reload -- --nocapture
    • cargo test -p photocraft-ui-egui --locked csh_open_imports_presets_and_shows_shapes_without_opening_a_document -- --nocapture
    • cargo test -p photocraft-ui-egui --locked malformed_csh_open_preserves_the_library -- --nocapture
  • Expected: Valid sets populate Shapes without a document; duplicate names stay individually addressable after reload; placed holes remain holes. Invalid later shape does not partially change library/revision. Malformed UI open preserves the library.

  • Separate coverage / prerequisites: Retain inline-data authorization test separately; capture native Shapes panel as additional visual evidence.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-5515: Selecting subject now automatically applies to selected layer, and us…

Concrete setup and expected assertion examples

select_subject_samples_the_selected_layer_else_the_composite setup/action excerpt:

        let (mut s, inside) = disc(8);
        // A layer above holding only a small square in the corner: the disc is not on it.
        s.execute("layer.new.layer", json!({})).unwrap();
        s.execute("paint.pencil", json!({"points": [[10, 10], [30, 10]], "size": 3, "color": "#000000"})).unwrap();
        s.execute("select.subject", json!({})).unwrap();
        assert!(cov(&s, 100, 80) < 0.5, "the disc on the layer below was selected");
        s.execute("select.deselect", json!({})).unwrap();
        // No layer selected: the whole image, so the disc is found.
        s.execute("select.deselectLayers", json!({})).unwrap();
        s.execute("select.subject", json!({})).unwrap();
        assert!(sel_iou(&s, 200, 160, &inside) > 0.9);
    }

Expected assertions from that fixture:

        assert!(cov(&s, 100, 80) < 0.5, "the disc on the layer below was selected");
        s.execute("select.deselect", json!({})).unwrap();
        // No layer selected: the whole image, so the disc is found.
        s.execute("select.deselectLayers", json!({})).unwrap();
        assert!(sel_iou(&s, 200, 160, &inside) > 0.9);
    }

select_subject_repeat_reuses_the_mask_until_pixels_change setup/action excerpt:

        let (mut s, _) = disc(8);
        let sel = |s: &Session| {
            let mut v = Vec::new();
            s.active().unwrap().doc.selection.as_ref().unwrap().read_region_into(Rect::new(0, 0, 200, 160), &mut v);
            v
        };
        let cached = |s: &Session| s.subject_cache.region.clone().unwrap();
        s.execute("select.subject", json!({})).unwrap();
        let (first, mask) = (sel(&s), cached(&s));
        s.execute("select.deselect", json!({})).unwrap();
        s.execute("select.subject", json!({})).unwrap();
        assert!(std::sync::Arc::ptr_eq(&mask, &cached(&s)), "the repeat recomputed the mask");

Expected assertions from that fixture:

        assert!(std::sync::Arc::ptr_eq(&mask, &cached(&s)), "the repeat recomputed the mask");
        assert_eq!(sel(&s), first);
        // Another sampled target (the composite rather than the layer) is a miss.
        s.execute("select.subject", json!({"sampleAllLayers": true})).unwrap();
        assert_eq!(sel(&s), first);
        // Another sampled target (the composite rather than the layer) is a miss.
        s.execute("select.subject", json!({"sampleAllLayers": true})).unwrap();
        assert!(!std::sync::Arc::ptr_eq(&mask, &cached(&s)));
        assert!(!std::sync::Arc::ptr_eq(&mask, &cached(&s)));
        // A pixel edit drops the entry; the next run recomputes it.
        s.edit("paint", |doc, _| {
            let bg = doc.layers[0].surface_mut().unwrap();

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5650: Guides: ⌥ turns a dragged guide; dropping one on either ruler deletes it (#5034)

Concrete setup and expected assertion examples

move_guide_turns_a_guide setup/action excerpt:

        let mut s = Session::new();
        s.execute("file.new", json!({"width": 200, "height": 100})).unwrap();
        s.execute("view.newGuide", json!({"orientation": "horizontal", "position": 30, "color": "magenta"})).unwrap();
        s.execute("view.newGuide", json!({"orientation": "horizontal", "position": 70})).unwrap();
        s.execute("view.moveGuide", json!({"orientation": "horizontal", "index": 0, "position": "50%", "toOrientation": "vertical"})).unwrap();
        let g = &s.active().unwrap().doc.guides;
        assert_eq!((g.horizontal.clone(), g.vertical.clone()), (vec![70.0], vec![100.0]), "50% of the width");
        assert_eq!(g.color(true, 100.0), Some([255, 0, 255]), "the colour moves with the guide");
        assert_eq!(g.color(false, 30.0), None);
        // The same orientation is a plain move.
        s.execute("view.moveGuide", json!({"orientation": "vertical", "index": 0, "position": 20, "toOrientation": "vertical"})).unwrap();
        assert_eq!(s.active().unwrap().doc.guides.vertical, vec![20.0]);

Expected assertions from that fixture:

        assert_eq!((g.horizontal.clone(), g.vertical.clone()), (vec![70.0], vec![100.0]), "50% of the width");
        assert_eq!(g.color(true, 100.0), Some([255, 0, 255]), "the colour moves with the guide");
        assert_eq!(g.color(false, 30.0), None);
        // The same orientation is a plain move.
        assert_eq!(g.color(true, 100.0), Some([255, 0, 255]), "the colour moves with the guide");
        assert_eq!(g.color(false, 30.0), None);
        // The same orientation is a plain move.
        s.execute("view.moveGuide", json!({"orientation": "vertical", "index": 0, "position": 20, "toOrientation": "vertical"})).unwrap();
        assert_eq!(g.color(false, 30.0), None);
        // The same orientation is a plain move.
        s.execute("view.moveGuide", json!({"orientation": "vertical", "index": 0, "position": 20, "toOrientation": "vertical"})).unwrap();
        assert_eq!(s.active().unwrap().doc.guides.vertical, vec![20.0]);

layer_panel_hover_outlines_layer_on_canvas setup/action excerpt:

        let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        app.session.execute("file.new", json!({"width": 64, "height": 64})).unwrap();
        app.session.execute("layer.new.layer", json!({})).unwrap();
        app.session
            .edit("paint", |doc, a| {
                doc.layer_mut(a.unwrap()).unwrap().surface_mut().unwrap().fill_rect(Rect::new(8, 8, 24, 24), &[1.0, 0.0, 0.0, 1.0]);
                Ok(())
            })
            .unwrap();
        let target_layer = app.session.active().unwrap().active_layer.unwrap();
        let a = Rect::new(8, 8, 24, 24);

Expected assertions from that fixture:

        assert_eq!(active_outline(&mut app, None, false, false), None);
        app.hover_layer = Some(target_layer);
        assert_eq!(active_outline(&mut app, None, false, false), Some(a));

        assert_eq!(active_outline(&mut app, None, false, false), Some(a));

        // When a drag or transform is in progress, no outline:
        app.guide_drag = Some(crate::rulers::GuideDrag { vertical: false, index: None, pos: 10.0, ..Default::default() });
        assert_eq!(active_outline(&mut app, None, false, false), None);
        app.guide_drag = None;

        // When rollover preference is disabled, no outline:

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6026: Marquee: Fixed Size selects exactly the entered width and height (fixes #6015)

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: fixed size px is exactly the size; fixed size selects exactly the entered size. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

fixed_size_px_is_exactly_the_size setup/action excerpt:

        use super::fixed_size_px;
        // Down-right from (10.4, 20.6): 10 × 10 from the closest pixel edges.
        assert_eq!(fixed_size_px([10.4, 20.6], [20.4, 30.6], 10.0, 10.0), [10.0, 21.0, 20.0, 31.0]);
        // Up-left: the far corner is the low one; still exactly the size.
        assert_eq!(fixed_size_px([10.4, 20.6], [-53.6, -11.4], 64.0, 32.0), [-54.0, -11.0, 10.0, 21.0]);
        // 1 × 1 is one pixel, and a fractional size rounds to whole pixels.
        assert_eq!(fixed_size_px([3.5, 3.5], [4.5, 4.5], 1.0, 1.0), [4.0, 4.0, 5.0, 5.0]);
        assert_eq!(fixed_size_px([0.0, 0.0], [2.6, 2.4], 2.6, 2.4), [0.0, 0.0, 3.0, 2.0]);
    }

Expected assertions from that fixture:

        assert_eq!(fixed_size_px([10.4, 20.6], [20.4, 30.6], 10.0, 10.0), [10.0, 21.0, 20.0, 31.0]);
        // Up-left: the far corner is the low one; still exactly the size.
        assert_eq!(fixed_size_px([10.4, 20.6], [-53.6, -11.4], 64.0, 32.0), [-54.0, -11.0, 10.0, 21.0]);
        // 1 × 1 is one pixel, and a fractional size rounds to whole pixels.
        assert_eq!(fixed_size_px([10.4, 20.6], [-53.6, -11.4], 64.0, 32.0), [-54.0, -11.0, 10.0, 21.0]);
        // 1 × 1 is one pixel, and a fractional size rounds to whole pixels.
        assert_eq!(fixed_size_px([3.5, 3.5], [4.5, 4.5], 1.0, 1.0), [4.0, 4.0, 5.0, 5.0]);
        assert_eq!(fixed_size_px([0.0, 0.0], [2.6, 2.4], 2.6, 2.4), [0.0, 0.0, 3.0, 2.0]);
        assert_eq!(fixed_size_px([3.5, 3.5], [4.5, 4.5], 1.0, 1.0), [4.0, 4.0, 5.0, 5.0]);
        assert_eq!(fixed_size_px([0.0, 0.0], [2.6, 2.4], 2.6, 2.4), [0.0, 0.0, 3.0, 2.0]);
    }

fixed_size_selects_exactly_the_entered_size setup/action excerpt:

    for tool in [Tool::RectMarquee, Tool::EllipseMarquee] {
        for (w, ht) in [(10, 10), (1, 1), (64, 32)] {
            let mut h = harness(tool);
            let o = &mut h.state_mut().ui.tool_options;
            o.marquee_style = "fixedSize".into();
            (o.marquee_width, o.marquee_height) = (w as f32, ht as f32);
            press_at(&mut h, 50.4, 60.6, Modifiers::NONE);
            assert!(readout(&h, w, ht), "{tool:?} {w} × {ht}: the readout shows the entered size");
            release_at(&mut h, 90.0, 90.0, Modifiers::NONE);
            assert_eq!(selection(&h), Rect::new(50, 61, 50 + w, 61 + ht), "{tool:?} {w} × {ht}: dragged");

            // A click: the button goes down and up at one point.

Expected assertions from that fixture:

            assert!(readout(&h, w, ht), "{tool:?} {w} × {ht}: the readout shows the entered size");
            release_at(&mut h, 90.0, 90.0, Modifiers::NONE);
            assert_eq!(selection(&h), Rect::new(50, 61, 50 + w, 61 + ht), "{tool:?} {w} × {ht}: dragged");

            assert_eq!(selection(&h), Rect::new(50, 61, 50 + w, 61 + ht), "{tool:?} {w} × {ht}: dragged");

            // A click: the button goes down and up at one point.
            h.state_mut().run("select.deselect", json!({})).unwrap();
            assert_eq!(selection(&h), Rect::new(121, 100, 121 + w, 100 + ht), "{tool:?} {w} × {ht}: clicked");
        }
    }
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6236: Shift+Alt+right-click opens Photoshop's HUD Color Picker (#6098)

Concrete setup and expected assertion examples

shift_alt_right_press_opens_the_hud_not_the_picker setup/action excerpt:

        let mut h = harness();
        let c = h.state().last_canvas_rect.center();
        h.event(egui::Event::PointerMoved(c));
        h.run_steps(1);
        button(&mut h, c, true, SHIFT_ALT);
        let hud = h.state().ui.color_hud.expect("the HUD opened");
        assert!(hud.hold);
        assert_eq!(hud.start, [0.0, 0.0, 1.0, 1.0], "seeded from the foreground");
        assert_eq!(h.state().ui.brush_picker, None, "the Brush Preset picker stays closed");
        // The HUD shows under the pointer, on screen.
        let (strip, square) = rects(&h);
        assert!(strip.height() > 100.0 && square.width() > 100.0);

Expected assertions from that fixture:

        assert!(hud.hold);
        assert_eq!(hud.start, [0.0, 0.0, 1.0, 1.0], "seeded from the foreground");
        assert_eq!(h.state().ui.brush_picker, None, "the Brush Preset picker stays closed");
        // The HUD shows under the pointer, on screen.
        assert_eq!(hud.start, [0.0, 0.0, 1.0, 1.0], "seeded from the foreground");
        assert_eq!(h.state().ui.brush_picker, None, "the Brush Preset picker stays closed");
        // The HUD shows under the pointer, on screen.
        let (strip, square) = rects(&h);
        assert_eq!(h.state().ui.brush_picker, None, "the Brush Preset picker stays closed");
        // The HUD shows under the pointer, on screen.
        let (strip, square) = rects(&h);
        assert!(strip.height() > 100.0 && square.width() > 100.0);

quick_selections_shift_alt_right_opens_only_the_selection_menu setup/action excerpt:

        let mut h = harness();
        h.state_mut().ui.tool = Tool::QuickSelection;
        h.run_steps(1);
        let c = h.state().last_canvas_rect.center();
        h.event(egui::Event::PointerMoved(c));
        h.run_steps(1);
        button(&mut h, c, true, SHIFT_ALT);
        assert_eq!(h.state().ui.color_hud, None, "no HUD for Quick Selection");
        assert_eq!(h.state().ui.brush_picker, None, "and no Brush Preset picker");
        button(&mut h, c, false, SHIFT_ALT);
        assert_eq!(h.state().ui.color_hud, None, "the release does not open it either");
        assert!(h.state().ui.canvas_tool_menu.is_some(), "the selection menu opens, once");

Expected assertions from that fixture:

        assert_eq!(h.state().ui.color_hud, None, "no HUD for Quick Selection");
        assert_eq!(h.state().ui.brush_picker, None, "and no Brush Preset picker");
        button(&mut h, c, false, SHIFT_ALT);
        assert_eq!(h.state().ui.color_hud, None, "the release does not open it either");
        assert_eq!(h.state().ui.brush_picker, None, "and no Brush Preset picker");
        button(&mut h, c, false, SHIFT_ALT);
        assert_eq!(h.state().ui.color_hud, None, "the release does not open it either");
        assert!(h.state().ui.canvas_tool_menu.is_some(), "the selection menu opens, once");
        assert_eq!(h.state().ui.color_hud, None, "the release does not open it either");
        assert!(h.state().ui.canvas_tool_menu.is_some(), "the selection menu opens, once");
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6510: Automation: resolve configured root aliases for absolute paths (#6426)

Concrete setup and expected assertion examples

canonical_windows_prefixes_match_without_ambient_resolution setup/action excerpt:

    for (request, root) in [
        (r"c:\root\new.bin", r"\\?\C:\root"),
        (r"\\?\C:\root\new.bin", r"C:\root"),
        (r"\\server\share\root\new.bin", r"\\?\UNC\SERVER\SHARE\root"),
        (r"c:\ROOT\new.bin", r"\\?\C:\root"),
        (r"\\?\C:\ROOT\new.bin", r"c:\root"),
        (r"\\server\share\ROOT\new.bin", r"\\?\UNC\SERVER\SHARE\root"),
    ] {
        let remaining = path_beneath_root(Path::new(request), Path::new(root)).unwrap();
        assert_eq!(remaining.as_path(), Path::new("new.bin"));
    }
    for request in [r"C:\root-sibling\new.bin", r"D:\root\new.bin", r"\\.\C:\root\new.bin", r"\\other\share\root\new.bin"] {

Expected assertions from that fixture:

        assert_eq!(remaining.as_path(), Path::new("new.bin"));
    }
    for request in [r"C:\root-sibling\new.bin", r"D:\root\new.bin", r"\\.\C:\root\new.bin", r"\\other\share\root\new.bin"] {
        assert!(path_beneath_root(Path::new(request), Path::new(r"\\?\C:\root")).is_none());
        assert!(path_beneath_root(Path::new(request), Path::new(r"\\?\C:\root")).is_none());
    }
}

windows_root_case_variants_keep_io_inside_the_grant setup/action excerpt:

    let (inside, outside, workspace) = roots("root-case");
    let configured = PathBuf::from(inside.to_string_lossy().to_ascii_uppercase());
    let canonical = PathBuf::from(std::fs::canonicalize(&inside).unwrap().to_string_lossy().to_ascii_uppercase());
    for spelling in [&configured, &canonical] {
        let path = spelling.join("new.bin");
        workspace.write(&path.to_string_lossy(), b"inside").unwrap();
        assert_eq!(workspace.read(&path.to_string_lossy()).unwrap(), b"inside");
        assert!(workspace.write(&spelling.join("..").join("outside").join("blocked.bin").to_string_lossy(), b"no").is_err());
    }
    assert_eq!(std::fs::read(inside.join("new.bin")).unwrap(), b"inside");
    assert!(workspace.write(&configured.with_file_name("INSIDE-SIBLING").join("blocked.bin").to_string_lossy(), b"no").is_err());
    assert!(!outside.join("blocked.bin").exists());

Expected assertions from that fixture:

        assert_eq!(workspace.read(&path.to_string_lossy()).unwrap(), b"inside");
        assert!(workspace.write(&spelling.join("..").join("outside").join("blocked.bin").to_string_lossy(), b"no").is_err());
    }
    assert_eq!(std::fs::read(inside.join("new.bin")).unwrap(), b"inside");
        assert!(workspace.write(&spelling.join("..").join("outside").join("blocked.bin").to_string_lossy(), b"no").is_err());
    }
    assert_eq!(std::fs::read(inside.join("new.bin")).unwrap(), b"inside");
    assert!(workspace.write(&configured.with_file_name("INSIDE-SIBLING").join("blocked.bin").to_string_lossy(), b"no").is_err());
    assert_eq!(std::fs::read(inside.join("new.bin")).unwrap(), b"inside");
    assert!(workspace.write(&configured.with_file_name("INSIDE-SIBLING").join("blocked.bin").to_string_lossy(), b"no").is_err());
    assert!(!outside.join("blocked.bin").exists());
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6621: Crop: right-click on the pending crop opens the crop menu (#6561)

Concrete setup and expected assertion examples

crop_menu_ratio_rows_fit_the_frame_like_the_options_bar setup/action excerpt:

        let mut app = crop_app();
        crop_open(&mut app);
        let menu = app.ui.canvas_tool_menu.clone().unwrap();
        assert!(menu.crop && available(&app, &menu, "crop.ratio.16:9"));
        choose(&mut app, &Context::default(), "crop.ratio.16:9");
        assert_eq!(app.ui.tool_options.crop_ratio, "16:9");
        assert!((frame_ratio(&app) - 16.0 / 9.0).abs() < 1e-9, "the frame is fitted to the ratio");
        // The current ratio shows checked; the others do not.
        crop_open(&mut app);
        let menu = app.ui.canvas_tool_menu.clone().unwrap();
        assert!(mode_checked(&app, &menu, "crop.ratio.16:9") && !mode_checked(&app, &menu, "crop.ratio.1:1"));
        choose(&mut app, &Context::default(), "crop.ratio.1:1");

Expected assertions from that fixture:

        assert!(menu.crop && available(&app, &menu, "crop.ratio.16:9"));
        choose(&mut app, &Context::default(), "crop.ratio.16:9");
        assert_eq!(app.ui.tool_options.crop_ratio, "16:9");
        assert!((frame_ratio(&app) - 16.0 / 9.0).abs() < 1e-9, "the frame is fitted to the ratio");
        assert_eq!(app.ui.tool_options.crop_ratio, "16:9");
        assert!((frame_ratio(&app) - 16.0 / 9.0).abs() < 1e-9, "the frame is fitted to the ratio");
        // The current ratio shows checked; the others do not.
        crop_open(&mut app);
        assert!((frame_ratio(&app) - 16.0 / 9.0).abs() < 1e-9, "the frame is fitted to the ratio");
        // The current ratio shows checked; the others do not.
        crop_open(&mut app);
        let menu = app.ui.canvas_tool_menu.clone().unwrap();

crop_menu_gear_rows_toggle_the_shield_options setup/action excerpt:

        let mut app = crop_app();
        crop_open(&mut app);
        choose(&mut app, &Context::default(), CROP_SHOW_CROPPED);
        assert!(!app.ui.tool_options.crop_shield.show_cropped_area);
        crop_open(&mut app);
        assert!(!mode_checked(&app, app.ui.canvas_tool_menu.as_ref().unwrap(), CROP_SHOW_CROPPED), "the row shows the option's state");
        choose(&mut app, &Context::default(), CROP_CLASSIC);
        assert!(app.ui.tool_options.crop_shield.classic_mode);
        // As in the gear menu, Classic Mode greys out the other two options.
        crop_open(&mut app);
        let menu = app.ui.canvas_tool_menu.clone().unwrap();
        assert!(!entry_enabled(&app, &menu, CROP_SHOW_CROPPED) && !entry_enabled(&app, &menu, CROP_AUTO_CENTER));

Expected assertions from that fixture:

        assert!(!app.ui.tool_options.crop_shield.show_cropped_area);
        crop_open(&mut app);
        assert!(!mode_checked(&app, app.ui.canvas_tool_menu.as_ref().unwrap(), CROP_SHOW_CROPPED), "the row shows the option's state");
        choose(&mut app, &Context::default(), CROP_CLASSIC);
        assert!(!mode_checked(&app, app.ui.canvas_tool_menu.as_ref().unwrap(), CROP_SHOW_CROPPED), "the row shows the option's state");
        choose(&mut app, &Context::default(), CROP_CLASSIC);
        assert!(app.ui.tool_options.crop_shield.classic_mode);
        // As in the gear menu, Classic Mode greys out the other two options.
        assert!(app.ui.tool_options.crop_shield.classic_mode);
        // As in the gear menu, Classic Mode greys out the other two options.
        crop_open(&mut app);
        let menu = app.ui.canvas_tool_menu.clone().unwrap();

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-7648: Pen: Path Operations menu, as in Photoshop (#1397)

Concrete setup and expected assertion examples

merge_shape_components_bakes_a_paths_operations setup/action excerpt:

        let mut s = session();
        s.execute("path.set", json!({"path": square()})).unwrap();
        let hole = json!({"subpaths": [{"closed": true, "knots": [[25, 25], [45, 25], [45, 45], [25, 45]]}]});
        s.execute("path.set", json!({"path": hole, "op": "subtract"})).unwrap();
        let before = work(&s);
        let r = s.execute("path.mergeShapeComponents", json!({})).unwrap();
        assert_eq!(r["name"], json!("work"));
        assert_eq!(s.active().unwrap().history.undo_label(), Some("Merge Shape Components"));
        let merged = work(&s);
        assert_eq!(merged.subpaths.len(), 2, "the outline and its hole");
        for (x, y) in [(15, 15), (35, 35), (55, 50), (5, 5)] {
            assert!((covered(&merged, x, y) - covered(&before, x, y)).abs() < 0.01, "({x}, {y})");

Expected assertions from that fixture:

        assert_eq!(r["name"], json!("work"));
        assert_eq!(s.active().unwrap().history.undo_label(), Some("Merge Shape Components"));
        let merged = work(&s);
        assert_eq!(merged.subpaths.len(), 2, "the outline and its hole");
        assert_eq!(s.active().unwrap().history.undo_label(), Some("Merge Shape Components"));
        let merged = work(&s);
        assert_eq!(merged.subpaths.len(), 2, "the outline and its hole");
        for (x, y) in [(15, 15), (35, 35), (55, 50), (5, 5)] {
        assert_eq!(merged.subpaths.len(), 2, "the outline and its hole");
        for (x, y) in [(15, 15), (35, 35), (55, 50), (5, 5)] {
            assert!((covered(&merged, x, y) - covered(&before, x, y)).abs() < 0.01, "({x}, {y})");
        }

merge_shape_components_on_a_shape_layer_rerenders_it setup/action excerpt:

        let mut s = session();
        let id = s.execute("shape.create", json!({"kind": "rect", "rect": [10, 10, 50, 50], "fill": "#ff0000"})).unwrap()["layer"].as_u64().unwrap();
        s.execute("shape.create", json!({"kind": "rect", "rect": [25, 25, 20, 20], "addTo": id, "op": "subtract"})).unwrap();
        s.execute("path.mergeShapeComponents", json!({"name": "layer", "layer": id})).unwrap();
        let st = s.active().unwrap();
        let photocraft_doc::LayerContent::Shape(ShapeLayer { path, cache, .. }) = &st.doc.layer(LayerId(id)).unwrap().content else { panic!("shape") };
        assert_eq!(covered(path, 35, 35), 0.0);
        assert!(cache.is_some());
    }

Expected assertions from that fixture:

        assert_eq!(covered(path, 35, 35), 0.0);
        assert!(cache.is_some());
    }
        assert!(cache.is_some());
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-8074: Transform: remember selected interpolation (#8067)

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: tool options survive a restart; interpolation selection is reused for the next transform. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

tool_options_survive_a_restart setup/action excerpt:

        let (mut app, store) = app_with_store();
        let ctx = egui::Context::default();
        tick(&mut app, &ctx);
        app.ui.tool_options.move_auto_select = false;
        app.ui.tool_options.feather = 12.0;
        app.ui.tool_options.transform_interpolation = Some(photocraft_engine::prefs::Interpolation::Nearest);
        crate::tool_memory::persist(&mut app, &ctx);
        tick(&mut app, &ctx);
        // Only what differs from the defaults is stored.
        assert_eq!(stored(&store)["toolOptions"], json!({"move_auto_select": false, "feather": 12.0, "transform_interpolation": "nearestNeighbor"}));
        let saved = store.lock().unwrap().clone().unwrap();
        let (mut app2, _) = app_with_saved(Some(saved));

Expected assertions from that fixture:

        assert_eq!(stored(&store)["toolOptions"], json!({"move_auto_select": false, "feather": 12.0, "transform_interpolation": "nearestNeighbor"}));
        let saved = store.lock().unwrap().clone().unwrap();
        let (mut app2, _) = app_with_saved(Some(saved));
        tick(&mut app2, &ctx);
        assert!(!app2.ui.tool_options.move_auto_select);
        assert_eq!(app2.ui.tool_options.feather, 12.0);
        assert_eq!(app2.ui.tool_options.transform_interpolation, Some(photocraft_engine::prefs::Interpolation::Nearest));
        assert_eq!(app2.ui.tool_options.tolerance, crate::state::ToolOptions::default().tolerance);
        assert_eq!(app2.ui.tool_options.feather, 12.0);
        assert_eq!(app2.ui.tool_options.transform_interpolation, Some(photocraft_engine::prefs::Interpolation::Nearest));
        assert_eq!(app2.ui.tool_options.tolerance, crate::state::ToolOptions::default().tolerance);
        // A hand-edited file: unreadable fields keep their defaults, the good ones still load.

interpolation_selection_is_reused_for_the_next_transform setup/action excerpt:

        use egui::accesskit::Role;
        use egui_kittest::{Harness, kittest::Queryable};

        let app = app_with_square(64, photocraft_geom::Rect::new(8, 8, 24, 24));
        let mut h = Harness::builder().with_size(vec2(2400.0, 360.0)).build_ui_state(
            |ui, app: &mut PhotocraftApp| {
                if ui.ctx().fonts(|f| f.families().contains(&egui::FontFamily::Name("medium".into()))) {
                    ui.horizontal_centered(|ui| options_bar(app, ui));
                }
            },
            app,
        );

Expected assertions from that fixture:

        assert!(h.state().ui.transform.is_some(), "transform stays open while the bar renders");

        h.get_by_role(Role::ComboBox).click();
        h.run();
        assert!(h.state().ui.transform.is_some(), "opening interpolation dropdown keeps the transform open");
        h.key_press(egui::Key::ArrowDown);
        h.run();
        assert!(h.state().ui.transform.is_some(), "first interpolation step keeps the transform open");
        assert!(h.state().ui.transform.is_some(), "first interpolation step keeps the transform open");
        h.key_press(egui::Key::ArrowDown);
        h.run();
        assert!(h.state().ui.transform.is_some(), "second interpolation step keeps the transform open");

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-8896: Select and Mask: Photoshop's full-window workspace (#7069, #8148)

  • Fixture/setup: Selected document/layer-mask fixtures in dialogs/select_mask_ui/tests.rs plus two-document controls; record viewport/theme/scale.

  • Action: run each command separately:

    • cargo test -p photocraft-ui-egui --locked opens_like_photoshop -- --nocapture
    • cargo test -p photocraft-ui-egui --locked opening_needs_a_selection_or_a_layer_mask -- --nocapture
    • cargo test -p photocraft-ui-egui --locked tools_make_edits_and_undo_and_redo_them -- --nocapture
    • cargo test -p photocraft-ui-egui --locked ok_is_one_step_and_matches_the_command -- --nocapture
    • cargo test -p photocraft-ui-egui --locked cancel_changes_nothing -- --nocapture
    • cargo test -p photocraft-ui-egui --locked decontaminate_outputs_to_a_new_layer -- --nocapture
    • cargo test -p photocraft-ui-egui --locked hostile_control_params_are_errors -- --nocapture
    • cargo test -p photocraft-ui-egui --locked ok_after_a_document_switch_changes_neither_document -- --nocapture
    • cargo test -p photocraft-ui-egui --locked ok_after_a_layer_switch_leaves_the_new_layer_alone -- --nocapture
    • cargo test -p photocraft-ui-egui --locked renders_photoshops_layout -- --nocapture
  • Expected: Workspace needs valid selection/mask; tools/preview affect intended target. OK matches engine output in one history step; Cancel changes nothing; decontamination outputs a new layer. Hostile controls refuse; stale document/layer targets edit neither new target.

  • Separate coverage / prerequisites: Native refinement → undo/redo → Cancel and commit are separate NOT RUN subcases. Capture the full workspace with reachable controls, not only an overlay helper.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-9493: fix(vector): a mask feather continues the mask past the canvas edge

Concrete setup and expected assertion examples

feather_at_the_canvas_edge_continues_the_mask_past_it setup/action excerpt:

        let mut layer = Layer::raster("edge", PixelFormat::RGBA8);
        let mut mask = photocraft_doc::LayerMask::hide_all();
        mask.surface.fill_rect(Rect::new(40, 20, 120, 60), &[1.0]);
        mask.feather = 32.0;
        layer.mask = Some(mask);
        let canvas = Rect::new(0, 0, 160, 80);
        let tight = combined_mask(&layer, canvas).unwrap().read_region(canvas);
        let tall = combined_mask(&layer, Rect::new(0, -200, 160, 280)).unwrap().read_region(canvas);
        let worst = tight.iter().zip(&tall).map(|(a, b)| (a - b).abs()).fold(0.0_f32, f32::max);
        assert!(worst < 1e-4, "the canvas edge changed the feather by {worst}");

        let mut half = photocraft_doc::LayerMask::hide_all();

Expected assertions from that fixture:

        assert!(worst < 1e-4, "the canvas edge changed the feather by {worst}");

        let mut half = photocraft_doc::LayerMask::hide_all();
        half.surface.fill_rect(Rect::new(0, 0, 80, 80), &[1.0]);
            assert!(row.iter().zip(middle).all(|(a, b)| (a - b).abs() < 1e-5), "row {y} differs from row 40");
        }
        assert!(middle[0] > 0.99, "the left edge stays white: {}", middle[0]);
    }
        assert!(middle[0] > 0.99, "the left edge stays white: {}", middle[0]);
    }

soft_shape_matches_an_equal_vector_mask setup/action excerpt:

    // 40 px reaches past the canvas edge: both feather it the same way there (#9475).
    for (density, feather) in [(1.0, 10.0), (0.45, 0.0), (0.3, 6.5), (0.0, 0.0), (1.0, 0.0), (0.5, 3.0), (1.0, 40.0)] {
        for depth in [SampleType::U8, SampleType::U16] {
            let (s, f) = (shape_doc(depth, density, feather, None), fill_doc(depth, density, feather));
            for y in [0, 45, 50, 100, 151, 199] {
                // An 8-bit shape keeps its rendering in 8-bit pixels (the fill layer's mask is
                // 16-bit), which can round its alpha one level apart, as at any anti-aliased edge.
                let d8 = max_diff(&row(&s, y), &row(&f, y));
                assert!(d8 <= i32::from(depth == SampleType::U8), "{depth:?} density {density} feather {feather}, row {y}: {d8} levels");
                // The compositor keeps masks at 16 bits (an 8-bit shape's pixels are 8-bit).
                if depth == SampleType::U16 {
                    let d16 = max_diff(&row16(&s, y), &row16(&f, y));

Expected assertions from that fixture:

                assert!(d8 <= i32::from(depth == SampleType::U8), "{depth:?} density {density} feather {feather}, row {y}: {d8} levels");
                // The compositor keeps masks at 16 bits (an 8-bit shape's pixels are 8-bit).
                if depth == SampleType::U16 {
                    let d16 = max_diff(&row16(&s, y), &row16(&f, y));
                    assert!(d16 <= 2, "{depth:?} density {density} feather {feather}, row {y}: {d16} 16-bit levels");
                }
            }
        }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3139: Draw right-to-left layer names in visual word order

  • Fixture/setup: Arabic, Hebrew, mixed Latin/digits and Persian joiner samples in bidi_label_tests.rs; record rendered font files.

  • Action: run each command separately:

    • cargo test -p photocraft-ui-egui --locked right_to_left_words_swap_places_but_keep_their_letters -- --nocapture
    • cargo test -p photocraft-ui-egui --locked numbers_and_latin_keep_reading_left_to_right -- --nocapture
    • cargo test -p photocraft-ui-egui --locked persian_joiners_stay_inside_their_word -- --nocapture
    • cargo test -p photocraft-ui-egui --locked arabic_draws_first_word_rightmost_with_letters_joined -- --nocapture
    • cargo test -p photocraft-ui-egui --locked hebrew_draws_first_word_rightmost -- --nocapture
    • cargo test -p photocraft-ui-egui --locked a_long_right_to_left_layer_name_keeps_its_beginning -- --nocapture
  • Expected: First RTL word is rightmost; Arabic joining and Persian joiners remain intact; Latin/digits retain direction; truncation keeps the beginning. Retain LTR-unchanged negative coverage.

  • Separate coverage / prerequisites: Inspect Layers at 1×/2×. Missing fonts block rendered coverage; an early return is not a pass.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-4783: Fix/4758 type extreme finite geometry 20261013s1215

Concrete setup and expected assertion examples

tysh_fallback_cannot_reintroduce_unusable_finite_or_nonfinite_transform setup/action excerpt:

        let mut tysh = vec![0u8; 52];
        tysh[..2].copy_from_slice(&1u16.to_be_bytes());
        for (i, value) in [1.0f64, 0.0, 0.0, 1.0, 30.0, 40.0].iter().enumerate() {
            tysh[2 + 8 * i..10 + 8 * i].copy_from_slice(&value.to_be_bytes());
        }
        assert!(parse_tysh(&tysh).is_some(), "normal matrices still import");
        for value in [1e62f64, -1e62, f64::INFINITY, f64::NAN] {
            tysh[2..10].copy_from_slice(&value.to_be_bytes());
            assert!(parse_tysh(&tysh).is_none(), "invalid matrix {value} cannot reach UI");
        }
    }

Expected assertions from that fixture:

        assert!(parse_tysh(&tysh).is_some(), "normal matrices still import");
        for value in [1e62f64, -1e62, f64::INFINITY, f64::NAN] {
            tysh[2..10].copy_from_slice(&value.to_be_bytes());
            assert!(parse_tysh(&tysh).is_none(), "invalid matrix {value} cannot reach UI");
            assert!(parse_tysh(&tysh).is_none(), "invalid matrix {value} cannot reach UI");
        }
    }

hostile_type_sizes_without_pixels_import_without_a_huge_render setup/action excerpt:

    // The import renders type layers that come without pixels; the model is untrusted, so an
    // absurd size or scale must not make opening the file allocate gigabytes (or fail).
    let scaled = |s: f64| Affine { m: [s, 0.0, 0.0, s, 10.0, 10.0] };
    for (size, transform, big) in
        [(12.0, scaled(1100.0), true), (1.0e30, scaled(1.0), false), (12.0, scaled(1.0e12), false), (f64::MAX, scaled(f64::MAX), false)]
    {
        let runs = vec![Run { len: 1, size, leading: None }];
        let data = tysh("M", &runs, None, transform, [10.0, 10.0, 20.0, 20.0]);
        // Extremely large TySh transforms are now rejected before any layout or rendering.
        // Keep testing the import-time render cap for matrices within the safety gate.
        if !photocraft_text::psd::usable_tysh_transform(&transform.m) {
            assert!(photocraft_text::psd::text_layer_from_tysh(&data, 72.0).is_none(), "unsafe transform must be rejected before layout");

Expected assertions from that fixture:

            assert!(photocraft_text::psd::text_layer_from_tysh(&data, 72.0).is_none(), "unsafe transform must be rejected before layout");
            continue;
        }
        let mut t = photocraft_text::psd::text_layer_from_tysh(&data, 72.0).expect("valid TySh transform");
            assert!(px > 64 * 1024 * 1024 && px <= 256 * 1024 * 1024, "{ink:?}");
        }
        t.cache = None;
        t.psd_raw = Some(std::sync::Arc::new(data));
        assert!(cache.content_bounds().is_empty(), "size {size}: drew {:?}", cache.content_bounds());
    }
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-8347: Type: Roman Hanging Punctuation (trailing marks hang, don't steer the alignment)

  • Fixture/setup: Trailing punctuation, centered/justified lines and forced breaks in text layout fixtures.

  • Action: run each command separately:

    • cargo test -p photocraft-text --locked hanging_marks_do_not_steer_the_alignment -- --nocapture
    • cargo test -p photocraft-text --locked hanging_centers_a_kerned_line_exactly -- --nocapture
    • cargo test -p photocraft-text --locked hanging_only_the_mark_list_trims -- --nocapture
    • cargo test -p photocraft-text --locked hanging_keeps_marks_after_a_forced_break_on_their_row -- --nocapture
    • cargo test -p photocraft-text --locked hanging_marks_stay_off_a_justified_paragraphs_last_line -- --nocapture
    • cargo test -p photocraft-text --locked hanging_leaves_plain_paragraphs_untouched -- --nocapture
    • cargo test -p photocraft-ui-egui --locked character_panel_hanging_checkbox_toggles_the_paragraph_style -- --nocapture
    • cargo test -p photocraft-engine --locked set_style_paragraph_hanging_round_trips -- --nocapture
  • Expected: Listed marks hang without steering alignment; kerning-aware centering stays exact; unlisted marks/plain paragraphs are unchanged; forced breaks and justified last lines retain their asserted rules. Checkbox edits actual paragraph style.

  • Separate coverage / prerequisites: A stored boolean alone is not layout evidence.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-9457: raw: faster tone stage (Shadows, Highlights, Clarity, Texture, Dehaze), same bytes

Concrete setup and expected assertion examples

gauss_gives_the_bits_of_the_tap_loop setup/action excerpt:

        // Sizes below, around and above the kernel (edges only, a few interior samples, mostly interior).
        for (w, h) in [(1, 1), (5, 3), (13, 40), (64, 7), (65, 70), (131, 97), (300, 41)] {
            let v = noise(w * h, (w * 31 + h) as u64);
            for sigma in [0.04, 0.42, 1.0, 2.1, 4.2, 8.4, 13.44] {
                assert!(same(&gauss(&v, w, h, sigma), &gauss_ref(&v, w, h, sigma)), "{w}x{h} sigma {sigma}");
            }
        }
    }

Expected assertions from that fixture:

                assert!(same(&gauss(&v, w, h, sigma), &gauss_ref(&v, w, h, sigma)), "{w}x{h} sigma {sigma}");
            }
        }
    }

box_mean_gives_the_bits_of_the_column_loop setup/action excerpt:

        for (w, h) in [(1, 1), (5, 3), (13, 40), (64, 7), (65, 70), (131, 97), (300, 41)] {
            let v = noise(w * h, (w * 17 + h) as u64);
            for r in [1, 2, 8, 32, 128] {
                assert!(same(&box_mean(&v, w, h, r), &box_mean_ref(&v, w, h, r)), "{w}x{h} r {r}");
            }
        }
    }

Expected assertions from that fixture:

                assert!(same(&box_mean(&v, w, h, r), &box_mean_ref(&v, w, h, r)), "{w}x{h} r {r}");
            }
        }
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9478: fix(text): vertical type sets roman letters upright, one per em, as Photoshop does (#9470)

  • Fixture/setup: Roman, ligature and half-width CJK fixtures in text/vertical_tests.rs; record fonts.

  • Action: run each command separately:

    • cargo test -p photocraft-text --locked latin_stands_upright_one_per_em_by_default -- --nocapture
    • cargo test -p photocraft-text --locked upright_roman_letters_stay_apart_without_ligatures -- --nocapture
    • cargo test -p photocraft-text --locked half_width_cjk_stays_rotated_beside_upright_roman -- --nocapture
    • cargo test -p photocraft-text --locked baseline_direction_round_trips -- --nocapture
  • Expected: Roman defaults upright at one-em advances without ligature collapse; half-width CJK retains specified rotation. Run BaselineDirection overrides sheet defaults and survives PSD persistence.

  • Separate coverage / prerequisites: Capture upright/rotated text at 1×/2× and save/reopen PSD separately. Font-unavailable geometry is BLOCKED; the flag round-trip alone is insufficient.

  • Status: NOT RUN

  • Actual:

  • Evidence:

  • Tester/date:

  • Tested SHA:

PR-9523: Fix Type on a Path wrapping and caret positioning across closed-path seams

Concrete setup and expected assertion examples

closed_rectangle_left_edge_continues_past_the_top_left_seam setup/action excerpt:

    let p = Subpath::polygon(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]);
    let start = arc_length_at(&p, false, 0.0, 50.0).unwrap();
    assert!((start - 350.0).abs() < 1e-6);
    let f = PathFrame::new(&p, start as f32, false).unwrap();
    let m = f.frame(75.0).expect("glyph after the seam remains on the closed path");
    let q = centre(m, 75.0);
    assert!((q.0 - 25.0).abs() < 1e-6 && q.1.abs() < 1e-6, "{q:?}");
    assert_eq!(&m[..4], &[1.0, 0.0, -0.0, 1.0], "text now travels right on the top side");
    assert_eq!(f.apply(75.0, 0.0), q, "caret and glyph use the same wrapped parameter");
}

Expected assertions from that fixture:

    assert!((start - 350.0).abs() < 1e-6);
    let f = PathFrame::new(&p, start as f32, false).unwrap();
    let m = f.frame(75.0).expect("glyph after the seam remains on the closed path");
    let q = centre(m, 75.0);
    assert!((q.0 - 25.0).abs() < 1e-6 && q.1.abs() < 1e-6, "{q:?}");
    assert_eq!(&m[..4], &[1.0, 0.0, -0.0, 1.0], "text now travels right on the top side");
    assert_eq!(f.apply(75.0, 0.0), q, "caret and glyph use the same wrapped parameter");
}
    assert_eq!(&m[..4], &[1.0, 0.0, -0.0, 1.0], "text now travels right on the top side");
    assert_eq!(f.apply(75.0, 0.0), q, "caret and glyph use the same wrapped parameter");
}

closed_ellipse_upper_left_and_apex_cross_the_seam_in_both_directions setup/action excerpt:

    for reverse in [false, true] {
        let mut p = ellipse();
        if reverse {
            p.knots.reverse();
            for k in &mut p.knots {
                std::mem::swap(&mut k.in_ctrl, &mut k.out_ctrl);
            }
        }
        for flip in [false, true] {
            let whole = PathFrame::new(&p, 0.0, flip).unwrap();
            let length = whole.length();
            for remaining in [0.001, 0.2, 30.0] {

Expected assertions from that fixture:

                assert!((actual.0 - point.0).hypot(actual.1 - point.1) < 1e-6);
                assert!((m[0] - tangent.0).abs() < 1e-6 && (m[1] - tangent.1).abs() < 1e-6);
                assert!((m[0] * m[3] - m[1] * m[2] - 1.0).abs() < 1e-6, "no reflection or scale jump");
                let before = f.apply(remaining - 0.001, 0.0);
                assert!((m[0] - tangent.0).abs() < 1e-6 && (m[1] - tangent.1).abs() < 1e-6);
                assert!((m[0] * m[3] - m[1] * m[2] - 1.0).abs() < 1e-6, "no reflection or scale jump");
                let before = f.apply(remaining - 0.001, 0.0);
                let after = f.apply(remaining + 0.001, 0.0);
                assert!((m[0] * m[3] - m[1] * m[2] - 1.0).abs() < 1e-6, "no reflection or scale jump");
                let before = f.apply(remaining - 0.001, 0.0);
                let after = f.apply(remaining + 0.001, 0.0);
                assert!((before.0 - after.0).hypot(before.1 - after.1) < 0.01, "seam is continuous");

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9594: fix(engine): copied type keeps its size across resolutions after an edit

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: copied type keeps its pixel size across resolutions after an edit. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

copied_type_keeps_its_pixel_size_across_resolutions_after_an_edit setup/action excerpt:

    for (from, to) in [(72, 300), (300, 72)] {
        let mut s = Session::new();
        s.execute("file.new", json!({"width": 400, "height": 200, "resolution": from})).unwrap();
        let t = LayerId(s.execute("type.create", json!({"text": "H", "size": 24, "x": 128, "y": 100})).unwrap()["layer"].as_u64().unwrap());
        let original = bounds(&s, 0, t);
        s.execute("file.new", json!({"width": 400, "height": 200, "resolution": to})).unwrap();
        s.set_active(0);
        s.execute("layer.select", json!({"layer": t.0})).unwrap();
        let id = copied(&s.execute("layer.copyToDocument", json!({"document": 1})).unwrap());
        assert_eq!(bounds(&s, 1, id).size(), original.size(), "{from}→{to}: the copy");
        s.execute("type.edit", json!({"layer": id.0, "text": "M"})).unwrap();
        s.execute("type.edit", json!({"layer": id.0, "text": "H"})).unwrap();

Expected assertions from that fixture:

        assert_eq!(bounds(&s, 1, id).size(), original.size(), "{from}→{to}: the copy");
        s.execute("type.edit", json!({"layer": id.0, "text": "M"})).unwrap();
        s.execute("type.edit", json!({"layer": id.0, "text": "H"})).unwrap();
        let after = bounds(&s, 1, id);
        assert_eq!(after.size(), original.size(), "{from}→{to}: after an edit, {after:?} vs {original:?}");
    }
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9596: fix(io): drop a kept Txt2 once no type layers are left

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: a kept txt2 goes with the last type layer. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

a_kept_txt2_goes_with_the_last_type_layer setup/action excerpt:

        let mut doc = Document::new("t", photocraft_geom::Size::new(8, 6), ColorMode::Rgb, SampleType::U8);
        doc.layers.push(Layer::new("Layer 1", LayerContent::Raster(Surface::new(doc.pixel_format()))));
        doc.metadata.psd_global_blocks = vec![(*b"8BIM", *b"Txt2", std::sync::Arc::new(b"<< /0 << /1 (stale) >> >>".to_vec()))];
        let file = document_to_psd(&doc);
        assert!(!file.global_blocks.iter().any(|b| b.key == *b"Txt2"), "no type layers: no Txt2");

        doc.layers.push(Layer::new("T", LayerContent::Text(photocraft_doc::TextLayer::default())));
        let file = document_to_psd(&doc);
        assert!(file.global_blocks.iter().any(|b| b.key == *b"Txt2"), "a type layer keeps its Txt2");
    }

Expected assertions from that fixture:

        assert!(!file.global_blocks.iter().any(|b| b.key == *b"Txt2"), "no type layers: no Txt2");

        doc.layers.push(Layer::new("T", LayerContent::Text(photocraft_doc::TextLayer::default())));
        let file = document_to_psd(&doc);
        assert!(file.global_blocks.iter().any(|b| b.key == *b"Txt2"), "a type layer keeps its Txt2");
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3345: Add grouped gradient presets and selectable style icons

Concrete setup and expected assertion examples

native_gradient_icons_persist_reset_and_preserve_legacy_defaults setup/action excerpt:

    let mut s = session();
    let history = s.active().unwrap().history.past_len();
    assert!(!s.prefs().interface.native_gradient_icons);
    s.execute("prefs.set", json!({"path": "interface.nativeGradientIcons", "value": true})).unwrap();
    let mut restarted = Session::new();
    restarted.load_prefs_json(&s.prefs_to_json()).unwrap();
    assert!(restarted.prefs().interface.native_gradient_icons);
    assert_eq!(s.active().unwrap().history.past_len(), history);
    s.execute("prefs.reset", json!({"path": "interface.nativeGradientIcons"})).unwrap();
    assert!(!s.prefs().interface.native_gradient_icons);
    let before = s.prefs().to_json();
    for invalid in [json!("true"), json!(1), Value::Null] {

Expected assertions from that fixture:

    assert!(!s.prefs().interface.native_gradient_icons);
    s.execute("prefs.set", json!({"path": "interface.nativeGradientIcons", "value": true})).unwrap();
    let mut restarted = Session::new();
    restarted.load_prefs_json(&s.prefs_to_json()).unwrap();
    assert!(restarted.prefs().interface.native_gradient_icons);
    assert_eq!(s.active().unwrap().history.past_len(), history);
    s.execute("prefs.reset", json!({"path": "interface.nativeGradientIcons"})).unwrap();
    assert!(!s.prefs().interface.native_gradient_icons);
    assert_eq!(s.active().unwrap().history.past_len(), history);
    s.execute("prefs.reset", json!({"path": "interface.nativeGradientIcons"})).unwrap();
    assert!(!s.prefs().interface.native_gradient_icons);
    let before = s.prefs().to_json();

opaque_ramp_has_no_checkerboard_fringe_at_fractional_pixel_edges setup/action excerpt:

        use egui_kittest::Harness;
        for scale in [1.0, 2.0] {
            let edge = 20.0 + 0.3 / scale;
            let rect = Rect::from_min_size(pos2(edge, edge), vec2(96.0, 20.0));
            let Ok(mut h) = std::panic::catch_unwind(|| {
                Harness::builder().with_size(vec2(160.0, 80.0)).with_pixels_per_point(scale).wgpu().build_ui(move |ui| {
                    paint_ramp(ui.painter(), rect, |_| [0.1, 0.25, 0.2, 1.0]);
                })
            }) else {
                eprintln!("skipping: no GPU adapter");
                return;
            };

Expected assertions from that fixture:

                    assert!(pixel[channel].abs_diff(background[channel]) <= 2, "scale={scale} at ({right}, {y}): {pixel:?} vs {background:?}");
                }
            }
        }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3461: Replay recorded layer visibility on the intended named layer

Concrete setup and expected assertion examples

recorded_eye_visibility_replays_on_another_document_and_duplicate_names setup/action excerpt:

        let mut s = Session::new();
        s.execute("file.new", json!({"width": 8, "height": 8})).unwrap();
        s.execute("layer.new.layer", json!({"name": "Same"})).unwrap();
        let hidden = s.execute("layer.new.layer", json!({"name": "Same"})).unwrap()["layer"].as_u64().unwrap();
        s.execute("actions.record", json!({"name": "Hide"})).unwrap();
        s.execute("layer.setProps", json!({"layer": hidden, "visible": false, "coalesce": "eye-sweep"})).unwrap();
        s.execute("actions.stop", json!({})).unwrap();
        s.execute("file.new", json!({"width": 8, "height": 8})).unwrap();
        let keep = s.execute("layer.new.layer", json!({"name": "Same"})).unwrap()["layer"].as_u64().unwrap();
        let target = s.execute("layer.new.layer", json!({"name": "Same"})).unwrap()["layer"].as_u64().unwrap();
        s.execute("select.deselectLayers", json!({})).unwrap();
        let result = s.execute("actions.play", json!({"action": "Hide"})).unwrap();

Expected assertions from that fixture:

        assert!(result.get("failed").is_none(), "{result}");
        assert!(s.active().unwrap().doc.layer(photocraft_doc::LayerId(keep)).unwrap().visible);
        assert!(!s.active().unwrap().doc.layer(photocraft_doc::LayerId(target)).unwrap().visible);
        s.execute("edit.undo", json!({})).unwrap();
        assert!(s.active().unwrap().doc.layer(photocraft_doc::LayerId(keep)).unwrap().visible);
        assert!(!s.active().unwrap().doc.layer(photocraft_doc::LayerId(target)).unwrap().visible);
        s.execute("edit.undo", json!({})).unwrap();
        assert!(s.active().unwrap().doc.layer(photocraft_doc::LayerId(target)).unwrap().visible);
        assert!(!s.active().unwrap().doc.layer(photocraft_doc::LayerId(target)).unwrap().visible);
        s.execute("edit.undo", json!({})).unwrap();
        assert!(s.active().unwrap().doc.layer(photocraft_doc::LayerId(target)).unwrap().visible);
    }

shell_playback_resolves_recorded_eye_target_on_new_document setup/action excerpt:

        let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        app.run("file.new", json!({"width": 8, "height": 8})).unwrap();
        app.run("layer.new.layer", json!({"name": "Target"})).unwrap();
        app.run("actions.record", json!({"name": "Hide"})).unwrap();
        let id = app.session.active().unwrap().active_layer.unwrap();
        app.run("layer.setProps", json!({"layer": id.0, "visible": false})).unwrap();
        app.run("actions.stop", json!({})).unwrap();
        app.run("file.new", json!({"width": 8, "height": 8})).unwrap();
        app.run("layer.new.layer", json!({"name": "Target"})).unwrap();
        let id = app.session.active().unwrap().active_layer.unwrap();
        let result = play(&mut app, &json!({"action": "Hide"})).unwrap();
        assert!(result.get("failed").is_none(), "{result}");

Expected assertions from that fixture:

        assert!(result.get("failed").is_none(), "{result}");
        assert!(!app.session.active().unwrap().doc.layer(id).unwrap().visible);
    }
        assert!(!app.session.active().unwrap().doc.layer(id).unwrap().visible);
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-3520: fix(ui): restore square color chips in Studio Dark theme

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: studio dark keeps square colour chips. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

studio_dark_keeps_square_colour_chips setup/action excerpt:

        assert!(!Tokens::for_kind(ThemeKind::Studio).round_chips);
        // Only the dark variant changes; the light palette retains its layout.
        assert!(Tokens::for_kind(ThemeKind::StudioLight).round_chips);
        assert!(!Tokens::for_kind(ThemeKind::Pro).round_chips);
    }

Expected assertions from that fixture:

        assert!(!Tokens::for_kind(ThemeKind::Studio).round_chips);
        // Only the dark variant changes; the light palette retains its layout.
        assert!(Tokens::for_kind(ThemeKind::StudioLight).round_chips);
        assert!(!Tokens::for_kind(ThemeKind::Pro).round_chips);
        assert!(Tokens::for_kind(ThemeKind::StudioLight).round_chips);
        assert!(!Tokens::for_kind(ThemeKind::Pro).round_chips);
    }
        assert!(!Tokens::for_kind(ThemeKind::Pro).round_chips);
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4035: fix(cmyk): Flatten Image and merges keep the inks of a tagged CMYK document (#3778)

Concrete setup and expected assertion examples

flatten_keeps_the_inks_of_a_single_layer_exactly setup/action excerpt:

    exact("Flatten Image", |s| {
        s.execute("layer.flattenImage", json!({})).unwrap();
    });
}

Expected assertions from that fixture:


stamp_visible_keeps_the_inks_of_a_single_layer_exactly setup/action excerpt:

    exact("Stamp Visible", |s| {
        s.execute("layer.stampVisible", json!({})).unwrap();
    });
}

Expected assertions from that fixture:


The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4042: feat(canvas): rotate view with Shift + middle-mouse drag

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: middle drag rotates about the pointer without panning; middle drag ignores invalid or stationary input. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

middle_drag_rotates_about_the_pointer_without_panning setup/action excerpt:

        let rect = Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0));
        let pointer = pos2(600.0, 180.0);
        for flip in [false, true] {
            let mut view = View { zoom: 2.0, center: [130.0, 80.0], ..Default::default() };
            let xf = ViewXform { rect, zoom: 2.0, center: view.center, flip, rotation: 0.0, aspect: 1.25 };
            let original = xf.to_doc(pointer);
            assert!(middle_drag(&mut view, &xf, pointer, 90.0));
            assert!((view.rotation - 31.5).abs() < 0.001);
            let next = ViewXform { center: view.center, rotation: view.rotation, ..xf }.to_doc(pointer);
            assert!((next[0] - original[0]).abs() < 0.001 && (next[1] - original[1]).abs() < 0.001, "{original:?} {next:?}");
        }
    }

Expected assertions from that fixture:

            assert!(middle_drag(&mut view, &xf, pointer, 90.0));
            assert!((view.rotation - 31.5).abs() < 0.001);
            let next = ViewXform { center: view.center, rotation: view.rotation, ..xf }.to_doc(pointer);
            assert!((next[0] - original[0]).abs() < 0.001 && (next[1] - original[1]).abs() < 0.001, "{original:?} {next:?}");
            assert!((view.rotation - 31.5).abs() < 0.001);
            let next = ViewXform { center: view.center, rotation: view.rotation, ..xf }.to_doc(pointer);
            assert!((next[0] - original[0]).abs() < 0.001 && (next[1] - original[1]).abs() < 0.001, "{original:?} {next:?}");
        }
            assert!((next[0] - original[0]).abs() < 0.001 && (next[1] - original[1]).abs() < 0.001, "{original:?} {next:?}");
        }
    }

middle_drag_ignores_invalid_or_stationary_input setup/action excerpt:

        let mut view = View::default();
        let xf = ViewXform {
            rect: Rect::from_min_size(pos2(0.0, 0.0), vec2(600.0, 400.0)),
            zoom: 1.0,
            center: view.center,
            flip: false,
            rotation: 0.0,
            aspect: 1.0,
        };
        assert!(!middle_drag(&mut view, &xf, pos2(200.0, 120.0), 0.0));
        assert!(!middle_drag(&mut view, &xf, pos2(200.0, 120.0), f32::NAN));
        assert!(!middle_drag(&mut view, &xf, pos2(f32::INFINITY, 120.0), 20.0));

Expected assertions from that fixture:

        assert!(!middle_drag(&mut view, &xf, pos2(200.0, 120.0), 0.0));
        assert!(!middle_drag(&mut view, &xf, pos2(200.0, 120.0), f32::NAN));
        assert!(!middle_drag(&mut view, &xf, pos2(f32::INFINITY, 120.0), 20.0));
        assert_eq!(view.rotation, 0.0);
        assert!(!middle_drag(&mut view, &xf, pos2(200.0, 120.0), f32::NAN));
        assert!(!middle_drag(&mut view, &xf, pos2(f32::INFINITY, 120.0), 20.0));
        assert_eq!(view.rotation, 0.0);
    }
        assert!(!middle_drag(&mut view, &xf, pos2(f32::INFINITY, 120.0), 20.0));
        assert_eq!(view.rotation, 0.0);
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4547: fix(icons): PhotoCraft icon corner radius matches other Craft apps (#4478)

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: windows and linux icons keep the round corners. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

windows_and_linux_icons_keep_the_round_corners setup/action excerpt:

        // rx=112 of 512 leaves (10, 10) of a 256 px render transparent; the old crop filled it (#4478).
        let png = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.photocraft.png");
        let icon = eframe::icon_data::from_png_bytes(png).unwrap();
        assert_eq!(icon.width, 256);
        assert_eq!(icon.rgba[(10 * 256 + 10) * 4 + 3], 0, "corner is transparent");
        assert_eq!(icon.rgba[(128 * 256 + 2) * 4 + 3], 255, "the tile is full bleed");
    }

Expected assertions from that fixture:

        assert_eq!(icon.width, 256);
        assert_eq!(icon.rgba[(10 * 256 + 10) * 4 + 3], 0, "corner is transparent");
        assert_eq!(icon.rgba[(128 * 256 + 2) * 4 + 3], 255, "the tile is full bleed");
    }
        assert_eq!(icon.rgba[(10 * 256 + 10) * 4 + 3], 0, "corner is transparent");
        assert_eq!(icon.rgba[(128 * 256 + 2) * 4 + 3], 255, "the tile is full bleed");
    }
        assert_eq!(icon.rgba[(128 * 256 + 2) * 4 + 3], 255, "the tile is full bleed");
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4883: feat(ui): show selection and active-layer bounds in the status bar

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: status bounds follow the actual selection and active layer; status dimensions follow ruler units and document resolution. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

status_bounds_follow_the_actual_selection_and_active_layer setup/action excerpt:

        let d = doc();
        let id = d.layers[0].id;
        assert_eq!(status_bounds_text(&d, Some(id)), "Layer: X 0  Y 0  W 2400  H 1500");
        assert_eq!(status_bounds_text(&d, None), "No selection or layer");

        let mut s = photocraft_engine::Session::new();
        s.execute("file.new", json!({"width": 120, "height": 80})).unwrap();
        s.execute("select.all", json!({})).unwrap();
        let doc = &s.active().unwrap().doc;
        assert_eq!(status_bounds_text(doc, None), "Selection: X 0  Y 0  W 120  H 80");
    }

Expected assertions from that fixture:

        assert_eq!(status_bounds_text(&d, Some(id)), "Layer: X 0  Y 0  W 2400  H 1500");
        assert_eq!(status_bounds_text(&d, None), "No selection or layer");

        let mut s = photocraft_engine::Session::new();
        assert_eq!(status_bounds_text(&d, None), "No selection or layer");

        let mut s = photocraft_engine::Session::new();
        s.execute("file.new", json!({"width": 120, "height": 80})).unwrap();
        assert_eq!(status_bounds_text(doc, None), "Selection: X 0  Y 0  W 120  H 80");
    }

status_dimensions_follow_ruler_units_and_document_resolution setup/action excerpt:

        use photocraft_engine::prefs::{PointSize, Unit, UnitsAndRulers};
        let mut d = doc();
        d.resolution_dpi = 300.0;
        for (unit, expected) in [
            (Unit::Pixels, "2400 px x 1500 px (300 ppi)"),
            (Unit::Inches, "8 in x 5 in (300 ppi)"),
            (Unit::Centimeters, "20.32 cm x 12.7 cm (300 ppi)"),
            (Unit::Millimeters, "203.2 mm x 127 mm (300 ppi)"),
            (Unit::Points, "576 pt x 360 pt (300 ppi)"),
            (Unit::Picas, "48 pica x 30 pica (300 ppi)"),
            (Unit::Percent, "100 % x 100 % (300 ppi)"),
        ] {

Expected assertions from that fixture:

            assert_eq!(status_info_text_with_units(&d, "dimensions", "", "", &units), expected);
            for (key, _) in STATUS_INFO.iter().filter(|(key, _)| *key != "dimensions" && *key != "coordinates") {
                assert_eq!(status_info_text_with_units(&d, key, "Brush Tool", "sRGB", &units), status_info_text(&d, key, "Brush Tool", "sRGB"));
            }
                assert_eq!(status_info_text_with_units(&d, key, "Brush Tool", "sRGB", &units), status_info_text(&d, key, "Brush Tool", "sRGB"));
            }
        }
        d.resolution_dpi = 240.0;
        assert_eq!(status_info_text_with_units(&d, "dimensions", "", "", &units), "722.7 pt x 451.7 pt (240 ppi)");
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4890: Add scrollbar visibility preferences (#3262)

Concrete setup and expected assertion examples

scrollbar_visibility_round_trips_and_old_preferences_keep_the_theme_default setup/action excerpt:

    let old = serde_json::from_value::<Preferences>(json!({"interface": {"uiFontSize": "small"}})).unwrap();
    assert_eq!(old.interface.scrollbar_visibility, ScrollbarVisibility::Default);
    assert_eq!(choices("interface.scrollbarVisibility"), Some(ScrollbarVisibility::NAMES));
    let mut s = Session::new();
    for mode in ScrollbarVisibility::NAMES {
        s.execute("prefs.set", json!({"values": {"interface.scrollbarVisibility": mode}})).unwrap();
        let saved = s.prefs().to_json();
        let loaded: Preferences = serde_json::from_value(saved.clone()).unwrap();
        assert_eq!(loaded.interface.scrollbar_visibility, s.prefs().interface.scrollbar_visibility);
        assert_eq!(saved["interface"]["scrollbarVisibility"], json!(mode));
    }
    let before = s.prefs().to_json();

Expected assertions from that fixture:

    assert_eq!(old.interface.scrollbar_visibility, ScrollbarVisibility::Default);
    assert_eq!(choices("interface.scrollbarVisibility"), Some(ScrollbarVisibility::NAMES));
    let mut s = Session::new();
    for mode in ScrollbarVisibility::NAMES {
    assert_eq!(choices("interface.scrollbarVisibility"), Some(ScrollbarVisibility::NAMES));
    let mut s = Session::new();
    for mode in ScrollbarVisibility::NAMES {
        s.execute("prefs.set", json!({"values": {"interface.scrollbarVisibility": mode}})).unwrap();
        assert_eq!(loaded.interface.scrollbar_visibility, s.prefs().interface.scrollbar_visibility);
        assert_eq!(saved["interface"]["scrollbarVisibility"], json!(mode));
    }
    let before = s.prefs().to_json();

scrollbar_visibility_applies_in_every_theme_and_survives_theme_changes setup/action excerpt:

        use photocraft_engine::prefs::ScrollbarVisibility as Mode;
        let ctx = egui::Context::default();
        for kind in ThemeKind::ALL {
            apply(&ctx, kind);
            let original = ctx.global_style().spacing.scroll;
            sync_scrollbars(&ctx, Mode::Default);
            assert_eq!(ctx.global_style().spacing.scroll, original);
            sync_scrollbars(&ctx, Mode::Always);
            assert!(!ctx.global_style().spacing.scroll.floating);
            assert!(ctx.global_style().spacing.scroll.bar_width > 0.0);
            sync_scrollbars(&ctx, Mode::AutoHide);
            assert!(ctx.global_style().spacing.scroll.floating);

Expected assertions from that fixture:

            assert_eq!(ctx.global_style().spacing.scroll, original);
            sync_scrollbars(&ctx, Mode::Always);
            assert!(!ctx.global_style().spacing.scroll.floating);
            assert!(ctx.global_style().spacing.scroll.bar_width > 0.0);
            assert!(!ctx.global_style().spacing.scroll.floating);
            assert!(ctx.global_style().spacing.scroll.bar_width > 0.0);
            sync_scrollbars(&ctx, Mode::AutoHide);
            assert!(ctx.global_style().spacing.scroll.floating);
            assert!(ctx.global_style().spacing.scroll.bar_width > 0.0);
            sync_scrollbars(&ctx, Mode::AutoHide);
            assert!(ctx.global_style().spacing.scroll.floating);
            assert_eq!(ctx.global_style().spacing.scroll.dormant_handle_opacity, 0.0);

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-4938: Layer Style: one dialog size on every page, and it fits short screens

Concrete setup and expected assertion examples

dragging_the_title_bar_moves_the_dialog setup/action excerpt:

        use egui_kittest::{Harness, kittest::Queryable};

        let app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        let mut harness = Harness::builder().with_size(egui::vec2(1400.0, 1100.0)).build_ui_state(|ui, app| show(app, ui.ctx()), app);
        PhotocraftApp::setup_context(&harness.ctx, crate::theme::ThemeKind::ALL[0]);
        harness.state_mut().ui.open_dialog(DialogKind::LayerStyle, serde_json::Map::new());
        harness.run_steps(3);
        let before = harness.get_by_label("Layer Style").rect();

        // Grab the title text itself: it must move the dialog, not select the text.
        let from = before.center();
        harness.hover_at(from);

Expected assertions from that fixture:

        assert!((moved - egui::vec2(-120.0, 80.0)).length() < 1.0, "dialog moved by {moved:?}");
        assert_eq!(harness.state().ui.dialogs.len(), 1, "dragging must not close the dialog");
    }
        assert_eq!(harness.state().ui.dialogs.len(), 1, "dragging must not close the dialog");
    }

layer_style_keeps_one_size_on_every_page setup/action excerpt:

        let mut h = layer_style_on_bevel(egui::vec2(1400.0, 900.0));
        let size = frame(&h).size();
        let dialog = h.state().ui.dialogs[0].id;
        for page in ["blendingOptions", "styles"] {
            h.state_mut().ui.dialog_mut(dialog).expect("still open").fields.insert("selected".into(), json!(page));
            h.run_steps(3);
            assert_eq!(frame(&h).size(), size, "{page}");
        }
        let grip = egui::Id::new(("dialog", dialog)).with("resize").with("__resize_corner");
        assert!(h.ctx.read_response(grip).is_none(), "no resize grip");
    }

Expected assertions from that fixture:

            assert_eq!(frame(&h).size(), size, "{page}");
        }
        let grip = egui::Id::new(("dialog", dialog)).with("resize").with("__resize_corner");
        assert!(h.ctx.read_response(grip).is_none(), "no resize grip");
        assert!(h.ctx.read_response(grip).is_none(), "no resize grip");
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5270: Layer Style: Gradient Overlay takes any number of colour stops (#5177)

Concrete setup and expected assertion examples

gradient_overlay_takes_explicit_stops setup/action excerpt:

        let mut s = session();
        let stops = json!([[0.0, "#ff0000"], [0.5, "#00ff00"], [1.0, "#0000ff"]]);
        s.execute("layer.layerStyle.gradientOverlay", json!({"stops": stops, "opacityStops": [[0, 100], [1, 50]]})).unwrap();
        let Effect::GradientOverlay { gradient, .. } = &effects(&s)[0] else { panic!() };
        assert_eq!(gradient.stops.len(), 3);
        assert_eq!(gradient.stops[1], (0.5, photocraft_color::Color::rgb(0.0, 1.0, 0.0)));
        assert_eq!(gradient.opacity_stops, vec![(0.0, 1.0), (1.0, 0.5)]);
        // Editing the same overlay: new stops replace the list (sorted), unsorted input is sorted,
        // and a geometry-only edit keeps the three stops.
        s.execute("layer.layerStyle.gradientOverlay", json!({"stops": [[1.0, "#ffffff"], [0.25, [0.0, 0.0, 0.0]]], "opacityStops": []})).unwrap();
        let Effect::GradientOverlay { gradient, .. } = &effects(&s)[0] else { panic!() };
        assert_eq!(gradient.stops.iter().map(|s| s.0).collect::<Vec<_>>(), vec![0.25, 1.0]);

Expected assertions from that fixture:

        assert_eq!(gradient.stops.len(), 3);
        assert_eq!(gradient.stops[1], (0.5, photocraft_color::Color::rgb(0.0, 1.0, 0.0)));
        assert_eq!(gradient.opacity_stops, vec![(0.0, 1.0), (1.0, 0.5)]);
        // Editing the same overlay: new stops replace the list (sorted), unsorted input is sorted,
        assert_eq!(gradient.stops[1], (0.5, photocraft_color::Color::rgb(0.0, 1.0, 0.0)));
        assert_eq!(gradient.opacity_stops, vec![(0.0, 1.0), (1.0, 0.5)]);
        // Editing the same overlay: new stops replace the list (sorted), unsorted input is sorted,
        // and a geometry-only edit keeps the three stops.
        assert_eq!(gradient.opacity_stops, vec![(0.0, 1.0), (1.0, 0.5)]);
        // Editing the same overlay: new stops replace the list (sorted), unsorted input is sorted,
        // and a geometry-only edit keeps the three stops.
        s.execute("layer.layerStyle.gradientOverlay", json!({"stops": [[1.0, "#ffffff"], [0.25, [0.0, 0.0, 0.0]]], "opacityStops": []})).unwrap();

layer_style_picker_control_binds_previews_confirms_and_cancels setup/action excerpt:

        let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        let ctx = egui::Context::default();
        app.run("file.new", json!({"width":16,"height":16})).unwrap();
        let parent = call(&mut app, &ctx, "ui.dialog.open", json!({"kind":"layerStyle","effect":"gradientOverlay"}))["result"]["dialog"].as_u64();
        let parent = parent.unwrap();
        let before = app.ui.dialogs[0].fields.clone();
        // The Gradient Overlay's colours are its stops (#5177): `stop` picks one.
        for stop in [0, 1] {
            let reply = call(&mut app, &ctx, "ui.dialog.open", json!({"kind":"colorPicker","parent":parent,"effect":"fx1","field":"stops","stop":stop}));
            assert_eq!(reply["ok"], true, "{reply}");
            let picker = app.ui.dialogs.last().unwrap().id;
            let inspected = call(&mut app, &ctx, "ui.inspect", json!({}));

Expected assertions from that fixture:

            assert_eq!(reply["ok"], true, "{reply}");
            let picker = app.ui.dialogs.last().unwrap().id;
            let inspected = call(&mut app, &ctx, "ui.inspect", json!({}));
            assert!(inspected.to_string().contains("__layerStyleColor"));
            assert!(inspected.to_string().contains("__layerStyleColor"));
            assert_eq!(call(&mut app, &ctx, "ui.dialog.confirm", json!({"dialog":parent}))["ok"], false);
            assert_eq!(call(&mut app, &ctx, "ui.dialog.set", json!({"dialog":picker,"field":"color","value":"#123456"}))["ok"], true);
            assert_eq!(call(&mut app, &ctx, "ui.dialog.confirm", json!({"dialog":picker}))["ok"], true);
            assert_eq!(call(&mut app, &ctx, "ui.dialog.confirm", json!({"dialog":parent}))["ok"], false);
            assert_eq!(call(&mut app, &ctx, "ui.dialog.set", json!({"dialog":picker,"field":"color","value":"#123456"}))["ok"], true);
            assert_eq!(call(&mut app, &ctx, "ui.dialog.confirm", json!({"dialog":picker}))["ok"], true);
            assert_eq!(app.ui.dialogs[0].fields["effects"][0]["params"]["stops"][stop][1], "#123456");

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-5744: fix(ui): keep detached documents in one interactive window

Concrete setup and expected assertion examples

torn_off_document_is_not_drawn_again_in_the_main_window setup/action excerpt:

        for kind in [crate::theme::ThemeKind::Pro, crate::theme::ThemeKind::Studio] {
            let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
            app.run("file.new", json!({"width": 80, "height": 60, "name": "floating"})).unwrap();
            app.run("file.new", json!({"width": 80, "height": 60, "name": "docked"})).unwrap();
            app.sync_views();
            let ctx = egui::Context::default();
            crate::menus::invoke(&mut app, &ctx, "window.arrange.floatInWindow", json!({"document": 0})).unwrap();
            assert!(app.ui.windows[0].floating);
            assert_eq!(app.session.active_index(), Some(0), "{kind:?}: the floated page remains active");
            assert_eq!(main_canvas_document(&app), Some(1), "{kind:?}: the main canvas picks a docked page instead");
            let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(680.0, 480.0)).build_ui_state(
                |ui, (app, ready): &mut (PhotocraftApp, bool)| {

Expected assertions from that fixture:

            assert!(app.ui.windows[0].floating);
            assert_eq!(app.session.active_index(), Some(0), "{kind:?}: the floated page remains active");
            assert_eq!(main_canvas_document(&app), Some(1), "{kind:?}: the main canvas picks a docked page instead");
            let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(680.0, 480.0)).build_ui_state(
            assert_eq!(app.session.active_index(), Some(0), "{kind:?}: the floated page remains active");
            assert_eq!(main_canvas_document(&app), Some(1), "{kind:?}: the main canvas picks a docked page instead");
            let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(680.0, 480.0)).build_ui_state(
                |ui, (app, ready): &mut (PhotocraftApp, bool)| {
            assert_eq!(main_canvas_document(&app), Some(1), "{kind:?}: the main canvas picks a docked page instead");
            let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(680.0, 480.0)).build_ui_state(
                |ui, (app, ready): &mut (PhotocraftApp, bool)| {
                    if *ready {

tab_reorder_keeps_floating_documents_in_place setup/action excerpt:

        let visible = [0, 2];
        assert_eq!(move_target_after_filter(&visible, 0, 1, 3), 2);
        assert_eq!(move_target_after_filter(&visible, 2, 0, 3), 0);
    }

Expected assertions from that fixture:

        assert_eq!(move_target_after_filter(&visible, 0, 1, 3), 2);
        assert_eq!(move_target_after_filter(&visible, 2, 0, 3), 0);
    }
        assert_eq!(move_target_after_filter(&visible, 2, 0, 3), 0);
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6387: Video Frames to Layers: every page of a multi-page TIFF (part of #6379)

Concrete setup and expected assertion examples

video_frames_to_layers_takes_every_tiff_page setup/action excerpt:

    let dir = tmpdir("tiff-pages");
    let path = format!("{dir}/pages.tif");
    write_tiff(&path, &[(2, 2, 9, 1), (6, 4, 51, 0), (3, 5, 204, 0)]);
    let mut s = session();
    let r = s.execute("file.import.videoFramesToLayers", json!({"path": path})).unwrap();
    assert_eq!(r["layers"], 2, "two pages, the thumbnail left out");
    let doc = &s.active().unwrap().doc;
    for (name, (w, h), level) in [("Frame 1", (6, 4), 51.0), ("Frame 2", (3, 5), 204.0)] {
        let l = doc.layers.iter().find(|l| l.name == name).unwrap_or_else(|| panic!("{name}"));
        let surf = l.surface().unwrap();
        let b = surf.content_bounds();
        assert_eq!((b.width(), b.height()), (w, h), "{name}");

Expected assertions from that fixture:

    assert_eq!(r["layers"], 2, "two pages, the thumbnail left out");
    let doc = &s.active().unwrap().doc;
    for (name, (w, h), level) in [("Frame 1", (6, 4), 51.0), ("Frame 2", (3, 5), 204.0)] {
        let l = doc.layers.iter().find(|l| l.name == name).unwrap_or_else(|| panic!("{name}"));
        assert_eq!((b.width(), b.height()), (w, h), "{name}");
        let px = surf.read_region(photocraft_geom::Rect::new(b.x0, b.y0, b.x0 + 1, b.y0 + 1));
        assert!((px[0] * 255.0 - level).abs() < 0.6 && px[3] == 1.0, "{name}: {px:?}");
    }
        assert!((px[0] * 255.0 - level).abs() < 0.6 && px[3] == 1.0, "{name}: {px:?}");
    }
    let single = format!("{dir}/single.tif");
    write_tiff(&single, &[(4, 4, 120, 0)]);

video_frames_to_layers_refuses_a_tiff_with_an_incomplete_page_list setup/action excerpt:

    let dir = tmpdir("tiff-loop");
    let path = format!("{dir}/loop.tif");
    write_tiff(&path, &[(2, 2, 51, 0), (2, 2, 204, 0)]);
    // The last directory's next-IFD offset points back to the first directory.
    let mut f = std::fs::read(&path).unwrap();
    let first: [u8; 4] = f[4..8].try_into().unwrap();
    let end = f.len();
    f[end - 4..].copy_from_slice(&first);
    std::fs::write(&path, f).unwrap();
    let mut s = session();
    let before = s.active().unwrap().doc.layers.len();
    let err = s.execute("file.import.videoFramesToLayers", json!({"path": path})).unwrap_err().to_string();

Expected assertions from that fixture:

    assert!(err.contains("only the first 2 pages"), "{err}");
    assert_eq!(s.active().unwrap().doc.layers.len(), before, "nothing imported");
}
    assert_eq!(s.active().unwrap().doc.layers.len(), before, "nothing imported");
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-6449: Hand tool: Cmd held zooms in and Alt zooms out, as in Photoshop

Concrete setup and expected assertion examples

cmd_is_not_the_move_tool_for_selection_hand_zoom_pen_shape_or_type_tools setup/action excerpt:

    for tool in [
        Tool::Hand,
        Tool::RotateView,
        Tool::Zoom,
        Tool::Pen,
        Tool::Rectangle,
        Tool::Type,
        Tool::VerticalType,
        Tool::Crop,
        Tool::PathSelection,
        Tool::DirectSelection,
        Tool::Move,

Expected assertions from that fixture:

        assert!(!cmd_moves(tool), "{tool:?}");
        let mut h = harness(tool);
        h.event(Event::ModifiersChanged(platform(Modifiers::COMMAND)));
        h.run_steps(1);
        assert_eq!(held_tool(h.state(), &h.ctx), (tool == Tool::Hand).then_some(Temporary::ZoomIn), "{tool:?}");
    }
    for tool in [Tool::Brush, Tool::Eraser, Tool::Gradient, Tool::CloneStamp, Tool::Eyedropper, Tool::MagneticLasso, Tool::QuickSelection] {
        assert!(cmd_moves(tool), "{tool:?}");
        assert!(cmd_moves(tool), "{tool:?}");
    }
}

cmd_and_alt_with_the_hand_tool_zoom_in_and_out_while_held setup/action excerpt:

    let mut h = harness(Tool::Hand);
    let z0 = h.state().ui.views[0].zoom;
    hold_mods(&mut h, Modifiers::COMMAND);
    assert_eq!(held_tool(h.state(), &h.ctx), Some(Temporary::ZoomIn));
    click(&mut h, 200.0, 150.0, Modifiers::COMMAND);
    let z1 = h.state().ui.views[0].zoom;
    assert!(z1 > z0, "{z0} -> {z1}");
    hold_mods(&mut h, Modifiers::ALT);
    assert_eq!(held_tool(h.state(), &h.ctx), Some(Temporary::ZoomOut));
    click(&mut h, 200.0, 150.0, Modifiers::ALT);
    let z2 = h.state().ui.views[0].zoom;
    assert!(z2 < z1, "{z1} -> {z2}");

Expected assertions from that fixture:

    assert_eq!(held_tool(h.state(), &h.ctx), Some(Temporary::ZoomIn));
    click(&mut h, 200.0, 150.0, Modifiers::COMMAND);
    let z1 = h.state().ui.views[0].zoom;
    assert!(z1 > z0, "{z0} -> {z1}");
    assert!(z1 > z0, "{z0} -> {z1}");
    hold_mods(&mut h, Modifiers::ALT);
    assert_eq!(held_tool(h.state(), &h.ctx), Some(Temporary::ZoomOut));
    click(&mut h, 200.0, 150.0, Modifiers::ALT);
    assert_eq!(held_tool(h.state(), &h.ctx), Some(Temporary::ZoomOut));
    click(&mut h, 200.0, 150.0, Modifiers::ALT);
    let z2 = h.state().ui.views[0].zoom;
    assert!(z2 < z1, "{z1} -> {z2}");

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-7255: fix(engine): guard trim and duplicate merged against documents exceeding render limits (#7223)

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: trim and duplicate merged on too large document fail gracefully without panic. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

trim_and_duplicate_merged_on_too_large_document_fail_gracefully_without_panic setup/action excerpt:

        let mut s = Session::new();
        s.execute("file.new", json!({"width": 40000, "height": 30000, "background": "transparent"})).unwrap();
        let initial_layers = doc(&s).layers.len();
        let initial_bounds = doc(&s).bounds();
        let initial_rev = s.active().unwrap().revision;
        let initial_docs = s.documents().len();

        let trim_res = s.execute("image.trim", json!({"basedOn": "topLeft"}));
        assert!(trim_res.is_err(), "trim on too-large document must return error");
        let err = trim_res.unwrap_err().to_string();
        assert!(err.contains("too large to trim"), "error should explain document is too large: {err}");
        assert_eq!(doc(&s).layers.len(), initial_layers, "layer count unchanged after failed trim");

Expected assertions from that fixture:

        assert!(trim_res.is_err(), "trim on too-large document must return error");
        let err = trim_res.unwrap_err().to_string();
        assert!(err.contains("too large to trim"), "error should explain document is too large: {err}");
        assert_eq!(doc(&s).layers.len(), initial_layers, "layer count unchanged after failed trim");
        assert!(err.contains("too large to trim"), "error should explain document is too large: {err}");
        assert_eq!(doc(&s).layers.len(), initial_layers, "layer count unchanged after failed trim");
        assert_eq!(doc(&s).bounds(), initial_bounds, "canvas size unchanged after failed trim");
        assert_eq!(s.active().unwrap().revision, initial_rev, "revision unchanged after failed trim");
        assert_eq!(doc(&s).layers.len(), initial_layers, "layer count unchanged after failed trim");
        assert_eq!(doc(&s).bounds(), initial_bounds, "canvas size unchanged after failed trim");
        assert_eq!(s.active().unwrap().revision, initial_rev, "revision unchanged after failed trim");

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-8931: feat(algo): describe the Gaussian blur as data for other devices

Concrete setup and expected assertion examples

the_description_is_the_cpus_blur setup/action excerpt:

        for radius in RADII {
            let params = FilterParams::GaussianBlur { radius };
            let blur = Separable::of(&params).unwrap();
            match &blur {
                Separable::Kernel(k) => {
                    assert!(radius <= 4.0, "radius {radius}");
                    assert_eq!(k, &blur::gaussian_kernel(radius));
                    assert_eq!(k.len() % 2, 1);
                    assert!((k.iter().sum::<f32>() - 1.0).abs() < 1e-5);
                }
                Separable::Boxes(b) => {
                    assert!(radius > 4.0, "radius {radius}");

Expected assertions from that fixture:

                    assert!(radius <= 4.0, "radius {radius}");
                    assert_eq!(k, &blur::gaussian_kernel(radius));
                    assert_eq!(k.len() % 2, 1);
                    assert!((k.iter().sum::<f32>() - 1.0).abs() < 1e-5);
                    assert_eq!(k, &blur::gaussian_kernel(radius));
                    assert_eq!(k.len() % 2, 1);
                    assert!((k.iter().sum::<f32>() - 1.0).abs() < 1e-5);
                }
                    assert_eq!(k.len() % 2, 1);
                    assert!((k.iter().sum::<f32>() - 1.0).abs() < 1e-5);
                }
                Separable::Boxes(b) => {

an_impulse_spreads_no_further_than_the_reach setup/action excerpt:

        for radius in RADII {
            let params = FilterParams::GaussianBlur { radius };
            let reach = Separable::of(&params).unwrap().reach();
            let fmt = PixelFormat::new(ColorMode::Rgb, SampleType::F32, true);
            let size = 2 * reach + 41;
            let canvas = Rect::new(0, 0, size, size);
            let mut s = Surface::new(fmt);
            let dot = Rect::new(size / 2, size / 2, size / 2 + 1, size / 2 + 1);
            s.fill_rect(dot, &[1.0, 1.0, 1.0, 1.0]);
            let out = crate::apply_in_with(&s, &params, canvas, canvas, None, canvas, &Interrupt::NONE).unwrap();
            let mut px = Vec::new();
            out.read_region_into(canvas, &mut px);

Expected assertions from that fixture:

            assert!(dot.inflate(reach).contains_rect(&touched), "radius {radius}: {touched:?} past the reach {reach}");
            if matches!(Separable::of(&params), Some(Separable::Kernel(_))) {
                assert_eq!(touched, dot.inflate(reach), "radius {radius}");
            }
                assert_eq!(touched, dot.inflate(reach), "radius {radius}");
            }
        }
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9004: fix(ui): enlarge Move tool alignment icons for legibility

  • Fixture/setup: Open a synthetic layered document with Move tool; use the supported themes and display scales.
  • Action: Inspect Move alignment icons and compare screenshots to the reviewed icon geometry in this PR.
  • Expected: Icons use the enlarged reviewed geometry, remain legible and do not clip or shift adjacent controls.
  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester:

PR-9015: fix(xtask): reject leading zeroes in numeric prerelease identifiers

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: rejects numeric prerelease identifiers with leading zeroes. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

rejects_numeric_prerelease_identifiers_with_leading_zeroes setup/action excerpt:

        for version in ["1.0.0-01", "1.0.0-rc.01", "1.0.0-00.1"] {
            assert!(validate(version).is_err(), "validator accepted invalid version {version}");
            assert!(replace(MANIFEST, version).is_err(), "accepted invalid version {version}");
        }
        for version in ["1.0.0-0", "1.0.0-rc.0", "1.0.0-01a", "1.0.0-01-2"] {
            let updated = replace(MANIFEST, version).expect("valid prerelease");
            assert_eq!(read(&updated).unwrap(), version);
        }
    }

Expected assertions from that fixture:

            assert!(validate(version).is_err(), "validator accepted invalid version {version}");
            assert!(replace(MANIFEST, version).is_err(), "accepted invalid version {version}");
        }
        for version in ["1.0.0-0", "1.0.0-rc.0", "1.0.0-01a", "1.0.0-01-2"] {
            assert!(replace(MANIFEST, version).is_err(), "accepted invalid version {version}");
        }
        for version in ["1.0.0-0", "1.0.0-rc.0", "1.0.0-01a", "1.0.0-01-2"] {
            let updated = replace(MANIFEST, version).expect("valid prerelease");
            assert_eq!(read(&updated).unwrap(), version);
        }
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9172: Gradient Fill layer: default to Foreground to Transparent (#8708)

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: new gradient fill layer defaults to foreground to transparent. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

new_gradient_fill_layer_defaults_to_foreground_to_transparent setup/action excerpt:

    // #8708: Photoshop's Layer › New Fill Layer › Gradient… starts from Foreground to
    // Transparent: the foreground at both ends, its opacity 100% to 0%. A `to` colour keeps a
    // two-colour gradient.
    let mut s = session(8, "transparent");
    s.tools.foreground = [0.2, 0.4, 0.6, 1.0];
    s.tools.background = [1.0, 1.0, 1.0, 1.0];
    s.execute("layer.newFillLayer.gradient", json!({})).unwrap();
    let Fill::Gradient { stops, opacity_stops, .. } = active_fill(&s) else { panic!("not a gradient") };
    let fg = Color::rgba(0.2, 0.4, 0.6, 1.0);
    assert_eq!(stops, vec![(0.0, fg), (1.0, fg)]);
    assert_eq!(opacity_stops, vec![(0.0, 1.0), (1.0, 0.0)]);
    s.execute("layer.newFillLayer.gradient", json!({"to": "#ff0000"})).unwrap();

Expected assertions from that fixture:

    assert_eq!(stops, vec![(0.0, fg), (1.0, fg)]);
    assert_eq!(opacity_stops, vec![(0.0, 1.0), (1.0, 0.0)]);
    s.execute("layer.newFillLayer.gradient", json!({"to": "#ff0000"})).unwrap();
    let Fill::Gradient { stops, opacity_stops, .. } = active_fill(&s) else { panic!("not a gradient") };
    assert_eq!(opacity_stops, vec![(0.0, 1.0), (1.0, 0.0)]);
    s.execute("layer.newFillLayer.gradient", json!({"to": "#ff0000"})).unwrap();
    let Fill::Gradient { stops, opacity_stops, .. } = active_fill(&s) else { panic!("not a gradient") };
    assert_eq!(stops[1].1, Color::rgba(1.0, 0.0, 0.0, 1.0));
    assert_eq!(stops[1].1, Color::rgba(1.0, 0.0, 0.0, 1.0));
    assert!(opacity_stops.is_empty());
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9282: feat(ui): Preset menus for Levels, Exposure, Black & White and Channel Mixer

Concrete setup and expected assertion examples

adjustment_preset_menus_load_presets_and_read_custom_after_an_edit setup/action excerpt:

        use egui_kittest::kittest::{NodeT, Queryable};
        for (kind, label, key) in [
            ("levels", "Increase Contrast 2", "inBlack"),
            ("exposure", "Plus 1.0", "exposure"),
            ("blackWhite", "Red Filter", "reds"),
            ("channelMixer", "Black & White with Red Filter (RGB)", "monochrome"),
        ] {
            let mut h = Harness::builder().with_size(egui::vec2(1200.0, 900.0)).build_ui_state(|ui, app| crate::dialogs::show(app, ui.ctx()), app_with_image());
            PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::ALL[0]);
            let id = open(h.state_mut(), &format!("image.adjustments.{kind}")).unwrap();
            h.run_steps(4);
            let preset = |h: &Harness<'_, PhotocraftApp>| {

Expected assertions from that fixture:

            assert_eq!(preset(&h).as_deref(), Some("Default"), "{kind} opens on Default");
            h.query_all_by_role(egui::accesskit::Role::ComboBox).next().unwrap().click();
            h.run_steps(2);
            h.get_by_label(label).click();
            assert_eq!(preset(&h).as_deref(), Some(label), "{kind}: the menu shows the preset");
            let want = crate::adjust_presets::presets(kind, false)
                .iter()
                .find(|p| p.label == label)
                assert_eq!(fields[k], *v, "{kind}: the preset's `{k}` is loaded");
            }
            assert_eq!(fields[key], want[key]);
            let edit = match kind {

properties_preset_menus_load_every_setting_in_one_step setup/action excerpt:

    use egui_kittest::kittest::NodeT;
    for (kind, label) in
        [("levels", "Increase Contrast 2"), ("exposure", "Plus 1.0"), ("blackWhite", "Red Filter"), ("channelMixer", "Black & White with Red Filter (RGB)")]
    {
        let mut h = app_harness(kind, "rgb", 8);
        let steps = h.state().session.active().unwrap().history.past_len();
        let menu = h
            .query_all_by_role(egui::accesskit::Role::ComboBox)
            .find(|n| n.accesskit_node().value().as_deref() == Some("Default"))
            .unwrap_or_else(|| panic!("{kind}: the Preset menu, at Default"));
        menu.click();
        h.run_steps(2);

Expected assertions from that fixture:

            assert_eq!(got[k], *v, "{kind}: the layer's `{k}`");
        }
        assert_eq!(h.state().session.active().unwrap().history.past_len(), steps + 1, "{kind}: one step");
    }
        assert_eq!(h.state().session.active().unwrap().history.past_len(), steps + 1, "{kind}: one step");
    }
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9354: fix(ui): Full Screen Mode needs a document (#9349)

Concrete setup and expected assertion examples

screen_mode_button_is_inert_without_a_document setup/action excerpt:

        let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        app.run("file.new", serde_json::json!({"width": 64, "height": 48})).unwrap();
        let ctx = egui::Context::default();
        PhotocraftApp::setup_context(&ctx, crate::theme::ThemeKind::ALL[0]);
        frame(&mut app, &ctx, 0.0, vec![]);
        frame(&mut app, &ctx, 0.1, vec![]);
        let bx = if Tokens::get(&ctx).pro { 30.0 } else { 36.0 };
        // Found while a document makes it clickable: the toolbar's last button.
        let at = ctx
            .viewport(|v| {
                v.prev_pass
                    .widgets

Expected assertions from that fixture:

        assert_eq!(crate::view_cmds::is_enabled(&app, "view.screenMode.cycle"), Some(false));
        let pointer = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
        let mut sent = Vec::new();
        for (t, events) in [(2.0, vec![egui::Event::PointerMoved(at), pointer(true)]), (2.1, vec![pointer(false)]), (2.15, vec![])] {
        assert!(!sent.iter().any(|c| matches!(c, egui::ViewportCommand::Fullscreen(_))), "no screen mode change was sent: {sent:?}");
        assert_eq!(app.ui.view.screen_mode, "standard");
    }
        assert_eq!(app.ui.view.screen_mode, "standard");
    }

long_press_screen_mode_opens_the_modes_without_cycling setup/action excerpt:

        let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        // A document open: Full Screen Mode needs one (#9349), so without it a click can't cycle.
        app.run("file.new", serde_json::json!({"width": 64, "height": 48})).unwrap();
        let ctx = egui::Context::default();
        PhotocraftApp::setup_context(&ctx, crate::theme::ThemeKind::ALL[0]);
        frame(&mut app, &ctx, 0.0, vec![]);
        frame(&mut app, &ctx, 0.1, vec![]);
        let bx = if Tokens::get(&ctx).pro { 30.0 } else { 36.0 };
        // The toolbar's last button: Change Screen Mode, after Quick Mask.
        let at = ctx
            .viewport(|v| {
                v.prev_pass

Expected assertions from that fixture:

        assert!(standard(&app));
        // Hold, then release: the list opens and stays open, and the mode is unchanged.
        frame(&mut app, &ctx, 1.0, vec![egui::Event::PointerMoved(at), pointer(at, true)]);
        frame(&mut app, &ctx, 1.2, vec![]);
        assert!(!menu_open(&ctx), "not before the hold time");
        frame(&mut app, &ctx, 1.4, vec![]);
        assert!(menu_open(&ctx), "open while still held");
        frame(&mut app, &ctx, 1.5, vec![pointer(at, false)]);
        assert!(menu_open(&ctx), "open while still held");
        frame(&mut app, &ctx, 1.5, vec![pointer(at, false)]);
        frame(&mut app, &ctx, 1.55, vec![]);
        assert!(menu_open(&ctx), "the release keeps it open");

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9418: Color Lookup: honour the .3dl Mesh header's cube edge and output depth (#9078, #9073)

Concrete setup and expected assertion examples

three_dl_without_a_header_reads_low_nodes_as_12_bit setup/action excerpt:

        let table = |peak: u32| -> String {
            let mut text = String::from("0 1023\n");
            for i in 0..8 {
                let on = |bit: u32| if i & bit != 0 { peak } else { 0 };
                text.push_str(&format!("{} {} {}\n", on(4), on(2), on(1)));
            }
            text
        };
        for peak in [255, 1023, 4095] {
            let white = parse_3dl(&table(peak)).unwrap().data[21..24].to_vec();
            assert_eq!(white, [peak as f32 / 4095.0; 3], "peak {peak}");
        }

Expected assertions from that fixture:

            assert_eq!(white, [peak as f32 / 4095.0; 3], "peak {peak}");
        }
        // A header keeps the inferred 10-bit depth. The bare `3DMESH` magic is a header without a
        // cube declaration; `Mesh 1 10` would declare a 3³ cube and refuse this 2³ table (#9078).
        assert_eq!(white, [1.0; 3]);
    }

three_dl_mesh_three_point_shaper_is_not_a_node setup/action excerpt:

        // #9078: Photoshop loads a 3³ cube whose shaper line has three values; counting it as a
        // 28th node rejected the whole file.
        let l = parse_3dl(&mesh_3dl(1, 12, 3, 4095.0, true)).unwrap();
        assert_eq!(l.size, 3);
        assert_eq!(l.data.len(), 3 * 3 * 3 * 3);
        // The inverse identity: node (0,0,0) is full, node (2,2,2) black.
        assert!((l.data[0] - 1.0).abs() < 1e-6 && (l.data[1] - 1.0).abs() < 1e-6 && (l.data[2] - 1.0).abs() < 1e-6);
        let last = &l.data[l.data.len() - 3..];
        assert_eq!(last, [0.0, 0.0, 0.0]);
        // The same 27 nodes without the shaper row still load (the node-only control).
        assert_eq!(parse_3dl(&mesh_3dl(1, 12, 3, 4095.0, false)).unwrap().data, l.data);
    }

Expected assertions from that fixture:

        assert_eq!(l.size, 3);
        assert_eq!(l.data.len(), 3 * 3 * 3 * 3);
        // The inverse identity: node (0,0,0) is full, node (2,2,2) black.
        assert!((l.data[0] - 1.0).abs() < 1e-6 && (l.data[1] - 1.0).abs() < 1e-6 && (l.data[2] - 1.0).abs() < 1e-6);
        assert_eq!(l.data.len(), 3 * 3 * 3 * 3);
        // The inverse identity: node (0,0,0) is full, node (2,2,2) black.
        assert!((l.data[0] - 1.0).abs() < 1e-6 && (l.data[1] - 1.0).abs() < 1e-6 && (l.data[2] - 1.0).abs() < 1e-6);
        let last = &l.data[l.data.len() - 3..];
        assert!((l.data[0] - 1.0).abs() < 1e-6 && (l.data[1] - 1.0).abs() < 1e-6 && (l.data[2] - 1.0).abs() < 1e-6);
        let last = &l.data[l.data.len() - 3..];
        assert_eq!(last, [0.0, 0.0, 0.0]);
        // The same 27 nodes without the shaper row still load (the node-only control).

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9481: fix(ui): new layer effects start from the Make Default values

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: new effects start from the made default. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

new_effects_start_from_the_made_default setup/action excerpt:

        let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        app.run("file.new", json!({"width": 32, "height": 32})).unwrap();
        app.run("layer.new.layer", json!({})).unwrap();
        app.run("layer.layerStyle.makeDefault", json!({"kind": "stroke", "params": {"size": 7, "position": "outside"}})).unwrap();

        let id = open(&mut app, Some("stroke")).unwrap();
        let mut f = app.ui.dialogs.iter().find(|d| d.id == id).unwrap().fields.clone();
        add_instance(&mut f, "stroke");
        add_instance(&mut f, "dropShadow");
        let fx = effects_of(&f);
        let strokes: Vec<_> = fx.iter().filter(|e| e["kind"] == json!("stroke")).collect();
        assert_eq!(strokes.len(), 2);

Expected assertions from that fixture:

        assert_eq!(strokes.len(), 2);
        for e in strokes {
            assert_eq!((&e["params"]["size"], &e["params"]["position"]), (&json!(7), &json!("outside")));
            assert_eq!(e["params"]["opacity"], defaults("stroke")["opacity"], "unsaved keys keep the factory value");
            assert_eq!((&e["params"]["size"], &e["params"]["position"]), (&json!(7), &json!("outside")));
            assert_eq!(e["params"]["opacity"], defaults("stroke")["opacity"], "unsaved keys keep the factory value");
        }
        let shadow = fx.iter().find(|e| e["kind"] == json!("dropShadow")).unwrap();
            assert_eq!(e["params"]["opacity"], defaults("stroke")["opacity"], "unsaved keys keep the factory value");
        }
        let shadow = fx.iter().find(|e| e["kind"] == json!("dropShadow")).unwrap();
        assert_eq!(shadow["params"]["size"], defaults("dropShadow")["size"]);

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9504: fix(ui): Keyboard Shortcuts records ⌘Z instead of undoing the capture

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: cmd z is recorded while capturing and undoes otherwise. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

cmd_z_is_recorded_while_capturing_and_undoes_otherwise setup/action excerpt:

    let (mut h, id) = dialog(1.0, "edit.search", "search…");
    capture(&mut h, &crate::shortcuts::pretty("Cmd+F"), Key::ControlLeft, Modifiers::COMMAND, Key::Z);
    assert_eq!(field(&h, id, "overrides")["edit.search"], json!("Cmd+Z"));
    assert_eq!(field(&h, id, "capture"), json!(false));
    key(&mut h, Key::Z, true, Modifiers::COMMAND);
    h.run_steps(2);
    assert_ne!(field(&h, id, "overrides")["edit.search"], json!("Cmd+Z"), "⌘Z outside a capture undoes the change");
    assert_eq!(field(&h, id, "capture"), json!(false), "and doesn't wait for keys again");
    // CodeRabbit on #9504: clearing the restored capture isn't a new edit, so Redo still works.
    h.run_steps(2);
    key(&mut h, Key::Z, true, Modifiers::COMMAND | Modifiers::SHIFT);
    h.run_steps(2);

Expected assertions from that fixture:

    assert_eq!(field(&h, id, "overrides")["edit.search"], json!("Cmd+Z"));
    assert_eq!(field(&h, id, "capture"), json!(false));
    key(&mut h, Key::Z, true, Modifiers::COMMAND);
    h.run_steps(2);
    assert_eq!(field(&h, id, "capture"), json!(false));
    key(&mut h, Key::Z, true, Modifiers::COMMAND);
    h.run_steps(2);
    assert_ne!(field(&h, id, "overrides")["edit.search"], json!("Cmd+Z"), "⌘Z outside a capture undoes the change");
    assert_ne!(field(&h, id, "overrides")["edit.search"], json!("Cmd+Z"), "⌘Z outside a capture undoes the change");
    assert_eq!(field(&h, id, "capture"), json!(false), "and doesn't wait for keys again");
    // CodeRabbit on #9504: clearing the restored capture isn't a new edit, so Redo still works.
    h.run_steps(2);

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9517: fix(ui): Layer Style scrolls on a short window so OK stays on screen

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: layer style buttons stay on a short window. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

layer_style_buttons_stay_on_a_short_window setup/action excerpt:

        use egui_kittest::{Harness, kittest::Queryable};

        let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
        app.run("file.new", serde_json::json!({"width": 64, "height": 64})).unwrap();
        app.run("layer.new.layer", serde_json::json!({})).unwrap();
        let mut h = Harness::builder().with_size(egui::vec2(1435.0, 858.0)).build_ui_state(|ui, app| show(app, ui.ctx()), app);
        PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::ALL[0]);
        crate::layer_style::open(h.state_mut(), Some("bevelEmboss")).unwrap();
        h.run_steps(4);
        for label in ["OK", "Cancel"] {
            let r = h.get_by_label(label).rect();
            assert!(r.bottom() <= 858.0, "{label} is off the window: {r:?}");

Expected assertions from that fixture:

            assert!(r.bottom() <= 858.0, "{label} is off the window: {r:?}");
        }
    }

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9597: fix(ui): the layer list doesn't move with no layer selected

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: layer rows do not move when no layer is selected. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

layer_rows_do_not_move_when_no_layer_is_selected setup/action excerpt:

    for theme in [crate::theme::ThemeKind::Studio, crate::theme::ThemeKind::Classic, crate::theme::ThemeKind::ProMedium] {
        let mut h = harness();
        PhotocraftApp::setup_context(&h.ctx, theme);
        h.run_steps(4);
        let row_top = |h: &Harness<'_, PhotocraftApp>| by_role(h, "Paint", Role::Button).map(|r| r.top());
        let selected = row_top(&h);
        h.state_mut().session.execute("select.deselectLayers", json!({})).unwrap();
        h.run_steps(4);
        let none = row_top(&h);
        assert!(selected.is_some(), "{theme:?}: the layer row");
        assert_eq!(none, selected, "{theme:?}: the row moved with no layer selected");
    }

Expected assertions from that fixture:

        assert!(selected.is_some(), "{theme:?}: the layer row");
        assert_eq!(none, selected, "{theme:?}: the row moved with no layer selected");
    }
}
        assert_eq!(none, selected, "{theme:?}: the row moved with no layer selected");
    }
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9598: fix(ui): Liquify's Twirl Counterclockwise gets a mirrored twirl icon

  • Fixture/setup and action: execute all identified changed regression functions below on the final frozen tree. Their checked-in setup constructs the concrete inputs; preserve those inputs, capability prerequisites and assertions.

  • Expected behavior: twirl counterclockwise has its own mirrored icon; alt reversed twirl survives replay undo and redo. Each listed test must actually execute its assertions; these are distinct behavior-specific oracles, not a single interchangeable guard.

Concrete setup and expected assertion examples

twirl_counterclockwise_has_its_own_mirrored_icon setup/action excerpt:

        assert_eq!(tool_icon(LiquifyTool::TwirlCcw), "rotate-ccw");
        let svg = |name: &str| crate::icon_data::ICONS.iter().find(|(n, _)| *n == name).map(|(_, b)| String::from_utf8_lossy(b).into_owned());
        let (cw, ccw) = (svg("rotate-cw").unwrap(), svg("rotate-ccw").expect("the rotate-ccw icon"));
        assert!(ccw.contains("M3 12a9 9 0 1 0 9-9") && cw.contains("M21 12a9 9 0 1 1-9-9"), "mirror images");
        for t in [LiquifyTool::ForwardWarp, LiquifyTool::TwirlCw, LiquifyTool::TwirlCcw, LiquifyTool::Pucker, LiquifyTool::Bloat] {
            assert!(svg(tool_icon(t)).is_some(), "{t:?}: {}", tool_icon(t));
        }
    }

Expected assertions from that fixture:

        assert_eq!(tool_icon(LiquifyTool::TwirlCcw), "rotate-ccw");
        let svg = |name: &str| crate::icon_data::ICONS.iter().find(|(n, _)| *n == name).map(|(_, b)| String::from_utf8_lossy(b).into_owned());
        let (cw, ccw) = (svg("rotate-cw").unwrap(), svg("rotate-ccw").expect("the rotate-ccw icon"));
        assert!(ccw.contains("M3 12a9 9 0 1 0 9-9") && cw.contains("M21 12a9 9 0 1 1-9-9"), "mirror images");
        assert!(ccw.contains("M3 12a9 9 0 1 0 9-9") && cw.contains("M21 12a9 9 0 1 1-9-9"), "mirror images");
        for t in [LiquifyTool::ForwardWarp, LiquifyTool::TwirlCw, LiquifyTool::TwirlCcw, LiquifyTool::Pucker, LiquifyTool::Bloat] {
            assert!(svg(tool_icon(t)).is_some(), "{t:?}: {}", tool_icon(t));
        }
            assert!(svg(tool_icon(t)).is_some(), "{t:?}: {}", tool_icon(t));
        }
    }

alt_reversed_twirl_survives_replay_undo_and_redo setup/action excerpt:

        let ctx = egui::Context::default();
        let mut app = app_with_layer();
        open(&mut app, &ctx).unwrap();
        control(&mut app, &json!({"tool": "twirlCw", "size": 36, "rate": 80})).unwrap();

        // Releasing Alt while dragging must not change the tool recorded at pointer-down.
        pointer(&mut app, ToolEvent::Down { x: 53.0, y: 40.0, pressure: 1.0 }, egui::Modifiers::ALT);
        pointer(&mut app, ToolEvent::Move { x: 58.0, y: 41.0, pressure: 1.0 }, egui::Modifiers::NONE);
        pointer(&mut app, ToolEvent::Up { x: 58.0, y: 41.0 }, egui::Modifiers::NONE);
        let d = app.distort.liquify.as_ref().unwrap();
        assert_eq!(d.opts.tool, LiquifyTool::TwirlCw, "Alt must not change the selected tool");
        assert_eq!(d.strokes.len(), 1);

Expected assertions from that fixture:

        assert_eq!(d.opts.tool, LiquifyTool::TwirlCw, "Alt must not change the selected tool");
        assert_eq!(d.strokes.len(), 1);
        assert_eq!(d.strokes[0].tool, LiquifyTool::TwirlCcw);
        assert_eq!(LiquifyField::from_strokes(d.canvas, d.cell, &d.strokes), d.field);
        assert_eq!(d.strokes.len(), 1);
        assert_eq!(d.strokes[0].tool, LiquifyTool::TwirlCcw);
        assert_eq!(LiquifyField::from_strokes(d.canvas, d.cell, &d.strokes), d.field);

        assert_eq!(d.strokes[0].tool, LiquifyTool::TwirlCcw);
        assert_eq!(LiquifyField::from_strokes(d.canvas, d.cell, &d.strokes), d.field);

        // A following stroke without Alt records the original tool.

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA:

PR-9606: fix(ui): the Color panel remembers its options across restarts

Concrete setup and expected assertion examples

color_panel_options_survive_a_restart setup/action excerpt:

    let mut first = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
    first.ui.color_panel.mode = ColorPanelMode::HsbSliders;
    first.ui.color_panel.ramp = ColorRamp::GrayscaleRamp;
    crate::color_panel_ui::persist(&mut first);
    let saved = first.session.prefs().clone();

    let mut next = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
    next.session.prefs.edit(|p| *p = saved);
    crate::color_panel_ui::restore(&mut next);
    assert_eq!((next.ui.color_panel.mode, next.ui.color_panel.ramp), (ColorPanelMode::HsbSliders, ColorRamp::GrayscaleRamp));

    let mut fresh = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());

Expected assertions from that fixture:

    assert_eq!((next.ui.color_panel.mode, next.ui.color_panel.ramp), (ColorPanelMode::HsbSliders, ColorRamp::GrayscaleRamp));

    let mut fresh = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
    crate::color_panel_ui::restore(&mut fresh);
    assert_eq!(fresh.ui.color_panel.mode, ColorPanelMode::HueCube, "no saved options: the default");
}

an_outside_preference_change_reaches_the_live_panel setup/action excerpt:

    let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
    app.ui.color_panel.mode = ColorPanelMode::HsbSliders;
    crate::color_panel_ui::persist(&mut app);
    // As prefs.set or a reset to defaults would: the stored options change under the panel.
    let mut other = app.ui.color_panel;
    other.mode = ColorPanelMode::GrayscaleSlider;
    app.session.prefs.edit(|p| p.color_panel = serde_json::to_value(other).unwrap());
    crate::color_panel_ui::persist(&mut app);
    assert_eq!(app.ui.color_panel.mode, ColorPanelMode::GrayscaleSlider, "the live panel follows the preference");
    let stored: crate::state::ColorPanelState = serde_json::from_value(app.session.prefs().color_panel.clone()).unwrap();
    assert_eq!(stored.mode, ColorPanelMode::GrayscaleSlider, "and the preference isn't overwritten");
    // A reset to defaults clears the stored options: the panel goes back to its defaults too.

Expected assertions from that fixture:

    assert_eq!(app.ui.color_panel.mode, ColorPanelMode::GrayscaleSlider, "the live panel follows the preference");
    let stored: crate::state::ColorPanelState = serde_json::from_value(app.session.prefs().color_panel.clone()).unwrap();
    assert_eq!(stored.mode, ColorPanelMode::GrayscaleSlider, "and the preference isn't overwritten");
    // A reset to defaults clears the stored options: the panel goes back to its defaults too.
    assert_eq!(stored.mode, ColorPanelMode::GrayscaleSlider, "and the preference isn't overwritten");
    // A reset to defaults clears the stored options: the panel goes back to its defaults too.
    app.session.prefs.edit(|p| p.color_panel = serde_json::Value::Null);
    crate::color_panel_ui::persist(&mut app);
    assert_eq!(app.ui.color_panel.mode, ColorPanelMode::HueCube, "reset: the default mode");
    crate::color_panel_ui::persist(&mut app);
    assert_eq!(app.ui.color_panel.mode, ColorPanelMode::HueCube, "and it stays reset");
}

The companion JSON retains setup and assertions for every listed regression. These source excerpts are instructions, not execution evidence.

  • Status: NOT RUN
  • Actual:
  • Evidence:
  • Tester/date:
  • Tested SHA: