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(); }