Compare commits

..

3 commits

Author SHA1 Message Date
arena-agent
d62cc13d34 style(nigig-build): cargo fmt the two test targets left unformatted
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
email.yml / style(nigig-build): cargo fmt the two test targets left unformatted (push) Failing after 0s
tests/ui.rs and tests/cost_estimator.rs drifted out of rustfmt shape
in the recent feature/merge series, failing the whole-crate fmt gate
(cargo fmt -p nigig-build -- --check). Mechanical reformat only.
2026-08-17 04:17:36 +00:00
arena-agent
ff0cba1b80 fix(project): replace retired SpreadsheetGrid::select_cell with set_selection_pair
spreadsheet-ui 6a3c467 removed the direct selection helper, leaving
apply_spreadsheet_op calling a method that no longer exists — the
nigig-build lib (and therefore every cargo gate) stopped compiling.
set_selection_pair((r, c), (r, c)) is the API the same migration
series already adopted (8fe7db2); both call sites redraw the view
right after, covering the paint the removed helper used to trigger.
2026-08-17 04:17:36 +00:00
arena-agent
5e5adf962d fix(doc): boot the CRDT editor with content and migrate persistence to the app-data store
The Android APK (pageflipnav) booted the doc workspace to a blank page.
Two compounding causes, both invisible to sandbox gates:

- CrdtDocEditor (the active editor since the navigation switch) had no
  boot init: it starts from DocumentController::default() and only the
  legacy DocEditor seeded the showcase document behind its initialized
  gate. The first event on a factory-fresh editor now runs
  init_document: load the on-disk save when it decodes as #MP_CRDT_V1
  wire (initial_document_source gates that so classic-format saves stay
  with the legacy workspace's first-edit migration), otherwise
  seed_demo_doc builds a CRDT mirror of demo_doc_blocks() -- styled
  headings, accent runs, divider, image node, the 4x3 table with bold
  header, and the closing hint. set_engine flips the same flag so a
  host-installed document is never overwritten.
- persistence.rs resolved its save file under
  env!("CARGO_MANIFEST_DIR"), baking the build machine's absolute
  source path into the binary; on device that path does not exist, so
  Open read nothing and Save wrote nowhere (the errors were swallowed),
  and on desktop the app polluted its own checkout. Writes now go only
  to app_data_dir()/nigig_build_store/generated/current.doc.json (the
  crate-wide convention the CAD store already uses); reads keep a
  one-way fallback to the legacy source-tree file so an unreplicated
  developer save is honored once. Boot and migration emit [DOC_TRACE]
  lines so a device logcat session names the branch that fired.

Tests (8 new): boot-source gate (CRDT wire boots verbatim; classic JSON
and None route to the demo seed), runtime boot on first event
(source-agnostic non-empty projection + flag), host-installed-engine
no-overwrite guard, full structural assertion of the seeded showcase,
and four temp-dir persistence tests (round trip, store-beats-manifest
precedence, manifest fallback, empty-file rejection).
DEVICE_VERIFICATION.md gains the matching section-9 hardware rows (9.0
fresh-install demo boot, 9.3 classic-save coexistence).
2026-08-17 04:17:36 +00:00
8 changed files with 802 additions and 85 deletions

View file

@ -214,12 +214,20 @@ eyeball; photograph failures.
| 8.5 | Advanced placeholders (image/divider nodes) | Fills/borders/labels drawn once per node; divider is one centered line |
| 8.6 | Rapid typing for 30 seconds in a ~200-line document | Frame pacing stays smooth; no visible full-document flicker between keystrokes (layout cache: unchanged content redraws from the cached tree; this is the perf smoke check) |
## 9. Persistence round trip (optional but cheap)
## 9. Boot content and persistence round trip
Since the Android empty-doc fix, saves live in the platform app-data
store (`app_data_dir()/nigig_build_store/generated/current.doc.json`)
— NOT the source tree (dev checkouts get a one-way read fallback for
old saves; new writes never go there). Boot emits `[DOC_TRACE] CRDT
init:` lines in logcat naming the branch that fired.
| # | Action | Expected |
|---|--------|----------|
| 9.1 | Edit, force-close, relaunch | Document restores (platform save/load path); table contents AND cell text intact |
| 9.0 | Fresh install, first launch (no save on device) | Demo document renders: bold title, styled paragraphs, divider, image placeholder, 4x3 table (bold header), closing hint. logcat: `CRDT init: no saved document; seeding demo document` |
| 9.1 | Edit, force-close, relaunch | Document restores (platform save/load path); table contents AND cell text intact. logcat: `CRDT init: loading saved document (N bytes)` |
| 9.2 | Open a previously saved file with a table | Grid renders; merges present; no phantom rows/cols |
| 9.3 | Boot the CRDT workspace with a CLASSIC-format save present | Demo document seeds (classic saves are not shadowed); logcat notes the classic-format branch. The legacy workspace still opens the classic file on Open. FAIL criteria: blank page, or the classic file silently dropped |
## 10. Sign-off

View file

@ -1446,3 +1446,55 @@ suite covers the logic behind each row, so the runbook deliberately
re-verifies only the platform-owned residuals. No code changes in
this milestone beyond documentation; the roadmap box gains a pointer
to the runbook for the hardware session.
## Boot-time document init and app-data persistence (Android empty-doc fix)
The first hardware run of the roadmap surface exposed a compound
defect no sandbox gate could see: the Android APK (`pageflipnav`)
booted the doc workspace to a BLANK page. Two independent causes:
1. **The CRDT editor had no boot init.** The legacy `DocEditor` seeds
the showcase document behind an `initialized` flag on first draw,
but `CrdtDocEditor` — the ACTIVE editor since the navigation switch
— starts from `DocumentController::default()` (an empty projection)
and only ever gained content through an interactive action a fresh
install has not performed yet.
2. **Persistence pointed at the build machine's source tree.**
`persistence.rs` resolved its save file under
`env!("CARGO_MANIFEST_DIR")`, an absolute path baked in at compile
time. On device that path does not exist, so Open silently read
nothing and Save silently wrote nowhere (`.ok()` swallowed the
failure); on a developer machine the app polluted its own checkout.
The fix mirrors the legacy boot contract exactly once, inside the
editor: the first event handled by a factory-fresh editor runs
`init_document`, which loads the on-disk save when it decodes as
`#MP_CRDT_V1` wire (`initial_document_source` is the gate — classic-
format saves belong to the legacy workspace's first-edit migration and
must not be shadowed) and otherwise calls `seed_demo_doc`, a CRDT
mirror of the legacy `demo_doc_blocks()` showcase: styled headings,
accent runs, a divider, an image node, the 4x3 table with a bold
header, and the closing hint. `set_engine` flips the same flag, so a
host that installs its own document before the first event is never
overwritten by the seed (this also keeps every runtime test harness
deterministic).
Persistence migrates to the crate-wide convention
(`crate::dir::app_data_dir()`, the root the CAD store already uses):
writes go ONLY to `nigig_build_store/generated/current.doc.json`
there, while reads keep a one-way fallback to the legacy source-tree
file so an unreplicated developer save is honored once. Both the boot
and the migration emit `[DOC_TRACE]` lines (mirroring the legacy
boot's instrumentation) so a device `logcat` session confirms which
branch fired.
Tests pin the whole contract: the boot-source gate (valid CRDT wire
boots verbatim; classic JSON and `None` both route to the demo seed),
a runtime boot test (first event on a factory-fresh editor flips the
flag and leaves a non-empty projection, source-agnostic by design), a
no-overwrite guard for host-installed engines, a full structural
assertion of the seeded showcase (heading runs, node kinds in order,
the 4x3/12-cell table with bold header), and four persistence tests
over temp dirs covering the round trip, store-beats-manifest
precedence, the manifest fallback, and empty-file rejection.
`DEVICE_VERIFICATION.md` section 9 gained the matching hardware rows.

View file

@ -1,3 +1,4 @@
use crate::construction_frame::pages::workspace::doc::persistence::load_saved_doc_state;
use makepad_widgets::makepad_platform::event::TouchState;
use makepad_widgets::*;
use std::cell::RefCell;
@ -22,6 +23,7 @@ use crate::construction_frame::pages::workspace::doc::{
InteractionMode, MobileGestureAction, MobileGestureRouter, MobileGestureState,
};
use doc_engine::controller::DocumentController;
use doc_engine::projection::TextStylePatch;
/// The mobile clipboard-menu request the widget hands to the platform
/// after a long-press selection (Edit mode), mirrored on the widget so
@ -94,6 +96,14 @@ pub struct CrdtDocEditor {
/// actually touched.
#[rust]
pub mobile_mode_initialized: bool,
/// Boot flag: the first handled event seeds the document — the saved
/// on-disk state when one exists and decodes as CRDT wire, otherwise
/// the demo document (mirroring the legacy `DocEditor::initialized`
/// boot so a fresh install, most visibly a mobile APK, never opens an
/// empty page). `set_engine` also sets it: a host that installs its
/// own document before the first event keeps it untouched.
#[rust]
pub initialized: bool,
/// Latest mobile clipboard-menu request; `None` until a long-press
/// selection in Edit mode requests one (View mode stays passive).
#[rust]
@ -117,6 +127,7 @@ impl CrdtDocEditor {
pub fn set_engine(&mut self, cx: &mut Cx, engine: doc_engine::controller::DocumentController) {
self.engine = engine;
self.layout_cache.borrow_mut().take();
self.initialized = true;
self.redraw(cx);
}
@ -293,6 +304,304 @@ impl CrdtDocEditor {
true
}
/// One-time boot content, mirroring the legacy editor's `initialized`
/// gate: load the saved document when it decodes as CRDT wire
/// (classic-format saves stay with the legacy workspace, which owns
/// their first-edit migration), otherwise seed the demo document so
/// the page never opens blank. No-op when the projection already
/// carries content (a host installed an engine before the first
/// event, or open/save has already run).
fn init_document(&mut self, cx: &mut Cx) {
if !self.engine.projection.blocks.is_empty() {
return;
}
let saved = load_saved_doc_state();
let had_classic_save = saved.is_some();
match initial_document_source(saved) {
Some(saved) => {
eprintln!(
"[DOC_TRACE] CRDT init: loading saved document ({} bytes)",
saved.len()
);
self.deserialize(cx, &saved);
}
None => {
eprintln!(
"[DOC_TRACE] CRDT init: {}; seeding demo document",
if had_classic_save {
"saved document is classic-format (open it in the legacy workspace)"
} else {
"no saved document"
}
);
self.seed_demo_doc(cx);
}
}
}
/// Seeds the CRDT mirror of the legacy `demo_doc_blocks()` showcase
/// (heading, styled paragraphs, divider, image node, 4x3 table, and
/// a closing hint), chained through `after` anchors into one
/// top-level document order so layout mirrors the legacy page.
pub(crate) fn seed_demo_doc(&mut self, cx: &mut Cx) {
const ACTOR: &str = "local";
let heading_patch = |font_size: f32, color: (f32, f32, f32, f32)| TextStylePatch {
bold: Some(true),
font_size: Some(font_size),
color: Some(Some(color)),
..Default::default()
};
let title_color = (0.12, 0.15, 0.35, 1.0);
let section_color = (0.18, 0.25, 0.45, 1.0);
// 1. Title heading.
let title = "Makepad Rich Text & Document Engineering Specification";
let title_block = self
.engine
.insert_block(ACTOR, None, "heading")
.expect("demo title block");
self.engine
.insert_text(ACTOR, title_block.clone(), None, title);
self.engine.set_text_style_at_offsets(
ACTOR,
title_block.clone(),
0,
title.chars().count(),
heading_patch(24.0, title_color),
);
// 2. Lead paragraph with a bold red accent run.
let lead_plain = "This rich document engine avoids standard ";
let lead_accent = "TextInput widgets ";
let lead_rest = "and instead implements an advanced custom widget layout engine. It \
calculates exact glyph metrics, word-wrapping spans across line \
boundaries, drawing custom blinking carets, and mapping character \
offsets directly to style spans.";
let lead_block = self
.engine
.insert_block(ACTOR, Some(title_block.clone()), "paragraph")
.expect("demo lead block");
let lead_len =
lead_plain.chars().count() + lead_accent.chars().count() + lead_rest.chars().count();
self.engine.insert_text(
ACTOR,
lead_block.clone(),
None,
format!("{lead_plain}{lead_accent}{lead_rest}"),
);
self.engine.set_text_style_at_offsets(
ACTOR,
lead_block.clone(),
0,
lead_len,
TextStylePatch {
font_size: Some(13.0),
..Default::default()
},
);
self.engine.set_text_style_at_offsets(
ACTOR,
lead_block.clone(),
lead_plain.chars().count(),
lead_plain.chars().count() + lead_accent.chars().count(),
TextStylePatch {
bold: Some(true),
color: Some(Some((0.8, 0.2, 0.2, 1.0))),
..Default::default()
},
);
// 3. Divider node.
let divider = self
.engine
.insert_node(ACTOR, None, Some(lead_block.clone()), "divider", "")
.expect("demo divider node");
// 4. First section heading.
let section_one = "1. Scenegraph & Immediate Pass Compositing";
let section_one_block = self
.engine
.insert_block(ACTOR, Some(divider.clone()), "heading")
.expect("demo section-one block");
self.engine
.insert_text(ACTOR, section_one_block.clone(), None, section_one);
self.engine.set_text_style_at_offsets(
ACTOR,
section_one_block.clone(),
0,
section_one.chars().count(),
heading_patch(18.0, section_color),
);
// 5. Body paragraph with a bold-italic run.
let body_plain_a =
"As noted in Makepad's architecture, renderpasses can be started and ended ";
let body_accent = "nestedly without flattening the execution flow. ";
let body_plain_b = "This allows creating composite trees in the same immediate mode flow \
as drawing your UI. For instance, font rendering is a pass that \
repaints incrementally, keeping static textures cached on hardware \
like the Raspberry Pi 4.";
let body_block = self
.engine
.insert_block(ACTOR, Some(section_one_block.clone()), "paragraph")
.expect("demo body block");
let body_len = body_plain_a.chars().count()
+ body_accent.chars().count()
+ body_plain_b.chars().count();
self.engine.insert_text(
ACTOR,
body_block.clone(),
None,
format!("{body_plain_a}{body_accent}{body_plain_b}"),
);
self.engine.set_text_style_at_offsets(
ACTOR,
body_block.clone(),
0,
body_len,
TextStylePatch {
font_size: Some(13.0),
..Default::default()
},
);
self.engine.set_text_style_at_offsets(
ACTOR,
body_block.clone(),
body_plain_a.chars().count(),
body_plain_a.chars().count() + body_accent.chars().count(),
TextStylePatch {
bold: Some(true),
italic: Some(true),
..Default::default()
},
);
// 6. Image node (state carries the caption, matching the widget's
// `insert_image` encoding).
let image = self
.engine
.insert_node(
ACTOR,
None,
Some(body_block.clone()),
"image",
"Render-Target Graph vs Scenegraph Incremental Repainting",
)
.expect("demo image node");
// 7. Second section heading.
let section_two = "2. Inline Interactive Table Example";
let section_two_block = self
.engine
.insert_block(ACTOR, Some(image.clone()), "heading")
.expect("demo section-two block");
self.engine
.insert_text(ACTOR, section_two_block.clone(), None, section_two);
self.engine.set_text_style_at_offsets(
ACTOR,
section_two_block.clone(),
0,
section_two.chars().count(),
heading_patch(18.0, section_color),
);
// 8. The interactive 4x3 table (4 rows x 3 columns), header row in
// bold like the legacy demo.
let table = self
.engine
.insert_table(ACTOR, Some(section_two_block.clone()))
.expect("demo table block");
let mut rows = Vec::new();
for _ in 0..4 {
rows.push(
self.engine
.insert_table_row(ACTOR, table.clone(), rows.last().cloned())
.expect("demo table row"),
);
}
let mut columns = Vec::new();
for _ in 0..3 {
columns.push(
self.engine
.insert_table_column(ACTOR, table.clone(), columns.last().cloned())
.expect("demo table column"),
);
}
let cells = [
[
"Feature Component",
"Implementation Technique",
"Performance Impact",
],
[
"Text Engine",
"Instance-packed clipping & glyph positioning",
"10,000+ items at 60 FPS",
],
[
"Spreadsheet Grid",
"Virtual viewport + turtle drawpass",
"O(visible cells) only",
],
[
"Doc Editor Table",
"Inline block layout & cell cursor mapping",
"Seamless multi-block edit",
],
];
let writes: Vec<(doc_engine::crdt::OpId, doc_engine::crdt::OpId, String)> = rows
.iter()
.zip(cells.iter())
.flat_map(|(row, texts)| {
columns
.iter()
.zip(texts.iter())
.map(|(column, text)| (row.clone(), column.clone(), text.to_string()))
})
.collect();
self.engine.set_table_cells(ACTOR, table.clone(), writes);
for (column, text) in columns.iter().zip(cells[0].iter()) {
self.engine.set_table_cell_style(
ACTOR,
doc_engine::controller::TableCellRef {
table: table.clone(),
row: rows[0].clone(),
column: column.clone(),
},
0,
text.chars().count(),
TextStylePatch {
bold: Some(true),
..Default::default()
},
);
}
// 9. Closing hint paragraph.
let closing = "Click anywhere on the text above or inside any table cell to test live \
caret placement and typing support!";
let closing_block = self
.engine
.insert_block(ACTOR, Some(table.clone()), "paragraph")
.expect("demo closing block");
self.engine
.insert_text(ACTOR, closing_block.clone(), None, closing);
self.engine.set_text_style_at_offsets(
ACTOR,
closing_block.clone(),
0,
closing.chars().count(),
TextStylePatch {
italic: Some(true),
font_size: Some(12.0),
color: Some(Some((0.4, 0.45, 0.55, 1.0))),
..Default::default()
},
);
self.redraw(cx);
}
/// The caret atom may have been consumed by undo/redo or a merge;
/// re-anchor caret and block on the closest live glyph.
fn sanitize_cursor(&mut self) {
@ -1939,8 +2248,25 @@ impl CrdtDocEditor {
}
}
/// Decides what a fresh `CrdtDocEditor` boots with: the on-disk save when
/// it decodes as `#MP_CRDT_V1` wire, otherwise nothing (the caller then
/// seeds the demo document). Classic-format saves return `None` here so
/// the demo page never shadows them — the legacy workspace still owns
/// their first-edit migration.
pub(crate) fn initial_document_source(saved: Option<String>) -> Option<String> {
saved.and_then(|saved| crdt_engine_from_saved(&saved).map(|_| saved))
}
impl Widget for CrdtDocEditor {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {
// Boot init runs once, before any event acts on the (factory-empty)
// projection: a fresh install otherwise renders a blank page — the
// exact failure seen on Android, where the CRDT editor starts with
// no document and no user action has seeded one yet.
if !self.initialized {
self.initialized = true;
self.init_document(cx);
}
// The first real touch sequence adopts the mobile interaction
// policy (mirroring the legacy editor): View mode keeps taps and
// long-press/handle gestures but closes the IME and gates the

View file

@ -1,42 +1,74 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
const GENERATED_DIR: &str = "generated";
const GENERATED_DOC_FILE: &str = "current.doc.json";
pub(crate) const GENERATED_DOC_FILE: &str = "current.doc.json";
pub(crate) const MAX_UNDO_LEVELS: usize = 100;
fn doc_manifest_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf()
/// Root directory for doc state written at runtime.
///
/// This used to be `env!("CARGO_MANIFEST_DIR")`, which bakes the **build
/// machine's** absolute source path into the shipped binary. On an Android
/// or iOS install that path does not exist, so the workspace's Open/Save
/// buttons silently did nothing (the CRDT editor additionally had no demo
/// fallback, so a fresh install booted to an empty document); and on a
/// developer machine the app wrote into its own source tree.
///
/// `app_data_dir()` is the convention the rest of this crate already uses
/// (see `cad_store::cad_projects_dir` / `cad_persistence::cad_data_dir`).
/// Reads keep a one-way compatibility fallback to the old source-tree
/// location so existing developer saves are honored once; new saves only
/// ever go to the app data dir.
pub fn doc_store_dir() -> PathBuf {
crate::dir::app_data_dir().join("nigig_build_store")
}
fn doc_generated_dir_path() -> PathBuf {
doc_manifest_path().join(GENERATED_DIR)
/// Directory holding the runtime-saved document (`nigig_build_store/generated`).
pub fn doc_generated_dir_path() -> PathBuf {
doc_store_dir().join(GENERATED_DIR)
}
fn doc_generated_file_path() -> PathBuf {
doc_generated_dir_path().join(GENERATED_DOC_FILE)
/// The legacy save location inside the source tree (development checkouts
/// only): read as a fallback, never written.
fn dev_manifest_generated_dir_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(GENERATED_DIR)
}
/// Load the last-saved document. Prefers the runtime store; falls back to
/// the legacy source-tree file for unreplicated developer saves. Returns
/// `None` when nothing exists or the file is empty. Split from the two
/// directory parameters so the migration logic is unit-testable against
/// temp dirs.
pub fn load_saved_doc_state() -> Option<String> {
fs::read_to_string(doc_generated_file_path())
load_saved_doc_state_with(doc_generated_dir_path(), dev_manifest_generated_dir_path())
}
pub(crate) fn load_saved_doc_state_with(
store_dir: PathBuf,
manifest_dir: PathBuf,
) -> Option<String> {
let read = |dir: &PathBuf| {
fs::read_to_string(dir.join(GENERATED_DOC_FILE))
.ok()
.filter(|source| !source.trim().is_empty())
};
read(&store_dir).or_else(|| read(&manifest_dir))
}
/// Save the current document to the runtime store (never the source tree).
pub fn save_doc_state(data: &str) -> Result<(), String> {
let dir = doc_generated_dir_path();
fs::create_dir_all(&dir)
.map_err(|err| format!("could not create generated directory: {err}"))?;
fs::write(dir.join(GENERATED_DOC_FILE), data)
.map_err(|err| format!("could not save doc state: {err}"))?;
Ok(())
save_doc_state_to(doc_generated_dir_path(), GENERATED_DOC_FILE, data)
}
/// Save a copy under a caller-chosen filename in the runtime store.
pub fn save_doc_state_as(filename: &str, data: &str) -> Result<(), String> {
let dir = doc_generated_dir_path();
save_doc_state_to(doc_generated_dir_path(), filename, data)
}
pub(crate) fn save_doc_state_to(dir: PathBuf, filename: &str, data: &str) -> Result<(), String> {
fs::create_dir_all(&dir)
.map_err(|err| format!("could not create generated directory: {err}"))?;
fs::write(dir.join(filename), data)
.map_err(|err| format!("could not save doc state as {filename}: {err}"))?;
.map_err(|err| format!("could not save doc state: {err}"))?;
Ok(())
}

View file

@ -5226,3 +5226,233 @@ fn layout_tree_cache_drops_on_engine_replacement() {
"the new document caches normally afterwards"
);
}
// == Boot-time document init + persistence store (Android empty-doc fix) ===
//
// The shipped APK booted to a blank doc workspace for two compounding
// reasons: the CRDT editor (the active one) had no boot init — only the
// legacy editor's `initialized` gate seeded the demo document — and
// persistence read from the build machine's absolute source-tree path,
// which does not exist on device. These tests pin the replacement
// behaviour: a one-time boot seed from the app-data store (CRDT wire
// only) falling back to the demo document, plus the store/manifest read
// precedence of the migrated persistence layer.
use super::crdt_widget::initial_document_source;
use super::persistence::{load_saved_doc_state_with, save_doc_state_to, GENERATED_DOC_FILE};
/// A factory-fresh editor with NO engine installed: the exact boot state
/// of the APK that opened blank.
fn crdt_editor_default() -> (Cx, CrdtDocEditor) {
let mut cx = Cx::new(Box::new(|_, _| {}));
let editor = {
let mut host = ();
let mut std = ();
let mut vm = ScriptVm {
host: &mut host,
std: &mut std,
bx: Box::new(ScriptVmBase::new()),
};
CrdtDocEditor::script_new(&mut vm)
};
(cx, editor)
}
#[test]
fn boot_source_prefers_crdt_save_and_rejects_classic_format() {
assert_eq!(initial_document_source(None), None);
// A classic-format save is neither loaded by the CRDT editor nor
// mistaken for a CRDT save: the demo document boots instead, and the
// legacy workspace keeps owning the classic file on Open.
let classic = "{\"blocks\":[{\"kind\":\"paragraph\"}]}".to_string();
assert_eq!(initial_document_source(Some(classic)), None);
// A valid CRDT save boots verbatim.
let mut engine = CrdtController::default();
let block = engine
.insert_block("local", None, "paragraph")
.expect("block");
engine.insert_text("local", block, None, "saved boot text");
let wire = crdt_save_wire(&engine.document).expect("wire");
assert_eq!(initial_document_source(Some(wire.clone())), Some(wire));
}
#[test]
fn fresh_editor_boots_document_on_first_event() {
let (mut cx, mut editor) = crdt_editor_default();
assert!(editor.engine().projection.blocks.is_empty());
assert!(!editor.initialized);
editor.handle_event(
&mut cx,
&runtime_key_down(KeyCode::Escape),
&mut Scope::empty(),
);
assert!(editor.initialized);
// Source-agnostic (a developer machine may have a real save on disk):
// whichever of save/demo won, the page is not blank.
assert!(
!editor.engine().projection.blocks.is_empty(),
"boot init must leave a document in the projection"
);
}
#[test]
fn host_installed_engine_is_not_overwritten_by_boot_seed() {
// `set_engine` marks the widget initialized: a host that installs its
// own document (every runtime test above included) must never get
// demo content or a disk save injected on the first event.
let (mut cx, mut editor) = crdt_editor_with_engine(CrdtController::default());
assert!(editor.initialized);
editor.handle_event(
&mut cx,
&runtime_key_down(KeyCode::Escape),
&mut Scope::empty(),
);
assert!(
editor.engine().projection.blocks.is_empty(),
"boot seed must not touch a document the host already installed"
);
}
#[test]
fn seed_demo_doc_builds_showcase_document() {
let (mut cx, mut editor) = crdt_editor_default();
editor.seed_demo_doc(&mut cx);
let projection = &editor.engine().projection;
let visible_text: Vec<&str> = projection
.blocks
.iter()
.map(|block| block.text.as_str())
.collect();
let title = projection
.blocks
.iter()
.find(|block| {
block
.text
.contains("Makepad Rich Text & Document Engineering Specification")
})
.expect("title block");
assert!(
title
.runs
.iter()
.any(|run| run.bold && run.font_size >= 24.0),
"title carries the bold 24pt heading run"
);
assert!(visible_text
.iter()
.any(|text| text.contains("Click anywhere on the text above or inside any table cell")));
// One divider and one image node, in that order.
let kinds: Vec<&str> = projection
.nodes
.iter()
.map(|node| node.kind.as_str())
.collect();
assert_eq!(kinds, ["divider", "image"]);
// The 4x3 demo table: 12 written cells and a bold header row.
let table = projection.tables.values().next().expect("demo table");
assert_eq!(table.rows.len(), 4);
assert_eq!(table.columns.len(), 3);
assert_eq!(
table.cells.values().filter(|text| !text.is_empty()).count(),
12
);
assert!(table
.cells
.values()
.any(|text| text == "10,000+ items at 60 FPS"));
assert_eq!(
table
.cells
.get(&(table.rows[0].clone(), table.columns[0].clone()))
.map(String::as_str),
Some("Feature Component")
);
let header_runs = table
.cell_runs
.get(&(table.rows[0].clone(), table.columns[0].clone()))
.expect("header cell runs");
assert!(
header_runs.iter().all(|run| run.bold),
"table header row renders bold"
);
}
// ---- persistence store migration --------------------------------------
fn temp_store_dir(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"nigig-doc-persistence-{}-{tag}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("temp dir");
dir
}
#[test]
fn saved_doc_round_trips_through_runtime_store() {
let store = temp_store_dir("round-trip");
save_doc_state_to(store.clone(), GENERATED_DOC_FILE, "{\"doc\":\"state\"}").expect("save");
assert_eq!(
load_saved_doc_state_with(store.clone(), store.join("no-manifest-here")),
Some("{\"doc\":\"state\"}".to_string())
);
let _ = std::fs::remove_dir_all(&store);
}
#[test]
fn runtime_store_takes_precedence_over_manifest_fallback() {
let store = temp_store_dir("store");
let manifest = temp_store_dir("manifest");
save_doc_state_to(store.clone(), GENERATED_DOC_FILE, "store version").expect("store save");
save_doc_state_to(manifest.clone(), GENERATED_DOC_FILE, "manifest version")
.expect("manifest save");
assert_eq!(
load_saved_doc_state_with(store.clone(), manifest.clone()),
Some("store version".to_string()),
"a document in the app-data store wins over the legacy source-tree copy"
);
let _ = std::fs::remove_dir_all(&store);
let _ = std::fs::remove_dir_all(&manifest);
}
#[test]
fn manifest_fallback_serves_legacy_saves_once() {
let store = temp_store_dir("empty-store");
let manifest = temp_store_dir("legacy-manifest");
// No file in the runtime store: the legacy source-tree copy is still
// honored (it is read-only now — new saves never go there).
save_doc_state_to(manifest.clone(), GENERATED_DOC_FILE, "legacy save").expect("manifest save");
assert_eq!(
load_saved_doc_state_with(store.clone(), manifest.clone()),
Some("legacy save".to_string())
);
let _ = std::fs::remove_dir_all(&store);
let _ = std::fs::remove_dir_all(&manifest);
}
#[test]
fn empty_or_missing_save_files_boot_fresh() {
let store = temp_store_dir("empty-file-store");
let manifest = temp_store_dir("missing-manifest");
save_doc_state_to(store.clone(), GENERATED_DOC_FILE, " \n").expect("save");
// An empty/whitespace save is treated as absent: the workspace boots
// fresh (demo seed) rather than feeding the loader a blank document.
assert_eq!(
load_saved_doc_state_with(store.clone(), manifest.clone()),
None
);
// And with nothing anywhere there is nothing to load.
let bare = temp_store_dir("bare");
assert_eq!(
load_saved_doc_state_with(bare.clone(), bare.join("none")),
None
);
let _ = std::fs::remove_dir_all(&store);
let _ = std::fs::remove_dir_all(&manifest);
let _ = std::fs::remove_dir_all(&bare);
}

View file

@ -1575,7 +1575,11 @@ impl BuildProjectsPage {
.borrow_mut::<spreadsheet_ui::SpreadsheetGrid>()
{
grid.set_cell_value(cx, target_row, target_col, &formula_out);
grid.select_cell(cx, target_row, target_col);
// select_cell() was retired upstream
// (spreadsheet-ui 6a3c467); set_selection_pair is the
// replacement selection API, and `self.view.redraw`
// below covers the paint.
grid.set_selection_pair((target_row, target_col), (target_row, target_col));
}
self.view
.label(cx, ids!(m_ss_cell_ref))
@ -1596,7 +1600,7 @@ impl BuildProjectsPage {
.borrow_mut::<spreadsheet_ui::SpreadsheetGrid>()
{
grid.set_cell_value(cx, target.0, target.1, &formula_text);
grid.select_cell(cx, target.0, target.1);
grid.set_selection_pair((target.0, target.1), (target.0, target.1));
}
self.view
.label(cx, ids!(m_ss_cell_ref))

View file

@ -131,7 +131,11 @@ fn workflow_lookup_rate() {
#[test]
fn workflow_default_rooms() {
let data = get_building_data();
let cost_data = data.get(&BuildingCategory::Residential).unwrap().first().unwrap();
let cost_data = data
.get(&BuildingCategory::Residential)
.unwrap()
.first()
.unwrap();
let default = get_default_rooms(cost_data, "Standard Bungalow");
assert!(
default.is_some(),

View file

@ -792,10 +792,13 @@ fn cad_toolbar_buttons_exist(app: TestApp) {
app.locator(Selector::id("beam_tool_btn")).wait_visible();
app.locator(Selector::id("column_tool_btn")).wait_visible();
app.locator(Selector::id("add_cube_button")).wait_visible();
app.locator(Selector::id("add_cylinder_button")).wait_visible();
app.locator(Selector::id("add_sphere_button")).wait_visible();
app.locator(Selector::id("add_cylinder_button"))
.wait_visible();
app.locator(Selector::id("add_sphere_button"))
.wait_visible();
app.locator(Selector::id("add_door_button")).wait_visible();
app.locator(Selector::id("add_window_button")).wait_visible();
app.locator(Selector::id("add_window_button"))
.wait_visible();
}
#[makepad_test]
@ -847,7 +850,8 @@ fn cad_toolbar_snap_ortho_polar_buttons_exist(app: TestApp) {
app.locator(Selector::id("snap_toggle_btn")).wait_visible();
app.locator(Selector::id("ortho_toggle_btn")).wait_visible();
app.locator(Selector::id("polar_toggle_btn")).wait_visible();
app.locator(Selector::id("snap_step_dropdown")).wait_visible();
app.locator(Selector::id("snap_step_dropdown"))
.wait_visible();
}
#[makepad_test]
@ -870,8 +874,10 @@ fn cad_toolbar_visibility_buttons_exist(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("clip_toggle_btn")).wait_visible();
app.locator(Selector::id("constr_toggle_btn")).wait_visible();
app.locator(Selector::id("constr_export_btn")).wait_visible();
app.locator(Selector::id("constr_toggle_btn"))
.wait_visible();
app.locator(Selector::id("constr_export_btn"))
.wait_visible();
}
// ── Tool selection tests ──────────────────────────────────────
@ -882,7 +888,8 @@ fn cad_select_tool_default(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("select_tool_btn")).wait_text("Sel");
app.locator(Selector::id("select_tool_btn"))
.wait_text("Sel");
}
#[makepad_test]
@ -918,7 +925,8 @@ fn cad_tool_key_switches_via_button_text(app: TestApp) {
app.locator(Selector::id("cad_viewport")).click();
app.press_key(makepad_test::KeyCode::KeyC);
// Verify button text changed to "Cir"
app.locator(Selector::id("circle_tool_btn")).wait_text("Cir");
app.locator(Selector::id("circle_tool_btn"))
.wait_text("Cir");
}
// ── Drawing creation tests ─────────────────────────────────────
@ -947,7 +955,8 @@ fn cad_draw_circle_with_radius(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("circle_tool_btn")).click();
app.locator(Selector::id("status_label")).wait_text("Circle");
app.locator(Selector::id("status_label"))
.wait_text("Circle");
// Start circle
app.locator(Selector::id("cad_viewport")).click();
// Type radius value + Enter (DDE)
@ -955,7 +964,6 @@ fn cad_draw_circle_with_radius(app: TestApp) {
app.press_key(makepad_test::KeyCode::ReturnKey);
}
#[makepad_test]
fn cad_draw_wall_segment(app: TestApp) {
if !require_project_loaded() {
@ -976,13 +984,13 @@ fn cad_draw_column_with_radius(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("column_tool_btn")).click();
app.locator(Selector::id("status_label")).wait_text("Column");
app.locator(Selector::id("status_label"))
.wait_text("Column");
app.locator(Selector::id("cad_viewport")).click();
app.press_key(makepad_test::KeyCode::Key3);
app.press_key(makepad_test::KeyCode::ReturnKey);
}
#[makepad_test]
fn cad_draw_beam_with_ibeam_section(app: TestApp) {
if !require_project_loaded() {
@ -1035,7 +1043,8 @@ fn cad_delete_tool_removes_parts(app: TestApp) {
app.locator(Selector::id("cad_viewport")).click();
// Switch to delete tool
app.locator(Selector::id("delete_tool_btn")).click();
app.locator(Selector::id("status_label")).wait_text("Delete");
app.locator(Selector::id("status_label"))
.wait_text("Delete");
// Select and delete
app.locator(Selector::id("cad_viewport")).click();
app.locator(Selector::id("cad_viewport")).wait_visible();
@ -1052,7 +1061,8 @@ fn cad_escape_cancels_active_tool(app: TestApp) {
// Cancel with Escape
app.press_key(makepad_test::KeyCode::Escape);
// Should return to Select
app.locator(Selector::id("status_label")).wait_text("Select");
app.locator(Selector::id("status_label"))
.wait_text("Select");
}
// ── View manipulation tests ────────────────────────────────────
@ -1064,16 +1074,20 @@ fn cad_plane_toggle_cycles_workplanes(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
// Initially the plane button should show "XY Plan"
app.locator(Selector::id("plane_toggle_btn")).wait_text("XY Plan");
app.locator(Selector::id("plane_toggle_btn"))
.wait_text("XY Plan");
// Toggle to XZ
app.locator(Selector::id("plane_toggle_btn")).click();
app.locator(Selector::id("plane_toggle_btn")).wait_text("XZ Front");
app.locator(Selector::id("plane_toggle_btn"))
.wait_text("XZ Front");
// Toggle to YZ
app.locator(Selector::id("plane_toggle_btn")).click();
app.locator(Selector::id("plane_toggle_btn")).wait_text("YZ Side");
app.locator(Selector::id("plane_toggle_btn"))
.wait_text("YZ Side");
// Toggle back to XY
app.locator(Selector::id("plane_toggle_btn")).click();
app.locator(Selector::id("plane_toggle_btn")).wait_text("XY Plan");
app.locator(Selector::id("plane_toggle_btn"))
.wait_text("XY Plan");
}
#[makepad_test]
@ -1084,15 +1098,20 @@ fn cad_workplane_rotation_cycle(app: TestApp) {
app.locator(Selector::id("cad_viewport")).wait_visible();
// Rot WP cycles: 0 -> 15 -> 30 -> 45 -> 90 -> -45 -> 0
app.locator(Selector::id("rot_wp_btn")).click();
app.locator(Selector::id("rot_wp_btn")).wait_text("XY Plan 15°");
app.locator(Selector::id("rot_wp_btn"))
.wait_text("XY Plan 15°");
app.locator(Selector::id("rot_wp_btn")).click();
app.locator(Selector::id("rot_wp_btn")).wait_text("XY Plan 30°");
app.locator(Selector::id("rot_wp_btn"))
.wait_text("XY Plan 30°");
app.locator(Selector::id("rot_wp_btn")).click();
app.locator(Selector::id("rot_wp_btn")).wait_text("XY Plan 45°");
app.locator(Selector::id("rot_wp_btn"))
.wait_text("XY Plan 45°");
app.locator(Selector::id("rot_wp_btn")).click();
app.locator(Selector::id("rot_wp_btn")).wait_text("XY Plan 90°");
app.locator(Selector::id("rot_wp_btn"))
.wait_text("XY Plan 90°");
app.locator(Selector::id("rot_wp_btn")).click();
app.locator(Selector::id("rot_wp_btn")).wait_text("XY Plan -45°");
app.locator(Selector::id("rot_wp_btn"))
.wait_text("XY Plan -45°");
app.locator(Selector::id("rot_wp_btn")).click();
app.locator(Selector::id("rot_wp_btn")).wait_text("XY Plan");
}
@ -1236,7 +1255,8 @@ fn cad_render_mode_dropdown_exists(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("render_mode_dropdown")).wait_visible();
app.locator(Selector::id("render_mode_dropdown"))
.wait_visible();
}
#[makepad_test]
@ -1245,7 +1265,8 @@ fn cad_view_toggle_button_exists(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("view_toggle_button")).wait_visible();
app.locator(Selector::id("view_toggle_button"))
.wait_visible();
}
// ── Export tests ───────────────────────────────────────────────
@ -1269,7 +1290,8 @@ fn cad_svg_export_switches_to_preview(app: TestApp) {
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("export_svg_btn")).click();
// SVG preview widget should become visible after export
app.locator(Selector::id("desktop_svg_preview")).wait_visible();
app.locator(Selector::id("desktop_svg_preview"))
.wait_visible();
}
#[makepad_test]
@ -1320,7 +1342,8 @@ fn cad_pdf_tab_button_exists(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("desktop_pdf_tab_btn")).wait_visible();
app.locator(Selector::id("desktop_pdf_tab_btn"))
.wait_visible();
}
#[makepad_test]
@ -1345,7 +1368,8 @@ fn cad_pdf_tab_then_back_to_script(app: TestApp) {
app.locator(Selector::id("desktop_pdf_page")).wait_visible();
// Switch back to Script
app.locator(Selector::id("desktop_editor_tab_btn")).click();
app.locator(Selector::id("desktop_script_page")).wait_visible();
app.locator(Selector::id("desktop_script_page"))
.wait_visible();
}
#[makepad_test]
@ -1355,7 +1379,8 @@ fn cad_pdf_preview_placeholder_visible(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("desktop_pdf_tab_btn")).click();
app.locator(Selector::id("desktop_preview_placeholder")).wait_visible();
app.locator(Selector::id("desktop_preview_placeholder"))
.wait_visible();
}
#[makepad_test]
@ -1368,7 +1393,8 @@ fn cad_pdf_tab_then_cost_tab(app: TestApp) {
app.locator(Selector::id("desktop_pdf_page")).wait_visible();
// Switch to Cost tab
app.locator(Selector::id("desktop_cost_tab_btn")).click();
app.locator(Selector::id("desktop_cost_page")).wait_visible();
app.locator(Selector::id("desktop_cost_page"))
.wait_visible();
}
// ── Code editor tests ──────────────────────────────────────────
@ -1379,7 +1405,8 @@ fn cad_script_editor_visible(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("desktop_script_page")).wait_visible();
app.locator(Selector::id("desktop_script_page"))
.wait_visible();
app.locator(Selector::id("cad_editor")).wait_visible();
}
@ -1389,10 +1416,14 @@ fn cad_editor_tabs_exist(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("desktop_editor_tab_btn")).wait_visible();
app.locator(Selector::id("desktop_cost_tab_btn")).wait_visible();
app.locator(Selector::id("desktop_split_tab_btn")).wait_visible();
app.locator(Selector::id("desktop_pdf_tab_btn")).wait_visible();
app.locator(Selector::id("desktop_editor_tab_btn"))
.wait_visible();
app.locator(Selector::id("desktop_cost_tab_btn"))
.wait_visible();
app.locator(Selector::id("desktop_split_tab_btn"))
.wait_visible();
app.locator(Selector::id("desktop_pdf_tab_btn"))
.wait_visible();
}
#[makepad_test]
@ -1401,12 +1432,16 @@ fn cad_code_editor_has_content(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("desktop_script_page")).wait_visible();
app.locator(Selector::id("desktop_script_page"))
.wait_visible();
// The editor should have some initial content
app.locator(Selector::id("cad_editor")).wait_visible();
app.locator(Selector::id("cad_editor")).click();
let dump = app.widget_dump();
assert!(dump.contains("cad_editor"), "Widget dump should contain cad_editor");
assert!(
dump.contains("cad_editor"),
"Widget dump should contain cad_editor"
);
}
// ── Cost estimation screen tests ───────────────────────────────
@ -1417,7 +1452,8 @@ fn cad_cost_tab_button_visible(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("desktop_cost_tab_btn")).wait_visible();
app.locator(Selector::id("desktop_cost_tab_btn"))
.wait_visible();
}
#[makepad_test]
@ -1427,8 +1463,10 @@ fn cad_cost_page_exists(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("desktop_cost_tab_btn")).click();
app.locator(Selector::id("desktop_cost_page")).wait_visible();
app.locator(Selector::id("cost_estimate_screen")).wait_visible();
app.locator(Selector::id("desktop_cost_page"))
.wait_visible();
app.locator(Selector::id("cost_estimate_screen"))
.wait_visible();
}
// ── AI pane tests ──────────────────────────────────────────────
@ -1440,7 +1478,8 @@ fn cad_ai_pane_exists(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("ai_status_label")).wait_visible();
app.locator(Selector::id("ai_generate_button")).wait_visible();
app.locator(Selector::id("ai_generate_button"))
.wait_visible();
app.locator(Selector::id("ai_cancel_button")).wait_visible();
app.locator(Selector::id("backend_dropdown")).wait_visible();
app.locator(Selector::id("cad_prompt_input")).wait_visible();
@ -1485,7 +1524,8 @@ fn cad_splitter_exists(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("workspace_split_toggle_btn")).wait_visible();
app.locator(Selector::id("workspace_split_toggle_btn"))
.wait_visible();
}
#[makepad_test]
@ -1494,11 +1534,14 @@ fn cad_workspace_split_toggle(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("workspace_split_toggle_btn")).click();
app.locator(Selector::id("workspace_split_toggle_btn"))
.click();
// Split viewport should appear
app.locator(Selector::id("split_viewport_layer")).wait_visible();
app.locator(Selector::id("split_viewport_layer"))
.wait_visible();
// Toggle back
app.locator(Selector::id("workspace_split_toggle_btn")).click();
app.locator(Selector::id("workspace_split_toggle_btn"))
.click();
app.locator(Selector::id("cad_viewport")).wait_visible();
}
@ -1510,7 +1553,8 @@ fn cad_properties_panel_exists(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("properties_panel_view")).wait_visible();
app.locator(Selector::id("properties_panel_view"))
.wait_visible();
}
// ── Status label tests ───────────────────────────────────────────
@ -1530,7 +1574,8 @@ fn cad_status_label_shows_select_initially(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("status_label")).wait_text("Select");
app.locator(Selector::id("status_label"))
.wait_text("Select");
}
// ── Mobile editor tests ──────────────────────────────────────────
@ -1541,7 +1586,8 @@ fn cad_mobile_editor_tab_button_exists(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("mobile_editor_tab_btn")).wait_visible();
app.locator(Selector::id("mobile_editor_tab_btn"))
.wait_visible();
}
#[makepad_test]
@ -1550,7 +1596,8 @@ fn cad_mobile_cost_tab_button_exists(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("mobile_cost_tab_btn")).wait_visible();
app.locator(Selector::id("mobile_cost_tab_btn"))
.wait_visible();
}
// ── Snap step dropdown tests ────────────────────────────────────
@ -1561,7 +1608,8 @@ fn cad_snap_step_dropdown_visible(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("snap_step_dropdown")).wait_visible();
app.locator(Selector::id("snap_step_dropdown"))
.wait_visible();
}
// ── Extrude button test ──────────────────────────────────────────
@ -1584,7 +1632,8 @@ fn cad_measure_tool_button(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("measure_tool_btn")).click();
app.locator(Selector::id("status_label")).wait_text("Measure");
app.locator(Selector::id("status_label"))
.wait_text("Measure");
}
// ── Delete tool test ─────────────────────────────────────────────
@ -1596,7 +1645,8 @@ fn cad_delete_tool_button(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("delete_tool_btn")).click();
app.locator(Selector::id("status_label")).wait_text("Delete");
app.locator(Selector::id("status_label"))
.wait_text("Delete");
}
// ── Arc tool test ────────────────────────────────────────────────
@ -1620,7 +1670,8 @@ fn cad_polyline_tool_button(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("polyline_tool_btn")).click();
app.locator(Selector::id("status_label")).wait_text("Polyline");
app.locator(Selector::id("status_label"))
.wait_text("Polyline");
}
// ── Area and Quad tool tests ─────────────────────────────────────
@ -1654,7 +1705,8 @@ fn cad_polygon_tool_button(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("polygon_tool_btn")).click();
app.locator(Selector::id("status_label")).wait_text("Polygon");
app.locator(Selector::id("status_label"))
.wait_text("Polygon");
}
#[makepad_test]
@ -1664,7 +1716,8 @@ fn cad_triplane_tool_button(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("triplane_tool_btn")).click();
app.locator(Selector::id("status_label")).wait_text("TriPlane");
app.locator(Selector::id("status_label"))
.wait_text("TriPlane");
}
// ── Extend and Chamfer tool tests ────────────────────────────────
@ -1676,7 +1729,8 @@ fn cad_extend_tool_button(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("extend_tool_btn")).click();
app.locator(Selector::id("status_label")).wait_text("Extend");
app.locator(Selector::id("status_label"))
.wait_text("Extend");
}
#[makepad_test]
@ -1686,7 +1740,8 @@ fn cad_chamfer_tool_button(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("chamfer_tool_btn")).click();
app.locator(Selector::id("status_label")).wait_text("Chamfer");
app.locator(Selector::id("status_label"))
.wait_text("Chamfer");
}
// ── Render mode test ─────────────────────────────────────────────
@ -1721,7 +1776,8 @@ fn cad_workspace_title_visible(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
app.locator(Selector::id("workspace_title_label")).wait_visible();
app.locator(Selector::id("workspace_title_label"))
.wait_visible();
}
// ── Status label text change test ────────────────────────────────
@ -1733,7 +1789,8 @@ fn cad_status_label_changes_with_tool(app: TestApp) {
}
app.locator(Selector::id("cad_viewport")).wait_visible();
// Start on Select
app.locator(Selector::id("status_label")).wait_text("Select");
app.locator(Selector::id("status_label"))
.wait_text("Select");
// Switch to Line
app.locator(Selector::id("cad_viewport")).click();
app.press_key(makepad_test::KeyCode::KeyL);
@ -1766,9 +1823,13 @@ fn cad_snap_toggle_button_checkable(app: TestApp) {
return;
}
app.locator(Selector::id("cad_viewport")).wait_visible();
let before = app.locator(Selector::id("snap_toggle_btn")).assert_enabled(true);
let before = app
.locator(Selector::id("snap_toggle_btn"))
.assert_enabled(true);
app.locator(Selector::id("snap_toggle_btn")).click();
let after = app.locator(Selector::id("snap_toggle_btn")).assert_enabled(true);
let after = app
.locator(Selector::id("snap_toggle_btn"))
.assert_enabled(true);
app.locator(Selector::id("cad_viewport")).wait_visible();
}