Files
panopainter/tests/app_core/document_session_tests.cpp

795 lines
28 KiB
C++

#include "app_core/document_session.h"
#include "test_harness.h"
#include <string>
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;
};
class FakeDocumentFileSaveServices final : public pp::app::DocumentFileSaveServices {
public:
void save_document_file(const pp::app::DocumentFileSavePlan& plan) override
{
saves += 1;
last_path = plan.target.path;
call_order += "save-file;";
}
void prompt_overwrite_document_file(const pp::app::DocumentFileSavePlan& plan) override
{
overwrite_prompts += 1;
last_path = plan.target.path;
call_order += "overwrite-file;";
}
int saves = 0;
int overwrite_prompts = 0;
std::string last_path;
std::string call_order;
};
class FakeDocumentVersionSaveServices final : public pp::app::DocumentVersionSaveServices {
public:
void save_document_version(const pp::app::DocumentVersionTarget& target) override
{
saves += 1;
last_name = target.name;
last_path = target.path;
call_order += "save-version;";
}
int saves = 0;
std::string last_name;
std::string last_path;
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_file_save_executor_dispatches_save_and_overwrite_paths(pp::tests::Harness& harness)
{
FakeDocumentFileSaveServices services;
auto save = pp::app::plan_document_file_save(
"D:/Paint",
"demo",
[](const std::string&) { return false; });
auto overwrite = pp::app::plan_document_file_save(
"D:/Paint",
"demo",
[](const std::string& path) { return path == "D:/Paint/demo.ppi"; });
PP_EXPECT(harness, save);
PP_EXPECT(harness, overwrite);
PP_EXPECT(harness, pp::app::execute_document_file_save_plan(save.value(), services).ok());
PP_EXPECT(harness, pp::app::execute_document_file_save_plan(overwrite.value(), services).ok());
PP_EXPECT(harness, services.saves == 1);
PP_EXPECT(harness, services.overwrite_prompts == 1);
PP_EXPECT(harness, services.last_path == "D:/Paint/demo.ppi");
PP_EXPECT(harness, services.call_order == "save-file;overwrite-file;");
}
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);
}
void document_version_executor_dispatches_and_rejects_empty_targets(pp::tests::Harness& harness)
{
FakeDocumentVersionSaveServices services;
const pp::app::DocumentVersionTarget valid {
.name = "demo.02",
.path = "D:/Paint/demo.02.ppi",
};
const pp::app::DocumentVersionTarget missing_name {
.name = "",
.path = "D:/Paint/demo.02.ppi",
};
const pp::app::DocumentVersionTarget missing_path {
.name = "demo.02",
.path = "",
};
PP_EXPECT(harness, pp::app::execute_document_version_save(valid, services).ok());
PP_EXPECT(harness, !pp::app::execute_document_version_save(missing_name, services).ok());
PP_EXPECT(harness, !pp::app::execute_document_version_save(missing_path, services).ok());
PP_EXPECT(harness, services.saves == 1);
PP_EXPECT(harness, services.last_name == "demo.02");
PP_EXPECT(harness, services.last_path == "D:/Paint/demo.02.ppi");
PP_EXPECT(harness, services.call_order == "save-version;");
}
}
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 file save executor dispatches save and overwrite paths", document_file_save_executor_dispatches_save_and_overwrite_paths);
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);
harness.run("document version executor dispatches and rejects empty targets", document_version_executor_dispatches_and_rejects_empty_targets);
return harness.finish();
}