nigig-org/crates/pageflipnav/tests/ui.rs
andodeki 560116189c
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
fix(cad-build): land CAD workspace on project dashboard, not a blank editor
The CAD dashboard's CadDashboard node never realized as a widget, so
tapping 'CAD Workspace' opened to a blank/editor-only sheet. Empirical
device dumps showed the first child after the root realizes reliably
while a sibling placed after the editor AdaptiveView does not, and a
root ':=' widget child alone never realizes here.

- Place dashboard_layer as the FIRST direct child of CadWorkspaceBase,
  wrapping mod.widgets.CadDashboard so it is a reliably-realizable,
  togglable View layer (matching the working doc workspace pattern).
- Wrap the editor AdaptiveView in editor_layer (a plain View) and toggle
  editor_layer/dashboard_layer visibility from apply_dashboard_visibility
  instead of toggling the AdaptiveView variants, which always draw their
  active variant regardless of visible.
- Add use mod.widgets.* to the CadDashboard script_mod (matches the
  CadEditorSheet registration that realizes inside this workspace).
- Add regression test candlands_on_dashboard asserting the dashboard
  title and + New Project button land when opening the workspace.
2026-09-02 09:06:20 +03:00

779 lines
27 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use makepad_test::{makepad_test, Selector, TestApp, WidgetSnapshot};
/// Skip test when NIGIG_TEST_MODE is not set (used for tests that need
/// the home screen to be visible, bypassing login).
fn require_test_mode() -> bool {
std::env::var("NIGIG_TEST_MODE").is_ok()
}
/// Navigate from home → work → construction_grid → m_workspace_docs_btn
/// → (dashboard) new_document_btn → crdt_editor (the CRDT doc workspace).
fn navigate_to_doc_workspace(app: &TestApp) {
if !require_test_mode() {
return;
}
app.locator(Selector::id("work_button"))
.wait_visible()
.click();
// The work screen opens with its bottom pull-up sheet collapsed, hiding
// the section grid buttons. Drag the sheet's pull handle upward to expand
// it so the additional nav buttons become visible.
app.locator(Selector::id("pull_handle"))
.wait_visible()
.drag_by(0.0, -600.0);
// The construction grid button opens nigig-build's BuildProjectsPage
// (mobile variant), whose Workspace list is the entry to the docs.
app.locator(Selector::id("construction_grid_button"))
.wait_visible()
.click();
app.locator(Selector::id("m_workspace_docs_btn"))
.wait_visible()
.click();
// The CRDT doc workspace opens on its dashboard; "New Document" flips it
// to the editor (the saved-doc cards are hit-tested rects, not widgets).
app.locator(Selector::id("new_document_btn"))
.wait_visible()
.click();
app.locator(Selector::id("crdt_editor")).wait_visible();
}
/// Poll the editor's snapshot until its height grows past `before.height`
/// (the insert_table grid needs a rendered frame to show up; a single
/// snapshot right after the button click can race the layout refresh).
/// Returns the grown snapshot, panicking if it never appears.
fn wait_editor_grows(app: &TestApp, before: &WidgetSnapshot) -> WidgetSnapshot {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
loop {
let snap = app.locator(Selector::id("crdt_editor")).snapshot();
if snap.height > before.height {
return snap;
}
if std::time::Instant::now() >= deadline {
panic!(
"editor never grew past {} (stayed at {}): insert_table produced no visible grid",
before.height, snap.height
);
}
std::thread::sleep(std::time::Duration::from_millis(150));
}
}
#[makepad_test]
fn app_launches_and_shows_login_screen(app: TestApp) {
if require_test_mode() {
return;
}
app.locator(Selector::id("login_button"))
.wait_visible();
app.locator(Selector::id("user_id_input"))
.wait_visible();
app.locator(Selector::id("password_input"))
.wait_visible();
app.locator(Selector::id("homeserver_input"))
.wait_visible();
app.locator(Selector::id("signup_button"))
.wait_visible();
app.locator(Selector::all().text_exact("Login to Robrix"))
.wait_visible();
}
#[makepad_test]
fn login_form_accepts_input(app: TestApp) {
if require_test_mode() {
return;
}
app.locator(Selector::id("user_id_input"))
.wait_visible()
.fill("@testuser:matrix.org")
.wait_value("@testuser:matrix.org");
app.locator(Selector::id("password_input"))
.wait_visible()
.fill("my_password")
.wait_value("my_password");
app.locator(Selector::id("homeserver_input"))
.wait_visible()
.fill("matrix.example.com")
.wait_value("matrix.example.com");
}
#[makepad_test]
fn show_password_toggle_works(app: TestApp) {
if require_test_mode() {
return;
}
let _pw = app.locator(Selector::id("password_input")).wait_visible();
app.locator(Selector::id("show_password_button"))
.wait_visible()
.click();
let hide_btn = app.locator(Selector::id("hide_password_button")).wait_visible();
hide_btn.click();
app.locator(Selector::id("show_password_button")).wait_visible();
}
#[makepad_test]
fn login_with_empty_fields_shows_modal(app: TestApp) {
if require_test_mode() {
return;
}
app.locator(Selector::id("login_button"))
.wait_visible()
.click();
app.locator(Selector::all().text_exact("Missing User ID"))
.wait_visible();
app.locator(Selector::id("login_status_modal_inner"))
.wait_visible();
app.locator(Selector::all().text_exact("Please enter a valid User ID."))
.wait_visible();
}
#[makepad_test]
fn login_with_password_only_shows_missing_user_id(app: TestApp) {
if require_test_mode() {
return;
}
app.locator(Selector::id("password_input"))
.wait_visible()
.fill("some_password");
app.locator(Selector::id("login_button"))
.wait_visible()
.click();
app.locator(Selector::all().text_exact("Missing User ID"))
.wait_visible();
}
#[makepad_test]
fn login_with_user_id_only_shows_missing_password(app: TestApp) {
if require_test_mode() {
return;
}
app.locator(Selector::id("user_id_input"))
.wait_visible()
.fill("@test:matrix.org");
app.locator(Selector::id("login_button"))
.wait_visible()
.click();
app.locator(Selector::all().text_exact("Missing Password"))
.wait_visible();
}
#[makepad_test]
fn login_status_modal_can_be_closed(app: TestApp) {
if require_test_mode() {
return;
}
app.locator(Selector::id("login_button")).wait_visible().click();
let modal = app.locator(Selector::id("login_status_modal_inner")).wait_visible();
let close_btn = app.locator(Selector::widget_type("Button").text_exact("Okay")).wait_visible();
close_btn.click();
modal.try_wait_hidden().unwrap();
}
#[makepad_test]
fn signup_button_fires_correctly(app: TestApp) {
if require_test_mode() {
return;
}
app.locator(Selector::id("signup_button"))
.wait_visible()
.click();
}
#[makepad_test]
fn sso_buttons_are_present(app: TestApp) {
if require_test_mode() {
return;
}
let sso_providers = [
"apple_button",
"facebook_button",
"github_button",
"gitlab_button",
"google_button",
"twitter_button",
];
for provider_id in &sso_providers {
app.locator(Selector::id(*provider_id)).wait_visible();
}
}
// ── Home screen tab tests (require NIGIG_TEST_MODE=1) ──
#[makepad_test]
fn home_screen_tab_buttons_visible_in_test_mode(app: TestApp) {
if !require_test_mode() {
return;
}
// Desktop nav rail buttons
app.locator(Selector::id("home_button")).wait_visible();
app.locator(Selector::id("chats_button")).wait_visible();
app.locator(Selector::id("shop_button")).wait_visible();
app.locator(Selector::id("mobility_button")).wait_visible();
app.locator(Selector::id("work_button")).wait_visible();
}
#[makepad_test]
fn home_screen_home_tab_active_by_default(app: TestApp) {
if !require_test_mode() {
return;
}
// Home tab should be visible and active
app.locator(Selector::id("home_button")).wait_visible();
}
#[makepad_test]
fn home_tab_switches_to_chats(app: TestApp) {
if !require_test_mode() {
return;
}
app.locator(Selector::id("chats_button"))
.wait_visible()
.click();
// After clicking Chats, the chats button should still be visible
app.locator(Selector::id("chats_button")).wait_visible();
}
#[makepad_test]
fn home_tab_switches_to_shop(app: TestApp) {
if !require_test_mode() {
return;
}
app.locator(Selector::id("shop_button"))
.wait_visible()
.click();
app.locator(Selector::id("shop_button")).wait_visible();
}
#[makepad_test]
fn home_tab_switches_to_mobility(app: TestApp) {
if !require_test_mode() {
return;
}
app.locator(Selector::id("mobility_button"))
.wait_visible()
.click();
app.locator(Selector::id("mobility_button")).wait_visible();
}
#[makepad_test]
fn home_tab_switches_to_work(app: TestApp) {
if !require_test_mode() {
return;
}
app.locator(Selector::id("work_button"))
.wait_visible()
.click();
app.locator(Selector::id("work_button")).wait_visible();
}
#[makepad_test]
fn home_sub_tabs_visible_in_test_mode(app: TestApp) {
if !require_test_mode() {
return;
}
// Mobile pull-up sheet sub-tabs
app.locator(Selector::id("home_content_tab_btn")).wait_visible();
app.locator(Selector::id("home_profile_tab_btn")).wait_visible();
app.locator(Selector::id("home_preferences_tab_btn")).wait_visible();
}
#[makepad_test]
fn home_sub_tab_profile_click(app: TestApp) {
if !require_test_mode() {
return;
}
app.locator(Selector::id("home_profile_tab_btn"))
.wait_visible()
.click();
app.locator(Selector::id("home_profile_tab_btn")).wait_visible();
}
// ── Device verification: doc workspace sections 1–5 ──
// Execute against the CRDT workspace (CrdtDocWorkspace).
// Full runbook: DEVICE_VERIFICATION.md (extracted from git history).
/// Section 1.1–1.4: Interaction mode View ↔ Edit.
///
/// Cold-launch in View mode; the Edit button switches to Edit mode;
/// Done switches back. Touch in View is passive (no keyboard).
#[makepad_test]
fn doc_interaction_mode_view_and_edit(app: TestApp) {
if !require_test_mode() {
return;
}
navigate_to_doc_workspace(&app);
// 1.1: Cold-launch — first tap should NOT open keyboard (View mode).
// We verify that Edit button is visible (the AdaptiveView Mobile variant).
app.locator(Selector::id("edit_mode_btn")).wait_visible();
// 1.3: Tap Edit → toolbar switches to Done.
app.locator(Selector::id("edit_mode_btn"))
.wait_visible()
.click();
app.locator(Selector::id("edit_mode_btn"))
.wait_visible()
.assert_text("Done");
// 1.4: Tap Done → back to Edit label.
app.locator(Selector::id("edit_mode_btn"))
.wait_visible()
.click();
app.locator(Selector::id("edit_mode_btn"))
.wait_visible()
.assert_text("Edit");
}
/// Section 1.2: View mode scroll — touch-move inside the document area
/// should scroll, not select text.
#[makepad_test]
fn doc_view_mode_scroll_by_touch(app: TestApp) {
if !require_test_mode() {
return;
}
navigate_to_doc_workspace(&app);
// In View mode, touch-drag should scroll the page.
let editor = app.locator(Selector::id("crdt_editor")).wait_visible();
let snap = editor.snapshot();
// Touch down at center, move down (scroll up), release.
let (cx, cy) = (snap.x as f64 + snap.width as f64 / 2.0, snap.y as f64 + snap.height as f64 / 2.0);
app.touch_down(cx, cy);
app.touch_move(cx, cy - 80.0);
app.touch_up(cx, cy - 80.0);
// Editor should still be visible (page scrolled, not crashed).
app.locator(Selector::id("crdt_editor")).wait_visible();
}
/// Section 2.1: IME text input in Edit mode.
///
/// Enter Edit mode, type text, verify the widget receives it.
#[makepad_test]
fn doc_ime_text_input_in_edit_mode(app: TestApp) {
if !require_test_mode() {
return;
}
navigate_to_doc_workspace(&app);
// Switch to Edit mode.
app.locator(Selector::id("edit_mode_btn"))
.wait_visible()
.click();
app.locator(Selector::id("edit_mode_btn"))
.wait_visible()
.assert_text("Done");
// Tap on the editor to focus, then type.
let editor = app.locator(Selector::id("crdt_editor")).wait_visible();
editor.click();
app.type_text("Hello from device verification");
// Status bar should still show the engine is ready.
app.locator(Selector::id("status_left")).wait_visible();
}
/// Section 2.2: IME composition in a table cell.
///
/// Tap inside the document (which may contain a table), verify IME
/// composition text can be sent.
#[makepad_test]
fn doc_ime_composition_sends_text(app: TestApp) {
if !require_test_mode() {
return;
}
navigate_to_doc_workspace(&app);
app.locator(Selector::id("edit_mode_btn"))
.wait_visible()
.click();
let editor = app.locator(Selector::id("crdt_editor")).wait_visible();
editor.click();
// Send an IME composition — this simulates composing text before commit.
app.ime_composition("composed text");
// The document should still be alive.
app.locator(Selector::id("crdt_editor")).wait_visible();
}
/// Section 5.1–5.2: Scroll handoff to parent ScrollYView.
///
/// In View mode, a vertical drag should scroll; in Edit mode, a quick
/// drag should also scroll (before long-press arms).
#[makepad_test]
fn doc_scroll_handoff_view_and_edit(app: TestApp) {
if !require_test_mode() {
return;
}
navigate_to_doc_workspace(&app);
// 5.1: View mode — drag should scroll.
let editor = app.locator(Selector::id("crdt_editor")).wait_visible();
let snap = editor.snapshot();
let (cx, cy) = (snap.x as f64 + snap.width as f64 / 2.0, snap.y as f64 + snap.height as f64 / 2.0);
app.touch_down(cx, cy);
// Quick drag (short distance, well under the long-press threshold).
app.touch_move(cx, cy - 20.0);
app.touch_up(cx, cy - 20.0);
// Editor still visible — page scrolled, not selected.
app.locator(Selector::id("crdt_editor")).wait_visible();
// 5.2: Switch to Edit mode — quick drag should still scroll.
app.locator(Selector::id("edit_mode_btn"))
.wait_visible()
.click();
let editor = app.locator(Selector::id("crdt_editor")).wait_visible();
let snap = editor.snapshot();
let (cx, cy) = (snap.x as f64 + snap.width as f64 / 2.0, snap.y as f64 + snap.height as f64 / 2.0);
app.touch_down(cx, cy);
app.touch_move(cx, cy - 20.0);
app.touch_up(cx, cy - 20.0);
app.locator(Selector::id("crdt_editor")).wait_visible();
}
/// Section 3.1: Long-press triggers selection.
///
/// In Edit mode, a long-press on text should arm word selection.
/// We verify the mechanism by sending a long-press event and confirming
/// the document is still responsive.
#[makepad_test]
fn doc_long_press_arms_selection(app: TestApp) {
if !require_test_mode() {
return;
}
navigate_to_doc_workspace(&app);
app.locator(Selector::id("edit_mode_btn"))
.wait_visible()
.click();
let editor = app.locator(Selector::id("crdt_editor")).wait_visible();
let snap = editor.snapshot();
let (cx, cy) = (snap.x as f64 + snap.width as f64 / 2.0, snap.y as f64 + snap.height as f64 / 2.0);
// Long-press at center for 500ms (the default threshold).
app.long_press(cx, cy, 500.0);
// Document should still be alive after the long-press.
app.locator(Selector::id("crdt_editor")).wait_visible();
}
#[makepad_test]
fn doc_debug_nav_walk(app: TestApp) {
if !require_test_mode() {
return;
}
app.locator(Selector::id("work_button"))
.wait_visible()
.click();
app.locator(Selector::id("pull_handle"))
.wait_visible()
.drag_by(0.0, -600.0);
app.locator(Selector::id("construction_grid_button"))
.wait_visible()
.click();
app.locator(Selector::id("m_workspace_docs_btn"))
.wait_visible()
.click();
app.locator(Selector::id("new_document_btn"))
.wait_visible()
.click();
app.locator(Selector::id("crdt_editor")).wait_visible();
}
/// Section 9.0: Boot content — the CRDT workspace renders the editor after
/// navigating to a new blank document; the status bar reflects the blank
/// state without switching to the loaded-document engine string.
#[makepad_test]
fn doc_boot_content_renders(app: TestApp) {
if !require_test_mode() {
return;
}
navigate_to_doc_workspace(&app);
// A freshly created blank document reports its status here before the
// engine switches to a loaded-document status string.
app.locator(Selector::id("status_left"))
.wait_visible()
.assert_text("New blank document");
// The CRDT editor is visible (document rendered).
app.locator(Selector::id("crdt_editor")).wait_visible();
}
/// Section 10.1: Typing actually inserts document content. After switching
/// to Edit and focusing the editor, IME text lands in the document: the
/// stats bar (refreshed by a toolbar click) reports the typed words/chars.
#[makepad_test]
fn doc_type_text_inserts_document(app: TestApp) {
if !require_test_mode() {
return;
}
navigate_to_doc_workspace(&app);
let snap_of = |id: &str| app.locator(Selector::id(id)).snapshot();
app.locator(Selector::id("edit_mode_btn")).wait_visible().click();
let editor = app.locator(Selector::id("crdt_editor")).wait_visible();
editor.click();
app.type_text("hello world");
// A toolbar click drives handle_actions, which refreshes stats_label
// from the projected document (typing alone doesn't refresh it).
app.locator(Selector::id("bold_btn")).click();
app.locator(Selector::id("stats_label"))
.wait_visible()
.assert_text("Page 1 of 1 | 2 words | 11 chars");
assert_eq!(snap_of("stats_label").text, Some("Page 1 of 1 | 2 words | 11 chars".into()));
}
/// Section 10.2: The bold/italic/underline toolbar buttons drive real
/// document styling through the editor without corrupting content. Each
/// toggle keeps the typed text intact (word/char stats unchanged after a
/// refresh), and toggling a style back off leaves the doc stable. Actual
/// style semantics (run spans on the selection) are asserted by the
/// doc-ui runtime unit suite (e.g. runtime_ctrl_b_applies_bold...).
#[makepad_test]
fn doc_bold_italic_underline_ops(app: TestApp) {
if !require_test_mode() {
return;
}
navigate_to_doc_workspace(&app);
let snap_of = |id: &str| app.locator(Selector::id(id)).snapshot();
app.locator(Selector::id("edit_mode_btn")).wait_visible().click();
let editor = app.locator(Selector::id("crdt_editor")).wait_visible();
editor.click();
app.type_text("hello world");
for id in ["bold_btn", "italic_btn", "underline_btn"] {
app.locator(Selector::id(id)).wait_visible().click();
app.locator(Selector::id(id)).wait_visible().click();
}
// Refresh stats; styling must not alter the text.
app.locator(Selector::id("bold_btn")).click();
assert_eq!(snap_of("stats_label").text, Some("Page 1 of 1 | 2 words | 11 chars".into()));
app.locator(Selector::id("crdt_editor")).wait_visible();
}
/// Section 10.3: Inserting a table produces a visible, usable grid and text
/// typed into a cell reaches the cell. The editor's height grows when the
/// seeded 2x2 grid appears (a bare table block renders at zero size), and
/// after a short tap on a visible cell + IME text the stats bar (which
/// counts table-cell words, refreshed by a toolbar click) reports the cell
/// content alongside the block text.
#[makepad_test]
fn doc_insert_table_and_cell_text(app: TestApp) {
if !require_test_mode() {
return;
}
navigate_to_doc_workspace(&app);
let snap_of = |id: &str| app.locator(Selector::id(id)).snapshot();
app.locator(Selector::id("edit_mode_btn")).wait_visible().click();
let editor = app.locator(Selector::id("crdt_editor")).wait_visible();
editor.click();
app.type_text("hello world");
app.locator(Selector::id("bold_btn")).click();
let before = app.locator(Selector::id("crdt_editor")).snapshot();
app.locator(Selector::id("insert_table_btn")).click();
let after = wait_editor_grows(&app, &before);
// column (col1) is tappable. A short tap focuses the cell and shows the
// IME (Edit mode), so the subsequent type_text lands in the cell.
let raw_left = after.x as f64 + (after.width as f64 - 612.0) / 2.0;
let table_top = after.y as f64 + before.height as f64;
let col0_mid = raw_left + 80.0;
let row0 = table_top + 14.0;
let col1_mid = raw_left + 240.0;
app.touch_down(col1_mid, row0);
app.touch_up(col1_mid, row0);
app.type_text("alpha beta");
app.locator(Selector::id("bold_btn")).click();
// block "hello world" (2 words / 11 chars) + cell "alpha beta"
// (2 words / 10 chars) = 4 words / 21 chars.
assert_eq!(
snap_of("stats_label").text,
Some("Page 1 of 1 | 4 words | 21 chars".into()),
"cell text must join the document stats: {:?}",
snap_of("stats_label").text
);
let _ = col0_mid;
}
/// Section 10.4: Merge and split actually operate on the table's cells. A
/// touch-hold-then-drag on the visible right column arms a cell range
/// (Shift+Arrow on desktop); the merge toolbar button then merges the two
/// cells and reports "Merged selected cells", and split re-splits it and
/// reports "Split merged cell".
#[makepad_test]
fn doc_merge_split_cell_ops(app: TestApp) {
if !require_test_mode() {
return;
}
navigate_to_doc_workspace(&app);
let snap_of = |id: &str| app.locator(Selector::id(id)).snapshot();
app.locator(Selector::id("edit_mode_btn")).wait_visible().click();
let editor = app.locator(Selector::id("crdt_editor")).wait_visible();
editor.click();
app.type_text("hello world");
app.locator(Selector::id("bold_btn")).click();
let before = app.locator(Selector::id("crdt_editor")).snapshot();
app.locator(Selector::id("insert_table_btn")).click();
let after = wait_editor_grows(&app, &before);
// Reachable right column (col1), rows 0-1 of the seeded 2x2 grid.
let raw_left = after.x as f64 + (after.width as f64 - 612.0) / 2.0;
let table_top = after.y as f64 + before.height as f64;
let col1 = raw_left + 240.0;
let row0 = table_top + 14.0;
let row1 = table_top + 42.0;
// Touch-hold (long-press arm) then drag down one row to span the range.
app.touch_down(col1, row0);
std::thread::sleep(std::time::Duration::from_millis(700));
app.touch_move(col1, row1);
app.touch_up(col1, row1);
app.locator(Selector::id("merge_cells_btn")).click();
assert_eq!(
snap_of("status_left").text,
Some("Merged selected cells".into()),
"merge feedback: {:?}",
snap_of("status_left").text
);
app.locator(Selector::id("split_cell_btn")).click();
assert_eq!(
snap_of("status_left").text,
Some("Split merged cell".into()),
"split feedback: {:?}",
snap_of("status_left").text
);
}
/// Section 3.4: A long-press on empty space between content must NOT arm a
/// cell range or start a word selection — the document stays inert. Proven
/// by long-pressing well below the only content, then clicking Merge: with
/// no range armed it must report the "span a range" guidance, not merge.
#[makepad_test]
fn doc_long_press_empty_space_does_not_arm(app: TestApp) {
if !require_test_mode() {
return;
}
navigate_to_doc_workspace(&app);
let snap_of = |id: &str| app.locator(Selector::id(id)).snapshot();
app.locator(Selector::id("edit_mode_btn")).wait_visible().click();
let editor = app.locator(Selector::id("crdt_editor")).wait_visible();
editor.click();
app.type_text("hello world");
app.locator(Selector::id("bold_btn")).click();
// Long-press well below the text block on empty space.
let snap = app.locator(Selector::id("crdt_editor")).snapshot();
let empty_y = snap.y as f64 + snap.height as f64 - 20.0;
let empty_x = snap.x as f64 + snap.width as f64 / 2.0;
app.long_press(empty_x, empty_y, 500.0);
// Nothing armed: Merge must report the guidance, not a merge.
app.locator(Selector::id("merge_cells_btn")).click();
assert_eq!(
snap_of("status_left").text,
Some("Span a cell range with Shift+Arrow first".into()),
"long-press on empty space must NOT arm a cell range: {:?}",
snap_of("status_left").text
);
}
/// Section 4.1: A touch-hold on the left column's visible sliver dragged
/// diagonally to the bottom-right cell spans a 2x2 cell range, which Merge
/// consumes (anchor keeps its content). Mirrors the proven 1x2 path but
/// crosses both axes. Coordinates are clamped to the on-screen editor so
/// they hold on either the 411 dp (A60) or 384 dp (A16) screen.
#[makepad_test]
fn doc_diagonal_cell_range_merges(app: TestApp) {
if !require_test_mode() {
return;
}
navigate_to_doc_workspace(&app);
let snap_of = |id: &str| app.locator(Selector::id(id)).snapshot();
app.locator(Selector::id("edit_mode_btn")).wait_visible().click();
let editor = app.locator(Selector::id("crdt_editor")).wait_visible();
editor.click();
app.type_text("hello world");
app.locator(Selector::id("bold_btn")).click();
let before = app.locator(Selector::id("crdt_editor")).snapshot();
app.locator(Selector::id("insert_table_btn")).click();
let after = wait_editor_grows(&app, &before);
// On-screen geometry (clamped so col0 and col1 stay reachable).
let raw_left = after.x as f64 + (after.width as f64 - 612.0) / 2.0;
let table_top = after.y as f64 + before.height as f64;
let row0 = table_top + 14.0;
let row1 = table_top + 42.0;
// col0's center can sit off the left edge; tap a point just inside the
// editor's visible left region, which still falls in col0.
let col0_tap = if raw_left + 80.0 > after.x as f64 + 8.0 {
raw_left + 80.0
} else {
after.x as f64 + 12.0
};
let col1 = (raw_left + 240.0).min(after.x as f64 + after.width as f64 - 20.0);
// Touch-hold in (col0,row0), drag diagonally to (col1,row1): spans 2x2.
app.touch_down(col0_tap, row0);
std::thread::sleep(std::time::Duration::from_millis(700));
app.touch_move(col1, row1);
app.touch_up(col1, row1);
app.locator(Selector::id("merge_cells_btn")).click();
assert_eq!(
snap_of("status_left").text,
Some("Merged selected cells".into()),
"diagonal 2x2 range must merge: {:?}",
snap_of("status_left").text
);
app.locator(Selector::id("split_cell_btn")).click();
assert_eq!(
snap_of("status_left").text,
Some("Split merged cell".into()),
"split back: {:?}",
snap_of("status_left").text
);
}
/// Navigate from home → work → construction_grid → m_workspace_cad_btn and
/// assert the CAD workspace lands on the project dashboard (project grid +
/// "+ New Project"), not a blank/editor-only sheet. Regression test for the
/// white-screen bug: the `dashboard_layer` must realize as the first child
/// of CadWorkspace with its own `dashboard`/`dashboard_title`/`new_project_btn`
/// visible.
#[makepad_test]
fn cad_lands_on_dashboard(app: TestApp) {
if !require_test_mode() {
return;
}
app.locator(Selector::id("work_button")).wait_visible().click();
app.locator(Selector::id("pull_handle")).wait_visible().drag_by(0.0, -600.0);
app.locator(Selector::id("construction_grid_button")).wait_visible().click();
app.locator(Selector::id("m_workspace_cad_btn")).wait_visible().click();
// Landing on the dashboard grid: the dashboard header/title and the
// "+ New Project" button must be visible. `wait_visible` panics on
// timeout, so this fails the test if the dashboard never realizes.
app.locator(Selector::id("dashboard_layer")).wait_visible();
app.locator(Selector::id("dashboard")).wait_visible();
app.locator(Selector::id("dashboard_title")).wait_visible();
app.locator(Selector::id("new_project_btn")).wait_visible();
}