fix(editor): 避免 defaultSelected 早于 root 就绪时 select 抛出未处理错误

This commit is contained in:
roymondchen 2026-09-01 15:30:25 +08:00
parent 58f2a943b8
commit 08b610a4f2
2 changed files with 48 additions and 2 deletions

View File

@ -29,6 +29,7 @@ import { getDepNodeIds, getNodes, isPage, isValueIncludeDataSource } from '@tmag
import PropsPanel from './layouts/PropsPanel.vue';
import { isIncludeDataSource } from './utils/editor';
import { error as logError } from './utils/logger';
import { EditorProps } from './editorProps';
import { Services } from './type';
@ -36,6 +37,19 @@ export declare type LooseRequired<T> = {
[P in string & keyof T]: T[P];
};
/**
* select DSL
* watch Vue Unhandled error during execution of watcher callback
*
*/
const selectNode = async (editorService: Services['editorService'], id: Id) => {
try {
await editorService.select(id);
} catch (e) {
logError(e);
}
};
export const initServiceState = (
props: EditorProps,
{
@ -193,9 +207,17 @@ export const initServiceState = (
},
);
// defaultSelected 可能早于 modelValue 到位immediate 首次执行时 root 还是空的,或节点尚未出现在 DSL 中),
// 此时 select 会因为「获取不到组件信息」而抛错,交由 root-change 中的选中逻辑在 root 就绪后补上即可。
watch(
() => props.defaultSelected,
(defaultSelected) => defaultSelected && editorService.select(defaultSelected),
(defaultSelected) => {
if (!defaultSelected || !editorService.getNodeById(defaultSelected)) {
return;
}
selectNode(editorService, defaultSelected);
},
{
immediate: true,
},

View File

@ -4,12 +4,13 @@
* Copyright (C) 2025 Tencent.
*/
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { defineComponent, h } from 'vue';
import { defineComponent, h, nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import { DepTargetType } from '@tmagic/core';
import { initServiceEvents, initServiceState } from '@editor/initService';
import * as logger from '@editor/utils/logger';
const mkServices = () => {
const handlers: Record<string, Record<string, any[]>> = {};
@ -151,6 +152,10 @@ vi.mock('@editor/utils/editor', () => ({
isIncludeDataSource: vi.fn(() => false),
}));
vi.mock('@editor/utils/logger', () => ({
error: vi.fn(),
}));
const Wrap = (props: any, services: any) =>
defineComponent({
setup() {
@ -233,11 +238,30 @@ describe('initServiceState', () => {
});
test('defaultSelected 调用 select', () => {
services.editorService.getNodeById.mockReturnValue({ id: 'n1' });
const props = { defaultSelected: 'n1' } as any;
mount(Wrap(props, services));
expect(services.editorService.select).toHaveBeenCalledWith('n1');
});
test('defaultSelected 对应节点不存在时不调用 select', () => {
services.editorService.getNodeById.mockReturnValue(null);
const props = { defaultSelected: 'n1' } as any;
mount(Wrap(props, services));
expect(services.editorService.select).not.toHaveBeenCalled();
});
test('defaultSelected select 失败时兜底成日志,不产生未处理的拒绝', async () => {
const err = new Error('获取不到组件信息');
vi.mocked(logger.error).mockClear();
services.editorService.getNodeById.mockReturnValue({ id: 'n1' });
services.editorService.select.mockRejectedValue(err);
const props = { defaultSelected: 'n1' } as any;
mount(Wrap(props, services));
await nextTick();
expect(logger.error).toHaveBeenCalledWith(err);
});
test('stageRect 设置 ui state', () => {
const props = { stageRect: { width: 100 } } as any;
mount(Wrap(props, services));