test(studio): Modify.vue 单测(4) + 修单测逮到的 i18n 占位符 bug

- src/views/create/Modify.spec.ts:验三方式提交各构造契约合法请求——扩展→extend、改玩法→modify(regenerate-module,空目标默认首个 behavior)、调参→modify(deterministic,JSON 解析)、JSON 非法→Toast 不提交;记 vant mock 须 vi.hoisted(直接返回非嵌套→被提升)坑
- 修 i18n bug(单测逮到):modify.tweakValuePlaceholder `如 {"speed":5}` 的 `{}` 被 vue-i18n 当插值语法("Invalid token in placeholder")→占位符 mis-render;改为无花括号 `JSON,如 "speed": 5`(zh/en)

验证:`npm test` 16/16 绿(i18n 编译告警消除);`npm run build` 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-17 16:41:11 -07:00
parent 644b95a3c9
commit 7980c717be
3 changed files with 102 additions and 2 deletions

View File

@ -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',

View File

@ -82,7 +82,7 @@ export default {
tweakPathLabel: '路径',
tweakPathPlaceholder: '如 /config/speed',
tweakValueLabel: '新值(JSON)',
tweakValuePlaceholder: '如 {"speed": 5}',
tweakValuePlaceholder: 'JSON,如 "speed": 5',
valueInvalid: '新值不是合法 JSON,请检查',
emptyIntent: '请先填写描述',
emptyPath: '请填写要修改的路径',

View File

@ -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: '<button class="app-btn" :disabled="disabled" @click="$emit(\'click\')"><slot/></button>',
}
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()
})
})