import test from 'node:test'; import assert from 'node:assert/strict'; import {createRequire} from 'node:module'; import {mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync} from 'node:fs'; import {tmpdir} from 'node:os'; import {join, resolve} from 'node:path'; const require = createRequire(import.meta.url); const corePath = '../../YouleNexus/extensions/youle-version-manifest/core.js'; const uuid = 'ec19e470-cf36-40e8-b420-e4314e5fbf07'; const type = 'ec19eRwzzZA6LQg5DFOX78H'; const source = readFileSync(new URL('../../YouleNexus/assets/games/erqiwang/resources/erqiwang/version.xml', import.meta.url)); function fixture(t, key = 'erqiwang') { const dir = mkdtempSync(join(tmpdir(), 'youle-version-build-')); t.after(() => rmSync(dir, {recursive: true, force: true})); const write = (p, data) => {mkdirSync(resolve(p, '..'), {recursive: true}); writeFileSync(p, data);}; write(join(dir, 'assets/app/composition/SubgameAssets.ts.meta'), JSON.stringify({uuid})); const scenePath = join(dir, 'assets/scenes/Chosen.scene'); const scene = [{__type__: 'cc.Scene', _active: true}, {__type__: 'cc.Node', _active: true, _parent: {__id__: 0}}, {__type__: type, _enabled: true, node: {__id__: 1}, gameKey: key}]; write(scenePath, JSON.stringify(scene)); const xmlPath = join(dir, 'assets/games/anything/resources', key, 'version.xml'); write(xmlPath, source); const bundleMetaPath = join(dir, 'assets/games/anything/resources.meta'); write(bundleMetaPath, JSON.stringify({userData: {isBundle: true, bundleName: 'version-' + key}})); const output = join(dir, 'custom-output/web'); write(join(output, 'index.html'), ''); const options = {startScene: 'scene-id', platform: 'web-desktop'}; const result = {paths: {output}, settings: {launch: {launchScene: 'db://assets/scenes/Chosen.scene'}, assets: {projectBundles: ['main', 'version-' + key]}}}; return {dir, write, scenePath, scene, xmlPath, bundleMetaPath, output, options, result, input: {projectPath: dir, scenePath, startScene: options.startScene}}; } test('publishes the selected scene manifest byte-for-byte beside the actual index.html', t => { const {createSnapshot, publishSnapshot} = require(corePath); const f = fixture(t); const snapshot = createSnapshot(f.input); publishSnapshot(snapshot, f.options, f.result); assert.deepEqual(readFileSync(join(f.output, 'version.xml')), source); assert.equal(snapshot.gameKey, 'erqiwang'); }); test('selects another game from its scene without a hardcoded game registry', t => { const {createSnapshot, publishSnapshot} = require(corePath); const f = fixture(t, 'Game_Surface_3'); const template = readFileSync(new URL('../../YouleNexus/assets/games/template/resources/Game_Surface_3/version.xml', import.meta.url)); f.write(f.xmlPath, template); const snapshot = createSnapshot(f.input); publishSnapshot(snapshot, f.options, f.result); assert.deepEqual(readFileSync(join(f.output, 'version.xml')), template); }); for (const problem of ['missing', 'invalid', 'duplicate', 'unselected', 'disabled', 'inactive', 'ambiguous', 'missing bundle metadata', 'disabled bundle', 'mismatched bundle name']) { test(`rejects ${problem} source or selection before build`, t => { const {createSnapshot} = require(corePath); const f = fixture(t); if (problem === 'missing') rmSync(f.xmlPath); if (problem === 'invalid') f.write(f.xmlPath, ''); if (problem === 'duplicate') f.write(join(f.dir, 'assets/games/second/resources/erqiwang/version.xml'), source); if (problem === 'unselected') delete f.scene[2].gameKey; if (problem === 'disabled') f.scene[2]._enabled = false; if (problem === 'inactive') f.scene[1]._active = false; if (problem === 'ambiguous') f.scene.push({...f.scene[2]}); if (problem === 'missing bundle metadata') rmSync(f.bundleMetaPath); if (problem === 'disabled bundle') f.write(f.bundleMetaPath, JSON.stringify({userData: {isBundle: false, bundleName: 'version-erqiwang'}})); if (problem === 'mismatched bundle name') f.write(f.bundleMetaPath, JSON.stringify({userData: {isBundle: true, bundleName: 'version-other'}})); f.write(f.scenePath, JSON.stringify(f.scene)); assert.throws(() => createSnapshot(f.input)); }); } for (const problem of ['xml changed', 'scene changed', 'start scene changed', 'launch scene changed', 'missing index', 'bundle metadata changed', 'excluded version bundle', 'missing packaged bundle list']) { test(`rejects ${problem} after build`, t => { const {createSnapshot, publishSnapshot} = require(corePath); const f = fixture(t); const snapshot = createSnapshot(f.input); if (problem === 'xml changed') f.write(f.xmlPath, Buffer.concat([source, Buffer.from('\n')])); if (problem === 'scene changed') f.write(f.scenePath, '[]'); if (problem === 'start scene changed') f.options.startScene = 'other'; if (problem === 'launch scene changed') f.result.settings.launch.launchScene = 'db://assets/scenes/Other.scene'; if (problem === 'missing index') rmSync(join(f.output, 'index.html')); if (problem === 'bundle metadata changed') f.write(f.bundleMetaPath, JSON.stringify({userData: {isBundle: true, bundleName: 'version-other'}})); if (problem === 'excluded version bundle') f.result.settings.assets.projectBundles = ['main', 'version-other']; if (problem === 'missing packaged bundle list') delete f.result.settings.assets.projectBundles; assert.throws(() => publishSnapshot(snapshot, f.options, f.result)); }); } test('Creator hooks use the task startScene and publish through the actual output path', async t => { const f = fixture(t); const previous = globalThis.Editor; t.after(() => {globalThis.Editor = previous;}); const calls = []; globalThis.Editor = { Project: {path: f.dir}, Message: {request: async (...args) => {calls.push(args); return f.scenePath;}}, }; const hooks = require('../../YouleNexus/extensions/youle-version-manifest/hooks.js'); assert.equal(hooks.throwError, true); await hooks.onBeforeBuild(f.options); await hooks.onAfterBuild(f.options, f.result); assert.deepEqual(calls, [['asset-db', 'query-path', 'scene-id']]); assert.deepEqual(readFileSync(join(f.output, 'version.xml')), source); await assert.rejects(hooks.onAfterBuild(f.options, f.result), /Missing pre-build/); }); test('only Web Desktop and Web Mobile register the required manifest hooks', () => { const {configs} = require('../../YouleNexus/extensions/youle-version-manifest/builder.js'); assert.deepEqual(Object.keys(configs).sort(), ['web-desktop', 'web-mobile']); assert.equal(configs['web-desktop'].hooks, './hooks'); assert.equal(configs['web-mobile'].hooks, './hooks'); });