Files
EVEngine/test/editor_ui_theme.cpp
西风逍遥游andCursor 24502babef Nest satellite packages under their host modules.
Keep CMake module names and trim switches, but stop flattening editing/editor and runtime bridges as top-level src/modules directories. Parent scans skip nested facets so runtime-3d can drop authoring by directory leaf.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 17:56:12 +08:00

127 lines
5.8 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "editor/EditorAuthority.h"
#include "editor/EditorIds.h"
#include "editor/EditorTransactionService.h"
#include "ui/Theme.h"
#include "ui/editing/UiTheme.h"
#include <cmath>
using namespace eve::ui_editing;
using namespace eve::editing;
using eve::editor::LocalTransactionBackend;
using eve::editor::LocalWorldAuthority;
namespace {
EditorResult<eve::editor::TransactionReceipt> commitTheme(UiThemeCatalogTarget& target,
LocalTransactionBackend& transactions,
const DomainOperation& operation,
const std::string& id) {
eve::editor::TransactionSpec specification;
specification.id = eve::editor::TransactionId(id);
specification.label = "Edit theme";
specification.target = TargetId(target.targetId());
specification.baseRevision = target.revision();
auto begun = transactions.begin(std::move(specification));
if (!begun.ok())
return eve::editing::failed<eve::editor::TransactionReceipt>(begun.code(), RuleId("test.ui-theme.begin"),
"Could not begin theme transaction");
auto appended = transactions.append(operation);
if (!appended.ok()) {
auto discarded = transactions.discard();
if (!discarded.ok()) discarded.ignore("theme test transaction already inactive");
return eve::editing::failed<eve::editor::TransactionReceipt>(appended.code(), RuleId("test.ui-theme.append"),
"Could not append theme operation");
}
return transactions.commit();
}
SelectionSnapshot themeSelection(const UiThemeCatalogTarget& target, const char* id) {
SelectionSnapshot selection;
selection.channel = "ui-theme";
SelectionItem item;
item.domain = SelectionDomain::Asset;
item.target = TargetId(target.targetId());
item.item = StableId(id);
item.type = "ui.theme";
selection.items.push_back(item);
selection.primary = item;
return selection;
}
} // namespace
TEST_CASE("editor.ui.theme_catalog_duplicate_recolor_and_undo_are_revision_safe") {
UiThemeCatalogTarget target("styles");
LocalWorldAuthority authority(&target);
LocalTransactionBackend transactions(&authority);
CHECK_EQ(target.themes().size(), static_cast<std::size_t>(2));
auto duplicated = target.makeDuplicate(ObjectId("dark"), ObjectId("studio"), "Studio");
REQUIRE(duplicated.ok());
REQUIRE(commitTheme(target, transactions, duplicated.value(), "theme.duplicate").ok());
CHECK_EQ(target.themes().size(), static_cast<std::size_t>(3));
const SelectionSnapshot selection = themeSelection(target, "studio");
auto recolor = target.makeSet(selection, PropertyPath("color.button"),
EditorValue::Array{0.1, 0.2, 0.3, 1.0}, PropertySetMode::Absolute);
REQUIRE(recolor.ok());
REQUIRE(commitTheme(target, transactions, recolor.value(), "theme.recolor").ok());
auto studio = target.theme(ObjectId("studio"));
REQUIRE(studio.ok());
CHECK(std::abs(studio.value().tokens.button[0] - 0.1f) < 0.0001f);
REQUIRE(transactions.undo().ok());
studio = target.theme(ObjectId("studio"));
REQUIRE(studio.ok());
CHECK(studio.value().tokens.button[0] > 0.15f);
}
TEST_CASE("editor.ui.theme_catalog_rejects_deleting_the_last_asset_and_loads_snapshots_atomically") {
UiThemeCatalogTarget target("styles");
LocalWorldAuthority authority(&target);
LocalTransactionBackend transactions(&authority);
auto removeLight = target.makeDelete(ObjectId("light"));
REQUIRE(removeLight.ok());
REQUIRE(commitTheme(target, transactions, removeLight.value(), "theme.delete-light").ok());
CHECK_EQ(static_cast<int>(target.makeDelete(ObjectId("dark")).code()),
static_cast<int>(EditorStatus::Rejected));
CHECK_EQ(target.themes().size(), static_cast<std::size_t>(1));
UiThemeCatalogTarget source("source");
LocalWorldAuthority sourceAuthority(&source);
LocalTransactionBackend sourceTx(&sourceAuthority);
auto copied = source.makeDuplicate(ObjectId("dark"), ObjectId("copy"), "Copy");
REQUIRE(copied.ok());
REQUIRE(commitTheme(source, sourceTx, copied.value(), "theme.copy").ok());
UiThemeCatalogTarget restored("restored");
REQUIRE(restored.loadSnapshot(source.snapshotValue()).ok());
CHECK_EQ(restored.themes().size(), source.themes().size());
EditorValue::Object invalid;
invalid["schemaVersion"] = int64_t{1};
invalid["content"] = EditorValue::Object{{"activeId", std::string("missing")}, {"themes", EditorValue::Array{}}};
CHECK_EQ(static_cast<int>(restored.loadSnapshot(EditorValue(std::move(invalid))).code()),
static_cast<int>(EditorStatus::Rejected));
CHECK_EQ(restored.themes().size(), source.themes().size());
}
TEST_CASE("editor.ui.theme_preview_rejects_stale_revision_and_publish_updates_global_theme") {
UiThemeCatalogTarget target("styles");
UiThemePreviewService previews;
const auto current = previews.build(target, ObjectId("dark"), target.revision());
CHECK_EQ(static_cast<int>(current.status), static_cast<int>(EditorStatus::Applied));
const auto stale = previews.build(target, ObjectId("dark"), target.revision() + 1);
CHECK_EQ(static_cast<int>(stale.status), static_cast<int>(EditorStatus::Conflict));
const eve::ui::Theme previous = eve::ui::globalTheme();
const std::string previousName = eve::ui::globalThemeName();
REQUIRE(target.applyDomainOperation(target.makeSetActive(ObjectId("light")).value()).ok());
REQUIRE(UiThemeRuntimePublisher().publish(target).ok());
CHECK_EQ(eve::ui::globalThemeName(), std::string("light"));
eve::ui::setGlobalTheme(previous, previousName);
}