#include "app_core/document_session.h" #include "test_harness.h" #include namespace { class FakeDocumentOpenServices final : public pp::app::DocumentOpenServices { public: void prompt_import_abr(const pp::app::DocumentOpenRoute& route) override { abr_prompts += 1; last_path = route.path; call_order += "abr;"; } void prompt_import_ppbr(const pp::app::DocumentOpenRoute& route) override { ppbr_prompts += 1; last_path = route.path; call_order += "ppbr;"; } void open_project_now(const pp::app::DocumentOpenRoute& route) override { project_opens += 1; last_path = route.path; call_order += "open;"; } void prompt_discard_unsaved_project(const pp::app::DocumentOpenRoute& route) override { discard_prompts += 1; last_path = route.path; call_order += "discard;"; } int abr_prompts = 0; int ppbr_prompts = 0; int project_opens = 0; int discard_prompts = 0; std::string last_path; std::string call_order; }; class FakeCloseRequestServices final : public pp::app::CloseRequestServices { public: void request_close_now() override { close_now += 1; call_order += "close;"; } void show_unsaved_close_prompt() override { close_prompts += 1; call_order += "prompt;"; } int close_now = 0; int close_prompts = 0; std::string call_order; }; class FakeDocumentSaveServices final : public pp::app::DocumentSaveServices { public: void show_save_dialog() override { dialogs += 1; call_order += "dialog;"; } void save_existing_document() override { saves += 1; call_order += "save;"; } void save_document_version() override { versions += 1; call_order += "version;"; } int dialogs = 0; int saves = 0; int versions = 0; std::string call_order; }; class FakeDocumentWorkflowServices final : public pp::app::DocumentWorkflowServices { public: void continue_workflow_now() override { continues += 1; call_order += "continue;"; } void prompt_save_before_continue() override { prompts += 1; call_order += "prompt-save;"; } int continues = 0; int prompts = 0; std::string call_order; }; class FakeNewDocumentServices final : public pp::app::NewDocumentServices { public: void create_new_document(const pp::app::NewDocumentPlan& plan) override { creates += 1; last_name = plan.target.name; last_resolution = plan.resolution; call_order += "create;"; } void prompt_overwrite_new_document(const pp::app::NewDocumentPlan& plan) override { overwrite_prompts += 1; last_name = plan.target.name; last_resolution = plan.resolution; call_order += "overwrite;"; } int creates = 0; int overwrite_prompts = 0; int last_resolution = 0; std::string last_name; std::string call_order; }; [[nodiscard]] pp::app::DocumentOpenRoute project_route() { return { .kind = pp::app::DocumentOpenKind::open_project, .path = "D:/Paint/demo.ppi", .directory = "D:/Paint", .name = "demo", .extension = "ppi", }; } [[nodiscard]] pp::app::DocumentOpenRoute abr_route() { return { .kind = pp::app::DocumentOpenKind::import_abr, .path = "D:/Paint/clouds.abr", .directory = "D:/Paint", .name = "clouds", .extension = "abr", }; } [[nodiscard]] pp::app::DocumentOpenRoute ppbr_route() { return { .kind = pp::app::DocumentOpenKind::import_ppbr, .path = "D:/Paint/brush.ppbr", .directory = "D:/Paint", .name = "brush", .extension = "ppbr", }; } void project_open_clean_document_executes_immediately(pp::tests::Harness& harness) { PP_EXPECT(harness, pp::app::plan_project_open(false) == pp::app::ProjectOpenDecision::open_now); } void project_open_dirty_document_prompts_for_discard(pp::tests::Harness& harness) { PP_EXPECT( harness, pp::app::plan_project_open(true) == pp::app::ProjectOpenDecision::prompt_discard_unsaved); } void document_open_project_respects_unsaved_state(pp::tests::Harness& harness) { PP_EXPECT( harness, pp::app::plan_document_open(pp::app::DocumentOpenKind::open_project, false) == pp::app::DocumentOpenPlanAction::open_project_now); PP_EXPECT( harness, pp::app::plan_document_open(pp::app::DocumentOpenKind::open_project, true) == pp::app::DocumentOpenPlanAction::prompt_discard_unsaved_project); } void document_open_brush_imports_prompt_regardless_of_unsaved_state(pp::tests::Harness& harness) { PP_EXPECT( harness, pp::app::plan_document_open(pp::app::DocumentOpenKind::import_abr, false) == pp::app::DocumentOpenPlanAction::prompt_import_abr); PP_EXPECT( harness, pp::app::plan_document_open(pp::app::DocumentOpenKind::import_abr, true) == pp::app::DocumentOpenPlanAction::prompt_import_abr); PP_EXPECT( harness, pp::app::plan_document_open(pp::app::DocumentOpenKind::import_ppbr, false) == pp::app::DocumentOpenPlanAction::prompt_import_ppbr); PP_EXPECT( harness, pp::app::plan_document_open(pp::app::DocumentOpenKind::import_ppbr, true) == pp::app::DocumentOpenPlanAction::prompt_import_ppbr); } void document_open_executor_dispatches_all_actions(pp::tests::Harness& harness) { FakeDocumentOpenServices services; const auto project = project_route(); const auto abr = abr_route(); const auto ppbr = ppbr_route(); PP_EXPECT( harness, pp::app::execute_document_open_plan( pp::app::DocumentOpenPlanAction::open_project_now, project, services) .ok()); PP_EXPECT( harness, pp::app::execute_document_open_plan( pp::app::DocumentOpenPlanAction::prompt_discard_unsaved_project, project, services) .ok()); PP_EXPECT( harness, pp::app::execute_document_open_plan( pp::app::DocumentOpenPlanAction::prompt_import_abr, abr, services) .ok()); PP_EXPECT( harness, pp::app::execute_document_open_plan( pp::app::DocumentOpenPlanAction::prompt_import_ppbr, ppbr, services) .ok()); PP_EXPECT(harness, services.project_opens == 1); PP_EXPECT(harness, services.discard_prompts == 1); PP_EXPECT(harness, services.abr_prompts == 1); PP_EXPECT(harness, services.ppbr_prompts == 1); PP_EXPECT(harness, services.last_path == "D:/Paint/brush.ppbr"); PP_EXPECT(harness, services.call_order == "open;discard;abr;ppbr;"); } void document_open_executor_rejects_mismatched_routes(pp::tests::Harness& harness) { FakeDocumentOpenServices services; PP_EXPECT( harness, !pp::app::execute_document_open_plan( pp::app::DocumentOpenPlanAction::open_project_now, abr_route(), services) .ok()); PP_EXPECT( harness, !pp::app::execute_document_open_plan( pp::app::DocumentOpenPlanAction::prompt_import_abr, project_route(), services) .ok()); PP_EXPECT( harness, !pp::app::execute_document_open_plan( pp::app::DocumentOpenPlanAction::prompt_import_ppbr, abr_route(), services) .ok()); PP_EXPECT(harness, services.call_order.empty()); } void close_clean_document_executes_immediately(pp::tests::Harness& harness) { PP_EXPECT( harness, pp::app::plan_close_request(false, false) == pp::app::CloseRequestDecision::close_now); PP_EXPECT( harness, pp::app::plan_close_request(false, true) == pp::app::CloseRequestDecision::close_now); } void close_dirty_document_opens_one_prompt(pp::tests::Harness& harness) { PP_EXPECT( harness, pp::app::plan_close_request(true, false) == pp::app::CloseRequestDecision::show_unsaved_prompt); PP_EXPECT( harness, pp::app::plan_close_request(true, true) == pp::app::CloseRequestDecision::wait_for_existing_prompt); } void close_request_executor_dispatches_and_preserves_wait(pp::tests::Harness& harness) { FakeCloseRequestServices services; PP_EXPECT( harness, pp::app::execute_close_request_decision(pp::app::CloseRequestDecision::close_now, services).ok()); PP_EXPECT( harness, pp::app::execute_close_request_decision(pp::app::CloseRequestDecision::show_unsaved_prompt, services).ok()); PP_EXPECT( harness, pp::app::execute_close_request_decision(pp::app::CloseRequestDecision::wait_for_existing_prompt, services).ok()); PP_EXPECT(harness, services.close_now == 1); PP_EXPECT(harness, services.close_prompts == 1); PP_EXPECT(harness, services.call_order == "close;prompt;"); } void save_clean_existing_document_is_no_op(pp::tests::Harness& harness) { PP_EXPECT( harness, pp::app::plan_document_save(false, false, pp::app::DocumentSaveIntent::save) == pp::app::DocumentSaveDecision::no_op); } void save_executor_dispatches_visible_work_and_no_ops_cleanly(pp::tests::Harness& harness) { FakeDocumentSaveServices services; PP_EXPECT( harness, pp::app::execute_document_save_decision(pp::app::DocumentSaveDecision::show_save_dialog, services).ok()); PP_EXPECT( harness, pp::app::execute_document_save_decision(pp::app::DocumentSaveDecision::save_existing, services).ok()); PP_EXPECT( harness, pp::app::execute_document_save_decision(pp::app::DocumentSaveDecision::save_version, services).ok()); PP_EXPECT( harness, pp::app::execute_document_save_decision(pp::app::DocumentSaveDecision::no_op, services).ok()); PP_EXPECT(harness, services.dialogs == 1); PP_EXPECT(harness, services.saves == 1); PP_EXPECT(harness, services.versions == 1); PP_EXPECT(harness, services.call_order == "dialog;save;version;"); } void save_new_or_dirty_document_has_user_visible_work(pp::tests::Harness& harness) { PP_EXPECT( harness, pp::app::plan_document_save(true, false, pp::app::DocumentSaveIntent::save) == pp::app::DocumentSaveDecision::show_save_dialog); PP_EXPECT( harness, pp::app::plan_document_save(false, true, pp::app::DocumentSaveIntent::save) == pp::app::DocumentSaveDecision::save_existing); } void save_as_always_shows_save_dialog(pp::tests::Harness& harness) { PP_EXPECT( harness, pp::app::plan_document_save(false, false, pp::app::DocumentSaveIntent::save_as) == pp::app::DocumentSaveDecision::show_save_dialog); PP_EXPECT( harness, pp::app::plan_document_save(false, true, pp::app::DocumentSaveIntent::save_as) == pp::app::DocumentSaveDecision::show_save_dialog); } void save_version_respects_menu_and_hotkey_behaviors(pp::tests::Harness& harness) { PP_EXPECT( harness, pp::app::plan_document_save(false, false, pp::app::DocumentSaveIntent::save_version) == pp::app::DocumentSaveDecision::save_version); PP_EXPECT( harness, pp::app::plan_document_save(false, false, pp::app::DocumentSaveIntent::save_dirty_version) == pp::app::DocumentSaveDecision::no_op); PP_EXPECT( harness, pp::app::plan_document_save(false, true, pp::app::DocumentSaveIntent::save_dirty_version) == pp::app::DocumentSaveDecision::save_version); PP_EXPECT( harness, pp::app::plan_document_save(true, false, pp::app::DocumentSaveIntent::save_version) == pp::app::DocumentSaveDecision::show_save_dialog); } void workflow_without_canvas_is_unavailable(pp::tests::Harness& harness) { PP_EXPECT( harness, pp::app::plan_document_workflow(false, false) == pp::app::DocumentWorkflowDecision::unavailable); PP_EXPECT( harness, pp::app::plan_document_workflow(false, true) == pp::app::DocumentWorkflowDecision::unavailable); } void workflow_with_clean_canvas_continues_now(pp::tests::Harness& harness) { PP_EXPECT( harness, pp::app::plan_document_workflow(true, false) == pp::app::DocumentWorkflowDecision::continue_now); } void workflow_with_dirty_canvas_prompts_for_save(pp::tests::Harness& harness) { PP_EXPECT( harness, pp::app::plan_document_workflow(true, true) == pp::app::DocumentWorkflowDecision::prompt_save_before_continue); } void workflow_executor_dispatches_continue_prompt_and_unavailable(pp::tests::Harness& harness) { FakeDocumentWorkflowServices services; PP_EXPECT( harness, pp::app::execute_document_workflow_decision( pp::app::DocumentWorkflowDecision::continue_now, services) .ok()); PP_EXPECT( harness, pp::app::execute_document_workflow_decision( pp::app::DocumentWorkflowDecision::prompt_save_before_continue, services) .ok()); PP_EXPECT( harness, pp::app::execute_document_workflow_decision( pp::app::DocumentWorkflowDecision::unavailable, services) .ok()); PP_EXPECT(harness, services.continues == 1); PP_EXPECT(harness, services.prompts == 1); PP_EXPECT(harness, services.call_order == "continue;prompt-save;"); } void document_file_target_rejects_empty_name(pp::tests::Harness& harness) { const auto target = pp::app::make_document_file_target("D:/Paint", ""); PP_EXPECT(harness, !target); PP_EXPECT(harness, target.status().code == pp::foundation::StatusCode::invalid_argument); } void document_file_target_builds_legacy_ppi_path(pp::tests::Harness& harness) { const auto target = pp::app::make_document_file_target("D:/Paint", "demo"); PP_EXPECT(harness, target); PP_EXPECT(harness, target.value().name == "demo"); PP_EXPECT(harness, target.value().directory == "D:/Paint"); PP_EXPECT(harness, target.value().path == "D:/Paint/demo.ppi"); } void document_file_write_prompts_only_for_existing_targets(pp::tests::Harness& harness) { PP_EXPECT( harness, pp::app::plan_document_file_write(false) == pp::app::DocumentFileWriteDecision::save_now); PP_EXPECT( harness, pp::app::plan_document_file_write(true) == pp::app::DocumentFileWriteDecision::prompt_overwrite); } void document_file_save_plan_combines_target_and_write_decision(pp::tests::Harness& harness) { const auto plan = pp::app::plan_document_file_save( "D:/Paint", "demo", [](const std::string& path) { return path == "D:/Paint/demo.ppi"; }); PP_EXPECT(harness, plan); PP_EXPECT(harness, plan.value().target.name == "demo"); PP_EXPECT(harness, plan.value().target.directory == "D:/Paint"); PP_EXPECT(harness, plan.value().target.path == "D:/Paint/demo.ppi"); PP_EXPECT( harness, plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite); } void document_file_save_plan_rejects_empty_name(pp::tests::Harness& harness) { const auto plan = pp::app::plan_document_file_save( "D:/Paint", "", [](const std::string&) { return false; }); PP_EXPECT(harness, !plan); PP_EXPECT(harness, plan.status().code == pp::foundation::StatusCode::invalid_argument); } void document_resolution_index_maps_legacy_choices(pp::tests::Harness& harness) { const auto first = pp::app::document_resolution_from_index(0); const auto last = pp::app::document_resolution_from_index(5); PP_EXPECT(harness, first); PP_EXPECT(harness, first.value() == 512); PP_EXPECT(harness, last); PP_EXPECT(harness, last.value() == 8192); } void document_resolution_index_rejects_out_of_range(pp::tests::Harness& harness) { const auto negative = pp::app::document_resolution_from_index(-1); const auto too_large = pp::app::document_resolution_from_index(6); PP_EXPECT(harness, !negative); PP_EXPECT(harness, negative.status().code == pp::foundation::StatusCode::out_of_range); PP_EXPECT(harness, !too_large); PP_EXPECT(harness, too_large.status().code == pp::foundation::StatusCode::out_of_range); } void new_document_plan_builds_target_resolution_and_write_decision(pp::tests::Harness& harness) { const auto plan = pp::app::plan_new_document( "D:/Paint", "demo", 2, [](const std::string&) { return false; }); PP_EXPECT(harness, plan); PP_EXPECT(harness, plan.value().target.name == "demo"); PP_EXPECT(harness, plan.value().target.path == "D:/Paint/demo.ppi"); PP_EXPECT(harness, plan.value().resolution == 1536); PP_EXPECT(harness, plan.value().write_decision == pp::app::DocumentFileWriteDecision::save_now); } void new_document_plan_prompts_for_existing_target(pp::tests::Harness& harness) { const auto plan = pp::app::plan_new_document( "D:/Paint", "demo", 1, [](const std::string& path) { return path == "D:/Paint/demo.ppi"; }); PP_EXPECT(harness, plan); PP_EXPECT(harness, plan.value().resolution == 1024); PP_EXPECT( harness, plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite); } void new_document_plan_rejects_invalid_inputs(pp::tests::Harness& harness) { const auto missing_name = pp::app::plan_new_document( "D:/Paint", "", 0, [](const std::string&) { return false; }); const auto invalid_resolution = pp::app::plan_new_document( "D:/Paint", "demo", 99, [](const std::string&) { return false; }); PP_EXPECT(harness, !missing_name); PP_EXPECT(harness, missing_name.status().code == pp::foundation::StatusCode::invalid_argument); PP_EXPECT(harness, !invalid_resolution); PP_EXPECT(harness, invalid_resolution.status().code == pp::foundation::StatusCode::out_of_range); } void new_document_executor_dispatches_save_and_overwrite_paths(pp::tests::Harness& harness) { FakeNewDocumentServices services; auto create = pp::app::plan_new_document( "D:/Paint", "demo", 1, [](const std::string&) { return false; }); auto overwrite = pp::app::plan_new_document( "D:/Paint", "demo", 2, [](const std::string& path) { return path == "D:/Paint/demo.ppi"; }); PP_EXPECT(harness, create); PP_EXPECT(harness, overwrite); PP_EXPECT(harness, pp::app::execute_new_document_plan(create.value(), services).ok()); PP_EXPECT(harness, pp::app::execute_new_document_plan(overwrite.value(), services).ok()); PP_EXPECT(harness, services.creates == 1); PP_EXPECT(harness, services.overwrite_prompts == 1); PP_EXPECT(harness, services.last_name == "demo"); PP_EXPECT(harness, services.last_resolution == 1536); PP_EXPECT(harness, services.call_order == "create;overwrite;"); } void document_version_target_starts_at_first_version(pp::tests::Harness& harness) { const auto target = pp::app::find_next_document_version_target( "D:/Paint", "demo", [](const std::string&) { return false; }); PP_EXPECT(harness, target); PP_EXPECT(harness, target.value().name == "demo.01"); PP_EXPECT(harness, target.value().path == "D:/Paint/demo.01.ppi"); } void document_version_target_increments_existing_suffix(pp::tests::Harness& harness) { const auto target = pp::app::find_next_document_version_target( "D:/Paint", "demo.07", [](const std::string&) { return false; }); PP_EXPECT(harness, target); PP_EXPECT(harness, target.value().name == "demo.08"); PP_EXPECT(harness, target.value().path == "D:/Paint/demo.08.ppi"); } void document_version_target_skips_existing_paths(pp::tests::Harness& harness) { const auto target = pp::app::find_next_document_version_target( "D:/Paint", "demo", [](const std::string& path) { return path == "D:/Paint/demo.01.ppi" || path == "D:/Paint/demo.02.ppi"; }); PP_EXPECT(harness, target); PP_EXPECT(harness, target.value().name == "demo.03"); PP_EXPECT(harness, target.value().path == "D:/Paint/demo.03.ppi"); } void document_version_target_preserves_legacy_nonnumeric_suffix_handling(pp::tests::Harness& harness) { const auto target = pp::app::find_next_document_version_target( "D:/Paint", "demo.ab", [](const std::string&) { return false; }); PP_EXPECT(harness, target); PP_EXPECT(harness, target.value().name == "demo.01"); PP_EXPECT(harness, target.value().path == "D:/Paint/demo.01.ppi"); } void document_version_target_reports_when_no_slots_remain(pp::tests::Harness& harness) { const auto target = pp::app::find_next_document_version_target( "D:/Paint", "demo.98", [](const std::string&) { return false; }); PP_EXPECT(harness, !target); PP_EXPECT(harness, target.status().code == pp::foundation::StatusCode::out_of_range); } } int main() { pp::tests::Harness harness; harness.run("project open clean document executes immediately", project_open_clean_document_executes_immediately); harness.run("project open dirty document prompts for discard", project_open_dirty_document_prompts_for_discard); harness.run("document open project respects unsaved state", document_open_project_respects_unsaved_state); harness.run( "document open brush imports prompt regardless of unsaved state", document_open_brush_imports_prompt_regardless_of_unsaved_state); harness.run("document open executor dispatches all actions", document_open_executor_dispatches_all_actions); harness.run("document open executor rejects mismatched routes", document_open_executor_rejects_mismatched_routes); harness.run("close clean document executes immediately", close_clean_document_executes_immediately); harness.run("close dirty document opens one prompt", close_dirty_document_opens_one_prompt); harness.run("close request executor dispatches and preserves wait", close_request_executor_dispatches_and_preserves_wait); harness.run("save clean existing document is no op", save_clean_existing_document_is_no_op); harness.run("save executor dispatches visible work and no ops cleanly", save_executor_dispatches_visible_work_and_no_ops_cleanly); harness.run("save new or dirty document has user visible work", save_new_or_dirty_document_has_user_visible_work); harness.run("save as always shows save dialog", save_as_always_shows_save_dialog); harness.run("save version respects menu and hotkey behaviors", save_version_respects_menu_and_hotkey_behaviors); harness.run("workflow without canvas is unavailable", workflow_without_canvas_is_unavailable); harness.run("workflow with clean canvas continues now", workflow_with_clean_canvas_continues_now); harness.run("workflow with dirty canvas prompts for save", workflow_with_dirty_canvas_prompts_for_save); harness.run("workflow executor dispatches continue prompt and unavailable", workflow_executor_dispatches_continue_prompt_and_unavailable); harness.run("document file target rejects empty name", document_file_target_rejects_empty_name); harness.run("document file target builds legacy ppi path", document_file_target_builds_legacy_ppi_path); harness.run("document file write prompts only for existing targets", document_file_write_prompts_only_for_existing_targets); harness.run( "document file save plan combines target and write decision", document_file_save_plan_combines_target_and_write_decision); harness.run("document file save plan rejects empty name", document_file_save_plan_rejects_empty_name); harness.run("document resolution index maps legacy choices", document_resolution_index_maps_legacy_choices); harness.run("document resolution index rejects out of range", document_resolution_index_rejects_out_of_range); harness.run( "new document plan builds target resolution and write decision", new_document_plan_builds_target_resolution_and_write_decision); harness.run("new document plan prompts for existing target", new_document_plan_prompts_for_existing_target); harness.run("new document plan rejects invalid inputs", new_document_plan_rejects_invalid_inputs); harness.run("new document executor dispatches save and overwrite paths", new_document_executor_dispatches_save_and_overwrite_paths); harness.run("document version target starts at first version", document_version_target_starts_at_first_version); harness.run("document version target increments existing suffix", document_version_target_increments_existing_suffix); harness.run("document version target skips existing paths", document_version_target_skips_existing_paths); harness.run( "document version target preserves legacy nonnumeric suffix handling", document_version_target_preserves_legacy_nonnumeric_suffix_handling); harness.run("document version target reports when no slots remain", document_version_target_reports_when_no_slots_remain); return harness.finish(); }