Compare commits
3 commits
ea98d4d95c
...
d62cc13d34
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d62cc13d34 | ||
|
|
ff0cba1b80 | ||
|
|
5e5adf962d |
8 changed files with 802 additions and 85 deletions
|
|
@ -214,12 +214,20 @@ eyeball; photograph failures.
|
|||
| 8.5 | Advanced placeholders (image/divider nodes) | Fills/borders/labels drawn once per node; divider is one centered line |
|
||||
| 8.6 | Rapid typing for 30 seconds in a ~200-line document | Frame pacing stays smooth; no visible full-document flicker between keystrokes (layout cache: unchanged content redraws from the cached tree; this is the perf smoke check) |
|
||||
|
||||
## 9. Persistence round trip (optional but cheap)
|
||||
## 9. Boot content and persistence round trip
|
||||
|
||||
Since the Android empty-doc fix, saves live in the platform app-data
|
||||
store (`app_data_dir()/nigig_build_store/generated/current.doc.json`)
|
||||
— NOT the source tree (dev checkouts get a one-way read fallback for
|
||||
old saves; new writes never go there). Boot emits `[DOC_TRACE] CRDT
|
||||
init:` lines in logcat naming the branch that fired.
|
||||
|
||||
| # | Action | Expected |
|
||||
|---|--------|----------|
|
||||
| 9.1 | Edit, force-close, relaunch | Document restores (platform save/load path); table contents AND cell text intact |
|
||||
| 9.0 | Fresh install, first launch (no save on device) | Demo document renders: bold title, styled paragraphs, divider, image placeholder, 4x3 table (bold header), closing hint. logcat: `CRDT init: no saved document; seeding demo document` |
|
||||
| 9.1 | Edit, force-close, relaunch | Document restores (platform save/load path); table contents AND cell text intact. logcat: `CRDT init: loading saved document (N bytes)` |
|
||||
| 9.2 | Open a previously saved file with a table | Grid renders; merges present; no phantom rows/cols |
|
||||
| 9.3 | Boot the CRDT workspace with a CLASSIC-format save present | Demo document seeds (classic saves are not shadowed); logcat notes the classic-format branch. The legacy workspace still opens the classic file on Open. FAIL criteria: blank page, or the classic file silently dropped |
|
||||
|
||||
## 10. Sign-off
|
||||
|
||||
|
|
|
|||
|
|
@ -1446,3 +1446,55 @@ suite covers the logic behind each row, so the runbook deliberately
|
|||
re-verifies only the platform-owned residuals. No code changes in
|
||||
this milestone beyond documentation; the roadmap box gains a pointer
|
||||
to the runbook for the hardware session.
|
||||
|
||||
## Boot-time document init and app-data persistence (Android empty-doc fix)
|
||||
|
||||
The first hardware run of the roadmap surface exposed a compound
|
||||
defect no sandbox gate could see: the Android APK (`pageflipnav`)
|
||||
booted the doc workspace to a BLANK page. Two independent causes:
|
||||
|
||||
1. **The CRDT editor had no boot init.** The legacy `DocEditor` seeds
|
||||
the showcase document behind an `initialized` flag on first draw,
|
||||
but `CrdtDocEditor` — the ACTIVE editor since the navigation switch
|
||||
— starts from `DocumentController::default()` (an empty projection)
|
||||
and only ever gained content through an interactive action a fresh
|
||||
install has not performed yet.
|
||||
2. **Persistence pointed at the build machine's source tree.**
|
||||
`persistence.rs` resolved its save file under
|
||||
`env!("CARGO_MANIFEST_DIR")`, an absolute path baked in at compile
|
||||
time. On device that path does not exist, so Open silently read
|
||||
nothing and Save silently wrote nowhere (`.ok()` swallowed the
|
||||
failure); on a developer machine the app polluted its own checkout.
|
||||
|
||||
The fix mirrors the legacy boot contract exactly once, inside the
|
||||
editor: the first event handled by a factory-fresh editor runs
|
||||
`init_document`, which loads the on-disk save when it decodes as
|
||||
`#MP_CRDT_V1` wire (`initial_document_source` is the gate — classic-
|
||||
format saves belong to the legacy workspace's first-edit migration and
|
||||
must not be shadowed) and otherwise calls `seed_demo_doc`, a CRDT
|
||||
mirror of the legacy `demo_doc_blocks()` showcase: styled headings,
|
||||
accent runs, a divider, an image node, the 4x3 table with a bold
|
||||
header, and the closing hint. `set_engine` flips the same flag, so a
|
||||
host that installs its own document before the first event is never
|
||||
overwritten by the seed (this also keeps every runtime test harness
|
||||
deterministic).
|
||||
|
||||
Persistence migrates to the crate-wide convention
|
||||
(`crate::dir::app_data_dir()`, the root the CAD store already uses):
|
||||
writes go ONLY to `nigig_build_store/generated/current.doc.json`
|
||||
there, while reads keep a one-way fallback to the legacy source-tree
|
||||
file so an unreplicated developer save is honored once. Both the boot
|
||||
and the migration emit `[DOC_TRACE]` lines (mirroring the legacy
|
||||
boot's instrumentation) so a device `logcat` session confirms which
|
||||
branch fired.
|
||||
|
||||
Tests pin the whole contract: the boot-source gate (valid CRDT wire
|
||||
boots verbatim; classic JSON and `None` both route to the demo seed),
|
||||
a runtime boot test (first event on a factory-fresh editor flips the
|
||||
flag and leaves a non-empty projection, source-agnostic by design), a
|
||||
no-overwrite guard for host-installed engines, a full structural
|
||||
assertion of the seeded showcase (heading runs, node kinds in order,
|
||||
the 4x3/12-cell table with bold header), and four persistence tests
|
||||
over temp dirs covering the round trip, store-beats-manifest
|
||||
precedence, the manifest fallback, and empty-file rejection.
|
||||
`DEVICE_VERIFICATION.md` section 9 gained the matching hardware rows.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::construction_frame::pages::workspace::doc::persistence::load_saved_doc_state;
|
||||
use makepad_widgets::makepad_platform::event::TouchState;
|
||||
use makepad_widgets::*;
|
||||
use std::cell::RefCell;
|
||||
|
|
@ -22,6 +23,7 @@ use crate::construction_frame::pages::workspace::doc::{
|
|||
InteractionMode, MobileGestureAction, MobileGestureRouter, MobileGestureState,
|
||||
};
|
||||
use doc_engine::controller::DocumentController;
|
||||
use doc_engine::projection::TextStylePatch;
|
||||
|
||||
/// The mobile clipboard-menu request the widget hands to the platform
|
||||
/// after a long-press selection (Edit mode), mirrored on the widget so
|
||||
|
|
@ -94,6 +96,14 @@ pub struct CrdtDocEditor {
|
|||
/// actually touched.
|
||||
#[rust]
|
||||
pub mobile_mode_initialized: bool,
|
||||
/// Boot flag: the first handled event seeds the document — the saved
|
||||
/// on-disk state when one exists and decodes as CRDT wire, otherwise
|
||||
/// the demo document (mirroring the legacy `DocEditor::initialized`
|
||||
/// boot so a fresh install, most visibly a mobile APK, never opens an
|
||||
/// empty page). `set_engine` also sets it: a host that installs its
|
||||
/// own document before the first event keeps it untouched.
|
||||
#[rust]
|
||||
pub initialized: bool,
|
||||
/// Latest mobile clipboard-menu request; `None` until a long-press
|
||||
/// selection in Edit mode requests one (View mode stays passive).
|
||||
#[rust]
|
||||
|
|
@ -117,6 +127,7 @@ impl CrdtDocEditor {
|
|||
pub fn set_engine(&mut self, cx: &mut Cx, engine: doc_engine::controller::DocumentController) {
|
||||
self.engine = engine;
|
||||
self.layout_cache.borrow_mut().take();
|
||||
self.initialized = true;
|
||||
self.redraw(cx);
|
||||
}
|
||||
|
||||
|
|
@ -293,6 +304,304 @@ impl CrdtDocEditor {
|
|||
true
|
||||
}
|
||||
|
||||
/// One-time boot content, mirroring the legacy editor's `initialized`
|
||||
/// gate: load the saved document when it decodes as CRDT wire
|
||||
/// (classic-format saves stay with the legacy workspace, which owns
|
||||
/// their first-edit migration), otherwise seed the demo document so
|
||||
/// the page never opens blank. No-op when the projection already
|
||||
/// carries content (a host installed an engine before the first
|
||||
/// event, or open/save has already run).
|
||||
fn init_document(&mut self, cx: &mut Cx) {
|
||||
if !self.engine.projection.blocks.is_empty() {
|
||||
return;
|
||||
}
|
||||
let saved = load_saved_doc_state();
|
||||
let had_classic_save = saved.is_some();
|
||||
match initial_document_source(saved) {
|
||||
Some(saved) => {
|
||||
eprintln!(
|
||||
"[DOC_TRACE] CRDT init: loading saved document ({} bytes)",
|
||||
saved.len()
|
||||
);
|
||||
self.deserialize(cx, &saved);
|
||||
}
|
||||
None => {
|
||||
eprintln!(
|
||||
"[DOC_TRACE] CRDT init: {}; seeding demo document",
|
||||
if had_classic_save {
|
||||
"saved document is classic-format (open it in the legacy workspace)"
|
||||
} else {
|
||||
"no saved document"
|
||||
}
|
||||
);
|
||||
self.seed_demo_doc(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Seeds the CRDT mirror of the legacy `demo_doc_blocks()` showcase
|
||||
/// (heading, styled paragraphs, divider, image node, 4x3 table, and
|
||||
/// a closing hint), chained through `after` anchors into one
|
||||
/// top-level document order so layout mirrors the legacy page.
|
||||
pub(crate) fn seed_demo_doc(&mut self, cx: &mut Cx) {
|
||||
const ACTOR: &str = "local";
|
||||
let heading_patch = |font_size: f32, color: (f32, f32, f32, f32)| TextStylePatch {
|
||||
bold: Some(true),
|
||||
font_size: Some(font_size),
|
||||
color: Some(Some(color)),
|
||||
..Default::default()
|
||||
};
|
||||
let title_color = (0.12, 0.15, 0.35, 1.0);
|
||||
let section_color = (0.18, 0.25, 0.45, 1.0);
|
||||
|
||||
// 1. Title heading.
|
||||
let title = "Makepad Rich Text & Document Engineering Specification";
|
||||
let title_block = self
|
||||
.engine
|
||||
.insert_block(ACTOR, None, "heading")
|
||||
.expect("demo title block");
|
||||
self.engine
|
||||
.insert_text(ACTOR, title_block.clone(), None, title);
|
||||
self.engine.set_text_style_at_offsets(
|
||||
ACTOR,
|
||||
title_block.clone(),
|
||||
0,
|
||||
title.chars().count(),
|
||||
heading_patch(24.0, title_color),
|
||||
);
|
||||
|
||||
// 2. Lead paragraph with a bold red accent run.
|
||||
let lead_plain = "This rich document engine avoids standard ";
|
||||
let lead_accent = "TextInput widgets ";
|
||||
let lead_rest = "and instead implements an advanced custom widget layout engine. It \
|
||||
calculates exact glyph metrics, word-wrapping spans across line \
|
||||
boundaries, drawing custom blinking carets, and mapping character \
|
||||
offsets directly to style spans.";
|
||||
let lead_block = self
|
||||
.engine
|
||||
.insert_block(ACTOR, Some(title_block.clone()), "paragraph")
|
||||
.expect("demo lead block");
|
||||
let lead_len =
|
||||
lead_plain.chars().count() + lead_accent.chars().count() + lead_rest.chars().count();
|
||||
self.engine.insert_text(
|
||||
ACTOR,
|
||||
lead_block.clone(),
|
||||
None,
|
||||
format!("{lead_plain}{lead_accent}{lead_rest}"),
|
||||
);
|
||||
self.engine.set_text_style_at_offsets(
|
||||
ACTOR,
|
||||
lead_block.clone(),
|
||||
0,
|
||||
lead_len,
|
||||
TextStylePatch {
|
||||
font_size: Some(13.0),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
self.engine.set_text_style_at_offsets(
|
||||
ACTOR,
|
||||
lead_block.clone(),
|
||||
lead_plain.chars().count(),
|
||||
lead_plain.chars().count() + lead_accent.chars().count(),
|
||||
TextStylePatch {
|
||||
bold: Some(true),
|
||||
color: Some(Some((0.8, 0.2, 0.2, 1.0))),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// 3. Divider node.
|
||||
let divider = self
|
||||
.engine
|
||||
.insert_node(ACTOR, None, Some(lead_block.clone()), "divider", "")
|
||||
.expect("demo divider node");
|
||||
|
||||
// 4. First section heading.
|
||||
let section_one = "1. Scenegraph & Immediate Pass Compositing";
|
||||
let section_one_block = self
|
||||
.engine
|
||||
.insert_block(ACTOR, Some(divider.clone()), "heading")
|
||||
.expect("demo section-one block");
|
||||
self.engine
|
||||
.insert_text(ACTOR, section_one_block.clone(), None, section_one);
|
||||
self.engine.set_text_style_at_offsets(
|
||||
ACTOR,
|
||||
section_one_block.clone(),
|
||||
0,
|
||||
section_one.chars().count(),
|
||||
heading_patch(18.0, section_color),
|
||||
);
|
||||
|
||||
// 5. Body paragraph with a bold-italic run.
|
||||
let body_plain_a =
|
||||
"As noted in Makepad's architecture, renderpasses can be started and ended ";
|
||||
let body_accent = "nestedly without flattening the execution flow. ";
|
||||
let body_plain_b = "This allows creating composite trees in the same immediate mode flow \
|
||||
as drawing your UI. For instance, font rendering is a pass that \
|
||||
repaints incrementally, keeping static textures cached on hardware \
|
||||
like the Raspberry Pi 4.";
|
||||
let body_block = self
|
||||
.engine
|
||||
.insert_block(ACTOR, Some(section_one_block.clone()), "paragraph")
|
||||
.expect("demo body block");
|
||||
let body_len = body_plain_a.chars().count()
|
||||
+ body_accent.chars().count()
|
||||
+ body_plain_b.chars().count();
|
||||
self.engine.insert_text(
|
||||
ACTOR,
|
||||
body_block.clone(),
|
||||
None,
|
||||
format!("{body_plain_a}{body_accent}{body_plain_b}"),
|
||||
);
|
||||
self.engine.set_text_style_at_offsets(
|
||||
ACTOR,
|
||||
body_block.clone(),
|
||||
0,
|
||||
body_len,
|
||||
TextStylePatch {
|
||||
font_size: Some(13.0),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
self.engine.set_text_style_at_offsets(
|
||||
ACTOR,
|
||||
body_block.clone(),
|
||||
body_plain_a.chars().count(),
|
||||
body_plain_a.chars().count() + body_accent.chars().count(),
|
||||
TextStylePatch {
|
||||
bold: Some(true),
|
||||
italic: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// 6. Image node (state carries the caption, matching the widget's
|
||||
// `insert_image` encoding).
|
||||
let image = self
|
||||
.engine
|
||||
.insert_node(
|
||||
ACTOR,
|
||||
None,
|
||||
Some(body_block.clone()),
|
||||
"image",
|
||||
"Render-Target Graph vs Scenegraph Incremental Repainting",
|
||||
)
|
||||
.expect("demo image node");
|
||||
|
||||
// 7. Second section heading.
|
||||
let section_two = "2. Inline Interactive Table Example";
|
||||
let section_two_block = self
|
||||
.engine
|
||||
.insert_block(ACTOR, Some(image.clone()), "heading")
|
||||
.expect("demo section-two block");
|
||||
self.engine
|
||||
.insert_text(ACTOR, section_two_block.clone(), None, section_two);
|
||||
self.engine.set_text_style_at_offsets(
|
||||
ACTOR,
|
||||
section_two_block.clone(),
|
||||
0,
|
||||
section_two.chars().count(),
|
||||
heading_patch(18.0, section_color),
|
||||
);
|
||||
|
||||
// 8. The interactive 4x3 table (4 rows x 3 columns), header row in
|
||||
// bold like the legacy demo.
|
||||
let table = self
|
||||
.engine
|
||||
.insert_table(ACTOR, Some(section_two_block.clone()))
|
||||
.expect("demo table block");
|
||||
let mut rows = Vec::new();
|
||||
for _ in 0..4 {
|
||||
rows.push(
|
||||
self.engine
|
||||
.insert_table_row(ACTOR, table.clone(), rows.last().cloned())
|
||||
.expect("demo table row"),
|
||||
);
|
||||
}
|
||||
let mut columns = Vec::new();
|
||||
for _ in 0..3 {
|
||||
columns.push(
|
||||
self.engine
|
||||
.insert_table_column(ACTOR, table.clone(), columns.last().cloned())
|
||||
.expect("demo table column"),
|
||||
);
|
||||
}
|
||||
let cells = [
|
||||
[
|
||||
"Feature Component",
|
||||
"Implementation Technique",
|
||||
"Performance Impact",
|
||||
],
|
||||
[
|
||||
"Text Engine",
|
||||
"Instance-packed clipping & glyph positioning",
|
||||
"10,000+ items at 60 FPS",
|
||||
],
|
||||
[
|
||||
"Spreadsheet Grid",
|
||||
"Virtual viewport + turtle drawpass",
|
||||
"O(visible cells) only",
|
||||
],
|
||||
[
|
||||
"Doc Editor Table",
|
||||
"Inline block layout & cell cursor mapping",
|
||||
"Seamless multi-block edit",
|
||||
],
|
||||
];
|
||||
let writes: Vec<(doc_engine::crdt::OpId, doc_engine::crdt::OpId, String)> = rows
|
||||
.iter()
|
||||
.zip(cells.iter())
|
||||
.flat_map(|(row, texts)| {
|
||||
columns
|
||||
.iter()
|
||||
.zip(texts.iter())
|
||||
.map(|(column, text)| (row.clone(), column.clone(), text.to_string()))
|
||||
})
|
||||
.collect();
|
||||
self.engine.set_table_cells(ACTOR, table.clone(), writes);
|
||||
for (column, text) in columns.iter().zip(cells[0].iter()) {
|
||||
self.engine.set_table_cell_style(
|
||||
ACTOR,
|
||||
doc_engine::controller::TableCellRef {
|
||||
table: table.clone(),
|
||||
row: rows[0].clone(),
|
||||
column: column.clone(),
|
||||
},
|
||||
0,
|
||||
text.chars().count(),
|
||||
TextStylePatch {
|
||||
bold: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 9. Closing hint paragraph.
|
||||
let closing = "Click anywhere on the text above or inside any table cell to test live \
|
||||
caret placement and typing support!";
|
||||
let closing_block = self
|
||||
.engine
|
||||
.insert_block(ACTOR, Some(table.clone()), "paragraph")
|
||||
.expect("demo closing block");
|
||||
self.engine
|
||||
.insert_text(ACTOR, closing_block.clone(), None, closing);
|
||||
self.engine.set_text_style_at_offsets(
|
||||
ACTOR,
|
||||
closing_block.clone(),
|
||||
0,
|
||||
closing.chars().count(),
|
||||
TextStylePatch {
|
||||
italic: Some(true),
|
||||
font_size: Some(12.0),
|
||||
color: Some(Some((0.4, 0.45, 0.55, 1.0))),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
self.redraw(cx);
|
||||
}
|
||||
|
||||
/// The caret atom may have been consumed by undo/redo or a merge;
|
||||
/// re-anchor caret and block on the closest live glyph.
|
||||
fn sanitize_cursor(&mut self) {
|
||||
|
|
@ -1939,8 +2248,25 @@ impl CrdtDocEditor {
|
|||
}
|
||||
}
|
||||
|
||||
/// Decides what a fresh `CrdtDocEditor` boots with: the on-disk save when
|
||||
/// it decodes as `#MP_CRDT_V1` wire, otherwise nothing (the caller then
|
||||
/// seeds the demo document). Classic-format saves return `None` here so
|
||||
/// the demo page never shadows them — the legacy workspace still owns
|
||||
/// their first-edit migration.
|
||||
pub(crate) fn initial_document_source(saved: Option<String>) -> Option<String> {
|
||||
saved.and_then(|saved| crdt_engine_from_saved(&saved).map(|_| saved))
|
||||
}
|
||||
|
||||
impl Widget for CrdtDocEditor {
|
||||
fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {
|
||||
// Boot init runs once, before any event acts on the (factory-empty)
|
||||
// projection: a fresh install otherwise renders a blank page — the
|
||||
// exact failure seen on Android, where the CRDT editor starts with
|
||||
// no document and no user action has seeded one yet.
|
||||
if !self.initialized {
|
||||
self.initialized = true;
|
||||
self.init_document(cx);
|
||||
}
|
||||
// The first real touch sequence adopts the mobile interaction
|
||||
// policy (mirroring the legacy editor): View mode keeps taps and
|
||||
// long-press/handle gestures but closes the IME and gates the
|
||||
|
|
|
|||
|
|
@ -1,42 +1,74 @@
|
|||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const GENERATED_DIR: &str = "generated";
|
||||
const GENERATED_DOC_FILE: &str = "current.doc.json";
|
||||
pub(crate) const GENERATED_DOC_FILE: &str = "current.doc.json";
|
||||
pub(crate) const MAX_UNDO_LEVELS: usize = 100;
|
||||
|
||||
fn doc_manifest_path() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf()
|
||||
/// Root directory for doc state written at runtime.
|
||||
///
|
||||
/// This used to be `env!("CARGO_MANIFEST_DIR")`, which bakes the **build
|
||||
/// machine's** absolute source path into the shipped binary. On an Android
|
||||
/// or iOS install that path does not exist, so the workspace's Open/Save
|
||||
/// buttons silently did nothing (the CRDT editor additionally had no demo
|
||||
/// fallback, so a fresh install booted to an empty document); and on a
|
||||
/// developer machine the app wrote into its own source tree.
|
||||
///
|
||||
/// `app_data_dir()` is the convention the rest of this crate already uses
|
||||
/// (see `cad_store::cad_projects_dir` / `cad_persistence::cad_data_dir`).
|
||||
/// Reads keep a one-way compatibility fallback to the old source-tree
|
||||
/// location so existing developer saves are honored once; new saves only
|
||||
/// ever go to the app data dir.
|
||||
pub fn doc_store_dir() -> PathBuf {
|
||||
crate::dir::app_data_dir().join("nigig_build_store")
|
||||
}
|
||||
|
||||
fn doc_generated_dir_path() -> PathBuf {
|
||||
doc_manifest_path().join(GENERATED_DIR)
|
||||
/// Directory holding the runtime-saved document (`nigig_build_store/generated`).
|
||||
pub fn doc_generated_dir_path() -> PathBuf {
|
||||
doc_store_dir().join(GENERATED_DIR)
|
||||
}
|
||||
|
||||
fn doc_generated_file_path() -> PathBuf {
|
||||
doc_generated_dir_path().join(GENERATED_DOC_FILE)
|
||||
/// The legacy save location inside the source tree (development checkouts
|
||||
/// only): read as a fallback, never written.
|
||||
fn dev_manifest_generated_dir_path() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(GENERATED_DIR)
|
||||
}
|
||||
|
||||
/// Load the last-saved document. Prefers the runtime store; falls back to
|
||||
/// the legacy source-tree file for unreplicated developer saves. Returns
|
||||
/// `None` when nothing exists or the file is empty. Split from the two
|
||||
/// directory parameters so the migration logic is unit-testable against
|
||||
/// temp dirs.
|
||||
pub fn load_saved_doc_state() -> Option<String> {
|
||||
fs::read_to_string(doc_generated_file_path())
|
||||
load_saved_doc_state_with(doc_generated_dir_path(), dev_manifest_generated_dir_path())
|
||||
}
|
||||
|
||||
pub(crate) fn load_saved_doc_state_with(
|
||||
store_dir: PathBuf,
|
||||
manifest_dir: PathBuf,
|
||||
) -> Option<String> {
|
||||
let read = |dir: &PathBuf| {
|
||||
fs::read_to_string(dir.join(GENERATED_DOC_FILE))
|
||||
.ok()
|
||||
.filter(|source| !source.trim().is_empty())
|
||||
};
|
||||
read(&store_dir).or_else(|| read(&manifest_dir))
|
||||
}
|
||||
|
||||
/// Save the current document to the runtime store (never the source tree).
|
||||
pub fn save_doc_state(data: &str) -> Result<(), String> {
|
||||
let dir = doc_generated_dir_path();
|
||||
fs::create_dir_all(&dir)
|
||||
.map_err(|err| format!("could not create generated directory: {err}"))?;
|
||||
fs::write(dir.join(GENERATED_DOC_FILE), data)
|
||||
.map_err(|err| format!("could not save doc state: {err}"))?;
|
||||
Ok(())
|
||||
save_doc_state_to(doc_generated_dir_path(), GENERATED_DOC_FILE, data)
|
||||
}
|
||||
|
||||
/// Save a copy under a caller-chosen filename in the runtime store.
|
||||
pub fn save_doc_state_as(filename: &str, data: &str) -> Result<(), String> {
|
||||
let dir = doc_generated_dir_path();
|
||||
save_doc_state_to(doc_generated_dir_path(), filename, data)
|
||||
}
|
||||
|
||||
pub(crate) fn save_doc_state_to(dir: PathBuf, filename: &str, data: &str) -> Result<(), String> {
|
||||
fs::create_dir_all(&dir)
|
||||
.map_err(|err| format!("could not create generated directory: {err}"))?;
|
||||
fs::write(dir.join(filename), data)
|
||||
.map_err(|err| format!("could not save doc state as {filename}: {err}"))?;
|
||||
.map_err(|err| format!("could not save doc state: {err}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5226,3 +5226,233 @@ fn layout_tree_cache_drops_on_engine_replacement() {
|
|||
"the new document caches normally afterwards"
|
||||
);
|
||||
}
|
||||
|
||||
// == Boot-time document init + persistence store (Android empty-doc fix) ===
|
||||
//
|
||||
// The shipped APK booted to a blank doc workspace for two compounding
|
||||
// reasons: the CRDT editor (the active one) had no boot init — only the
|
||||
// legacy editor's `initialized` gate seeded the demo document — and
|
||||
// persistence read from the build machine's absolute source-tree path,
|
||||
// which does not exist on device. These tests pin the replacement
|
||||
// behaviour: a one-time boot seed from the app-data store (CRDT wire
|
||||
// only) falling back to the demo document, plus the store/manifest read
|
||||
// precedence of the migrated persistence layer.
|
||||
|
||||
use super::crdt_widget::initial_document_source;
|
||||
use super::persistence::{load_saved_doc_state_with, save_doc_state_to, GENERATED_DOC_FILE};
|
||||
|
||||
/// A factory-fresh editor with NO engine installed: the exact boot state
|
||||
/// of the APK that opened blank.
|
||||
fn crdt_editor_default() -> (Cx, CrdtDocEditor) {
|
||||
let mut cx = Cx::new(Box::new(|_, _| {}));
|
||||
let editor = {
|
||||
let mut host = ();
|
||||
let mut std = ();
|
||||
let mut vm = ScriptVm {
|
||||
host: &mut host,
|
||||
std: &mut std,
|
||||
bx: Box::new(ScriptVmBase::new()),
|
||||
};
|
||||
CrdtDocEditor::script_new(&mut vm)
|
||||
};
|
||||
(cx, editor)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boot_source_prefers_crdt_save_and_rejects_classic_format() {
|
||||
assert_eq!(initial_document_source(None), None);
|
||||
// A classic-format save is neither loaded by the CRDT editor nor
|
||||
// mistaken for a CRDT save: the demo document boots instead, and the
|
||||
// legacy workspace keeps owning the classic file on Open.
|
||||
let classic = "{\"blocks\":[{\"kind\":\"paragraph\"}]}".to_string();
|
||||
assert_eq!(initial_document_source(Some(classic)), None);
|
||||
// A valid CRDT save boots verbatim.
|
||||
let mut engine = CrdtController::default();
|
||||
let block = engine
|
||||
.insert_block("local", None, "paragraph")
|
||||
.expect("block");
|
||||
engine.insert_text("local", block, None, "saved boot text");
|
||||
let wire = crdt_save_wire(&engine.document).expect("wire");
|
||||
assert_eq!(initial_document_source(Some(wire.clone())), Some(wire));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fresh_editor_boots_document_on_first_event() {
|
||||
let (mut cx, mut editor) = crdt_editor_default();
|
||||
assert!(editor.engine().projection.blocks.is_empty());
|
||||
assert!(!editor.initialized);
|
||||
editor.handle_event(
|
||||
&mut cx,
|
||||
&runtime_key_down(KeyCode::Escape),
|
||||
&mut Scope::empty(),
|
||||
);
|
||||
assert!(editor.initialized);
|
||||
// Source-agnostic (a developer machine may have a real save on disk):
|
||||
// whichever of save/demo won, the page is not blank.
|
||||
assert!(
|
||||
!editor.engine().projection.blocks.is_empty(),
|
||||
"boot init must leave a document in the projection"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_installed_engine_is_not_overwritten_by_boot_seed() {
|
||||
// `set_engine` marks the widget initialized: a host that installs its
|
||||
// own document (every runtime test above included) must never get
|
||||
// demo content or a disk save injected on the first event.
|
||||
let (mut cx, mut editor) = crdt_editor_with_engine(CrdtController::default());
|
||||
assert!(editor.initialized);
|
||||
editor.handle_event(
|
||||
&mut cx,
|
||||
&runtime_key_down(KeyCode::Escape),
|
||||
&mut Scope::empty(),
|
||||
);
|
||||
assert!(
|
||||
editor.engine().projection.blocks.is_empty(),
|
||||
"boot seed must not touch a document the host already installed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_demo_doc_builds_showcase_document() {
|
||||
let (mut cx, mut editor) = crdt_editor_default();
|
||||
editor.seed_demo_doc(&mut cx);
|
||||
let projection = &editor.engine().projection;
|
||||
|
||||
let visible_text: Vec<&str> = projection
|
||||
.blocks
|
||||
.iter()
|
||||
.map(|block| block.text.as_str())
|
||||
.collect();
|
||||
let title = projection
|
||||
.blocks
|
||||
.iter()
|
||||
.find(|block| {
|
||||
block
|
||||
.text
|
||||
.contains("Makepad Rich Text & Document Engineering Specification")
|
||||
})
|
||||
.expect("title block");
|
||||
assert!(
|
||||
title
|
||||
.runs
|
||||
.iter()
|
||||
.any(|run| run.bold && run.font_size >= 24.0),
|
||||
"title carries the bold 24pt heading run"
|
||||
);
|
||||
assert!(visible_text
|
||||
.iter()
|
||||
.any(|text| text.contains("Click anywhere on the text above or inside any table cell")));
|
||||
|
||||
// One divider and one image node, in that order.
|
||||
let kinds: Vec<&str> = projection
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|node| node.kind.as_str())
|
||||
.collect();
|
||||
assert_eq!(kinds, ["divider", "image"]);
|
||||
|
||||
// The 4x3 demo table: 12 written cells and a bold header row.
|
||||
let table = projection.tables.values().next().expect("demo table");
|
||||
assert_eq!(table.rows.len(), 4);
|
||||
assert_eq!(table.columns.len(), 3);
|
||||
assert_eq!(
|
||||
table.cells.values().filter(|text| !text.is_empty()).count(),
|
||||
12
|
||||
);
|
||||
assert!(table
|
||||
.cells
|
||||
.values()
|
||||
.any(|text| text == "10,000+ items at 60 FPS"));
|
||||
assert_eq!(
|
||||
table
|
||||
.cells
|
||||
.get(&(table.rows[0].clone(), table.columns[0].clone()))
|
||||
.map(String::as_str),
|
||||
Some("Feature Component")
|
||||
);
|
||||
let header_runs = table
|
||||
.cell_runs
|
||||
.get(&(table.rows[0].clone(), table.columns[0].clone()))
|
||||
.expect("header cell runs");
|
||||
assert!(
|
||||
header_runs.iter().all(|run| run.bold),
|
||||
"table header row renders bold"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- persistence store migration --------------------------------------
|
||||
|
||||
fn temp_store_dir(tag: &str) -> std::path::PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"nigig-doc-persistence-{}-{tag}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).expect("temp dir");
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_doc_round_trips_through_runtime_store() {
|
||||
let store = temp_store_dir("round-trip");
|
||||
save_doc_state_to(store.clone(), GENERATED_DOC_FILE, "{\"doc\":\"state\"}").expect("save");
|
||||
assert_eq!(
|
||||
load_saved_doc_state_with(store.clone(), store.join("no-manifest-here")),
|
||||
Some("{\"doc\":\"state\"}".to_string())
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&store);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_store_takes_precedence_over_manifest_fallback() {
|
||||
let store = temp_store_dir("store");
|
||||
let manifest = temp_store_dir("manifest");
|
||||
save_doc_state_to(store.clone(), GENERATED_DOC_FILE, "store version").expect("store save");
|
||||
save_doc_state_to(manifest.clone(), GENERATED_DOC_FILE, "manifest version")
|
||||
.expect("manifest save");
|
||||
assert_eq!(
|
||||
load_saved_doc_state_with(store.clone(), manifest.clone()),
|
||||
Some("store version".to_string()),
|
||||
"a document in the app-data store wins over the legacy source-tree copy"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&store);
|
||||
let _ = std::fs::remove_dir_all(&manifest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_fallback_serves_legacy_saves_once() {
|
||||
let store = temp_store_dir("empty-store");
|
||||
let manifest = temp_store_dir("legacy-manifest");
|
||||
// No file in the runtime store: the legacy source-tree copy is still
|
||||
// honored (it is read-only now — new saves never go there).
|
||||
save_doc_state_to(manifest.clone(), GENERATED_DOC_FILE, "legacy save").expect("manifest save");
|
||||
assert_eq!(
|
||||
load_saved_doc_state_with(store.clone(), manifest.clone()),
|
||||
Some("legacy save".to_string())
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&store);
|
||||
let _ = std::fs::remove_dir_all(&manifest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_or_missing_save_files_boot_fresh() {
|
||||
let store = temp_store_dir("empty-file-store");
|
||||
let manifest = temp_store_dir("missing-manifest");
|
||||
save_doc_state_to(store.clone(), GENERATED_DOC_FILE, " \n").expect("save");
|
||||
// An empty/whitespace save is treated as absent: the workspace boots
|
||||
// fresh (demo seed) rather than feeding the loader a blank document.
|
||||
assert_eq!(
|
||||
load_saved_doc_state_with(store.clone(), manifest.clone()),
|
||||
None
|
||||
);
|
||||
// And with nothing anywhere there is nothing to load.
|
||||
let bare = temp_store_dir("bare");
|
||||
assert_eq!(
|
||||
load_saved_doc_state_with(bare.clone(), bare.join("none")),
|
||||
None
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&store);
|
||||
let _ = std::fs::remove_dir_all(&manifest);
|
||||
let _ = std::fs::remove_dir_all(&bare);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1575,7 +1575,11 @@ impl BuildProjectsPage {
|
|||
.borrow_mut::<spreadsheet_ui::SpreadsheetGrid>()
|
||||
{
|
||||
grid.set_cell_value(cx, target_row, target_col, &formula_out);
|
||||
grid.select_cell(cx, target_row, target_col);
|
||||
// select_cell() was retired upstream
|
||||
// (spreadsheet-ui 6a3c467); set_selection_pair is the
|
||||
// replacement selection API, and `self.view.redraw`
|
||||
// below covers the paint.
|
||||
grid.set_selection_pair((target_row, target_col), (target_row, target_col));
|
||||
}
|
||||
self.view
|
||||
.label(cx, ids!(m_ss_cell_ref))
|
||||
|
|
@ -1596,7 +1600,7 @@ impl BuildProjectsPage {
|
|||
.borrow_mut::<spreadsheet_ui::SpreadsheetGrid>()
|
||||
{
|
||||
grid.set_cell_value(cx, target.0, target.1, &formula_text);
|
||||
grid.select_cell(cx, target.0, target.1);
|
||||
grid.set_selection_pair((target.0, target.1), (target.0, target.1));
|
||||
}
|
||||
self.view
|
||||
.label(cx, ids!(m_ss_cell_ref))
|
||||
|
|
|
|||
|
|
@ -131,7 +131,11 @@ fn workflow_lookup_rate() {
|
|||
#[test]
|
||||
fn workflow_default_rooms() {
|
||||
let data = get_building_data();
|
||||
let cost_data = data.get(&BuildingCategory::Residential).unwrap().first().unwrap();
|
||||
let cost_data = data
|
||||
.get(&BuildingCategory::Residential)
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap();
|
||||
let default = get_default_rooms(cost_data, "Standard Bungalow");
|
||||
assert!(
|
||||
default.is_some(),
|
||||
|
|
|
|||
|
|
@ -792,10 +792,13 @@ fn cad_toolbar_buttons_exist(app: TestApp) {
|
|||
app.locator(Selector::id("beam_tool_btn")).wait_visible();
|
||||
app.locator(Selector::id("column_tool_btn")).wait_visible();
|
||||
app.locator(Selector::id("add_cube_button")).wait_visible();
|
||||
app.locator(Selector::id("add_cylinder_button")).wait_visible();
|
||||
app.locator(Selector::id("add_sphere_button")).wait_visible();
|
||||
app.locator(Selector::id("add_cylinder_button"))
|
||||
.wait_visible();
|
||||
app.locator(Selector::id("add_sphere_button"))
|
||||
.wait_visible();
|
||||
app.locator(Selector::id("add_door_button")).wait_visible();
|
||||
app.locator(Selector::id("add_window_button")).wait_visible();
|
||||
app.locator(Selector::id("add_window_button"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -847,7 +850,8 @@ fn cad_toolbar_snap_ortho_polar_buttons_exist(app: TestApp) {
|
|||
app.locator(Selector::id("snap_toggle_btn")).wait_visible();
|
||||
app.locator(Selector::id("ortho_toggle_btn")).wait_visible();
|
||||
app.locator(Selector::id("polar_toggle_btn")).wait_visible();
|
||||
app.locator(Selector::id("snap_step_dropdown")).wait_visible();
|
||||
app.locator(Selector::id("snap_step_dropdown"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -870,8 +874,10 @@ fn cad_toolbar_visibility_buttons_exist(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("clip_toggle_btn")).wait_visible();
|
||||
app.locator(Selector::id("constr_toggle_btn")).wait_visible();
|
||||
app.locator(Selector::id("constr_export_btn")).wait_visible();
|
||||
app.locator(Selector::id("constr_toggle_btn"))
|
||||
.wait_visible();
|
||||
app.locator(Selector::id("constr_export_btn"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
// ── Tool selection tests ──────────────────────────────────────
|
||||
|
|
@ -882,7 +888,8 @@ fn cad_select_tool_default(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("select_tool_btn")).wait_text("Sel");
|
||||
app.locator(Selector::id("select_tool_btn"))
|
||||
.wait_text("Sel");
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -918,7 +925,8 @@ fn cad_tool_key_switches_via_button_text(app: TestApp) {
|
|||
app.locator(Selector::id("cad_viewport")).click();
|
||||
app.press_key(makepad_test::KeyCode::KeyC);
|
||||
// Verify button text changed to "Cir"
|
||||
app.locator(Selector::id("circle_tool_btn")).wait_text("Cir");
|
||||
app.locator(Selector::id("circle_tool_btn"))
|
||||
.wait_text("Cir");
|
||||
}
|
||||
|
||||
// ── Drawing creation tests ─────────────────────────────────────
|
||||
|
|
@ -947,7 +955,8 @@ fn cad_draw_circle_with_radius(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("circle_tool_btn")).click();
|
||||
app.locator(Selector::id("status_label")).wait_text("Circle");
|
||||
app.locator(Selector::id("status_label"))
|
||||
.wait_text("Circle");
|
||||
// Start circle
|
||||
app.locator(Selector::id("cad_viewport")).click();
|
||||
// Type radius value + Enter (DDE)
|
||||
|
|
@ -955,7 +964,6 @@ fn cad_draw_circle_with_radius(app: TestApp) {
|
|||
app.press_key(makepad_test::KeyCode::ReturnKey);
|
||||
}
|
||||
|
||||
|
||||
#[makepad_test]
|
||||
fn cad_draw_wall_segment(app: TestApp) {
|
||||
if !require_project_loaded() {
|
||||
|
|
@ -976,13 +984,13 @@ fn cad_draw_column_with_radius(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("column_tool_btn")).click();
|
||||
app.locator(Selector::id("status_label")).wait_text("Column");
|
||||
app.locator(Selector::id("status_label"))
|
||||
.wait_text("Column");
|
||||
app.locator(Selector::id("cad_viewport")).click();
|
||||
app.press_key(makepad_test::KeyCode::Key3);
|
||||
app.press_key(makepad_test::KeyCode::ReturnKey);
|
||||
}
|
||||
|
||||
|
||||
#[makepad_test]
|
||||
fn cad_draw_beam_with_ibeam_section(app: TestApp) {
|
||||
if !require_project_loaded() {
|
||||
|
|
@ -1035,7 +1043,8 @@ fn cad_delete_tool_removes_parts(app: TestApp) {
|
|||
app.locator(Selector::id("cad_viewport")).click();
|
||||
// Switch to delete tool
|
||||
app.locator(Selector::id("delete_tool_btn")).click();
|
||||
app.locator(Selector::id("status_label")).wait_text("Delete");
|
||||
app.locator(Selector::id("status_label"))
|
||||
.wait_text("Delete");
|
||||
// Select and delete
|
||||
app.locator(Selector::id("cad_viewport")).click();
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
|
|
@ -1052,7 +1061,8 @@ fn cad_escape_cancels_active_tool(app: TestApp) {
|
|||
// Cancel with Escape
|
||||
app.press_key(makepad_test::KeyCode::Escape);
|
||||
// Should return to Select
|
||||
app.locator(Selector::id("status_label")).wait_text("Select");
|
||||
app.locator(Selector::id("status_label"))
|
||||
.wait_text("Select");
|
||||
}
|
||||
|
||||
// ── View manipulation tests ────────────────────────────────────
|
||||
|
|
@ -1064,16 +1074,20 @@ fn cad_plane_toggle_cycles_workplanes(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
// Initially the plane button should show "XY Plan"
|
||||
app.locator(Selector::id("plane_toggle_btn")).wait_text("XY Plan");
|
||||
app.locator(Selector::id("plane_toggle_btn"))
|
||||
.wait_text("XY Plan");
|
||||
// Toggle to XZ
|
||||
app.locator(Selector::id("plane_toggle_btn")).click();
|
||||
app.locator(Selector::id("plane_toggle_btn")).wait_text("XZ Front");
|
||||
app.locator(Selector::id("plane_toggle_btn"))
|
||||
.wait_text("XZ Front");
|
||||
// Toggle to YZ
|
||||
app.locator(Selector::id("plane_toggle_btn")).click();
|
||||
app.locator(Selector::id("plane_toggle_btn")).wait_text("YZ Side");
|
||||
app.locator(Selector::id("plane_toggle_btn"))
|
||||
.wait_text("YZ Side");
|
||||
// Toggle back to XY
|
||||
app.locator(Selector::id("plane_toggle_btn")).click();
|
||||
app.locator(Selector::id("plane_toggle_btn")).wait_text("XY Plan");
|
||||
app.locator(Selector::id("plane_toggle_btn"))
|
||||
.wait_text("XY Plan");
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -1084,15 +1098,20 @@ fn cad_workplane_rotation_cycle(app: TestApp) {
|
|||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
// Rot WP cycles: 0 -> 15 -> 30 -> 45 -> 90 -> -45 -> 0
|
||||
app.locator(Selector::id("rot_wp_btn")).click();
|
||||
app.locator(Selector::id("rot_wp_btn")).wait_text("XY Plan 15°");
|
||||
app.locator(Selector::id("rot_wp_btn"))
|
||||
.wait_text("XY Plan 15°");
|
||||
app.locator(Selector::id("rot_wp_btn")).click();
|
||||
app.locator(Selector::id("rot_wp_btn")).wait_text("XY Plan 30°");
|
||||
app.locator(Selector::id("rot_wp_btn"))
|
||||
.wait_text("XY Plan 30°");
|
||||
app.locator(Selector::id("rot_wp_btn")).click();
|
||||
app.locator(Selector::id("rot_wp_btn")).wait_text("XY Plan 45°");
|
||||
app.locator(Selector::id("rot_wp_btn"))
|
||||
.wait_text("XY Plan 45°");
|
||||
app.locator(Selector::id("rot_wp_btn")).click();
|
||||
app.locator(Selector::id("rot_wp_btn")).wait_text("XY Plan 90°");
|
||||
app.locator(Selector::id("rot_wp_btn"))
|
||||
.wait_text("XY Plan 90°");
|
||||
app.locator(Selector::id("rot_wp_btn")).click();
|
||||
app.locator(Selector::id("rot_wp_btn")).wait_text("XY Plan -45°");
|
||||
app.locator(Selector::id("rot_wp_btn"))
|
||||
.wait_text("XY Plan -45°");
|
||||
app.locator(Selector::id("rot_wp_btn")).click();
|
||||
app.locator(Selector::id("rot_wp_btn")).wait_text("XY Plan");
|
||||
}
|
||||
|
|
@ -1236,7 +1255,8 @@ fn cad_render_mode_dropdown_exists(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("render_mode_dropdown")).wait_visible();
|
||||
app.locator(Selector::id("render_mode_dropdown"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -1245,7 +1265,8 @@ fn cad_view_toggle_button_exists(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("view_toggle_button")).wait_visible();
|
||||
app.locator(Selector::id("view_toggle_button"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
// ── Export tests ───────────────────────────────────────────────
|
||||
|
|
@ -1269,7 +1290,8 @@ fn cad_svg_export_switches_to_preview(app: TestApp) {
|
|||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("export_svg_btn")).click();
|
||||
// SVG preview widget should become visible after export
|
||||
app.locator(Selector::id("desktop_svg_preview")).wait_visible();
|
||||
app.locator(Selector::id("desktop_svg_preview"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -1320,7 +1342,8 @@ fn cad_pdf_tab_button_exists(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("desktop_pdf_tab_btn")).wait_visible();
|
||||
app.locator(Selector::id("desktop_pdf_tab_btn"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -1345,7 +1368,8 @@ fn cad_pdf_tab_then_back_to_script(app: TestApp) {
|
|||
app.locator(Selector::id("desktop_pdf_page")).wait_visible();
|
||||
// Switch back to Script
|
||||
app.locator(Selector::id("desktop_editor_tab_btn")).click();
|
||||
app.locator(Selector::id("desktop_script_page")).wait_visible();
|
||||
app.locator(Selector::id("desktop_script_page"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -1355,7 +1379,8 @@ fn cad_pdf_preview_placeholder_visible(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("desktop_pdf_tab_btn")).click();
|
||||
app.locator(Selector::id("desktop_preview_placeholder")).wait_visible();
|
||||
app.locator(Selector::id("desktop_preview_placeholder"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -1368,7 +1393,8 @@ fn cad_pdf_tab_then_cost_tab(app: TestApp) {
|
|||
app.locator(Selector::id("desktop_pdf_page")).wait_visible();
|
||||
// Switch to Cost tab
|
||||
app.locator(Selector::id("desktop_cost_tab_btn")).click();
|
||||
app.locator(Selector::id("desktop_cost_page")).wait_visible();
|
||||
app.locator(Selector::id("desktop_cost_page"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
// ── Code editor tests ──────────────────────────────────────────
|
||||
|
|
@ -1379,7 +1405,8 @@ fn cad_script_editor_visible(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("desktop_script_page")).wait_visible();
|
||||
app.locator(Selector::id("desktop_script_page"))
|
||||
.wait_visible();
|
||||
app.locator(Selector::id("cad_editor")).wait_visible();
|
||||
}
|
||||
|
||||
|
|
@ -1389,10 +1416,14 @@ fn cad_editor_tabs_exist(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("desktop_editor_tab_btn")).wait_visible();
|
||||
app.locator(Selector::id("desktop_cost_tab_btn")).wait_visible();
|
||||
app.locator(Selector::id("desktop_split_tab_btn")).wait_visible();
|
||||
app.locator(Selector::id("desktop_pdf_tab_btn")).wait_visible();
|
||||
app.locator(Selector::id("desktop_editor_tab_btn"))
|
||||
.wait_visible();
|
||||
app.locator(Selector::id("desktop_cost_tab_btn"))
|
||||
.wait_visible();
|
||||
app.locator(Selector::id("desktop_split_tab_btn"))
|
||||
.wait_visible();
|
||||
app.locator(Selector::id("desktop_pdf_tab_btn"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -1401,12 +1432,16 @@ fn cad_code_editor_has_content(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("desktop_script_page")).wait_visible();
|
||||
app.locator(Selector::id("desktop_script_page"))
|
||||
.wait_visible();
|
||||
// The editor should have some initial content
|
||||
app.locator(Selector::id("cad_editor")).wait_visible();
|
||||
app.locator(Selector::id("cad_editor")).click();
|
||||
let dump = app.widget_dump();
|
||||
assert!(dump.contains("cad_editor"), "Widget dump should contain cad_editor");
|
||||
assert!(
|
||||
dump.contains("cad_editor"),
|
||||
"Widget dump should contain cad_editor"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Cost estimation screen tests ───────────────────────────────
|
||||
|
|
@ -1417,7 +1452,8 @@ fn cad_cost_tab_button_visible(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("desktop_cost_tab_btn")).wait_visible();
|
||||
app.locator(Selector::id("desktop_cost_tab_btn"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -1427,8 +1463,10 @@ fn cad_cost_page_exists(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("desktop_cost_tab_btn")).click();
|
||||
app.locator(Selector::id("desktop_cost_page")).wait_visible();
|
||||
app.locator(Selector::id("cost_estimate_screen")).wait_visible();
|
||||
app.locator(Selector::id("desktop_cost_page"))
|
||||
.wait_visible();
|
||||
app.locator(Selector::id("cost_estimate_screen"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
// ── AI pane tests ──────────────────────────────────────────────
|
||||
|
|
@ -1440,7 +1478,8 @@ fn cad_ai_pane_exists(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("ai_status_label")).wait_visible();
|
||||
app.locator(Selector::id("ai_generate_button")).wait_visible();
|
||||
app.locator(Selector::id("ai_generate_button"))
|
||||
.wait_visible();
|
||||
app.locator(Selector::id("ai_cancel_button")).wait_visible();
|
||||
app.locator(Selector::id("backend_dropdown")).wait_visible();
|
||||
app.locator(Selector::id("cad_prompt_input")).wait_visible();
|
||||
|
|
@ -1485,7 +1524,8 @@ fn cad_splitter_exists(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("workspace_split_toggle_btn")).wait_visible();
|
||||
app.locator(Selector::id("workspace_split_toggle_btn"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -1494,11 +1534,14 @@ fn cad_workspace_split_toggle(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("workspace_split_toggle_btn")).click();
|
||||
app.locator(Selector::id("workspace_split_toggle_btn"))
|
||||
.click();
|
||||
// Split viewport should appear
|
||||
app.locator(Selector::id("split_viewport_layer")).wait_visible();
|
||||
app.locator(Selector::id("split_viewport_layer"))
|
||||
.wait_visible();
|
||||
// Toggle back
|
||||
app.locator(Selector::id("workspace_split_toggle_btn")).click();
|
||||
app.locator(Selector::id("workspace_split_toggle_btn"))
|
||||
.click();
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
}
|
||||
|
||||
|
|
@ -1510,7 +1553,8 @@ fn cad_properties_panel_exists(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("properties_panel_view")).wait_visible();
|
||||
app.locator(Selector::id("properties_panel_view"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
// ── Status label tests ───────────────────────────────────────────
|
||||
|
|
@ -1530,7 +1574,8 @@ fn cad_status_label_shows_select_initially(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("status_label")).wait_text("Select");
|
||||
app.locator(Selector::id("status_label"))
|
||||
.wait_text("Select");
|
||||
}
|
||||
|
||||
// ── Mobile editor tests ──────────────────────────────────────────
|
||||
|
|
@ -1541,7 +1586,8 @@ fn cad_mobile_editor_tab_button_exists(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("mobile_editor_tab_btn")).wait_visible();
|
||||
app.locator(Selector::id("mobile_editor_tab_btn"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -1550,7 +1596,8 @@ fn cad_mobile_cost_tab_button_exists(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("mobile_cost_tab_btn")).wait_visible();
|
||||
app.locator(Selector::id("mobile_cost_tab_btn"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
// ── Snap step dropdown tests ────────────────────────────────────
|
||||
|
|
@ -1561,7 +1608,8 @@ fn cad_snap_step_dropdown_visible(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("snap_step_dropdown")).wait_visible();
|
||||
app.locator(Selector::id("snap_step_dropdown"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
// ── Extrude button test ──────────────────────────────────────────
|
||||
|
|
@ -1584,7 +1632,8 @@ fn cad_measure_tool_button(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("measure_tool_btn")).click();
|
||||
app.locator(Selector::id("status_label")).wait_text("Measure");
|
||||
app.locator(Selector::id("status_label"))
|
||||
.wait_text("Measure");
|
||||
}
|
||||
|
||||
// ── Delete tool test ─────────────────────────────────────────────
|
||||
|
|
@ -1596,7 +1645,8 @@ fn cad_delete_tool_button(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("delete_tool_btn")).click();
|
||||
app.locator(Selector::id("status_label")).wait_text("Delete");
|
||||
app.locator(Selector::id("status_label"))
|
||||
.wait_text("Delete");
|
||||
}
|
||||
|
||||
// ── Arc tool test ────────────────────────────────────────────────
|
||||
|
|
@ -1620,7 +1670,8 @@ fn cad_polyline_tool_button(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("polyline_tool_btn")).click();
|
||||
app.locator(Selector::id("status_label")).wait_text("Polyline");
|
||||
app.locator(Selector::id("status_label"))
|
||||
.wait_text("Polyline");
|
||||
}
|
||||
|
||||
// ── Area and Quad tool tests ─────────────────────────────────────
|
||||
|
|
@ -1654,7 +1705,8 @@ fn cad_polygon_tool_button(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("polygon_tool_btn")).click();
|
||||
app.locator(Selector::id("status_label")).wait_text("Polygon");
|
||||
app.locator(Selector::id("status_label"))
|
||||
.wait_text("Polygon");
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -1664,7 +1716,8 @@ fn cad_triplane_tool_button(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("triplane_tool_btn")).click();
|
||||
app.locator(Selector::id("status_label")).wait_text("TriPlane");
|
||||
app.locator(Selector::id("status_label"))
|
||||
.wait_text("TriPlane");
|
||||
}
|
||||
|
||||
// ── Extend and Chamfer tool tests ────────────────────────────────
|
||||
|
|
@ -1676,7 +1729,8 @@ fn cad_extend_tool_button(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("extend_tool_btn")).click();
|
||||
app.locator(Selector::id("status_label")).wait_text("Extend");
|
||||
app.locator(Selector::id("status_label"))
|
||||
.wait_text("Extend");
|
||||
}
|
||||
|
||||
#[makepad_test]
|
||||
|
|
@ -1686,7 +1740,8 @@ fn cad_chamfer_tool_button(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("chamfer_tool_btn")).click();
|
||||
app.locator(Selector::id("status_label")).wait_text("Chamfer");
|
||||
app.locator(Selector::id("status_label"))
|
||||
.wait_text("Chamfer");
|
||||
}
|
||||
|
||||
// ── Render mode test ─────────────────────────────────────────────
|
||||
|
|
@ -1721,7 +1776,8 @@ fn cad_workspace_title_visible(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
app.locator(Selector::id("workspace_title_label")).wait_visible();
|
||||
app.locator(Selector::id("workspace_title_label"))
|
||||
.wait_visible();
|
||||
}
|
||||
|
||||
// ── Status label text change test ────────────────────────────────
|
||||
|
|
@ -1733,7 +1789,8 @@ fn cad_status_label_changes_with_tool(app: TestApp) {
|
|||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
// Start on Select
|
||||
app.locator(Selector::id("status_label")).wait_text("Select");
|
||||
app.locator(Selector::id("status_label"))
|
||||
.wait_text("Select");
|
||||
// Switch to Line
|
||||
app.locator(Selector::id("cad_viewport")).click();
|
||||
app.press_key(makepad_test::KeyCode::KeyL);
|
||||
|
|
@ -1766,9 +1823,13 @@ fn cad_snap_toggle_button_checkable(app: TestApp) {
|
|||
return;
|
||||
}
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
let before = app.locator(Selector::id("snap_toggle_btn")).assert_enabled(true);
|
||||
let before = app
|
||||
.locator(Selector::id("snap_toggle_btn"))
|
||||
.assert_enabled(true);
|
||||
app.locator(Selector::id("snap_toggle_btn")).click();
|
||||
let after = app.locator(Selector::id("snap_toggle_btn")).assert_enabled(true);
|
||||
let after = app
|
||||
.locator(Selector::id("snap_toggle_btn"))
|
||||
.assert_enabled(true);
|
||||
app.locator(Selector::id("cad_viewport")).wait_visible();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue