Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/scene/scene-process/service/editors/prefab-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class PrefabEditor extends BaseEditor {

// 实例化预制体
const instance = instantiate(prefabAsset);
editorPrefabUtils.preparePrefabRootForEditing(instance);
this.virtualScene.addChild(instance);
await this.ensurePreviewCanvasForUI(instance);

Expand Down Expand Up @@ -84,6 +85,7 @@ export class PrefabEditor extends BaseEditor {
if (!instance) {
throw new Error(`reload 失败,找不到预制体根节点: ${prefabName}`);
}
editorPrefabUtils.preparePrefabRootForEditing(instance);
this.entity.instance = instance;
Prefab._utils.applyTargetOverrides(this.entity.instance);
await this.ensurePreviewCanvasForUI(this.entity.instance);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import { prefabUtils } from './utils';
type UUIDMap = Map<string, string | UUIDMap>;

class EditorPrefabUtils {
preparePrefabRootForEditing(node: Node) {
const prefabInfo = node['_prefab'];
if (prefabInfo) {
prefabInfo.instance = undefined;
}
}

serialize(node: Node) {
// 校验数据
prefabUtils.checkMountedRootData(node, true);
Expand Down
89 changes: 65 additions & 24 deletions src/core/scene/test/prefab-editor-preview-canvas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,42 +49,65 @@ jest.mock('../scene-process/service/prefab/prefab-editor-utils', () => ({
restorePrefabUUID: jest.fn(),
generateSceneAsset: jest.fn(),
removePrefabInstanceRoots: jest.fn(),
preparePrefabRootForEditing: jest.fn((node: any) => {
if (node?._prefab) {
node._prefab.instance = undefined;
}
}),
},
}));

import { instantiate } from 'cc';
import { find, instantiate } from 'cc';
import { sceneUtils } from '../scene-process/service/scene/utils';
import { PrefabEditor } from '../scene-process/service/editors/prefab-editor';

async function openPrefabWith(prefabRoot: any, scene = new MockScene('virtual-scene')): Promise<MockScene> {
function createPrefabRoot(name: string, options?: { hasCanvas?: boolean; hasUI?: boolean; nestedChild?: any }) {
return {
name,
uuid: `${name}-uuid`,
parent: null,
children: options?.nestedChild ? [options.nestedChild] : [],
_prefab: { fileId: `${name}-file-id`, instance: { fileId: `${name}-instance-file-id` } },
getComponentInChildren: jest.fn((type: unknown) => type === MockCanvas && options?.hasCanvas ? {} : null),
getComponentsInChildren: jest.fn((type: unknown) => type === MockUITransform && options?.hasUI !== false ? [{}] : []),
};
}

async function openPrefabWith(prefabRoot: any, scene = new MockScene('virtual-scene')): Promise<{ editor: PrefabEditor; scene: MockScene; }> {
(sceneUtils.runScene as jest.Mock).mockResolvedValue(scene);
(sceneUtils.loadAny as jest.Mock).mockResolvedValue({});
(instantiate as unknown as jest.Mock).mockReturnValue(prefabRoot);
const editor = new PrefabEditor();

await new PrefabEditor().open({
await editor.open({
uuid: 'prefab-uuid',
name: 'LabelPrefab',
type: 'prefab',
url: 'db://assets/LabelPrefab.prefab',
} as never);

return scene;
return { editor, scene };
}

describe('PrefabEditor preview Canvas', () => {
beforeEach(() => {
jest.clearAllMocks();
(globalThis as any).EditorExtends = {
serialize: jest.fn(() => ({ json: 'scene-asset' })),
Node: {
getNode: jest.fn(),
},
};
});

afterEach(() => {
delete (globalThis as any).EditorExtends;
});

it('hosts a UI prefab without its own Canvas under an editor-only Canvas when opened', async () => {
const scene = new MockScene('virtual-scene');
const previewCanvas = { name: 'should_hide_in_hierarchy' };
const prefabRoot = {
name: 'LabelPrefab',
parent: null,
getComponentInChildren: jest.fn((type: unknown) => type === MockCanvas ? null : null),
getComponentsInChildren: jest.fn((type: unknown) => type === MockUITransform ? [{}] : []),
};
const prefabRoot = createPrefabRoot('LabelPrefab');

createShouldHideInHierarchyCanvasNode.mockResolvedValue(previewCanvas);

Expand All @@ -95,30 +118,48 @@ describe('PrefabEditor preview Canvas', () => {
});

it('does not create a preview Canvas when the prefab already owns one', async () => {
const prefabRoot = {
name: 'CanvasPrefab',
parent: null,
getComponentInChildren: jest.fn((type: unknown) => type === MockCanvas ? {} : null),
getComponentsInChildren: jest.fn((type: unknown) => type === MockUITransform ? [{}] : []),
};
const prefabRoot = createPrefabRoot('CanvasPrefab', { hasCanvas: true });

const scene = await openPrefabWith(prefabRoot);
const { scene } = await openPrefabWith(prefabRoot);

expect(createShouldHideInHierarchyCanvasNode).not.toHaveBeenCalled();
expect(prefabRoot.parent).toBe(scene);
});

it('does not create a preview Canvas for prefabs without UI components', async () => {
const prefabRoot = {
name: 'MeshPrefab',
parent: null,
getComponentInChildren: jest.fn(() => null),
getComponentsInChildren: jest.fn(() => []),
};
const prefabRoot = createPrefabRoot('MeshPrefab', { hasUI: false });

const scene = await openPrefabWith(prefabRoot);
const { scene } = await openPrefabWith(prefabRoot);

expect(createShouldHideInHierarchyCanvasNode).not.toHaveBeenCalled();
expect(prefabRoot.parent).toBe(scene);
});

it('clears root prefab instance but keeps nested child instance when opened', async () => {
const nestedChild = createPrefabRoot('NestedChild');
const prefabRoot = createPrefabRoot('RootPrefab', { nestedChild });

await openPrefabWith(prefabRoot);

expect(prefabRoot._prefab.instance).toBeUndefined();
expect(nestedChild._prefab.instance).toEqual({ fileId: 'NestedChild-instance-file-id' });
});

it('clears reloaded root prefab instance but keeps nested child instance after reload', async () => {
const nestedChild = createPrefabRoot('NestedChild');
const openedRoot = createPrefabRoot('RootPrefab', { nestedChild });
const { editor } = await openPrefabWith(openedRoot);
const reloadedNestedChild = createPrefabRoot('ReloadedNestedChild');
const reloadedRoot = createPrefabRoot('RootPrefab', { nestedChild: reloadedNestedChild });
const reloadedScene = new MockScene('reloaded-scene');

(sceneUtils.runSceneImmediateByJson as jest.Mock).mockResolvedValue(reloadedScene);
((globalThis as any).EditorExtends.Node.getNode as jest.Mock).mockReturnValue(reloadedRoot);
(find as unknown as jest.Mock).mockReturnValue(null);

await editor.reload();

expect(reloadedRoot._prefab.instance).toBeUndefined();
expect(reloadedNestedChild._prefab.instance).toEqual({ fileId: 'ReloadedNestedChild-instance-file-id' });
});
});
Loading