Compare commits

..

No commits in common. "d62cc13d344877deed876a615fa59c1ca877f2a8" and "ea98d4d95c0a0a93846853bf3f113ab262af564a" have entirely different histories.

8 changed files with 85 additions and 802 deletions

View file

@ -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.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) | | 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 ## 9. Persistence round trip (optional but cheap)
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 | | # | 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 |
| 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.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 ## 10. Sign-off

View file

@ -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 re-verifies only the platform-owned residuals. No code changes in
this milestone beyond documentation; the roadmap box gains a pointer this milestone beyond documentation; the roadmap box gains a pointer
to the runbook for the hardware session. 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,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::makepad_platform::event::TouchState;
use makepad_widgets::*; use makepad_widgets::*;
use std::cell::RefCell; use std::cell::RefCell;
@ -23,7 +22,6 @@ use crate::construction_frame::pages::workspace::doc::{
InteractionMode, MobileGestureAction, MobileGestureRouter, MobileGestureState, InteractionMode, MobileGestureAction, MobileGestureRouter, MobileGestureState,
}; };
use doc_engine::controller::DocumentController; use doc_engine::controller::DocumentController;
use doc_engine::projection::TextStylePatch;
/// The mobile clipboard-menu request the widget hands to the platform /// The mobile clipboard-menu request the widget hands to the platform
/// after a long-press selection (Edit mode), mirrored on the widget so /// after a long-press selection (Edit mode), mirrored on the widget so
@ -96,14 +94,6 @@ pub struct CrdtDocEditor {
/// actually touched. /// actually touched.
#[rust] #[rust]
pub mobile_mode_initialized: bool, 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 /// Latest mobile clipboard-menu request; `None` until a long-press
/// selection in Edit mode requests one (View mode stays passive). /// selection in Edit mode requests one (View mode stays passive).
#[rust] #[rust]
@ -127,7 +117,6 @@ impl CrdtDocEditor {
pub fn set_engine(&mut self, cx: &mut Cx, engine: doc_engine::controller::DocumentController) { pub fn set_engine(&mut self, cx: &mut Cx, engine: doc_engine::controller::DocumentController) {
self.engine = engine; self.engine = engine;
self.layout_cache.borrow_mut().take(); self.layout_cache.borrow_mut().take();
self.initialized = true;
self.redraw(cx); self.redraw(cx);
} }
@ -304,304 +293,6 @@ impl CrdtDocEditor {
true 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; /// The caret atom may have been consumed by undo/redo or a merge;
/// re-anchor caret and block on the closest live glyph. /// re-anchor caret and block on the closest live glyph.
fn sanitize_cursor(&mut self) { 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<String>) -> Option<String> {
saved.and_then(|saved| crdt_engine_from_saved(&saved).map(|_| saved))
}
impl Widget for CrdtDocEditor { impl Widget for CrdtDocEditor {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) { 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 // The first real touch sequence adopts the mobile interaction
// policy (mirroring the legacy editor): View mode keeps taps and // policy (mirroring the legacy editor): View mode keeps taps and
// long-press/handle gestures but closes the IME and gates the // long-press/handle gestures but closes the IME and gates the

View file

@ -1,74 +1,42 @@
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::{Path, PathBuf};
const GENERATED_DIR: &str = "generated"; 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; pub(crate) const MAX_UNDO_LEVELS: usize = 100;
/// Root directory for doc state written at runtime. fn doc_manifest_path() -> PathBuf {
/// Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf()
/// 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")
} }
/// Directory holding the runtime-saved document (`nigig_build_store/generated`). fn doc_generated_dir_path() -> PathBuf {
pub fn doc_generated_dir_path() -> PathBuf { doc_manifest_path().join(GENERATED_DIR)
doc_store_dir().join(GENERATED_DIR)
} }
/// The legacy save location inside the source tree (development checkouts fn doc_generated_file_path() -> PathBuf {
/// only): read as a fallback, never written. doc_generated_dir_path().join(GENERATED_DOC_FILE)
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> { pub fn load_saved_doc_state() -> Option<String> {
load_saved_doc_state_with(doc_generated_dir_path(), dev_manifest_generated_dir_path()) fs::read_to_string(doc_generated_file_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() .ok()
.filter(|source| !source.trim().is_empty()) .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> { pub fn save_doc_state(data: &str) -> Result<(), String> {
save_doc_state_to(doc_generated_dir_path(), GENERATED_DOC_FILE, data) let dir = doc_generated_dir_path();
}
/// 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> {
fs::create_dir_all(&dir) fs::create_dir_all(&dir)
.map_err(|err| format!("could not create generated directory: {err}"))?; .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}"))?; .map_err(|err| format!("could not save doc state: {err}"))?;
Ok(()) 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(())
}

View file

@ -5226,233 +5226,3 @@ fn layout_tree_cache_drops_on_engine_replacement() {
"the new document caches normally afterwards" "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,11 +1575,7 @@ impl BuildProjectsPage {
.borrow_mut::<spreadsheet_ui::SpreadsheetGrid>() .borrow_mut::<spreadsheet_ui::SpreadsheetGrid>()
{ {
grid.set_cell_value(cx, target_row, target_col, &formula_out); grid.set_cell_value(cx, target_row, target_col, &formula_out);
// select_cell() was retired upstream grid.select_cell(cx, target_row, target_col);
// (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 self.view
.label(cx, ids!(m_ss_cell_ref)) .label(cx, ids!(m_ss_cell_ref))
@ -1600,7 +1596,7 @@ impl BuildProjectsPage {
.borrow_mut::<spreadsheet_ui::SpreadsheetGrid>() .borrow_mut::<spreadsheet_ui::SpreadsheetGrid>()
{ {
grid.set_cell_value(cx, target.0, target.1, &formula_text); 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 self.view
.label(cx, ids!(m_ss_cell_ref)) .label(cx, ids!(m_ss_cell_ref))

View file

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

View file

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