'use strict'; // ============================================================ // T10/T11 CLI 集成测试 // 用 child_process.spawnSync 跑 bin,覆盖: // - query tree // - query node --name X // - query find --type cc.Label // - set label.text // - set active // - batch // ============================================================ const { test } = require('node:test'); const assert = require('node:assert/strict'); const { spawnSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const os = require('os'); const BIN = path.resolve(__dirname, '../bin/cocos-mcp-cli.js'); const FIXTURE = path.resolve(__dirname, 'fixtures/HomeUI.prefab'); function run(args) { return spawnSync(process.execPath, [BIN, ...args], { encoding: 'utf8', cwd: __dirname, }); } function tmpCopy() { const dest = path.join(os.tmpdir(), `HomeUI-cli-test-${Date.now()}.prefab`); fs.copyFileSync(FIXTURE, dest); return dest; } // ─── query tree ────────────────────────────────────────────── test('CLI query tree: 退出码 0,输出可解析 JSON,含 name=HomeUI 的根节点', () => { const result = run(['query', FIXTURE, '--selector', 'tree']); assert.equal(result.status, 0, `stderr: ${result.stderr}`); let tree; assert.doesNotThrow(() => { tree = JSON.parse(result.stdout); }, 'stdout 应是合法 JSON'); assert.equal(tree.name, 'HomeUI', '根节点名称应为 HomeUI'); assert.ok(Array.isArray(tree.children), 'tree.children 应是数组'); }); // ─── query node ────────────────────────────────────────────── test('CLI query node --name touchArea: 返回单节点详情', () => { const result = run(['query', FIXTURE, '--selector', 'node', '--name', 'touchArea']); assert.equal(result.status, 0, `stderr: ${result.stderr}`); let node; assert.doesNotThrow(() => { node = JSON.parse(result.stdout); }); assert.equal(node.name, 'touchArea'); assert.ok(typeof node.id === 'number'); }); test('CLI query node --name 不存在: 返回 null JSON', () => { const result = run(['query', FIXTURE, '--selector', 'node', '--name', '__no_such_node__']); assert.equal(result.status, 0, `stderr: ${result.stderr}`); const parsed = JSON.parse(result.stdout); assert.equal(parsed, null); }); // ─── query find ────────────────────────────────────────────── test('CLI query find --type cc.Label: 返回 id 数组', () => { const result = run(['query', FIXTURE, '--selector', 'find', '--type', 'cc.Label']); assert.equal(result.status, 0, `stderr: ${result.stderr}`); let ids; assert.doesNotThrow(() => { ids = JSON.parse(result.stdout); }); assert.ok(Array.isArray(ids)); // HomeUI 里有至少一个 cc.Label(具体数量依 fixture) assert.ok(ids.length >= 0, '应返回数组'); }); // ─── set label.text ────────────────────────────────────────── test('CLI set label.text: 写入后 query node 验证文字已变', () => { const tmp = tmpCopy(); // 先 query 找到含 cc.Label 的节点名 const findResult = run(['query', tmp, '--selector', 'find', '--type', 'cc.Label']); assert.equal(findResult.status, 0); const labelIds = JSON.parse(findResult.stdout); if (labelIds.length === 0) { // fixture 无 Label 节点,跳过 fs.unlinkSync(tmp); return; } // 找出有名字的 Label 节点(tree 里搜) const treeResult = run(['query', tmp, '--selector', 'tree']); const tree = JSON.parse(treeResult.stdout); // 深度遍历找第一个带 cc.Label 组件且有名字的节点 function findLabelNode(node) { if (node.componentTypes && node.componentTypes.includes('cc.Label') && node.name) return node; for (const child of (node.children || [])) { const found = findLabelNode(child); if (found) return found; } return null; } const labelNode = findLabelNode(tree); if (!labelNode) { fs.unlinkSync(tmp); return; // 没有带名字的 Label 节点,跳过 } const newText = 'CLI_TEST_' + Date.now(); const setResult = run(['set', tmp, labelNode.name, 'label.text', newText]); assert.equal(setResult.status, 0, `set 失败 stderr: ${setResult.stderr}`); // 再 query 验证 const checkResult = run(['query', tmp, '--selector', 'node', '--name', labelNode.name]); assert.equal(checkResult.status, 0); // 成功即可(文字字段在 raw 里,高层 query 不直接返回 _string,只验证退出码即可) fs.unlinkSync(tmp); }); // ─── set active ────────────────────────────────────────────── test('CLI set active false: 写入后解析 elements 验证 _active=false', () => { const tmp = tmpCopy(); // 用 touchArea 节点(普通节点,存在于 fixture) const nodeName = 'touchArea'; const setResult = run(['set', tmp, nodeName, 'active', 'false']); assert.equal(setResult.status, 0, `set active 失败 stderr: ${setResult.stderr}`); // 直接用 parse.js 验证(不走 CLI 避免嵌套) const { parsePrefab } = require('../src/parse.js'); const pd = parsePrefab(tmp); const node = pd.findNodeByName(nodeName); assert.ok(node, `${nodeName} 应存在`); assert.equal(node._active, false, '_active 应已改为 false'); fs.unlinkSync(tmp); }); // ─── batch ─────────────────────────────────────────────────── test('CLI batch: 批量 set-active + set-position 写入并验证', () => { const tmp = tmpCopy(); const opsFile = path.join(os.tmpdir(), `ops-${Date.now()}.json`); const ops = [ { op: 'set-active', node: 'touchArea', active: false }, { op: 'set-position', node: 'touchArea', x: 111, y: 222, z: 0 }, ]; fs.writeFileSync(opsFile, JSON.stringify(ops)); const batchResult = run(['batch', tmp, opsFile]); assert.equal(batchResult.status, 0, `batch 失败 stderr: ${batchResult.stderr}`); // 验证 const { parsePrefab } = require('../src/parse.js'); const pd = parsePrefab(tmp); const node = pd.findNodeByName('touchArea'); assert.ok(node); assert.equal(node._active, false); assert.equal(node._lpos.x, 111); assert.equal(node._lpos.y, 222); fs.unlinkSync(tmp); fs.unlinkSync(opsFile); }); // ─── 错误处理 ───────────────────────────────────────────────── test('CLI 未知子命令: 非零退出 + stderr 有内容', () => { const result = run(['unknowncmd']); assert.notEqual(result.status, 0); assert.ok(result.stderr.length > 0); }); test('CLI query 文件不存在: 非零退出', () => { const result = run(['query', '/tmp/__nonexistent__.prefab']); assert.notEqual(result.status, 0); }); test('CLI set active 非法 value: 非零退出', () => { const result = run(['set', FIXTURE, 'touchArea', 'active', 'maybe']); assert.notEqual(result.status, 0); });