import { readFileSync } from 'fs' import { describe, expect, it } from 'vitest' const readClientFile = (path: string) => readFileSync(`packages/client/src/${path}`, 'utf8') // 从 `computed(` 的左括号开始做括号配平,精确切出这个表达式的范围。 // 用空行切分不行:箭头函数写成单行后,紧邻的下一个顶层声明只隔一个空行。 function balancedBlock(source: string, header: string): string { const start = source.indexOf(header) expect(start, `${header} not found`).toBeGreaterThan(-1) let depth = 0 for (let i = start + header.length - 1; i < source.length; i += 1) { if (source[i] === '(') depth += 1 if (source[i] === ')') { depth -= 1 if (depth === 0) return source.slice(start, i + 1) } } return source.slice(start) } // known-issues #1: `providerOptions` 的 computed getter 内部给 customProvider // 赋值。用户手动挑好的 provider 会被下一次 getter 重算无声改回去,表现为 // 「自己跳」。修法是把同步移进 watch,让 getter 恢复成纯读。 describe('ModelSelector provider selection', () => { it('keeps the providerOptions getter free of side effects', () => { const source = readClientFile('components/layout/ModelSelector.vue') const block = balancedBlock(source, 'const providerOptions = computed(') expect(block).not.toContain('customProvider.value =') }) it('syncs customProvider from the store through a watch', () => { const source = readClientFile('components/layout/ModelSelector.vue') const block = balancedBlock(source, 'watch(') // 同步点必须挂在 selectedProvider 上,而不是挂在 providerOptions 上 —— // 后者等于把同一个副作用换个地方写一遍。 expect(block.slice(0, 200)).toContain('appStore.selectedProvider') expect(block).toContain('customProvider.value =') }) it('still initialises the provider when the picker opens', () => { // watch 是懒的:不会在初始渲染时触发。openModal 的这一次赋值是首帧 // 唯一来源,不能跟着一起去掉。 const source = readClientFile('components/layout/ModelSelector.vue') expect(source).toMatch(/function openModal\(\)[\s\S]*?customProvider\.value = appStore\.selectedProvider/) }) })