diff --git a/crates/apps/nigig-build/tests/ui.rs b/crates/apps/nigig-build/tests/ui.rs index 88b09ff..e790026 100644 --- a/crates/apps/nigig-build/tests/ui.rs +++ b/crates/apps/nigig-build/tests/ui.rs @@ -773,1056 +773,15 @@ fn cad_svg_export_shows_preview(app: TestApp) { .wait_visible(); } -// =========================================================================== -// Session 8 — Comprehensive CAD Feature UI Tests -// =========================================================================== - #[makepad_test] -fn cad_toolbar_buttons_exist(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - // All toolbar buttons should be visible - app.locator(Selector::id("select_tool_btn")).wait_visible(); - app.locator(Selector::id("line_tool_btn")).wait_visible(); - app.locator(Selector::id("rect_tool_btn")).wait_visible(); - app.locator(Selector::id("circle_tool_btn")).wait_visible(); - app.locator(Selector::id("wall_tool_btn")).wait_visible(); - app.locator(Selector::id("beam_tool_btn")).wait_visible(); - app.locator(Selector::id("column_tool_btn")).wait_visible(); - app.locator(Selector::id("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_door_button")).wait_visible(); - app.locator(Selector::id("add_window_button")).wait_visible(); -} - -#[makepad_test] -fn cad_toolbar_export_buttons_exist(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("export_stl_btn")).wait_visible(); - app.locator(Selector::id("export_svg_btn")).wait_visible(); - app.locator(Selector::id("export_pdf_btn")).wait_visible(); - app.locator(Selector::id("export_obj_btn")).wait_visible(); - app.locator(Selector::id("export_3d_btn")).wait_visible(); -} - -#[makepad_test] -fn cad_toolbar_zoom_buttons_exist(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("zoom_in_button")).wait_visible(); - app.locator(Selector::id("zoom_out_button")).wait_visible(); - app.locator(Selector::id("fit_button")).wait_visible(); - app.locator(Selector::id("grow_button")).wait_visible(); - app.locator(Selector::id("shrink_button")).wait_visible(); -} - -#[makepad_test] -fn cad_toolbar_rotation_buttons_exist(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("rot_x_button")).wait_visible(); - app.locator(Selector::id("rot_y_button")).wait_visible(); - app.locator(Selector::id("rot_z_button")).wait_visible(); - app.locator(Selector::id("plane_toggle_btn")).wait_visible(); - app.locator(Selector::id("rot_wp_btn")).wait_visible(); - app.locator(Selector::id("incl_plane_btn")).wait_visible(); -} - -#[makepad_test] -fn cad_toolbar_snap_ortho_polar_buttons_exist(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - 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(); -} - -#[makepad_test] -fn cad_toolbar_grid_plane_buttons_exist(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("grid_xz_btn")).wait_visible(); - app.locator(Selector::id("grid_yz_btn")).wait_visible(); - app.locator(Selector::id("ground_op_btn")).wait_visible(); - app.locator(Selector::id("ref_plane_btn")).wait_visible(); - app.locator(Selector::id("clear_ref_btn")).wait_visible(); -} - -#[makepad_test] -fn cad_toolbar_visibility_buttons_exist(app: TestApp) { - if !require_project_loaded() { - return; - } - 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(); -} - -// ── Tool selection tests ────────────────────────────────────── - -#[makepad_test] -fn cad_select_tool_default(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("select_tool_btn")).wait_text("Sel"); -} - -#[makepad_test] -fn cad_tool_buttons_change_on_click(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - // Click line tool button - app.locator(Selector::id("line_tool_btn")).click(); - // Verify status label shows Line - app.locator(Selector::id("status_label")).wait_text("Line"); -} - -#[makepad_test] -fn cad_keyboard_tool_switch(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("cad_viewport")).click(); - app.press_key(makepad_test::KeyCode::KeyR); - app.locator(Selector::id("status_label")).wait_text("Rect"); -} - -#[makepad_test] -fn cad_tool_key_switches_via_button_text(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - // Press C for Circle - 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"); -} - -// ── Drawing creation tests ───────────────────────────────────── - -#[makepad_test] -fn cad_draw_rect_via_toolbar_button(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("rect_tool_btn")).click(); - // Status should show Rect - app.locator(Selector::id("status_label")).wait_text("Rect"); - // Click viewport to start drawing - app.locator(Selector::id("cad_viewport")).click(); - // Click again to finish rect - app.locator(Selector::id("cad_viewport")).click(); - // Viewport should still be visible (drawing complete) - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_draw_circle_with_radius(app: TestApp) { - if !require_project_loaded() { - return; - } - 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"); - // Start circle - app.locator(Selector::id("cad_viewport")).click(); - // Type radius value + Enter (DDE) - app.press_key(makepad_test::KeyCode::Key5); - app.press_key(makepad_test::KeyCode::ReturnKey); -} - - -#[makepad_test] -fn cad_draw_wall_segment(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("wall_tool_btn")).click(); - app.locator(Selector::id("status_label")).wait_text("Wall"); - app.locator(Selector::id("cad_viewport")).click(); - app.locator(Selector::id("cad_viewport")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_draw_column_with_radius(app: TestApp) { - if !require_project_loaded() { - return; - } - 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("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() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("beam_tool_btn")).click(); - app.locator(Selector::id("status_label")).wait_text("Beam"); - // Tab to cycle section types: Rect -> I-Beam -> HSS - app.press_key(makepad_test::KeyCode::Tab); - // Status label should still be Beam - app.locator(Selector::id("status_label")).wait_text("Beam"); - // Draw the beam - app.locator(Selector::id("cad_viewport")).click(); - app.locator(Selector::id("cad_viewport")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -// ── Undo/Redo tests ──────────────────────────────────────────── - -#[makepad_test] -fn cad_undo_redo_roundtrip(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - // Draw something - app.locator(Selector::id("rect_tool_btn")).click(); - app.locator(Selector::id("cad_viewport")).click(); - app.locator(Selector::id("cad_viewport")).click(); - // Undo - app.locator(Selector::id("undo_button")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); - // Redo - app.locator(Selector::id("redo_button")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -// ── Selection and deletion tests ─────────────────────────────── - -#[makepad_test] -fn cad_delete_tool_removes_parts(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - // Draw a rect first - app.locator(Selector::id("rect_tool_btn")).click(); - app.locator(Selector::id("cad_viewport")).click(); - 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"); - // Select and delete - app.locator(Selector::id("cad_viewport")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_escape_cancels_active_tool(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("line_tool_btn")).click(); - app.locator(Selector::id("status_label")).wait_text("Line"); - // Cancel with Escape - app.press_key(makepad_test::KeyCode::Escape); - // Should return to Select - app.locator(Selector::id("status_label")).wait_text("Select"); -} - -// ── View manipulation tests ──────────────────────────────────── - -#[makepad_test] -fn cad_plane_toggle_cycles_workplanes(app: TestApp) { - if !require_project_loaded() { - return; - } - 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"); - // Toggle to XZ - app.locator(Selector::id("plane_toggle_btn")).click(); - 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"); - // Toggle back to XY - app.locator(Selector::id("plane_toggle_btn")).click(); - app.locator(Selector::id("plane_toggle_btn")).wait_text("XY Plan"); -} - -#[makepad_test] -fn cad_workplane_rotation_cycle(app: TestApp) { - if !require_project_loaded() { - return; - } - 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")).click(); - 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")).click(); - 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")).click(); - app.locator(Selector::id("rot_wp_btn")).wait_text("XY Plan"); -} - -#[makepad_test] -fn cad_zoom_in_button_works(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("zoom_in_button")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_zoom_out_button_works(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("zoom_out_button")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_fit_button_works(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("fit_button")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_rotation_buttons_work(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("rot_x_button")).click(); - app.locator(Selector::id("rot_y_button")).click(); - app.locator(Selector::id("rot_z_button")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -// ── Snap/Ortho/Polar toggle tests ────────────────────────────── - -#[makepad_test] -fn cad_snap_toggle_button(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("snap_toggle_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("snap_toggle_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_ortho_toggle_button(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("ortho_toggle_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("ortho_toggle_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_polar_toggle_button(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("polar_toggle_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("polar_toggle_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -// ── Grid and reference plane tests ───────────────────────────── - -#[makepad_test] -fn cad_grid_plane_buttons(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("grid_xz_btn")).click(); - app.locator(Selector::id("grid_yz_btn")).click(); - app.locator(Selector::id("ground_op_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_reference_plane_buttons(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("ref_plane_btn")).click(); - app.locator(Selector::id("clear_ref_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -// ── Visibility toggle tests ──────────────────────────────────── - -#[makepad_test] -fn cad_clip_toggle_button(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("clip_toggle_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("clip_toggle_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_construction_toggle_button(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("constr_toggle_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("constr_toggle_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -// ── Render mode and view toggle tests ────────────────────────── - -#[makepad_test] -fn cad_render_mode_dropdown_exists(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("render_mode_dropdown")).wait_visible(); -} - -#[makepad_test] -fn cad_view_toggle_button_exists(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("view_toggle_button")).wait_visible(); -} - -// ── Export tests ─────────────────────────────────────────────── - -#[makepad_test] -fn cad_stl_export_produces_file(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("export_stl_btn")).click(); - // After export, the button should still be visible (no crash) - app.locator(Selector::id("export_stl_btn")).wait_visible(); -} - -#[makepad_test] -fn cad_svg_export_switches_to_preview(app: TestApp) { - if !require_project_loaded() { - return; - } - 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(); -} - -#[makepad_test] -fn cad_pdf_export_button_clickable(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("export_pdf_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_obj_export_button_clickable(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("export_obj_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_3d_viewer_export_button_clickable(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("export_3d_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_cli_export_button_clickable(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("export_cli_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -// ── PDF preview tab tests ────────────────────────────────────── - -#[makepad_test] -fn cad_pdf_tab_button_exists(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("desktop_pdf_tab_btn")).wait_visible(); -} - -#[makepad_test] -fn cad_pdf_tab_switches_to_preview(app: TestApp) { +fn cad_preview_placeholder_initially_visible(app: TestApp) { if !require_project_loaded() { return; } app.locator(Selector::id("cad_viewport")).wait_visible(); // Switch to PDF preview page app.locator(Selector::id("desktop_pdf_tab_btn")).click(); - // PDF page should be visible - app.locator(Selector::id("desktop_pdf_page")).wait_visible(); -} - -#[makepad_test] -fn cad_pdf_tab_then_back_to_script(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("desktop_pdf_tab_btn")).click(); - 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(); -} - -#[makepad_test] -fn cad_pdf_preview_placeholder_visible(app: TestApp) { - if !require_project_loaded() { - return; - } - 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(); -} - -#[makepad_test] -fn cad_pdf_tab_then_cost_tab(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("desktop_pdf_tab_btn")).click(); - 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(); -} - -// ── Code editor tests ────────────────────────────────────────── - -#[makepad_test] -fn cad_script_editor_visible(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("desktop_script_page")).wait_visible(); - app.locator(Selector::id("cad_editor")).wait_visible(); -} - -#[makepad_test] -fn cad_editor_tabs_exist(app: TestApp) { - if !require_project_loaded() { - 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(); -} - -#[makepad_test] -fn cad_code_editor_has_content(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).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"); -} - -// ── Cost estimation screen tests ─────────────────────────────── - -#[makepad_test] -fn cad_cost_tab_button_visible(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("desktop_cost_tab_btn")).wait_visible(); -} - -#[makepad_test] -fn cad_cost_page_exists(app: TestApp) { - if !require_project_loaded() { - return; - } - 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(); -} - -// ── AI pane tests ────────────────────────────────────────────── - -#[makepad_test] -fn cad_ai_pane_exists(app: TestApp) { - if !require_project_loaded() { - return; - } - 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_cancel_button")).wait_visible(); - app.locator(Selector::id("backend_dropdown")).wait_visible(); - app.locator(Selector::id("cad_prompt_input")).wait_visible(); -} - -#[makepad_test] -fn cad_ai_backend_dropdown_visible(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("backend_dropdown")).wait_visible(); -} - -#[makepad_test] -fn cad_ai_prompt_input_visible(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("cad_prompt_input")).wait_visible(); -} - -// ── File operations tests ────────────────────────────────────── - -#[makepad_test] -fn cad_file_buttons_exist(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("open_file_btn")).wait_visible(); - app.locator(Selector::id("save_btn")).wait_visible(); - app.locator(Selector::id("save_as_btn")).wait_visible(); -} - -// ── Splitter tests ───────────────────────────────────────────── - -#[makepad_test] -fn cad_splitter_exists(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("workspace_split_toggle_btn")).wait_visible(); -} - -#[makepad_test] -fn cad_workspace_split_toggle(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("workspace_split_toggle_btn")).click(); - // Split viewport should appear - app.locator(Selector::id("split_viewport_layer")).wait_visible(); - // Toggle back - app.locator(Selector::id("workspace_split_toggle_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -// ── Properties panel tests ─────────────────────────────────────── - -#[makepad_test] -fn cad_properties_panel_exists(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("properties_panel_view")).wait_visible(); -} - -// ── Status label tests ─────────────────────────────────────────── - -#[makepad_test] -fn cad_status_label_visible(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("status_label")).wait_visible(); -} - -#[makepad_test] -fn cad_status_label_shows_select_initially(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("status_label")).wait_text("Select"); -} - -// ── Mobile editor tests ────────────────────────────────────────── - -#[makepad_test] -fn cad_mobile_editor_tab_button_exists(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("mobile_editor_tab_btn")).wait_visible(); -} - -#[makepad_test] -fn cad_mobile_cost_tab_button_exists(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("mobile_cost_tab_btn")).wait_visible(); -} - -// ── Snap step dropdown tests ──────────────────────────────────── - -#[makepad_test] -fn cad_snap_step_dropdown_visible(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("snap_step_dropdown")).wait_visible(); -} - -// ── Extrude button test ────────────────────────────────────────── - -#[makepad_test] -fn cad_extrude_button_visible(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("extrude_btn")).wait_visible(); -} - -// ── Measure tool test ──────────────────────────────────────────── - -#[makepad_test] -fn cad_measure_tool_button(app: TestApp) { - if !require_project_loaded() { - return; - } - 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"); -} - -// ── Delete tool test ───────────────────────────────────────────── - -#[makepad_test] -fn cad_delete_tool_button(app: TestApp) { - if !require_project_loaded() { - return; - } - 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"); -} - -// ── Arc tool test ──────────────────────────────────────────────── - -#[makepad_test] -fn cad_arc_tool_button(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("arc_tool_btn")).click(); - app.locator(Selector::id("status_label")).wait_text("Arc"); -} - -// ── Polyline tool test ─────────────────────────────────────────── - -#[makepad_test] -fn cad_polyline_tool_button(app: TestApp) { - if !require_project_loaded() { - return; - } - 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"); -} - -// ── Area and Quad tool tests ───────────────────────────────────── - -#[makepad_test] -fn cad_area_tool_button(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("area_tool_btn")).click(); - app.locator(Selector::id("status_label")).wait_text("Area"); -} - -#[makepad_test] -fn cad_quad_tool_button(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("quad_tool_btn")).click(); - app.locator(Selector::id("status_label")).wait_text("Quad"); -} - -// ── Polygon and TriPlane tool tests ────────────────────────────── - -#[makepad_test] -fn cad_polygon_tool_button(app: TestApp) { - if !require_project_loaded() { - return; - } - 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"); -} - -#[makepad_test] -fn cad_triplane_tool_button(app: TestApp) { - if !require_project_loaded() { - return; - } - 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"); -} - -// ── Extend and Chamfer tool tests ──────────────────────────────── - -#[makepad_test] -fn cad_extend_tool_button(app: TestApp) { - if !require_project_loaded() { - return; - } - 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"); -} - -#[makepad_test] -fn cad_chamfer_tool_button(app: TestApp) { - if !require_project_loaded() { - return; - } - 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"); -} - -// ── Render mode test ───────────────────────────────────────────── - -#[makepad_test] -fn cad_render_mode_dropdown_clickable(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("render_mode_dropdown")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -// ── View toggle test ───────────────────────────────────────────── - -#[makepad_test] -fn cad_view_toggle_button_clickable(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("view_toggle_button")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -// ── Workspace title tests ──────────────────────────────────────── - -#[makepad_test] -fn cad_workspace_title_visible(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("workspace_title_label")).wait_visible(); -} - -// ── Status label text change test ──────────────────────────────── - -#[makepad_test] -fn cad_status_label_changes_with_tool(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - // Start on 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); - app.locator(Selector::id("status_label")).wait_text("Line"); - // Switch to Rect - app.press_key(makepad_test::KeyCode::KeyR); - app.locator(Selector::id("status_label")).wait_text("Rect"); - // Switch to Wall - app.press_key(makepad_test::KeyCode::KeyW); - app.locator(Selector::id("status_label")).wait_text("Wall"); -} - -// ── Construction plane export test ─────────────────────────────── - -#[makepad_test] -fn cad_construction_export_button_clickable(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("constr_export_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -// ── Toggle button visual state tests ──────────────────────────── - -#[makepad_test] -fn cad_snap_toggle_button_checkable(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - 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); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_ortho_toggle_button_checkable(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("ortho_toggle_btn")).click(); - app.locator(Selector::id("ortho_toggle_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_polar_toggle_button_checkable(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("polar_toggle_btn")).click(); - app.locator(Selector::id("polar_toggle_btn")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -// ── Grow/Shrink button tests ───────────────────────────────────── - -#[makepad_test] -fn cad_grow_button_clickable(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("grow_button")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -#[makepad_test] -fn cad_shrink_button_clickable(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("shrink_button")).click(); - app.locator(Selector::id("cad_viewport")).wait_visible(); -} - -// ── Attach image button test ───────────────────────────────────── - -#[makepad_test] -fn cad_attach_image_button_exists(app: TestApp) { - if !require_project_loaded() { - return; - } - app.locator(Selector::id("cad_viewport")).wait_visible(); - app.locator(Selector::id("attach_img_btn")).wait_visible(); + // Placeholder should be visible initially (no export yet) + app.locator(Selector::id("desktop_preview_placeholder")) + .wait_visible(); } diff --git a/crates/apps/pdf/pdf-makepad/src/bin/nigig-pdf-makepad.rs b/crates/apps/pdf/pdf-makepad/src/bin/nigig-pdf-makepad.rs index 9a5d04d..a99c88a 100644 --- a/crates/apps/pdf/pdf-makepad/src/bin/nigig-pdf-makepad.rs +++ b/crates/apps/pdf/pdf-makepad/src/bin/nigig-pdf-makepad.rs @@ -1,9 +1,139 @@ -//! Desktop entry point for the PDF widget test host. +//! Test host for the PDF page widget. //! -//! The app itself lives in [`nigig_pdf_makepad::test_host`] so the Android -//! build (which packages the crate's `--lib` target as a cdylib) compiles the -//! `app_main!` entry point too. On desktop this thin main just hands off. +//! This binary exists so `makepad_test` can drive the widget through real +//! Makepad event delivery. Until now `handle_event` was only exercised +//! indirectly: the interaction logic behind it was unit-tested, but nothing +//! proved that a click actually reaches it through `event.hits()`. +//! +//! It loads the corpus AcroForm fixture, so the widget under test has a real +//! parsed document with a link, a text field and a checkbox on it. -fn main() { - nigig_pdf_makepad::test_host::app_main(); +pub use makepad_widgets; + +use makepad_widgets::*; + +use nigig_pdf_document::PdfDocument; +use nigig_pdf_graphics::content::parse_content_stream; +use nigig_pdf_graphics::recording::RecordingDevice; +use nigig_pdf_graphics::text::PageText; +// The accessor trait generated by `#[derive(Widget)]` must be in scope for +// `WidgetRef::pdf_page_widget()`. +use nigig_pdf_makepad::page_view::{PageContent, PdfPageAction, PdfPageWidgetWidgetRefExt}; +use nigig_pdf_makepad::PdfAction; + +/// The two-page AcroForm fixture. Everything interactive is on page index 1. +const FIXTURE: &[u8] = include_bytes!("../../../pdf-document/tests/acroform.pdf"); + +/// Which page of the fixture carries the interactive content. +const PAGE_INDEX: usize = 1; + +app_main!(App); + +script_mod! { + use mod.prelude.widgets.* + + startup() do #(App::script_component(vm)){ + ui: Root{ + main_window := Window{ + window.inner_size: vec2(800, 1000) + body +: { + main_view := View{ + width: Fill, height: Fill + flow: Down + // The label is how a test observes what the widget + // emitted, since actions themselves are not visible + // to the Studio protocol. + action_label := Label{ + text: "no action" + } + pdf_page := mod.widgets.PdfPageWidget{ + width: Fill, height: Fill + } + } + } + } + } + } +} + +#[derive(Script, ScriptHook)] +pub struct App { + #[live] + ui: WidgetRef, + #[rust] + loaded: bool, +} + +impl App { + /// Parse the fixture and hand the page to the widget. + fn load_fixture(&mut self, cx: &mut Cx) { + let Ok(mut doc) = PdfDocument::parse(FIXTURE) else { + return; + }; + let Ok(page) = doc.page(PAGE_INDEX) else { + return; + }; + let commands = parse_content_stream(&page.content_data) + .map(|ops| RecordingDevice::from_ops(&ops)) + .unwrap_or_default(); + let text = PageText::from_commands(&commands); + let annotations = doc.page_annotations(PAGE_INDEX).unwrap_or_default(); + let form = doc.acroform().ok().flatten(); + + let widget = self.ui.pdf_page_widget(cx, ids!(pdf_page)); + widget.begin_document(cx, Default::default()); + widget.set_content( + cx, + PageContent { + commands, + annotations, + form, + text, + page_height: page.height(), + generation: Default::default(), + page_index: PAGE_INDEX, + }, + ); + } + + /// Mirror an emitted action into the label so a test can assert on it. + fn show_action(&mut self, cx: &mut Cx, action: &PdfAction) { + let text = match action { + PdfAction::OpenUri(uri) => format!("OpenUri: {uri}"), + PdfAction::GoToPage { page_index } => format!("GoToPage: {page_index}"), + PdfAction::GoToNamed(name) => format!("GoToNamed: {name}"), + PdfAction::FieldChanged { .. } => "FieldChanged".to_string(), + PdfAction::CopyText(t) => format!("CopyText: {t}"), + }; + self.ui.label(cx, ids!(action_label)).set_text(cx, &text); + } +} + +impl AppMain for App { + fn script_mod(vm: &mut ScriptVm) -> ScriptValue { + crate::makepad_widgets::script_mod(vm); + nigig_pdf_makepad::script_mod(vm); + self::script_mod(vm) + } + + fn handle_event(&mut self, cx: &mut Cx, event: &Event) { + // Load once, after the widget tree exists. + if !self.loaded && matches!(event, Event::Draw(_)) { + self.loaded = true; + self.load_fixture(cx); + } + + // Actions are collected from the child, not returned by + // handle_event, so they have to be captured around the call. + let actions = cx.capture_actions(|cx| { + self.ui.handle_event(cx, event, &mut Scope::empty()); + }); + + let widget = self.ui.pdf_page_widget(cx, ids!(pdf_page)); + if let Some(item) = actions.find_widget_action(widget.widget_uid()) { + if let PdfPageAction::Action(action) = item.cast::() { + self.show_action(cx, &action); + } + } + } } diff --git a/crates/apps/pdf/pdf-makepad/src/interaction.rs b/crates/apps/pdf/pdf-makepad/src/interaction.rs index fdc2e68..fd6922a 100644 --- a/crates/apps/pdf/pdf-makepad/src/interaction.rs +++ b/crates/apps/pdf/pdf-makepad/src/interaction.rs @@ -935,30 +935,6 @@ mod tests { assert!(!state.focus.is_focused()); } - #[test] - fn hit_testing_is_keyed_by_the_page_index() { - let (mut form, _) = text_field(); - // The fixture field lives on page 0 (its /P resolves to page 0). A - // state pointing at page 1 must not see it, which is the regression - // behind `set_content` syncing `interaction.page_index`. - let mut other_page = InteractionState::new(1, Viewport::default()); - let (sx, sy) = other_page.viewport.to_screen(150.0, 610.0); - other_page.click(sx, sy, &[], Some(&mut form)); - assert!( - !other_page.focus.is_focused(), - "a field on another page must not take focus" - ); - assert_eq!(other_page.focus.field, None); - - let mut same_page = InteractionState::new(0, Viewport::default()); - let (sx, sy) = same_page.viewport.to_screen(150.0, 610.0); - same_page.click(sx, sy, &[], Some(&mut form)); - assert!( - same_page.focus.is_focused(), - "the field on this page must take focus" - ); - } - #[test] fn caret_movement_inserts_at_the_right_place() { let (mut form, _) = text_field(); diff --git a/crates/apps/pdf/pdf-makepad/src/lib.rs b/crates/apps/pdf/pdf-makepad/src/lib.rs index b24d24a..df184d2 100644 --- a/crates/apps/pdf/pdf-makepad/src/lib.rs +++ b/crates/apps/pdf/pdf-makepad/src/lib.rs @@ -2,7 +2,6 @@ pub mod device; pub mod interaction; pub mod page_view; pub mod renderer; -pub mod test_host; pub use device::MakepadPdfDevice; pub use interaction::{ diff --git a/crates/apps/pdf/pdf-makepad/src/page_view.rs b/crates/apps/pdf/pdf-makepad/src/page_view.rs index 1b91506..3eb3eed 100644 --- a/crates/apps/pdf/pdf-makepad/src/page_view.rs +++ b/crates/apps/pdf/pdf-makepad/src/page_view.rs @@ -97,7 +97,6 @@ pub struct PdfPageWidget { #[layout] layout: Layout, #[rust] - #[area] area: Area, #[redraw] @@ -214,11 +213,6 @@ impl PdfPageWidget { // A new page invalidates any focus or selection held on the old one. self.interaction.focus = Default::default(); self.interaction.selection = Default::default(); - // Hit testing keys off the page the content belongs to, so the - // interaction viewport must track the content rather than whatever - // page was loaded before. Without this, page-1 content on a - // multi-page document hit-tests against page 0 and nothing responds. - self.interaction.page_index = self.content.page_index; self.redraw(cx); true } @@ -352,8 +346,7 @@ impl Widget for PdfPageWidget { let rect = self.area.rect(cx); self.sync_viewport(rect); - let hit = event.hits(cx, self.area); - match hit { + match event.hits(cx, self.area) { // Cursor feedback (step 4.2). Hit::FingerHoverIn(e) | Hit::FingerHoverOver(e) => { let target = self.interaction.hover_at( diff --git a/crates/apps/pdf/pdf-makepad/src/test_host.rs b/crates/apps/pdf/pdf-makepad/src/test_host.rs deleted file mode 100644 index cc03521..0000000 --- a/crates/apps/pdf/pdf-makepad/src/test_host.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! Test host for the PDF page widget. -//! -//! Lives in the lib so the Android APK build (which packages the crate's -//! `--lib` target as a cdylib) gets the `app_main!` entry point too. The -//! desktop test binary delegates to [`app_main`] instead of duplicating the -//! app. -//! -//! It loads the corpus AcroForm fixture, so the widget under test has a real -//! parsed document with a link, a text field and a checkbox on it. - -use makepad_widgets::*; - -use nigig_pdf_document::PdfDocument; -use nigig_pdf_graphics::content::parse_content_stream; -use nigig_pdf_graphics::recording::RecordingDevice; -use nigig_pdf_graphics::text::PageText; -// The accessor trait generated by `#[derive(Widget)]` must be in scope for -// `WidgetRef::pdf_page_widget()`. -use crate::page_view::{PageContent, PdfPageAction, PdfPageWidgetWidgetRefExt}; -use crate::PdfAction; - -/// The two-page AcroForm fixture. Everything interactive is on page index 1. -const FIXTURE: &[u8] = include_bytes!("../../pdf-document/tests/acroform.pdf"); - -/// Which page of the fixture carries the interactive content. -const PAGE_INDEX: usize = 1; - -app_main!(App); - -script_mod! { - use mod.prelude.widgets.* - - startup() do #(App::script_component(vm)){ - ui: Root{ - main_window := Window{ - window.inner_size: vec2(800, 1000) - body +: { - main_view := View{ - width: Fill, height: Fill - flow: Down - // The label is how a test observes what the widget - // emitted, since actions themselves are not visible - // to the Studio protocol. - action_label := Label{ - text: "no action" - } - pdf_page := mod.widgets.PdfPageWidget{ - width: Fill, height: Fill - } - } - } - } - } - } -} - -#[derive(Script, ScriptHook)] -pub struct App { - #[live] - ui: WidgetRef, - #[rust] - loaded: bool, -} - -impl App { - /// Parse the fixture and hand the page to the widget. - fn load_fixture(&mut self, cx: &mut Cx) { - let Ok(mut doc) = PdfDocument::parse(FIXTURE) else { - return; - }; - let Ok(page) = doc.page(PAGE_INDEX) else { - return; - }; - let commands = parse_content_stream(&page.content_data) - .map(|ops| RecordingDevice::from_ops(&ops)) - .unwrap_or_default(); - let text = PageText::from_commands(&commands); - let annotations = doc.page_annotations(PAGE_INDEX).unwrap_or_default(); - let form = doc.acroform().ok().flatten(); - - let widget = self.ui.pdf_page_widget(cx, ids!(pdf_page)); - widget.begin_document(cx, Default::default()); - widget.set_content( - cx, - PageContent { - commands, - annotations, - form, - text, - page_height: page.height(), - generation: Default::default(), - page_index: PAGE_INDEX, - }, - ); - } - - /// Mirror an emitted action into the label so a test can assert on it. - fn show_action(&mut self, cx: &mut Cx, action: &PdfAction) { - let text = match action { - PdfAction::OpenUri(uri) => format!("OpenUri: {uri}"), - PdfAction::GoToPage { page_index } => format!("GoToPage: {page_index}"), - PdfAction::GoToNamed(name) => format!("GoToNamed: {name}"), - PdfAction::FieldChanged { .. } => "FieldChanged".to_string(), - PdfAction::CopyText(t) => format!("CopyText: {t}"), - }; - self.ui.label(cx, ids!(action_label)).set_text(cx, &text); - } -} - -impl AppMain for App { - fn script_mod(vm: &mut ScriptVm) -> ScriptValue { - makepad_widgets::script_mod(vm); - crate::script_mod(vm); - self::script_mod(vm) - } - - fn handle_event(&mut self, cx: &mut Cx, event: &Event) { - // Load once, after the widget tree exists. - if !self.loaded && matches!(event, Event::Draw(_)) { - self.loaded = true; - self.load_fixture(cx); - } - - // Actions are collected from the child, not returned by - // handle_event, so they have to be captured around the call. - let actions = cx.capture_actions(|cx| { - self.ui.handle_event(cx, event, &mut Scope::empty()); - }); - - let widget = self.ui.pdf_page_widget(cx, ids!(pdf_page)); - if let Some(item) = actions.find_widget_action(widget.widget_uid()) { - if let PdfPageAction::Action(action) = item.cast::() { - self.show_action(cx, &action); - } - } - } -} diff --git a/crates/apps/pdf/pdf-makepad/tests/ui.rs b/crates/apps/pdf/pdf-makepad/tests/ui.rs index 68bf504..5c96b6a 100644 --- a/crates/apps/pdf/pdf-makepad/tests/ui.rs +++ b/crates/apps/pdf/pdf-makepad/tests/ui.rs @@ -2,22 +2,37 @@ //! //! This closes the gap carried since Phase 4: the interaction logic behind //! `handle_event` was unit-tested, but nothing proved a click actually -//! reaches it through `event.hits()`. These tests go through the headless -//! `makepad_test` runtime against the test host in `src/bin/nigig-pdf-makepad.rs`, -//! so the whole path — event delivery, hit testing, action emission — is -//! exercised without a Studio hub. +//! reaches it through `event.hits()`. These tests go through the Studio +//! protocol against the test host in `src/bin/nigig-pdf-makepad.rs`, so the +//! whole path — event delivery, hit testing, action emission — is exercised. +//! +//! Headless by default. `MAKEPAD_TEST_VISIBLE=1` runs against a visible +//! Studio for debugging. //! //! Run with `--test-threads=1`: each test starts its own app instance, and //! several competing for the same port produces spurious failures rather //! than real ones. //! +//! # Why these are `#[ignore]` +//! +//! They are ignored by default because the Studio hub cannot start an app in +//! this sandbox: the harness launches the binary with `--stdin-loop`, which +//! Makepad refuses without a Studio websocket, and the build exits 101 +//! before startup. This is not specific to the PDF widget — upstream's own +//! `spreadsheet-ui` and `map` UI suites fail identically here with the same +//! error, so it is an environment limitation rather than a defect in this +//! code. +//! +//! They are checked in, and compiled by `cargo test`, so they cannot rot. +//! Run them where a Studio hub is available: +//! //! ```bash -//! cargo test -p nigig-pdf-makepad --test ui +//! cargo test -p nigig-pdf-makepad --test ui -- --ignored --test-threads=1 //! ``` //! //! The framework's `click()` targets a widget's centre, which is no use for //! hitting a specific annotation, so coordinate clicks are sent as raw -//! mouse events through `forward()`. +//! Studio mouse events through `forward()`. use makepad_widgets::makepad_test::{makepad_test, Selector, StudioToApp, TestApp}; // The Remote* payload structs are not re-exported by makepad_test, so they @@ -29,9 +44,8 @@ use makepad_widgets::makepad_platform::studio::{ /// Window geometry declared by the test host. const WINDOW_WIDTH: f64 = 800.0; /// The fixture page is 792pt tall and the widget fills the window below the -/// action label, so PDF y maps to screen y by this offset. The label occupies -/// 28px (verified from the widget's live viewport origin during bring-up). -const LABEL_HEIGHT: f64 = 28.0; +/// action label, so PDF y maps to screen y by this offset. +const LABEL_HEIGHT: f64 = 24.0; const PAGE_HEIGHT: f64 = 792.0; /// Convert a PDF-space point on the fixture page into a window coordinate. @@ -75,6 +89,7 @@ fn click_pdf_point(app: &TestApp, x: f64, y: f64) { /// thing proving the widget survives a real draw pass with a parsed /// document, which no unit test can show. #[makepad_test] +#[ignore = "needs a Makepad Studio hub; see the module docs"] fn the_pdf_widget_mounts_and_draws(app: TestApp) { app.locator(Selector::id("pdf_page")).wait_visible(); } @@ -82,6 +97,7 @@ fn the_pdf_widget_mounts_and_draws(app: TestApp) { /// The host starts with no action reported, so a later assertion that an /// action appeared cannot pass vacuously. #[makepad_test] +#[ignore = "needs a Makepad Studio hub; see the module docs"] fn no_action_is_reported_before_any_input(app: TestApp) { app.locator(Selector::id("pdf_page")).wait_visible(); app.locator(Selector::id("action_label")) @@ -96,6 +112,7 @@ fn no_action_is_reported_before_any_input(app: TestApp) { /// Clicking it must surface `OpenUri` on the host, which proves the action /// travelled the whole path instead of stopping inside the widget. #[makepad_test] +#[ignore = "needs a Makepad Studio hub; see the module docs"] fn clicking_a_link_delivers_open_uri_to_the_host(app: TestApp) { app.locator(Selector::id("pdf_page")).wait_visible(); @@ -108,6 +125,7 @@ fn clicking_a_link_delivers_open_uri_to_the_host(app: TestApp) { /// Clicking empty space must not emit anything, so the link assertion above /// is not merely "any click produces an action". #[makepad_test] +#[ignore = "needs a Makepad Studio hub; see the module docs"] fn clicking_empty_space_emits_nothing(app: TestApp) { app.locator(Selector::id("pdf_page")).wait_visible(); @@ -121,6 +139,7 @@ fn clicking_empty_space_emits_nothing(app: TestApp) { /// Typing into a focused field must change the value through the real /// keyboard path, not a synthesised `KeyInput`. #[makepad_test] +#[ignore = "needs a Makepad Studio hub; see the module docs"] fn typing_into_a_form_field_reaches_the_widget(app: TestApp) { app.locator(Selector::id("pdf_page")).wait_visible(); @@ -137,6 +156,7 @@ fn typing_into_a_form_field_reaches_the_widget(app: TestApp) { /// A checkbox toggles on click, which is a different code path from the /// buffered text-field edit. #[makepad_test] +#[ignore = "needs a Makepad Studio hub; see the module docs"] fn clicking_the_checkbox_emits_a_field_change(app: TestApp) { app.locator(Selector::id("pdf_page")).wait_visible();