#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "level_editing/Brush.h" #include "editor/BrushKernel.h" #include "editor/Editor.h" #include "editor/EditorDock.h" #include "level_editing/EditorHistory.h" #include "editor/EditorInspector.h" #include "editor/EditorSession.h" #include "editor/EditorPresentation.h" #include "editor/EditorTransactions.h" #include "editor/EditConstraint.h" #include "editor/EditorToolbar.h" #include "editor/EditorWorkspace.h" #include "level_editing/FieldTargets.h" #include "editor/FieldBrushTool.h" #include "editor/GizmoManager.h" #include "level_editing/TileBuffer.h" #include "level_editor/LevelEditorModule.h" #include "editor/TransformGizmo.h" #include "heightmap_target/HeightmapTargetModule.h" #include "common/Exception.h" #include "procgen/heightmap/Heightmap.h" #include "procgen_editing/HeightmapTarget.h" #include #include #include #include #include using namespace eve::editor; using namespace eve::level_editing; namespace { class TestEditorTool final : public IEditorTool { public: explicit TestEditorTool(std::string id) { desc.id = std::move(id); } const ToolDescriptor &descriptor() const override { return desc; } void activate(EditorContext &) override { ++activations; } void deactivate(EditorContext &) override { ++deactivations; } void cancel(EditorContext &) override { ++cancellations; } ToolResponse pointerEvent(EditorContext &, const EditorPointerEvent &event) override { ++pointerEvents; if (event.phase == EditorPointerEvent::Phase::Down) return ToolResponse::capture(); if (event.phase == EditorPointerEvent::Phase::Up) return ToolResponse::release(); return ToolResponse::consumed(); } ToolResponse keyEvent(EditorContext &, const EditorKeyEvent &) override { ++keyEvents; return ToolResponse::consumed(); } void update(EditorContext &, float) override { ++updates; } void drawOverlay(EditorContext &, IEditorOverlay &overlay) override { ++overlays; overlay.circle({1.f, 2.f, 3.f}, 4.f, {}); } void inspect(EditorContext &, IEditorInspector &inspector) override { ++inspections; inspector.scalar("radius", "Radius", radius, 0.f, 10.f); } ToolDescriptor desc; int activations = 0; int deactivations = 0; int cancellations = 0; int pointerEvents = 0; int keyEvents = 0; int updates = 0; int overlays = 0; int inspections = 0; float radius = 2.f; }; class ReentrantEditorTool final : public IEditorTool { public: explicit ReentrantEditorTool(std::string id) { desc.id = std::move(id); } const ToolDescriptor& descriptor() const override { return desc; } ToolResponse pointerEvent(EditorContext& context, const EditorPointerEvent&) override { if (activateDuringPointer) context.session()->activateTool("replacement"); return ToolResponse::capture(); } void deactivate(EditorContext& context) override { if (removeDuringDeactivate) context.session()->removeTool(desc.id); if (throwDuringDeactivate) throw std::runtime_error("deactivate failure"); } void cancel(EditorContext&) override { if (throwDuringCancel) throw std::runtime_error("cancel failure"); } ToolDescriptor desc; bool activateDuringPointer = false; bool removeDuringDeactivate = false; bool throwDuringCancel = false; bool throwDuringDeactivate = false; }; class TestPresentation final : public IEditorOverlay, public IEditorInspector { public: void line(const OverlayPoint &, const OverlayPoint &, const OverlayStyle &) override { ++primitives; } void circle(const OverlayPoint &, float, const OverlayStyle &) override { ++primitives; } void rectangle(const OverlayPoint &, const OverlayPoint &, const OverlayStyle &) override { ++primitives; } void text(const OverlayPoint &, const std::string &, const OverlayStyle &) override { ++primitives; } void beginGroup(const std::string &, const std::string &) override {} void endGroup() override {} bool boolean(const std::string &, const std::string &, bool &) override { return false; } bool integer(const std::string &, const std::string &, int &, int, int) override { return false; } bool scalar(const std::string &, const std::string &, float &value, float, float) override { value = 6.f; return true; } bool string(const std::string &, const std::string &, std::string &) override { return false; } int primitives = 0; }; class RejectLargeEdit final : public IEditConstraint { public: ConstraintResult evaluate(EditorContext &, IEditCommand &command) override { const EditRegion region = command.dirtyRegion(); if (!region.empty() && region.maxX - region.minX > 1) return ConstraintResult::reject("edit too wide"); return ConstraintResult::warning("checked"); } }; } // namespace TEST_CASE("editor.session.routes_tool_lifecycle_and_capture") { EditorSession session; TestEditorTool first("first"); TestEditorTool second("second"); CHECK(session.addTool(&first)); CHECK(session.addTool(&second)); CHECK(!session.addTool(&first)); CHECK_EQ(session.getToolCount(), 2); CHECK(session.activateTool("first")); CHECK_EQ(first.activations, 1); EditorPointerEvent down; down.phase = EditorPointerEvent::Phase::Down; down.pointerId = 7; CHECK(session.dispatchPointer(down).handled); CHECK(session.hasPointerCapture()); CHECK_EQ(session.capturedPointerId(), 7); EditorPointerEvent other = down; other.phase = EditorPointerEvent::Phase::Move; other.pointerId = 8; CHECK(!session.dispatchPointer(other).handled); CHECK_EQ(first.pointerEvents, 1); EditorPointerEvent up = down; up.phase = EditorPointerEvent::Phase::Up; CHECK(session.dispatchPointer(up).releasePointer); CHECK(!session.hasPointerCapture()); EditorKeyEvent key; key.key = "W"; key.pressed = true; CHECK(session.dispatchKey(key).handled); session.update(0.016f); CHECK_EQ(first.keyEvents, 1); CHECK_EQ(first.updates, 1); CHECK(session.activateTool("second")); CHECK_EQ(first.cancellations, 1); CHECK_EQ(first.deactivations, 1); CHECK_EQ(second.activations, 1); CHECK(session.removeTool("second")); CHECK_EQ(second.deactivations, 1); CHECK_EQ(session.activeToolId(), std::string("")); } TEST_CASE("editor.session_tool_reentrancy_cannot_apply_stale_pointer_capture") { EditorSession session; ReentrantEditorTool first("first"); ReentrantEditorTool replacement("replacement"); first.activateDuringPointer = true; first.removeDuringDeactivate = true; REQUIRE(session.addTool(&first)); REQUIRE(session.addTool(&replacement)); REQUIRE(session.activateTool("first")); EditorPointerEvent down; down.phase = EditorPointerEvent::Phase::Down; down.pointerId = 17; const ToolResponse response = session.dispatchPointer(down); CHECK(response.handled); CHECK(!response.capturePointer); CHECK(!session.hasPointerCapture()); CHECK_EQ(session.activeToolId(), std::string("replacement")); CHECK(session.findTool("first") == nullptr); } TEST_CASE("editor.session_destructor_contains_throwing_tool_callbacks") { ReentrantEditorTool tool("throwing"); tool.throwDuringCancel = true; tool.throwDuringDeactivate = true; { EditorSession session; REQUIRE(session.addTool(&tool)); REQUIRE(session.activateTool("throwing")); } CHECK(true); } TEST_CASE("editor.presentation.is_host_and_renderer_independent") { EditorSession session; TestEditorTool tool("presentable"); TestPresentation presentation; CHECK(session.addTool(&tool)); CHECK(session.activateTool("presentable")); session.drawOverlay(presentation); session.inspect(presentation); CHECK_EQ(tool.overlays, 1); CHECK_EQ(tool.inspections, 1); CHECK_EQ(presentation.primitives, 1); CHECK_EQ(tool.radius, 6.f); } TEST_CASE("editor.field_brush.composes_kernel_and_operation") { ConstantBrushFalloff hard; CircleBrushKernel kernel(&hard); PaintIntFieldOperation paint(7); FieldBrushTool tool("paint", "Paint", &kernel, &paint); tool.setRadius(0.5f); TileBuffer buffer(4, 4); TileBufferTarget tiles("tiles", &buffer); EditorSession session; session.bindTarget(&tiles); CHECK(session.addTool(&tool)); CHECK(session.activateTool("paint")); EditorPointerEvent event; event.phase = EditorPointerEvent::Phase::Down; event.x = 1.f; event.y = 2.f; CHECK(session.dispatchPointer(event).capturePointer); event.phase = EditorPointerEvent::Phase::Up; CHECK(session.dispatchPointer(event).releasePointer); CHECK_EQ(buffer.getGid(1, 2), 7); CHECK(session.transactions().undo()); CHECK_EQ(buffer.getGid(1, 2), 0); eve::procgen::Heightmap heightmap(4, 4); eve::procgen_editing::HeightmapTarget terrain("terrain", &heightmap); AddScalarFieldOperation raise; tool.setOperation(&raise); tool.setStrength(0.25f); session.bindTarget(&terrain); event.phase = EditorPointerEvent::Phase::Down; CHECK(session.dispatchPointer(event).handled); event.phase = EditorPointerEvent::Phase::Up; session.dispatchPointer(event); CHECK_EQ(terrain.readScalar(1, 2), 0.25f); } TEST_CASE("editor.heightmap_brush.applies_native_circular_falloff") { eve::heightmap_target::HeightmapTargetModule heightmapTargets; eve::procgen::Heightmap heightmap(9, 9); CHECK_EQ(heightmapTargets.applyBrush(&heightmap, 4.f, 4.f, 2.f, 0.5f), 13); CHECK_EQ(heightmap.height(4, 4), 0.5f); CHECK(heightmap.height(5, 4) > heightmap.height(6, 4)); CHECK_EQ(heightmap.height(0, 0), 0.f); CHECK(heightmapTargets.applyBrush(&heightmap, 4.f, 4.f, 2.f, -0.25f) > 0); CHECK_EQ(heightmap.height(4, 4), 0.25f); CHECK_EQ(heightmapTargets.applyBrush(nullptr, 4.f, 4.f, 2.f, 1.f), 0); CHECK_EQ(heightmapTargets.applyBrush(&heightmap, 4.f, 4.f, -1.f, 1.f), 0); } TEST_CASE("editor.script_tool.implements_the_same_session_protocol") { ssq::VM vm(1024, ssq::Libs::ALL); eve::ModuleManager::expose(vm); vm.run(vm.compileSource(R"( editor <- eve.Editor(); session <- editor.newSession(); tool <- editor.newScriptTool("custom", "Custom Tool"); activations <- 0; pointerX <- -1.0; tool.setActivateCallback(function() { activations += 1; }); tool.setPointerCallback(function(phase, pointerId, button, x, y, dx, dy, pressure, shift, control, alt) { pointerX = x; return phase == 0 ? 3 : 5; }); added <- session.addTool(tool); selected <- session.activateTool("custom"); response <- session.dispatchPointer(0, 9, 0, 12.5, 4.0, 0.0, 0.0, 1.0); )")); CHECK(vm.find("added").toBool()); CHECK(vm.find("selected").toBool()); CHECK_EQ(vm.find("activations").toInt(), 1); CHECK_EQ(vm.find("response").toInt(), 3); CHECK_EQ(vm.find("pointerX").toFloat(), 12.5f); } TEST_CASE("editor.targets.expose_capabilities_and_dirty_regions") { TileBuffer buffer(4, 3); TileBufferTarget tiles("ground", &buffer); IEditableTarget *base = &tiles; auto *ints = base->query(); CHECK(ints != nullptr); CHECK(base->query() == nullptr); CHECK_EQ(static_cast(ints->writeInt(2, 1, 9)), static_cast(FieldWriteStatus::Applied)); CHECK_EQ(static_cast(ints->writeInt(2, 1, 9)), static_cast(FieldWriteStatus::Unchanged)); CHECK_EQ(ints->readInt(2, 1), 9); CHECK_EQ(tiles.revision(), 1ULL); CHECK_EQ(tiles.dirtyRegion().minX, 2); tiles.clearDirtyRegion(); CHECK(tiles.dirtyRegion().empty()); eve::procgen::Heightmap heightmap(3, 3); eve::procgen_editing::HeightmapTarget terrain("height", &heightmap); auto *scalars = terrain.query(); CHECK(scalars != nullptr); CHECK_EQ(static_cast(scalars->writeScalar(1, 1, 0.75f)), static_cast(FieldWriteStatus::Applied)); CHECK_EQ(scalars->readScalar(1, 1), 0.75f); CHECK(scalars->sampleScalar(1.f, 1.f) == 0.75f); EditorSession session; session.bindTarget(&terrain); CHECK(session.context().targetCapability() == scalars); CHECK(session.context().targetCapability() == nullptr); } TEST_CASE("editor.brush_kernel.separates_shape_and_falloff") { LinearBrushFalloff linear; CircleBrushKernel circle(&linear); BrushSample sample; sample.centerX = 2.f; sample.centerY = 2.f; sample.radius = 2.f; BrushSampleBuffer points; circle.sample(sample, points); CHECK(points.size() > 1); bool foundCenter = false; bool foundEdge = false; for (int i = 0; i < points.size(); ++i) { const auto &point = points.point(i); if (point.x == 2 && point.y == 2) { foundCenter = true; CHECK_EQ(point.weight, 1.f); } if (point.x == 4 && point.y == 2) { foundEdge = true; CHECK_EQ(point.weight, 0.f); } } CHECK(foundCenter); CHECK(foundEdge); ConstantBrushFalloff hard; BoxBrushKernel box(&hard); points.clear(); sample.radius = 1.f; box.sample(sample, points); CHECK_EQ(points.size(), 9); } TEST_CASE("editor.transactions.undo_redo_and_rollback_any_field") { TileBuffer buffer(3, 3); TileBufferTarget target("tiles", &buffer); EditorTransactions transactions; CHECK(transactions.begin("paint")); auto first = std::make_unique("paint", &target); CHECK(first->record(1, 1, 5)); CHECK(transactions.execute(std::move(first))); auto second = std::make_unique("paint", &target); CHECK(second->record(1, 1, 7)); CHECK(second->record(2, 1, 4)); CHECK(transactions.execute(std::move(second))); CHECK_EQ(buffer.getGid(1, 1), 7); CHECK(transactions.commit()); CHECK_EQ(transactions.undoCount(), 1); CHECK(transactions.undo()); CHECK_EQ(buffer.getGid(1, 1), 0); CHECK_EQ(buffer.getGid(2, 1), 0); CHECK(transactions.redo()); CHECK_EQ(buffer.getGid(1, 1), 7); CHECK(transactions.begin("cancel")); auto cancelled = std::make_unique("paint", &target); CHECK(cancelled->record(0, 0, 9)); CHECK(transactions.execute(std::move(cancelled))); CHECK_EQ(buffer.getGid(0, 0), 9); CHECK(transactions.rollback()); CHECK_EQ(buffer.getGid(0, 0), 0); } TEST_CASE("editor.constraints.accept_warn_and_reject_without_core_types") { TileBuffer buffer(4, 2); TileBufferTarget target("tiles", &buffer); EditorSession session; session.bindTarget(&target); RejectLargeEdit constraint; CHECK(session.constraints().add(&constraint)); CHECK(session.transactions().begin("constrained")); auto allowed = std::make_unique("small", &target); CHECK(allowed->record(0, 0, 1)); auto allowedResult = session.context().executeChecked(std::move(allowed)); CHECK(allowedResult.ok()); REQUIRE(!allowedResult.diagnostics().empty()); CHECK_EQ(static_cast(allowedResult.diagnostics().front().severity()), static_cast(DiagnosticSeverity::Warning)); CHECK_EQ(session.constraints().diagnosticCount(), 1); CHECK_EQ(session.constraints().diagnostic(0), std::string("checked")); auto rejected = std::make_unique("wide", &target); CHECK(rejected->record(0, 1, 2)); CHECK(rejected->record(3, 1, 2)); CHECK(!session.context().execute(std::move(rejected))); CHECK(session.constraints().rejected()); CHECK_EQ(buffer.getGid(0, 1), 0); CHECK(session.transactions().commit()); } TEST_CASE("editor.module.name") { auto* mod = Editor::create(); CHECK_EQ(mod->getName(), std::string("Editor")); CHECK_EQ(Editor::create(), mod); } TEST_CASE("editor.gizmo.translate.pick_and_drag") { std::unique_ptr g(new TransformGizmo()); g->setMode("translate"); g->setSpace("world"); g->setPosition(0.f, 0.f, 0.f); g->setSize(1.f); // Ray from +Z looking at origin, aimed near +X axis tip std::string axis = g->pick(2.f, 0.f, 5.f, -0.3f, 0.f, -1.f); // May hit x or miss depending on exact geometry; force beginDrag on x CHECK(g->beginDrag("x", 0.f, 2.f, 5.f, 0.f, 0.f, -1.f)); CHECK(g->isDragging()); CHECK_EQ(g->getActiveAxis(), std::string("x")); // Drag along +X by moving hit point CHECK(g->updateDrag(1.5f, 2.f, 5.f, 0.f, 0.f, -1.f)); CHECK(g->getPositionX() > 0.f); float y = g->getPositionY(); float z = g->getPositionZ(); CHECK(std::fabs(y) < 1e-3f); CHECK(std::fabs(z) < 1e-3f); g->endDrag(); CHECK(!g->isDragging()); CHECK_EQ(g->getPartCount() > 0, true); CHECK_EQ(g->getPartKind(0), std::string("axis")); (void)axis; } TEST_CASE("editor.gizmo.snap_translate") { std::unique_ptr g(new TransformGizmo()); g->setMode("translate"); g->setSnapTranslate(1.f, 1.f, 1.f); g->setPosition(0.f, 0.f, 0.f); CHECK(g->beginDrag("x", 0.f, 2.f, 5.f, 0.f, 0.f, -1.f)); g->updateDrag(0.4f, 2.f, 5.f, 0.f, 0.f, -1.f); // With snap 1, small moves round toward 0 or 1 float x = g->getPositionX(); CHECK(std::fabs(x - std::round(x)) < 1e-4f); g->endDrag(); } TEST_CASE("editor.gizmo.rotate_and_scale_modes") { std::unique_ptr g(new TransformGizmo()); g->setMode("rotate"); CHECK_EQ(g->getPartCount(), 3); CHECK_EQ(g->getPartKind(0), std::string("ring")); g->setMode("scale"); CHECK(g->getPartCount() >= 3); CHECK(g->beginDrag("xyz", 0.f, 0.f, 5.f, 0.f, 0.f, -1.f)); float s0 = g->getScaleX(); g->updateDrag(0.f, 1.f, 5.f, 0.f, 0.f, -1.f); // uniform scale may change CHECK(g->getScaleX() > 0.f); CHECK_EQ(g->getScaleX(), g->getScaleY()); (void)s0; g->endDrag(); g->setMode("bound"); CHECK(g->getPartCount() >= 6); } TEST_CASE("editor.gizmo.invalid_mode") { std::unique_ptr g(new TransformGizmo()); bool threw = false; try { g->setMode("skew"); } catch (const eve::Exception&) { threw = true; } CHECK(threw); } TEST_CASE("editor.gizmo_manager.multi_mode") { std::unique_ptr mgr(new GizmoManager()); CHECK(!mgr->isAttached()); CHECK(mgr->pick(0, 0, 5, 0, 0, -1).empty()); mgr->attach(); mgr->setPositionEnabled(true); mgr->setRotationEnabled(true); TransformGizmo* g = mgr->getGizmo(); g->setPosition(0.f, 0.f, 0.f); g->setSize(1.f); // Pick should succeed on some handle when looking at origin std::string axis = mgr->pick(0.f, 0.f, 5.f, 0.f, 0.f, -1.f); // center or axis may be hit if (!axis.empty()) { CHECK(mgr->beginDrag(axis, 0.f, 0.f, 5.f, 0.f, 0.f, -1.f)); mgr->endDrag(); } mgr->detach(); CHECK(!mgr->isAttached()); } TEST_CASE("editor.brush.paint_fill_line_rect") { std::unique_ptr ed(Editor::create()); // Module singleton — don't delete; use factories only std::unique_ptr buf(new TileBuffer(8, 8)); std::unique_ptr brush(new Brush()); brush->setTool("paint"); brush->setTile(3); brush->setSize(1); CHECK_EQ(brush->paintAt(buf.get(), 2, 2), 1); CHECK_EQ(buf->getGid(2, 2), 3); brush->setSize(3); brush->setShape("square"); int n = brush->paintAt(buf.get(), 4, 4); CHECK(n >= 1); CHECK_EQ(buf->getGid(4, 4), 3); buf->fill(1); buf->setGid(0, 0, 0); brush->setTile(7); CHECK(brush->floodFill(buf.get(), 1, 1) > 1); CHECK_EQ(buf->getGid(3, 3), 7); CHECK_EQ(buf->getGid(0, 0), 0); // separated buf->clear(); brush->setTile(2); brush->setSize(1); CHECK(brush->paintLine(buf.get(), 0, 0, 3, 0) >= 4); CHECK_EQ(buf->getGid(2, 0), 2); buf->clear(); CHECK(brush->paintRect(buf.get(), 1, 1, 3, 3, true) >= 9); CHECK_EQ(buf->getGid(2, 2), 2); brush->previewAt(buf.get(), 5, 5); CHECK(brush->getPreviewCount() >= 1); } TEST_CASE("editor.brush.stamp_and_erase") { std::unique_ptr buf(new TileBuffer(5, 5)); std::unique_ptr brush(new Brush()); brush->setStampSize(2, 2); brush->setStampTile(0, 0, 9); brush->setStampTile(1, 0, 8); brush->setStampTile(0, 1, 7); brush->setStampTile(1, 1, 6); brush->setTool("stamp"); CHECK(brush->paintAt(buf.get(), 2, 2) >= 1); CHECK(brush->getChangeCount() >= 1); brush->setTool("erase"); brush->setEraseTile(0); brush->setSize(1); brush->eraseAt(buf.get(), 2, 2); } TEST_CASE("editor.toolbar_inspector_dock") { std::unique_ptr tb(new EditorToolbar()); tb->addTool("move", "Move"); tb->addTool("paint", "Paint"); tb->setShortcut("move", "W"); CHECK_EQ(tb->getActive(), std::string("move")); CHECK(tb->matchShortcut("W")); CHECK(tb->setActive("paint")); CHECK_EQ(tb->getActive(), std::string("paint")); CHECK_EQ(tb->getToolCount(), 2); std::unique_ptr insp(new EditorInspector()); insp->addFloat("size", "Size", 1.f, 0.f, 10.f, 0.1f); insp->addFloat3("pos", "Position", 1.f, 2.f, 3.f); insp->addBool("snap", "Snap", false); insp->addChoice("mode", "Mode", "translate,rotate,scale", "translate"); insp->setFloat("size", 2.5f); CHECK(insp->isDirty("size")); CHECK_EQ(insp->pollChangedId(), std::string("size")); CHECK_EQ(insp->getFloat3Y("pos"), 2.f); insp->setChoice("mode", "rotate"); CHECK_EQ(insp->getChoice("mode"), std::string("rotate")); std::unique_ptr dock(new EditorDock()); dock->setRegionSize("left", 100.f); dock->setRegionSize("right", 120.f); dock->setRegionSize("top", 30.f); dock->setRegionSize("bottom", 20.f); dock->layout(800.f, 600.f); CHECK_EQ(dock->getRegionW("center"), 800.f - 100.f - 120.f); CHECK_EQ(dock->getRegionH("center"), 600.f - 30.f - 20.f); CHECK_EQ(dock->getRegionX("right"), 800.f - 120.f); } TEST_CASE("editor.history.tiles_undo_redo") { std::unique_ptr buf(new TileBuffer(4, 4)); std::unique_ptr brush(new Brush()); std::unique_ptr hist(new EditorHistory()); brush->setTile(5); brush->paintAt(buf.get(), 1, 1); hist->beginGroup("paint"); for (int i = 0; i < brush->getChangeCount(); ++i) { hist->recordTile(brush->getChangeX(i), brush->getChangeY(i), brush->getChangeOldGid(i), brush->getChangeNewGid(i)); } hist->endGroup(); CHECK_EQ(buf->getGid(1, 1), 5); CHECK(hist->canUndo()); CHECK(hist->undo()); CHECK(hist->applyLastToBuffer(buf.get())); CHECK_EQ(buf->getGid(1, 1), 0); CHECK(hist->canRedo()); CHECK(hist->redo()); CHECK(hist->applyLastToBuffer(buf.get())); CHECK_EQ(buf->getGid(1, 1), 5); hist->push("rename", "npc_01"); CHECK(hist->undo()); CHECK_EQ(hist->getLastActionKind(), std::string("opaque")); CHECK_EQ(hist->getLastPayload(), std::string("npc_01")); } TEST_CASE("editor.factories") { Editor* ed = Editor::create(); eve::level_editor::LevelEditorModule levelEditor; std::unique_ptr g = ed->newGizmo(); std::unique_ptr m = ed->newGizmoManager(); std::unique_ptr b = levelEditor.newTileBuffer(2, 2); std::unique_ptr br = levelEditor.newBrush(); std::unique_ptr t = ed->newToolbar(); std::unique_ptr i = ed->newInspector(); std::unique_ptr h = levelEditor.newHistory(); std::unique_ptr d = ed->newDock(); std::unique_ptr s = ed->newSession(); auto workspace = ed->newWorkspace("level", "Level"); auto rejected = ed->newWorkspace("", "nope"); // zeroerr CHECK copies the expression for printing — use raw pointers, not unique_ptr. CHECK(g.get() != nullptr); CHECK(m.get() != nullptr); CHECK_EQ(b->getWidth(), 2); CHECK(br.get() != nullptr); CHECK(t.get() != nullptr); CHECK(i.get() != nullptr); CHECK(d.get() != nullptr); CHECK(h.get() != nullptr); CHECK(s.get() != nullptr); CHECK(workspace.ok()); CHECK_EQ(workspace.value()->getId(), std::string("level")); CHECK_NOT(rejected.ok()); CHECK_NOT(rejected.hasValue()); }