diff --git a/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/DEVICE_VERIFICATION.md b/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/DEVICE_VERIFICATION.md index c2e9d96..6ad4af2 100644 --- a/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/DEVICE_VERIFICATION.md +++ b/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/DEVICE_VERIFICATION.md @@ -214,20 +214,12 @@ 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. 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. +## 9. Persistence round trip (optional but cheap) | # | Action | Expected | |---|--------|----------| -| 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.1 | Edit, force-close, relaunch | Document restores (platform save/load path); table contents AND cell text intact | | 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 diff --git a/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/README.md b/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/README.md index 8e826a6..41e6a1a 100644 --- a/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/README.md +++ b/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/README.md @@ -1446,55 +1446,3 @@ 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. diff --git a/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/crdt_widget.rs b/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/crdt_widget.rs index 9a659a2..9bfce34 100644 --- a/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/crdt_widget.rs +++ b/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/crdt_widget.rs @@ -1,4 +1,3 @@ -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; @@ -23,7 +22,6 @@ 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 @@ -96,14 +94,6 @@ 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] @@ -127,7 +117,6 @@ 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); } @@ -304,304 +293,6 @@ 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) { @@ -2248,25 +1939,8 @@ 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) -> Option { - 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 diff --git a/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/persistence.rs b/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/persistence.rs index ec72b39..985fcf7 100644 --- a/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/persistence.rs +++ b/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/persistence.rs @@ -1,74 +1,42 @@ use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; const GENERATED_DIR: &str = "generated"; -pub(crate) const GENERATED_DOC_FILE: &str = "current.doc.json"; +const GENERATED_DOC_FILE: &str = "current.doc.json"; pub(crate) const MAX_UNDO_LEVELS: usize = 100; -/// 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_manifest_path() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf() } -/// 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_dir_path() -> PathBuf { + doc_manifest_path().join(GENERATED_DIR) } -/// 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) +fn doc_generated_file_path() -> PathBuf { + doc_generated_dir_path().join(GENERATED_DOC_FILE) } -/// 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 { - load_saved_doc_state_with(doc_generated_dir_path(), dev_manifest_generated_dir_path()) + fs::read_to_string(doc_generated_file_path()) + .ok() + .filter(|source| !source.trim().is_empty()) } -pub(crate) fn load_saved_doc_state_with( - store_dir: PathBuf, - manifest_dir: PathBuf, -) -> Option { - 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> { - 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> { - 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> { + 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(filename), data) + fs::write(dir.join(GENERATED_DOC_FILE), data) .map_err(|err| format!("could not save doc state: {err}"))?; Ok(()) } + +pub fn save_doc_state_as(filename: &str, 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(filename), data) + .map_err(|err| format!("could not save doc state as {filename}: {err}"))?; + Ok(()) +} diff --git a/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/tests.rs b/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/tests.rs index b0d7303..49a001a 100644 --- a/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/tests.rs +++ b/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/tests.rs @@ -5226,233 +5226,3 @@ 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); -} diff --git a/crates/apps/nigig-build/src/construction_frame/pages/workspace/project/mod.rs b/crates/apps/nigig-build/src/construction_frame/pages/workspace/project/mod.rs index c10e852..7e05224 100644 --- a/crates/apps/nigig-build/src/construction_frame/pages/workspace/project/mod.rs +++ b/crates/apps/nigig-build/src/construction_frame/pages/workspace/project/mod.rs @@ -1575,11 +1575,7 @@ impl BuildProjectsPage { .borrow_mut::() { grid.set_cell_value(cx, target_row, target_col, &formula_out); - // 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)); + grid.select_cell(cx, target_row, target_col); } self.view .label(cx, ids!(m_ss_cell_ref)) @@ -1600,7 +1596,7 @@ impl BuildProjectsPage { .borrow_mut::() { grid.set_cell_value(cx, target.0, target.1, &formula_text); - grid.set_selection_pair((target.0, target.1), (target.0, target.1)); + grid.select_cell(cx, target.0, target.1); } self.view .label(cx, ids!(m_ss_cell_ref)) diff --git a/crates/apps/nigig-build/tests/cost_estimator.rs b/crates/apps/nigig-build/tests/cost_estimator.rs index cb43f4e..47575f2 100644 --- a/crates/apps/nigig-build/tests/cost_estimator.rs +++ b/crates/apps/nigig-build/tests/cost_estimator.rs @@ -131,11 +131,7 @@ 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(), diff --git a/crates/apps/nigig-build/tests/ui.rs b/crates/apps/nigig-build/tests/ui.rs index 9329bf6..88b09ff 100644 --- a/crates/apps/nigig-build/tests/ui.rs +++ b/crates/apps/nigig-build/tests/ui.rs @@ -792,13 +792,10 @@ 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] @@ -850,8 +847,7 @@ 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] @@ -874,10 +870,8 @@ 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 ────────────────────────────────────── @@ -888,8 +882,7 @@ 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] @@ -925,8 +918,7 @@ 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 ───────────────────────────────────── @@ -955,8 +947,7 @@ 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) @@ -964,6 +955,7 @@ 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() { @@ -984,13 +976,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() { @@ -1043,8 +1035,7 @@ 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(); @@ -1061,8 +1052,7 @@ 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 ──────────────────────────────────── @@ -1074,20 +1064,16 @@ 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] @@ -1098,20 +1084,15 @@ 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"); } @@ -1255,8 +1236,7 @@ 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] @@ -1265,8 +1245,7 @@ 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 ─────────────────────────────────────────────── @@ -1290,8 +1269,7 @@ 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] @@ -1342,8 +1320,7 @@ 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] @@ -1368,8 +1345,7 @@ 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] @@ -1379,8 +1355,7 @@ 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] @@ -1393,8 +1368,7 @@ 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 ────────────────────────────────────────── @@ -1405,8 +1379,7 @@ 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(); } @@ -1416,14 +1389,10 @@ 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] @@ -1432,16 +1401,12 @@ 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 ─────────────────────────────── @@ -1452,8 +1417,7 @@ 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] @@ -1463,10 +1427,8 @@ 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 ────────────────────────────────────────────── @@ -1478,8 +1440,7 @@ 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(); @@ -1524,8 +1485,7 @@ 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] @@ -1534,14 +1494,11 @@ 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(); } @@ -1553,8 +1510,7 @@ 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 ─────────────────────────────────────────── @@ -1574,8 +1530,7 @@ 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 ────────────────────────────────────────── @@ -1586,8 +1541,7 @@ 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] @@ -1596,8 +1550,7 @@ 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 ──────────────────────────────────── @@ -1608,8 +1561,7 @@ 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 ────────────────────────────────────────── @@ -1632,8 +1584,7 @@ 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 ───────────────────────────────────────────── @@ -1645,8 +1596,7 @@ 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 ──────────────────────────────────────────────── @@ -1670,8 +1620,7 @@ 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 ───────────────────────────────────── @@ -1705,8 +1654,7 @@ 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] @@ -1716,8 +1664,7 @@ 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 ──────────────────────────────── @@ -1729,8 +1676,7 @@ 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] @@ -1740,8 +1686,7 @@ 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 ───────────────────────────────────────────── @@ -1776,8 +1721,7 @@ 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 ──────────────────────────────── @@ -1789,8 +1733,7 @@ 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); @@ -1823,13 +1766,9 @@ 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(); }