// 对显式指定的自有测试工程执行真实操作;先用 native creator_smoke 建立桥接。 import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import fs from 'node:fs'; import net from 'node:net'; import path from 'node:path'; import { buildCocosOperationCode, COCOS_EDITOR_OPERATIONS, } from '../src/cocos-editor-operations.mjs'; const project = fs.realpathSync(process.argv[2] || ''); const pid = Number(process.argv[3]); assert.ok(Number.isInteger(pid) && pid > 0, '需要显式指定 Creator PID'); assert.equal( JSON.parse(fs.readFileSync(path.join(project, 'package.json'), 'utf8')).name, 'agc-cocos-capability-smoke', '只允许自有测试工程,不操作用户项目', ); const report = []; const reportPath = path.join(project, 'temp/agc-operation-smoke.json'); fs.mkdirSync(path.dirname(reportPath), { recursive: true }); async function raw(code) { const request = { schemaVersion: 'game-creator-cocos-editor-bridge.v1', requestId: randomUUID(), processId: pid, projectPath: project, command: { op: 'execute', code }, }; return await new Promise((resolve, reject) => { const socket = net.connect('\\\\.\\pipe\\genarrative-cocos-editor-' + pid); let output = ''; socket.setTimeout(60000); socket.on('connect', () => socket.write(JSON.stringify(request) + '\n')); socket.on('data', (data) => { output += data.toString(); if (!output.includes('\n')) return; socket.destroy(); const response = JSON.parse(output.split('\n')[0]); assert.equal(response.requestId, request.requestId); resolve(response); }); socket.on('error', reject); socket.on('timeout', () => { socket.destroy(); reject(new Error('结果不确定:禁止自动重放')); }); socket.on('end', () => { if (!output.includes('\n')) reject(new Error('回执不完整')); }); }); } async function op(name, args = {}) { const start = Date.now(); const response = await raw(buildCocosOperationCode(name, args)); const copy = structuredClone(response); if (copy.result?.result?.__image) copy.result.result.__image.data = '[PNG]'; report.push({ name, elapsedMs: Date.now() - start, response: copy }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); assert.equal(response.ok, true, response.error); assert.equal( response.result.status, 'completed', JSON.stringify(response.result), ); return response.result.result ?? response.result; } async function evalScene(source) { const result = await raw( "return await require('electron').webContents.getAllWebContents().find(w=>w.getURL().startsWith('packages://scene/')).executeJavaScript(" + JSON.stringify(source) + ');', ); assert.equal(result.ok, true, result.error); return result.result; } await op('cocos_ping'); await op('cocos_get_capabilities'); await op('cocos_get_project_info'); const meta = await op('cocos_get_current_scene_meta'); assert.ok(meta.named && !meta.dirty, '测试要求已保存且无未保存修改的 2D 场景'); const mainUuid = meta.sceneAssetUuid; const tree = await op('cocos_get_hierarchy'); const canvas = tree.tree.children.find((node) => node.name === 'Canvas'); assert.ok(canvas, '测试工程需要 Canvas'); const parent = canvas.uuid; const baseline = await evalScene( 'cce.SceneFacadeManager.getCurrentFacade()._sceneProxy.serialize(true)', ); const node = await op('cocos_create_node', { parentNid: parent, name: 'AGC_TestNode', }); await op('cocos_set_node_name', { nid: node.nid, name: 'AGC_Renamed' }); await op('cocos_set_node_transform', { nid: node.nid, position: { x: 12, y: 34, z: 0 }, rotationEuler: { x: 0, y: 0, z: 15 }, scale: { x: 2, y: 2, z: 1 }, }); await op('cocos_set_node_active', { nid: node.nid, active: false }); const component = await op('cocos_add_component', { nid: node.nid, componentType: 'Label', }); await op('cocos_set_component_property', { nid: node.nid, componentIndex: component.componentIndex, property: 'string', value: '测试', }); await op('cocos_remove_component', { nid: node.nid, componentIndex: component.componentIndex, expectedComponentType: 'Label', }); await op('cocos_mcp_undo_last'); const clone = await op('cocos_duplicate_node', { nid: node.nid }); await op('cocos_reparent_node', { childNid: clone.nid, parentNid: node.nid }); await op('cocos_set_node_sibling_index', { nid: node.nid, index: 0 }); await op('cocos_search_nodes', { nameSubstring: 'AGC_Renamed', componentFilter: 'Label', }); await op('cocos_inspect_node', { nid: node.nid }); await op('cocos_set_node_name', { nid: node.nid, name: 'AGC_BeforeEditorUndo', }); await op('cocos_editor_undo'); assert.equal( (await op('cocos_inspect_node', { nid: node.nid })).name, 'AGC_Renamed', ); await op('cocos_delete_node', { nid: node.nid, confirm: true, cascade: true }); await op('cocos_save_scene'); for (const [kind, args] of [ ['shape', { shape: 'circle', color: '#FF0000' }], ['label', { text: 'Cocos smoke', fontSize: 24 }], ['sprite', { assetPath: 'assets/agc-ui-shapes/circle.png', nineSlice: true }], ['button', { label: 'Start', color: '#0088FF' }], ]) { await op('cocos_create_ui_' + kind, { parentNid: parent, name: 'AGC_UI_' + kind, ...args, save: true, }); await op('cocos_mcp_undo_last'); } await op('cocos_apply_ui_spec', { parentNid: parent, save: true, nodes: [ { kind: 'container', layout: { type: 'horizontal', spacingX: 12 }, children: [ { kind: 'shape', shape: 'circle', color: '#FF0000' }, { kind: 'shape', shape: 'rectangle', color: '#0000FF' }, { kind: 'button', buttonLabel: 'Start' }, ], }, ], }); await op('cocos_mcp_undo_last'); assert.equal( await evalScene( 'cce.SceneFacadeManager.getCurrentFacade()._sceneProxy.serialize(true)', ), baseline, ); await op('cocos_get_log_tail', { maxLines: 10 }); await op('cocos_get_build_diagnostics', { maxFiles: 2, maxLinesPerFile: 20 }); await op('cocos_diagnose', { maxLogLines: 10 }); await op('cocos_list_assets', { extensions: ['png'], maxResults: 10 }); const seed = await op('cocos_create_node', { parentNid: parent, name: 'AGC_PrefabSeed', }); const data = await evalScene( '(()=>{const cc=require("cc"),p=new cc.Prefab();p.data=cc.instantiate(cce.Node.query(' + JSON.stringify(seed.uuid) + '));try{return cce.Utils.serialize(p);}finally{p.data.destroy();p.destroy();}})()', ); const prefabPath = 'assets/AGC-Smoke-' + Date.now() + '.prefab'; const asset = await raw( "return await Editor.Message.request('asset-db','create-asset'," + JSON.stringify('db://' + prefabPath) + ',' + JSON.stringify(data) + ');', ); assert.equal(asset.ok, true, asset.error); await op('cocos_get_prefab_info', { prefabPath }); const instance = await op('cocos_instantiate_prefab', { parentNid: parent, prefabPath, }); await op('cocos_delete_node', { nid: instance.nid, confirm: true, cascade: true, }); await op('cocos_delete_node', { nid: seed.nid, confirm: true, cascade: true }); await op('cocos_save_scene'); assert.equal( ( await raw( "return await Editor.Message.request('scene','load-empty-scene');", ) ).ok, true, ); await op('cocos_save_scene', { path: 'assets/AGC-Fresh-' + Date.now() + '.scene', }); assert.equal( ( await raw( "return await Editor.Message.request('scene','open-scene'," + JSON.stringify(mainUuid) + ');', ) ).ok, true, ); const urls = await raw( "return require('electron').webContents.getAllWebContents().filter(w=>w.getType()==='webview').map(w=>w.getURL()).filter(url=>url.startsWith('http://localhost:'));", ); const url = new URL(urls.result[0]).origin + '/'; await op('cocos_preview_debug_start', { url }); try { await new Promise((resolve) => setTimeout(resolve, 1500)); await op('cocos_preview_debug_read', { maxEvents: 30 }); const capture = await op('cocos_preview_debug_capture'); assert.equal(capture.__image.mimeType, 'image/png'); fs.writeFileSync( path.join(project, 'temp/agc-preview-smoke.png'), Buffer.from(capture.__image.data, 'base64'), ); } finally { await op('cocos_preview_debug_stop'); } const names = new Set(report.map((item) => item.name)); assert.deepEqual([...names].sort(), [...COCOS_EDITOR_OPERATIONS].sort()); console.log('36/36 Cocos operations passed; report: ' + reportPath);