diff --git a/src/utils/core.ts b/src/utils/core.ts index e399e5d..7b4ef05 100644 --- a/src/utils/core.ts +++ b/src/utils/core.ts @@ -1549,6 +1549,50 @@ export const COMMANDS: { return ref; }, }, + { + text: "MOVEBLOCK", + help: "Moves a block under a new parent\n\n1. Block ref or uid to move (optional, defaults to current block)\n\n2. Parent ref, uid, or page title (optional, defaults to current parent)\n\n3. Order index or 'last' (optional, defaults to last)", + handler: async (blockArg = "", parentArg = "", orderArg = "last") => { + const sourceArg = smartBlocksContext.variables[blockArg] || blockArg; + const sourceUid = + extractRef(sourceArg) || sourceArg || smartBlocksContext.currentUid || ""; + if (!sourceUid) { + return "--> MOVEBLOCK failed: source block was not found <--"; + } + + const rawParentArg = smartBlocksContext.variables[parentArg] || parentArg; + const targetParentUid = + getUidFromText(rawParentArg) || + extractRef(rawParentArg) || + rawParentArg || + getParentUidByBlockUid(sourceUid); + if (!targetParentUid) { + return "--> MOVEBLOCK failed: target parent was not found <--"; + } + if (sourceUid === targetParentUid) { + return "--> MOVEBLOCK failed: source block cannot be its own parent <--"; + } + + const order = + /^last$/i.test(orderArg) || !orderArg + ? getBasicTreeByParentUid(targetParentUid).length + : Math.max(0, Number(orderArg) || 0); + + if (typeof (window.roamAlphaAPI as any).moveBlock !== "function") { + return "--> MOVEBLOCK failed: moveBlock API not available <--"; + } + try { + await (window.roamAlphaAPI as any).moveBlock({ + location: { "parent-uid": targetParentUid, order }, + block: { uid: sourceUid }, + }); + return `((${sourceUid}))`; + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + return `--> MOVEBLOCK failed: ${message} <--`; + } + }, + }, { text: "INDENT", help: "Indents the current block if indentation can be done at current block. ", diff --git a/tests/core.test.ts b/tests/core.test.ts index 1e76efa..d5e6c18 100644 --- a/tests/core.test.ts +++ b/tests/core.test.ts @@ -2,45 +2,112 @@ import { test, expect } from "@playwright/test"; import mockRoamEnvironment from "roamjs-components/testing/mockRoamEnvironment"; import createBlock from "roamjs-components/writes/createBlock"; import createPage from "roamjs-components/writes/createPage"; -import { sbBomb } from "../src/core"; import nanoid from "nanoid"; -import { JSDOM } from "jsdom"; + +// @ts-ignore +global.window = global.window || {}; +// @ts-ignore +global.window.roamAlphaAPI = global.window.roamAlphaAPI || { + graph: { name: "test" }, +}; +// @ts-ignore +global.localStorage = global.localStorage || { + getItem: () => "", + setItem: () => {}, + removeItem: () => {}, + clear: () => {}, + key: () => null, + length: 0, +}; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { COMMANDS, resetContext } = require("../src/utils/core"); + +const moveBlockCommand = COMMANDS.find((c) => c.text === "MOVEBLOCK")?.handler; test.beforeAll(() => { mockRoamEnvironment(); - const { roamAlphaAPI } = window; - const jsdom = new JSDOM(); - // @ts-ignore - global.window = jsdom.window; - global.document = jsdom.window.document; - window.roamAlphaAPI = roamAlphaAPI; }); -test("Nested CURSOR command", async () => { - const parentUid = await createPage({ title: `page-${nanoid()}` }); - const srcUid = await createBlock({ - node: { - text: "#SmartBlock Nested", - children: [ - { - text: "<%SMARTBLOCK:Cursor%>", - }, - ], - }, - parentUid, +test.beforeEach(() => { + resetContext(); +}); + +test("MOVEBLOCK returns API-not-available message when moveBlock is missing", async () => { + const command = moveBlockCommand; + expect(command).toBeDefined(); + + const pageUid = await createPage({ title: `page-${nanoid()}` }); + const sourceUid = await createBlock({ parentUid: pageUid, node: { text: "s" } }); + const targetUid = await createBlock({ parentUid: pageUid, node: { text: "t" } }); + + const originalMoveBlock = (window.roamAlphaAPI as any).moveBlock; + (window.roamAlphaAPI as any).moveBlock = undefined; + + try { + const result = await command!(sourceUid, targetUid); + expect(result).toBe("--> MOVEBLOCK failed: moveBlock API not available <--"); + } finally { + (window.roamAlphaAPI as any).moveBlock = originalMoveBlock; + } +}); + +test("MOVEBLOCK reports source/target validation failures", async () => { + const command = moveBlockCommand; + expect(command).toBeDefined(); + + const noSourceResult = await command!(); + expect(noSourceResult).toBe( + "--> MOVEBLOCK failed: source block was not found <--" + ); + + const pageUid = await createPage({ title: `page-${nanoid()}` }); + const sourceUid = await createBlock({ + parentUid: pageUid, + node: { text: "source" }, + }); + const selfParentResult = await command!(sourceUid, sourceUid); + expect(selfParentResult).toBe( + "--> MOVEBLOCK failed: source block cannot be its own parent <--" + ); +}); + +test("MOVEBLOCK returns moved block ref and calls moveBlock with expected args", async () => { + const command = moveBlockCommand; + expect(command).toBeDefined(); + + const pageUid = await createPage({ title: `page-${nanoid()}` }); + const sourceParentUid = await createBlock({ + parentUid: pageUid, + node: { text: "source-parent" }, + }); + const sourceUid = await createBlock({ + parentUid: sourceParentUid, + node: { text: "source" }, + }); + const targetParentUid = await createBlock({ + parentUid: pageUid, + node: { text: "target-parent" }, }); await createBlock({ - node: { - text: "#SmartBlock Cursor", - children: [ - { - text: "Place the cursor here: <%CURSOR%>", - }, - ], - }, - parentUid, + parentUid: targetParentUid, + node: { text: "existing-child" }, }); - const targetUid = await createBlock({ node: { text: "" }, parentUid }); - const result = targetUid;// TODO: await sbBomb({ srcUid, target: { uid: targetUid } }); - expect(result).toEqual(targetUid); + + const originalMoveBlock = (window.roamAlphaAPI as any).moveBlock; + let moveBlockArgs: unknown; + (window.roamAlphaAPI as any).moveBlock = async (args: unknown) => { + moveBlockArgs = args; + }; + + try { + const result = await command!(sourceUid, targetParentUid, "last"); + expect(result).toBe(`((${sourceUid}))`); + expect(moveBlockArgs).toEqual({ + location: { "parent-uid": targetParentUid, order: 1 }, + block: { uid: sourceUid }, + }); + } finally { + (window.roamAlphaAPI as any).moveBlock = originalMoveBlock; + } });