diff --git a/game-studio/src/locales/taskflow.en.ts b/game-studio/src/locales/taskflow.en.ts index 6ccde46e..b43d891f 100644 --- a/game-studio/src/locales/taskflow.en.ts +++ b/game-studio/src/locales/taskflow.en.ts @@ -70,7 +70,7 @@ export default { tweakPathLabel: 'Path', tweakPathPlaceholder: 'e.g. /config/speed', tweakValueLabel: 'New value (JSON)', - tweakValuePlaceholder: 'e.g. {"speed": 5}', + tweakValuePlaceholder: 'JSON, e.g. "speed": 5', valueInvalid: 'New value is not valid JSON', emptyIntent: 'Please enter a description first', emptyPath: 'Please enter the path to modify', diff --git a/game-studio/src/locales/taskflow.zh.ts b/game-studio/src/locales/taskflow.zh.ts index f5b920ce..160be00f 100644 --- a/game-studio/src/locales/taskflow.zh.ts +++ b/game-studio/src/locales/taskflow.zh.ts @@ -82,7 +82,7 @@ export default { tweakPathLabel: '路径', tweakPathPlaceholder: '如 /config/speed', tweakValueLabel: '新值(JSON)', - tweakValuePlaceholder: '如 {"speed": 5}', + tweakValuePlaceholder: 'JSON,如 "speed": 5', valueInvalid: '新值不是合法 JSON,请检查', emptyIntent: '请先填写描述', emptyPath: '请填写要修改的路径', diff --git a/game-studio/src/views/create/Modify.spec.ts b/game-studio/src/views/create/Modify.spec.ts new file mode 100644 index 00000000..91663d38 --- /dev/null +++ b/game-studio/src/views/create/Modify.spec.ts @@ -0,0 +1,100 @@ +/** + * Modify.spec.ts —— 改进入口组件单测(plan 002 U6)。 + * 验三方式提交各构造【契约合法】请求:扩展→extend / 改玩法→modify(regenerate-module) / 调参→modify(deterministic); + * 调参 JSON 非法 → Toast 不提交。经 mock store/router/vant 隔离。跑 `npm test`。 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' +import { createI18n } from 'vue-i18n' + +// router push 间谍 +const push = vi.fn() +const back = vi.fn() +vi.mock('vue-router', () => ({ useRouter: () => ({ push, back }) })) + +// vant showToast 间谍(JSON 非法路径校验)。 +// 注:vant mock 工厂【直接返回】showToast(非嵌套在函数内),会被 vi.mock 提升到顶→须用 vi.hoisted 先建间谍, +// 否则「Cannot access 'showToast' before initialization」(router/store 的 spy 嵌在函数内惰性引用故无此问题)。 +const { showToast } = vi.hoisted(() => ({ showToast: vi.fn() })) +vi.mock('vant', () => ({ showToast })) + +// store 间谍(直接断言 Modify 构造的请求对象) +const modify = vi.fn().mockResolvedValue({ id: 1002, gameId: 1 }) +const extend = vi.fn().mockResolvedValue({ id: 1003, gameId: 1 }) +vi.mock('../../store/create', () => ({ useCreateStore: () => ({ modify, extend }) })) + +import Modify from './Modify.vue' + +const i18n = createI18n({ legacy: false, locale: 'zh-CN', messages: { 'zh-CN': {} }, missingWarn: false, fallbackWarn: false }) +const AppButtonStub = { + props: ['disabled', 'loading', 'block', 'size', 'type'], + emits: ['click'], + template: '', +} + +function mountModify() { + return mount(Modify, { + props: { versionId: '9001' }, + global: { plugins: [i18n], stubs: { Icon: true, AppButton: AppButtonStub } }, + }) +} + +describe('Modify.vue(改进入口·三方式→契约请求)', () => { + beforeEach(() => { + vi.clearAllMocks() + modify.mockResolvedValue({ id: 1002, gameId: 1 }) + extend.mockResolvedValue({ id: 1003, gameId: 1 }) + }) + + it('扩展(默认 tab) → extend({baseVersionId, intent}) + 跳进度页', async () => { + const w = mountModify() + await w.find('.modify-body textarea').setValue('再加一个 boss 关卡') + await w.find('.modify-foot .app-btn').trigger('click') + await flushPromises() + expect(extend).toHaveBeenCalledWith({ baseVersionId: 9001, intent: '再加一个 boss 关卡' }) + expect(modify).not.toHaveBeenCalled() + expect(push).toHaveBeenCalledOnce() + expect(push.mock.calls[0][0].params.taskId).toBe('1003') + }) + + it('改玩法 tab → modify(regenerate-module, kind=behavior, payload.intent);目标空走默认首个 behavior 路径', async () => { + const w = mountModify() + await w.findAll('.modify-body .seg-item')[1].trigger('click') // 改玩法 + await w.find('.modify-body textarea').setValue('把陨石改成会左右摇摆') + await w.find('.modify-foot .app-btn').trigger('click') + await flushPromises() + expect(modify).toHaveBeenCalledOnce() + const arg = modify.mock.calls[0][0] + expect(arg.mode).toBe('regenerate-module') + expect(arg.target.kind).toBe('behavior') + expect(arg.target.path).toBe('/gameDefinition/behaviors/0') // 空目标 → 默认首个 + expect(arg.payload.intent).toBe('把陨石改成会左右摇摆') + }) + + it('调参 tab → modify(deterministic, kind/path, payload.value 解析 JSON)', async () => { + const w = mountModify() + await w.findAll('.modify-body .seg-item')[2].trigger('click') // 调参(默认 kind=config) + const inputs = w.findAll('.modify-body input') + await inputs[0].setValue('/config/speed') // path + await w.find('.modify-body textarea').setValue('{"speed": 5}') + await w.find('.modify-foot .app-btn').trigger('click') + await flushPromises() + expect(modify).toHaveBeenCalledOnce() + const arg = modify.mock.calls[0][0] + expect(arg.mode).toBe('deterministic') + expect(arg.target).toEqual({ kind: 'config', path: '/config/speed' }) + expect(arg.payload.value).toEqual({ speed: 5 }) + }) + + it('调参 JSON 非法 → showToast 且不提交', async () => { + const w = mountModify() + await w.findAll('.modify-body .seg-item')[2].trigger('click') + await w.findAll('.modify-body input')[0].setValue('/config/speed') + await w.find('.modify-body textarea').setValue('{速度: 5}') // 非法 JSON + await w.find('.modify-foot .app-btn').trigger('click') + await flushPromises() + expect(showToast).toHaveBeenCalledOnce() + expect(modify).not.toHaveBeenCalled() + expect(push).not.toHaveBeenCalled() + }) +})