diff --git a/docs/api/editor/editorServiceMethods.md b/docs/api/editor/editorServiceMethods.md index fbfba75c..181d9045 100644 --- a/docs/api/editor/editorServiceMethods.md +++ b/docs/api/editor/editorServiceMethods.md @@ -170,6 +170,29 @@ const parent = editorService.getParentById("text_123"); console.log(parent); ``` +## isOnDifferentPage + +- **参数:** + - {`MNode`} node 节点配置 + +- **返回:** + - `{boolean}` true 表示该节点位于非当前页面(即选中该节点将会引起当前页面切换) + +- **详情:** + + 判断给定节点是否位于非当前页面,通常用于配合 `doNotSwitchPage` 选项判断 DSL 操作是否会引起页面切换 + +- **示例:** + +```js +import { editorService } from "@tmagic/editor"; + +const otherPageNode = editorService.getNodeById("text_456"); +if (editorService.isOnDifferentPage(otherPageNode)) { + console.log("该节点在其它页面,操作会触发页面切换"); +} +``` + ## getLayout - **[扩展支持](../../guide/editor-expand#行为扩展):** 是 @@ -334,6 +357,7 @@ editorService.highlight("text_123"); - `{Object}` options 可选配置 - `{boolean}` doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点) + - `{boolean}` doNotSwitchPage 添加后是否不切换当前页面(默认 false;新增页面 / 跨页新增时为 true 会跳过会引发页面切换的选中操作) - **返回:** - {Promise<`MNode` | `MNode`[]>} 新增的组件或组件集合 @@ -357,6 +381,7 @@ editorService.highlight("text_123"); - {`MNode`} node 要删除的节点 - `{Object}` options 可选配置 - `{boolean}` doNotSelect 删除后是否不更新当前选中节点(默认 false) + - `{boolean}` doNotSwitchPage 删除后是否不切换当前页面(默认 false;删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面) - **返回:** - `{Promise}` @@ -365,6 +390,10 @@ editorService.highlight("text_123"); 删除指定的组件或者页面 + :::tip + 无论是否传入 `doNotSelect` / `doNotSwitchPage`,当被删除节点在当前选中列表中时,state 都会自动移除该节点的引用;当被删除的正好是当前页面时,state.page 也会同步清空,避免持有已删除节点 + ::: + ## remove - **[扩展支持](../../guide/editor-expand#行为扩展):** 是 @@ -373,6 +402,7 @@ editorService.highlight("text_123"); - {`MNode` | `MNode`[])} node 要删除的节点或节点集合 - `{Object}` options 可选配置 - `{boolean}` doNotSelect 删除后是否不更新当前选中节点(默认 false,删除后会选中父节点或首个页面) + - `{boolean}` doNotSwitchPage 删除后是否不切换当前页面(默认 false;删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面) - **返回:** - `{Promise}` @@ -413,6 +443,8 @@ editorService.highlight("text_123"); 节点中应该要有id,不然不知道要更新哪个节点 当被更新节点正好在当前选中列表中时,state 会自动同步到新的节点引用,无需调用方处理 + + 当被更新节点正好是当前页面时,state.page 也会同步到新的节点引用;更新非当前页面(不同 ID)时不会把编辑器切到该页 ::: ## update @@ -448,6 +480,7 @@ editorService.highlight("text_123"); - `{ string | number }` id2 - `{Object}` options 可选配置 - `{boolean}` doNotSelect 排序后是否不更新当前选中节点(默认 false) + - `{boolean}` doNotSwitchPage 排序后是否不切换当前页面(排序只发生在同一父节点内,方法内为空操作;保留以与其它 DSL 操作 API 一致) - **返回:** - `{Promise}` @@ -514,6 +547,7 @@ editorService.highlight("text_123"); - `{TargetOptions}` collectorOptions 可选的依赖收集器配置 - `{Object}` options 可选配置 - `{boolean}` doNotSelect 粘贴后是否不更新当前选中节点(默认 false) + - `{boolean}` doNotSwitchPage 粘贴后是否不切换当前页面(默认 false;跨页粘贴时为 true 会跳过页面切换) - **返回:** - {Promise<`MNode` | `MNode`[]>} 添加后的组件节点配置 @@ -550,6 +584,7 @@ editorService.highlight("text_123"); - {`MNode` | `MNode`[]} config 需要居中的组件或者组件集合 - `{Object}` options 可选配置 - `{boolean}` doNotSelect 居中后是否不更新当前选中节点(默认 false) + - `{boolean}` doNotSwitchPage 居中后是否不切换当前页面(居中只更新节点 style,方法内为空操作;保留以与其它 DSL 操作 API 一致) - **返回:** - {Promise<`MNode` | `MNode`[]>} @@ -589,6 +624,7 @@ alignCenter可以支持一次水平居中多个组件,alignCenter是通过调 - `{string | number}` targetId 容器ID - `{Object}` options 可选配置 - `{boolean}` doNotSelect 移动后是否不更新当前选中节点(默认 false) + - `{boolean}` doNotSwitchPage 移动后是否不切换当前页面(默认 false;目标容器位于其它页面时为 true 会跳过自动选中以避免页面切换) - **返回:** - Promise<`MNode` | undefined> diff --git a/packages/editor/src/services/editor.ts b/packages/editor/src/services/editor.ts index c74e183b..738d0f35 100644 --- a/packages/editor/src/services/editor.ts +++ b/packages/editor/src/services/editor.ts @@ -33,6 +33,7 @@ import type { AddMNode, AsyncHookPlugin, AsyncMethodName, + DslOpOptions, EditorEvents, EditorNodeInfo, HistoryOpType, @@ -191,6 +192,22 @@ class Editor extends BaseService { return parent; } + /** + * 判断给定节点是否位于非当前页面(即选中该节点将会引起当前页面切换) + * @param node 节点 + * @returns true 表示该节点位于非当前页面 + */ + public isOnDifferentPage(node: MNode): boolean { + const currentPageId = this.get('page')?.id; + if (currentPageId === undefined || currentPageId === null) return false; + if (isPage(node) || isPageFragment(node)) { + return `${node.id}` !== `${currentPageId}`; + } + const nodePage = this.getNodeInfo(node.id, false).page; + if (!nodePage) return false; + return `${nodePage.id}` !== `${currentPageId}`; + } + /** * 只有容器拥有布局 */ @@ -370,15 +387,16 @@ class Editor extends BaseService { * @param parent 要添加到的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点 * @param options 可选配置 * @param options.doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点) + * @param options.doNotSwitchPage 添加后是否不切换当前页面(默认 false;新增页面 / 跨页新增时为 true 会跳过会引发页面切换的选中操作) * @returns 添加后的节点 */ public async add( addNode: AddMNode | MNode[], parent?: MContainer | null, - options?: { doNotSelect?: boolean }, + options?: DslOpOptions, ): Promise { const safeParentNode = safeParent(parent); - const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options); + const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options); this.captureSelectionBeforeOp(); @@ -409,14 +427,19 @@ class Editor extends BaseService { ); if (newNodes.length > 1) { - if (!doNotSelect) { + // 多选时只要任一新增节点位于非当前页面,触发的 multiSelect 就会引起页面切换 + const wouldSwitchPage = newNodes.some((n) => this.isOnDifferentPage(n)); + if (!doNotSelect && !(doNotSwitchPage && wouldSwitchPage)) { const newNodeIds = newNodes.map((node) => node.id); // 触发选中样式 stage?.multiSelect(newNodeIds); await this.multiSelect(newNodeIds); } } else { - if (!doNotSelect) { + const wouldSwitchPage = this.isOnDifferentPage(newNodes[0]); + const skipSelect = doNotSelect || (doNotSwitchPage && wouldSwitchPage); + + if (!skipSelect) { await this.select(newNodes[0]); } @@ -424,7 +447,7 @@ class Editor extends BaseService { this.state.pageLength += 1; } else if (isPageFragment(newNodes[0])) { this.state.pageFragmentLength += 1; - } else if (!doNotSelect) { + } else if (!skipSelect) { // 新增页面,这个时候页面还有渲染出来,此时select会出错,在runtime-ready的时候回去select stage?.select(newNodes[0].id); } @@ -453,8 +476,8 @@ class Editor extends BaseService { return Array.isArray(addNode) ? newNodes : newNodes[0]; } - public async doRemove(node: MNode, options?: { doNotSelect?: boolean }): Promise { - const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options); + public async doRemove(node: MNode, options?: DslOpOptions): Promise { + const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options); const root = this.get('root'); if (!root) throw new Error('root不能为空'); @@ -471,21 +494,19 @@ class Editor extends BaseService { const stage = this.get('stage'); stage?.remove({ id: node.id, parentId: parent.id, root: cloneDeep(root) }); - if (doNotSelect) { - // 当被删除节点正好在当前选中列表中时,必须从 state 中移除引用,避免 state 持有已删除节点(与 doNotSelect 无关) - const selectedNodes = this.get('nodes'); - const removedSelectedIndex = selectedNodes.findIndex((n: MNode) => `${n.id}` === `${node.id}`); - if (removedSelectedIndex !== -1) { - const nextSelected = [...selectedNodes]; - nextSelected.splice(removedSelectedIndex, 1); - this.set('nodes', nextSelected); - } - // 同理,如果被删除的是当前 page,也清空 state.page,避免持有已删除页面 - if (isPage(node) || isPageFragment(node)) { - const currentPage = this.get('page'); - if (currentPage && `${currentPage.id}` === `${node.id}`) { - this.set('page', null); - } + // 始终清理已删除节点在 state 中的残留引用: + // - 即使后续会调用 selectDefault / select(parent) 覆盖,跳过这些调用(doNotSelect / doNotSwitchPage)时也不能让 state 持有已删除节点 + const selectedNodes = this.get('nodes'); + const removedSelectedIndex = selectedNodes.findIndex((n: MNode) => `${n.id}` === `${node.id}`); + if (removedSelectedIndex !== -1) { + const nextSelected = [...selectedNodes]; + nextSelected.splice(removedSelectedIndex, 1); + this.set('nodes', nextSelected); + } + if (isPage(node) || isPageFragment(node)) { + const currentPage = this.get('page'); + if (currentPage && `${currentPage.id}` === `${node.id}`) { + this.set('page', null); } } @@ -505,13 +526,14 @@ class Editor extends BaseService { if (isPage(node)) { this.state.pageLength -= 1; - if (!doNotSelect) { + // 删除页面后默认会切到首个剩余页面(selectDefault),doNotSwitchPage 时跳过这次自动切换 + if (!doNotSelect && !doNotSwitchPage) { await selectDefault(rootItems); } } else if (isPageFragment(node)) { this.state.pageFragmentLength -= 1; - if (!doNotSelect) { + if (!doNotSelect && !doNotSwitchPage) { await selectDefault(rootItems); } } else { @@ -534,9 +556,10 @@ class Editor extends BaseService { * @param {Object} node 要删除的节点或节点集合 * @param options 可选配置 * @param options.doNotSelect 删除后是否不更新当前选中节点(默认 false,删除后会选中父节点或首个页面) + * @param options.doNotSwitchPage 删除后是否不切换当前页面(默认 false;删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面) */ - public async remove(nodeOrNodeList: MNode | MNode[], options?: { doNotSelect?: boolean }): Promise { - const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options); + public async remove(nodeOrNodeList: MNode | MNode[], options?: DslOpOptions): Promise { + const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options); this.captureSelectionBeforeOp(); @@ -561,7 +584,7 @@ class Editor extends BaseService { } } - await Promise.all(nodes.map((node) => this.doRemove(node, { doNotSelect }))); + await Promise.all(nodes.map((node) => this.doRemove(node, { doNotSelect, doNotSwitchPage }))); if (removedItems.length > 0 && pageForOp) { this.pushOpHistory('remove', { removedItems }, pageForOp); @@ -624,8 +647,12 @@ class Editor extends BaseService { this.set('nodes', [...selectedNodes]); } + // 只有被更新节点正好是当前选中页面时才同步 state.page,避免「更新非当前页」误将编辑器切到该页 if (isPage(newConfig) || isPageFragment(newConfig)) { - this.set('page', newConfig as MPage | MPageFragment); + const currentPage = this.get('page'); + if (currentPage && `${currentPage.id}` === `${newConfig.id}`) { + this.set('page', newConfig as MPage | MPageFragment); + } } this.addModifiedNodeId(newConfig.id); @@ -681,10 +708,11 @@ class Editor extends BaseService { * @param id2 组件ID * @param options 可选配置 * @param options.doNotSelect 排序后是否不更新当前选中节点(默认 false) + * @param options.doNotSwitchPage 排序后是否不切换当前页面(排序只发生在同一父节点内,方法内为空操作;保留以与其它 DSL 操作 API 一致) * @returns void */ - public async sort(id1: Id, id2: Id, options?: { doNotSelect?: boolean }): Promise { - const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options); + public async sort(id1: Id, id2: Id, options?: DslOpOptions): Promise { + const { doNotSelect = false } = safeOptions(options); this.captureSelectionBeforeOp(); @@ -750,14 +778,15 @@ class Editor extends BaseService { * @param collectorOptions 可选的依赖收集器配置 * @param options 可选配置 * @param options.doNotSelect 粘贴后是否不更新当前选中节点(默认 false) + * @param options.doNotSwitchPage 粘贴后是否不切换当前页面(默认 false;跨页粘贴时为 true 会跳过页面切换) * @returns 添加后的组件节点配置 */ public async paste( position: PastePosition = {}, collectorOptions?: TargetOptions, - options?: { doNotSelect?: boolean }, + options?: DslOpOptions, ): Promise { - const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options); + const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options); const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY); if (!Array.isArray(config)) return; @@ -778,7 +807,7 @@ class Editor extends BaseService { propsService.replaceRelateId(config, pasteConfigs, collectorOptions); } - return this.add(pasteConfigs, parent, { doNotSelect }); + return this.add(pasteConfigs, parent, { doNotSelect, doNotSwitchPage }); } public async doPaste(config: MNode[], position: PastePosition = {}): Promise { @@ -808,10 +837,11 @@ class Editor extends BaseService { * @param config 组件节点配置 * @param options 可选配置 * @param options.doNotSelect 居中后是否不更新当前选中节点(默认 false) + * @param options.doNotSwitchPage 居中后是否不切换当前页面(居中只更新节点 style,方法内为空操作;保留以与其它 DSL 操作 API 一致) * @returns 当前组件节点配置 */ - public async alignCenter(config: MNode | MNode[], options?: { doNotSelect?: boolean }): Promise { - const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options); + public async alignCenter(config: MNode | MNode[], options?: DslOpOptions): Promise { + const { doNotSelect = false } = safeOptions(options); const nodes = Array.isArray(config) ? config : [config]; const stage = this.get('stage'); @@ -890,13 +920,10 @@ class Editor extends BaseService { * @param targetId 容器ID * @param options 可选配置 * @param options.doNotSelect 移动后是否不更新当前选中节点(默认 false) + * @param options.doNotSwitchPage 移动后是否不切换当前页面(默认 false;目标容器位于其它页面时为 true 会跳过自动选中以避免页面切换) */ - public async moveToContainer( - config: MNode, - targetId: Id, - options?: { doNotSelect?: boolean }, - ): Promise { - const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options); + public async moveToContainer(config: MNode, targetId: Id, options?: DslOpOptions): Promise { + const { doNotSelect = false, doNotSwitchPage = false } = safeOptions(options); this.captureSelectionBeforeOp(); @@ -925,7 +952,11 @@ class Editor extends BaseService { target.items.push(newConfig); - if (!doNotSelect) { + // 目标容器是否在非当前页面:选中目标会触发当前页面切换 + const targetWouldSwitchPage = this.isOnDifferentPage(target); + const skipSelect = doNotSelect || (doNotSwitchPage && targetWouldSwitchPage); + + if (!skipSelect) { await stage.select(targetId); } @@ -936,7 +967,7 @@ class Editor extends BaseService { root: cloneDeep(root), }); - if (!doNotSelect) { + if (!skipSelect) { await this.select(newConfig); stage.select(newConfig.id); } diff --git a/packages/editor/src/type.ts b/packages/editor/src/type.ts index 452dd2b5..c82dd1f3 100644 --- a/packages/editor/src/type.ts +++ b/packages/editor/src/type.ts @@ -873,3 +873,13 @@ export const canUsePluginMethods = { }; export type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>; + +/** + * DSL 修改类操作的通用配置 + * - doNotSelect: 操作后是否不要自动触发选中(不调用 this.select / this.multiSelect / stage.select / stage.multiSelect) + * - doNotSwitchPage: 操作若会引发当前页面切换(如新增 / 删除 / 跨页移动),是否跳过这次切换 + */ +export type DslOpOptions = { + doNotSelect?: boolean; + doNotSwitchPage?: boolean; +}; diff --git a/packages/editor/tests/unit/services/editor.spec.ts b/packages/editor/tests/unit/services/editor.spec.ts index 92568ce6..d62eb928 100644 --- a/packages/editor/tests/unit/services/editor.spec.ts +++ b/packages/editor/tests/unit/services/editor.spec.ts @@ -190,6 +190,43 @@ describe('getParentById', () => { }); }); +describe('isOnDifferentPage', () => { + test('当前未选中任何页面时返回 false', () => { + editorService.set('root', cloneDeep(root)); + editorService.resetState(); + const pageNode = editorService.getNodeById(NodeId.PAGE_ID)!; + expect(editorService.isOnDifferentPage(pageNode)).toBe(false); + }); + + test('节点在当前页面内时返回 false', async () => { + editorService.set('root', cloneDeep(root)); + await editorService.select(NodeId.PAGE_ID); + const innerNode = editorService.getNodeById(NodeId.NODE_ID)!; + expect(editorService.isOnDifferentPage(innerNode)).toBe(false); + }); + + test('页面节点本身就是当前页面时返回 false', async () => { + editorService.set('root', cloneDeep(root)); + await editorService.select(NodeId.PAGE_ID); + const pageNode = editorService.getNodeById(NodeId.PAGE_ID)!; + expect(editorService.isOnDifferentPage(pageNode)).toBe(false); + }); + + test('节点位于非当前页面时返回 true', async () => { + editorService.set('root', cloneDeep(root)); + const rootNode = editorService.get('root'); + await editorService.select(NodeId.PAGE_ID); + // 加一个新页面 + const newPage = await editorService.add({ type: NodeType.PAGE }, rootNode, { doNotSwitchPage: true }); + const newPageId = Array.isArray(newPage) ? newPage[0].id : newPage.id; + + // 当前还在第一个页面 + expect(editorService.get('page')?.id).toBe(NodeId.PAGE_ID); + const newPageNode = editorService.getNodeById(newPageId)!; + expect(editorService.isOnDifferentPage(newPageNode)).toBe(true); + }); +}); + describe('select', () => { beforeAll(() => editorService.set('root', cloneDeep(root))); @@ -303,6 +340,24 @@ describe('add', () => { // 但当前选中节点保持原状(未自动选中新增节点) expect(editorService.get('node')?.id).toBe(beforeNodeId); }); + + test('doNotSwitchPage: true 新增页面时保持当前页面不切换', async () => { + editorService.set('root', cloneDeep(root)); + await editorService.select(NodeId.PAGE_ID); + const beforePageId = editorService.get('page')?.id; + expect(beforePageId).toBe(NodeId.PAGE_ID); + + const rootNode = editorService.get('root'); + const newPage = await editorService.add({ type: NodeType.PAGE }, rootNode, { doNotSwitchPage: true }); + + // 新页面已加入 dsl + const addedId = Array.isArray(newPage) ? newPage[0].id : newPage.id; + expect(editorService.getNodeById(addedId)).toBeTruthy(); + expect(rootNode?.items.length).toBe(2); + + // 当前 page 保持不变(没有自动切到新加的页面) + expect(editorService.get('page')?.id).toBe(beforePageId); + }); }); describe('remove', () => { @@ -363,6 +418,51 @@ describe('remove', () => { // state.nodes 中不再包含被删除的节点 expect(editorService.get('nodes').some((n) => n.id === NodeId.NODE_ID)).toBe(false); }); + + test('doNotSwitchPage: true 删除当前页面后不自动切到其它页面', async () => { + editorService.set('root', cloneDeep(root)); + const rootNode = editorService.get('root'); + // 先加一个页面,确保 root 下有 2 个页面 + await editorService.select(NodeId.PAGE_ID); + const newPage = await editorService.add({ type: NodeType.PAGE }, rootNode); + expect(rootNode?.items.length).toBe(2); + + // 选中第一个页面,作为当前页面 + await editorService.select(NodeId.PAGE_ID); + expect(editorService.get('page')?.id).toBe(NodeId.PAGE_ID); + + // 删除当前页面,并要求不切换页面 + await editorService.remove({ id: NodeId.PAGE_ID, type: NodeType.PAGE }, { doNotSwitchPage: true }); + + // 被删除页面在 dsl 中确实已不存在 + expect(editorService.getNodeById(NodeId.PAGE_ID)).toBeNull(); + // 当前 page 引用被清空,不会被自动切到剩余页面 + expect(editorService.get('page')).toBeNull(); + // 仍保留 newPage 在 dsl 中 + const addedId = Array.isArray(newPage) ? newPage[0].id : newPage.id; + expect(editorService.getNodeById(addedId)).toBeTruthy(); + }); + + test('默认删除当前页面后会自动切到剩余首个页面', async () => { + editorService.set('root', cloneDeep(root)); + const rootNode = editorService.get('root'); + // 先加一个页面 + await editorService.select(NodeId.PAGE_ID); + const newPage = await editorService.add({ type: NodeType.PAGE }, rootNode); + const addedId = Array.isArray(newPage) ? newPage[0].id : newPage.id; + expect(rootNode?.items.length).toBe(2); + + // 选中第一个页面作为当前页面 + await editorService.select(NodeId.PAGE_ID); + + // 删除当前页面,使用默认行为 + await editorService.remove({ id: NodeId.PAGE_ID, type: NodeType.PAGE }); + + // 被删除页面在 dsl 中已不存在 + expect(editorService.getNodeById(NodeId.PAGE_ID)).toBeNull(); + // 自动切到剩余首个页面 + expect(editorService.get('page')?.id).toBe(addedId); + }); }); describe('update', () => { @@ -439,6 +539,27 @@ describe('update', () => { expect(editorService.get('node')?.id).toBe(NodeId.NODE_ID); expect(editorService.get('node')).toBe(beforeSelected); }); + + test('更新非当前页面时,不会把编辑器切到该页', async () => { + editorService.set('root', cloneDeep(root)); + const rootNode = editorService.get('root'); + // 先加一个新页面 + await editorService.select(NodeId.PAGE_ID); + const newPage = await editorService.add({ type: NodeType.PAGE }, rootNode); + const newPageId = Array.isArray(newPage) ? newPage[0].id : newPage.id; + + // 选中第一个页面作为当前页 + await editorService.select(NodeId.PAGE_ID); + expect(editorService.get('page')?.id).toBe(NodeId.PAGE_ID); + + // 更新非当前页面(newPage)的配置 + await editorService.update({ id: newPageId, type: NodeType.PAGE, name: 'page-renamed' }); + + // dsl 中该页已更新 + expect(editorService.getNodeById(newPageId)?.name).toBe('page-renamed'); + // 当前 page 没有被切走 + expect(editorService.get('page')?.id).toBe(NodeId.PAGE_ID); + }); }); describe('sort', () => { @@ -495,6 +616,24 @@ describe('paste', () => { // 当前选中节点保持原状 expect(editorService.get('node')?.id).toBe(beforeNodeId); }); + + test('doNotSwitchPage: true 粘贴页面时不切换当前页面', async () => { + editorService.set('root', cloneDeep(root)); + await editorService.select(NodeId.PAGE_ID); + + // 复制当前页面 + const pageNode = editorService.getNodeById(NodeId.PAGE_ID); + await editorService.copy(pageNode!); + const beforePageId = editorService.get('page')?.id; + expect(beforePageId).toBe(NodeId.PAGE_ID); + + const pasted = await editorService.paste({}, undefined, { doNotSwitchPage: true }); + + // 粘贴成功 + expect(pasted).toBeTruthy(); + // 当前 page 保持不变 + expect(editorService.get('page')?.id).toBe(beforePageId); + }); }); describe('moveLayer', () => {