/** * OmniCard AI (CRMmp) - 名片主极速编辑测试套件 (TDD Red -> Green) * * 严格按照 AGENTS.md 3.3、4.3、5.1 规范与 pages/owner/card/edit 设计编写 */ import * as fs from 'fs'; import * as path from 'path'; const EDIT_PAGE = path.resolve( __dirname, '../../../mpuniapp/pages/owner/card/edit.uvue' ); const EDIT_UTS = path.resolve( __dirname, '../../../mpuniapp/pages/owner/card/useCardEdit.uts' ); type BrandKitFixture = { logoUrl?: string; primaryColor?: string; lockedFields?: string[] | null; whitelist?: string[] | null; }; type CardProfile = { id?: string | number; name: string; title?: string; company?: string; slogan?: string; avatarUrl?: string; phone?: string; email?: string; whatsapp?: string; linkedin?: string; telegram?: string; twitter?: string; wechat?: string; timezone?: string; lockedFields?: string[] | null; brandKit?: BrandKitFixture | null; }; type CardEditForm = { id?: string | number | null; name?: string; title?: string; company?: string; slogan?: string; avatarUrl?: string; phone?: string; email?: string; whatsapp?: string; linkedin?: string; telegram?: string; twitter?: string; wechat?: string; timezone?: string; lockedFields?: string[] | null; }; type CardUpdatePayload = { id: string | number; name?: string; title?: string; company?: string; slogan?: string; avatarUrl?: string; phone?: string; email?: string; whatsapp?: string; linkedin?: string; telegram?: string; twitter?: string; wechat?: string; timezone?: string; }; type LoadCardApi = { fetchOwnerCardInfo?: (id: string | number) => Promise; }; type LoadCardResult = { card: CardProfile | null; empty: boolean; errorMessage: string; }; type SaveCardApi = { updateOwnerCard?: ( payload: CardProfile & { id: string | number } ) => Promise; }; type SaveCardResult = { ok: boolean; code?: number; card: CardProfile | null; errorMessage: string; }; type CardEditState = { rawId: string; cardId: number | null; name: string; title: string; company: string; slogan: string; avatarUrl: string; phone: string; email: string; whatsapp: string; linkedin: string; timezone: string; moduleOrder: string[]; lockedFields: string[]; loading: boolean; saving: boolean; empty: boolean; missingId: boolean; errorMessage: string; }; type UseCardEditModule = { parseCardId: (raw: string | number | null | undefined) => number | null; buildUpdatePayload: ( form: CardEditForm, lockedFields?: string[] | null ) => CardUpdatePayload | null; extractLockedFields: ( source: CardProfile | BrandKitFixture | null | undefined ) => string[]; isFieldLocked: ( lockedFields: string[] | null | undefined, field: string ) => boolean; resolveCardEditText: (key: string) => string; createCardEditState: () => CardEditState; applyLoadResult: (state: CardEditState, result: LoadCardResult) => void; formFromState: (state: CardEditState) => CardEditForm; DEFAULT_MODULE_ORDER: string[]; normalizeModuleOrder: (raw: unknown) => string[]; moveModuleInOrder: ( order: string[], index: number, delta: number ) => string[]; loadCard: ( id: string | number | null | undefined, api?: LoadCardApi ) => Promise; saveCard: ( form: CardEditForm, api?: SaveCardApi, lockedFields?: string[] | null ) => Promise; }; const EMOJI_RE = /[\u2600-\u27BF\uFE0F]|[\uD83C-\uDBFF][\uDC00-\uDFFF]/; function buildCard(overrides?: Partial): CardProfile { return { id: 8, name: 'Alex Vance', title: 'Partner', company: 'Nexus Tech', slogan: 'Build trust first', phone: '+971500000008', email: 'alex@nexus.example', whatsapp: '+971500000008', linkedin: 'https://linkedin.com/in/alexvance', ...overrides, }; } function loadUseCardEdit(): UseCardEditModule { // eslint-disable-next-line @typescript-eslint/no-require-imports return require(EDIT_UTS) as UseCardEditModule; } describe('OmniCard AI 名片主极速编辑业务逻辑 (useCardEdit.uts)', () => { beforeEach(() => { jest.resetModules(); (global as { uni?: Record }).uni = { getLocale: () => 'zh-CN', showToast: jest.fn(), navigateBack: jest.fn(), navigateTo: jest.fn(), }; }); afterEach(() => { delete (global as { uni?: Record }).uni; }); describe('1. parseCardId', () => { it('1.1 正整数与数字字符串解析为 id,非法值丢弃', () => { const { parseCardId } = loadUseCardEdit(); expect(parseCardId(8)).toBe(8); expect(parseCardId('8')).toBe(8); expect(parseCardId(' 8 ')).toBe(8); expect(parseCardId('')).toBeNull(); expect(parseCardId('0')).toBeNull(); expect(parseCardId(-3)).toBeNull(); expect(parseCardId('abc')).toBeNull(); expect(parseCardId(null)).toBeNull(); expect(parseCardId(undefined)).toBeNull(); }); }); describe('2. loadCard 调 fetchOwnerCardInfo', () => { it('2.1 注入后按解析后的 id 拉取名片', async () => { const { loadCard } = loadUseCardEdit(); const card = buildCard(); const fetchOwnerCardInfo = jest.fn(async (id: string | number) => { expect(id).toBe(8); return card; }); const result = await loadCard(' 8 ', { fetchOwnerCardInfo }); expect(fetchOwnerCardInfo).toHaveBeenCalledTimes(1); expect(result.card).not.toBeNull(); expect(result.card?.name).toBe('Alex Vance'); expect(result.card?.company).toBe('Nexus Tech'); expect(result.empty).toBe(false); expect(result.errorMessage).toBe(''); }); it('2.2 缺 id 不请求接口,标记空态', async () => { const { loadCard } = loadUseCardEdit(); const fetchOwnerCardInfo = jest.fn(async () => buildCard()); const result = await loadCard('', { fetchOwnerCardInfo }); expect(fetchOwnerCardInfo).not.toHaveBeenCalled(); expect(result.card).toBeNull(); expect(result.empty).toBe(true); expect(result.errorMessage).toBe(''); }); it('2.3 接口失败写入错误,不伪装空态成功', async () => { const { loadCard } = loadUseCardEdit(); const fetchOwnerCardInfo = jest.fn(async () => { throw new Error('card info unavailable'); }); const result = await loadCard(8, { fetchOwnerCardInfo }); expect(fetchOwnerCardInfo).toHaveBeenCalledTimes(1); expect(result.card).toBeNull(); expect(result.empty).toBe(true); expect(result.errorMessage).toBe('card info unavailable'); }); }); describe('3. saveCard 调 updateOwnerCard', () => { it('3.1 buildUpdatePayload 只带已填字段与 id', () => { const { buildUpdatePayload } = loadUseCardEdit(); const payload = buildUpdatePayload({ id: '8', name: 'Alex Vance', title: ' ', company: 'Nexus Tech', phone: '', email: undefined, whatsapp: '', linkedin: 'https://linkedin.com/in/alexvance', slogan: '', }); expect(payload).not.toBeNull(); expect(payload?.id).toBe(8); expect(payload?.name).toBe('Alex Vance'); expect(payload?.company).toBe('Nexus Tech'); expect(payload?.linkedin).toBe('https://linkedin.com/in/alexvance'); expect(payload?.title).toBeUndefined(); expect(payload?.phone).toBeUndefined(); expect(payload?.email).toBeUndefined(); expect(payload?.whatsapp).toBeUndefined(); expect(payload?.slogan).toBeUndefined(); expect(Object.keys(payload as CardUpdatePayload).sort()).toEqual([ 'company', 'id', 'linkedin', 'name', ]); }); it('3.2 saveCard 把精简 payload 交给 updateOwnerCard', async () => { const { saveCard } = loadUseCardEdit(); const saved = buildCard({ title: undefined, phone: undefined }); const updateOwnerCard = jest.fn( async (payload: CardProfile & { id: string | number }) => { expect(payload.id).toBe(8); expect(payload.name).toBe('Alex Vance'); expect(payload.company).toBe('Nexus Tech'); expect(payload.title).toBeUndefined(); expect(payload.phone).toBeUndefined(); return saved; } ); const result = await saveCard( { id: 8, name: 'Alex Vance', title: '', company: 'Nexus Tech', phone: ' ', }, { updateOwnerCard } ); expect(updateOwnerCard).toHaveBeenCalledTimes(1); expect(result.ok).toBe(true); expect(result.card?.name).toBe('Alex Vance'); expect(result.errorMessage).toBe(''); }); it('3.3 缺 id 不请求 updateOwnerCard', async () => { const { saveCard } = loadUseCardEdit(); const updateOwnerCard = jest.fn(async () => buildCard()); const result = await saveCard( { id: '', name: 'Alex Vance', company: 'Nexus Tech' }, { updateOwnerCard } ); expect(updateOwnerCard).not.toHaveBeenCalled(); expect(result.ok).toBe(false); expect(result.card).toBeNull(); }); }); describe('4. 品牌套件 lockedFields 锁定', () => { it('4.1 无 lockedFields / 空数组 / 仅有 brandKit 资产时不锁任何字段', () => { const { extractLockedFields, isFieldLocked, buildUpdatePayload } = loadUseCardEdit(); expect(extractLockedFields(null)).toEqual([]); expect(extractLockedFields(undefined)).toEqual([]); expect(extractLockedFields(buildCard())).toEqual([]); expect( extractLockedFields( buildCard({ brandKit: { logoUrl: 'https://cdn.example/logo.png', primaryColor: '#0088FF' }, }) ) ).toEqual([]); expect( extractLockedFields(buildCard({ lockedFields: [] })) ).toEqual([]); expect( extractLockedFields(buildCard({ brandKit: { lockedFields: null } })) ).toEqual([]); expect(isFieldLocked([], 'company')).toBe(false); expect(isFieldLocked(null, 'company')).toBe(false); expect(isFieldLocked(undefined, 'name')).toBe(false); const payload = buildUpdatePayload( { id: 8, name: 'Alex Vance', company: 'Edited Co', avatarUrl: 'https://cdn.example/a.png', }, [] ); expect(payload?.company).toBe('Edited Co'); expect(payload?.name).toBe('Alex Vance'); expect(payload?.avatarUrl).toBe('https://cdn.example/a.png'); }); it('4.2 读取 brandKit.lockedFields 与顶层 lockedFields,别名映射生效', () => { const { extractLockedFields, isFieldLocked } = loadUseCardEdit(); expect( extractLockedFields( buildCard({ brandKit: { logoUrl: 'https://cdn.example/logo.png', primaryColor: '#0088FF', lockedFields: ['companyName', 'logoUrl', 'primaryColor'], }, }) ) ).toEqual(['companyName', 'logoUrl', 'primaryColor']); expect( extractLockedFields( buildCard({ lockedFields: ['company', 'logo'] }) ) ).toEqual(['company', 'logo']); const locked = ['companyName', 'logoUrl', 'primaryColor']; expect(isFieldLocked(locked, 'company')).toBe(true); expect(isFieldLocked(locked, 'companyName')).toBe(true); expect(isFieldLocked(locked, 'logoUrl')).toBe(true); expect(isFieldLocked(locked, 'logo')).toBe(true); expect(isFieldLocked(locked, 'primaryColor')).toBe(true); expect(isFieldLocked(locked, 'name')).toBe(false); expect(isFieldLocked(locked, 'phone')).toBe(false); }); it('4.3 whitelist 存在时,未列入白名单的品牌字段视为锁定', () => { const { extractLockedFields, isFieldLocked } = loadUseCardEdit(); const locked = extractLockedFields( buildCard({ brandKit: { logoUrl: 'https://cdn.example/logo.png', primaryColor: '#0088FF', whitelist: ['name', 'phone', 'email', 'avatarUrl'], }, }) ); expect(locked).toContain('company'); expect(locked).toContain('logoUrl'); expect(locked).toContain('primaryColor'); expect(isFieldLocked(locked, 'company')).toBe(true); expect(isFieldLocked(locked, 'name')).toBe(false); expect(isFieldLocked(locked, 'phone')).toBe(false); }); it('4.4 buildUpdatePayload / saveCard 不提交锁定字段的新值', async () => { const { buildUpdatePayload, saveCard } = loadUseCardEdit(); const locked = ['companyName', 'logoUrl', 'primaryColor']; const payload = buildUpdatePayload( { id: 8, name: 'Alex Vance', company: 'Hacked Corp', avatarUrl: 'https://cdn.example/me.png', phone: '+971500000008', }, locked ); expect(payload).not.toBeNull(); expect(payload?.name).toBe('Alex Vance'); expect(payload?.phone).toBe('+971500000008'); // 品牌 Logo 锁定不影响个人头像提交 expect(payload?.avatarUrl).toBe('https://cdn.example/me.png'); expect(payload?.company).toBeUndefined(); const updateOwnerCard = jest.fn( async (body: CardProfile & { id: string | number }) => { expect(body.company).toBeUndefined(); expect(body.avatarUrl).toBe('https://cdn.example/me.png'); expect(body.name).toBe('Alex Vance'); return buildCard({ name: body.name }); } ); const result = await saveCard( { id: 8, name: 'Alex Vance', company: 'Hacked Corp', avatarUrl: 'https://cdn.example/me.png', }, { updateOwnerCard }, locked ); expect(updateOwnerCard).toHaveBeenCalledTimes(1); expect(result.ok).toBe(true); }); it('4.5 applyLoadResult 写入 lockedFields;resolve 提供锁定提示文案', () => { const { createCardEditState, applyLoadResult, formFromState, resolveCardEditText, isFieldLocked, } = loadUseCardEdit(); const state = createCardEditState(); applyLoadResult(state, { card: buildCard({ brandKit: { lockedFields: ['companyName', 'logoUrl'], }, }), empty: false, errorMessage: '', }); expect(state.lockedFields).toEqual(['companyName', 'logoUrl']); expect(isFieldLocked(state.lockedFields, 'company')).toBe(true); expect(formFromState(state).lockedFields).toEqual([ 'companyName', 'logoUrl', ]); expect(resolveCardEditText('lockedHint')).toBe('品牌规范已锁定,不可修改'); }); it('4.6 resolveCardEditText(lockedHint) 英文自然文案', () => { (global as { uni?: Record }).uni = { getLocale: () => 'en-US', showToast: jest.fn(), navigateBack: jest.fn(), navigateTo: jest.fn(), }; jest.resetModules(); const { resolveCardEditText } = loadUseCardEdit(); expect(resolveCardEditText('lockedHint')).toBe( 'Locked by brand kit' ); }); }); describe('5. slogan 口号字段', () => { it('5.1 applyLoadResult / formFromState 读写 slogan,空口号不进 payload', () => { const { createCardEditState, applyLoadResult, formFromState, buildUpdatePayload, resolveCardEditText, } = loadUseCardEdit(); const state = createCardEditState(); expect(state.slogan).toBe(''); applyLoadResult(state, { card: buildCard({ slogan: 'Build trust first' }), empty: false, errorMessage: '', }); expect(state.slogan).toBe('Build trust first'); expect(formFromState(state).slogan).toBe('Build trust first'); const filled = buildUpdatePayload({ id: 8, name: 'Alex Vance', slogan: ' Build trust first ', }); expect(filled?.slogan).toBe('Build trust first'); const emptySlogan = buildUpdatePayload({ id: 8, name: 'Alex Vance', slogan: ' ', }); expect(emptySlogan?.slogan).toBeUndefined(); expect(Object.keys(emptySlogan as CardUpdatePayload)).not.toContain( 'slogan' ); expect(resolveCardEditText('slogan')).toBe('口号'); }); it('5.2 saveCard 提交已填 slogan,不写入假文案', async () => { const { saveCard } = loadUseCardEdit(); const updateOwnerCard = jest.fn( async (payload: CardProfile & { id: string | number }) => { expect(payload.slogan).toBe('Trusted globally'); return buildCard({ slogan: payload.slogan }); } ); const ok = await saveCard( { id: 8, name: 'Alex Vance', slogan: 'Trusted globally', }, { updateOwnerCard } ); expect(ok.ok).toBe(true); expect(updateOwnerCard).toHaveBeenCalledTimes(1); const updateEmpty = jest.fn( async (payload: CardProfile & { id: string | number }) => { expect(payload.slogan).toBeUndefined(); return buildCard(); } ); await saveCard( { id: 8, name: 'Alex Vance', slogan: '' }, { updateOwnerCard: updateEmpty } ); expect(updateEmpty).toHaveBeenCalledTimes(1); }); it('5.3 锁定 slogan 时不提交', () => { const { buildUpdatePayload } = loadUseCardEdit(); const payload = buildUpdatePayload( { id: 8, name: 'Alex Vance', slogan: 'Should not send', }, ['slogan'] ); expect(payload?.name).toBe('Alex Vance'); expect(payload?.slogan).toBeUndefined(); }); }); describe('6. timezone 时区字段', () => { it('6.1 applyLoadResult / formFromState 读写 timezone,空时区不进 payload、无默认值', () => { const { createCardEditState, applyLoadResult, formFromState, buildUpdatePayload, resolveCardEditText, } = loadUseCardEdit(); const state = createCardEditState(); expect(state.timezone).toBe(''); expect(state.timezone).not.toBe('UTC+8'); expect(state.timezone).not.toBe('Asia/Shanghai'); applyLoadResult(state, { card: buildCard({ timezone: 'Asia/Dubai' }), empty: false, errorMessage: '', }); expect(state.timezone).toBe('Asia/Dubai'); expect(formFromState(state).timezone).toBe('Asia/Dubai'); const filled = buildUpdatePayload({ id: 8, name: 'Alex Vance', timezone: ' America/New_York ', }); expect(filled?.timezone).toBe('America/New_York'); const emptyTz = buildUpdatePayload({ id: 8, name: 'Alex Vance', timezone: ' ', }); expect(emptyTz?.timezone).toBeUndefined(); expect(Object.keys(emptyTz as CardUpdatePayload)).not.toContain('timezone'); expect(Object.prototype.hasOwnProperty.call(emptyTz, 'timezone')).toBe( false ); const omitted = buildUpdatePayload({ id: 8, name: 'Alex Vance', timezone: '', }); expect(omitted?.timezone).toBeUndefined(); expect(Object.keys(omitted as CardUpdatePayload)).not.toContain( 'timezone' ); expect(resolveCardEditText('timezone')).toBe('时区'); }); it('6.2 saveCard 提交已填 timezone;清空时不带 timezone、不默认 UTC+8/Asia/Shanghai', async () => { const { saveCard } = loadUseCardEdit(); const updateOwnerCard = jest.fn( async (payload: CardProfile & { id: string | number }) => { expect(payload.timezone).toBe('Europe/Paris'); return buildCard({ timezone: payload.timezone }); } ); const ok = await saveCard( { id: 8, name: 'Alex Vance', timezone: 'Europe/Paris', }, { updateOwnerCard } ); expect(ok.ok).toBe(true); expect(ok.code).toBe(1000); expect(updateOwnerCard).toHaveBeenCalledTimes(1); const updateEmpty = jest.fn( async (payload: CardProfile & { id: string | number }) => { expect(payload.timezone).toBeUndefined(); expect( Object.prototype.hasOwnProperty.call(payload, 'timezone') ).toBe(false); expect(payload.timezone).not.toBe('UTC+8'); expect(payload.timezone).not.toBe('Asia/Shanghai'); return buildCard({ timezone: undefined }); } ); const cleared = await saveCard( { id: 8, name: 'Alex Vance', timezone: '' }, { updateOwnerCard: updateEmpty } ); expect(cleared.ok).toBe(true); expect(cleared.code).toBe(1000); expect(updateEmpty).toHaveBeenCalledTimes(1); const sent = updateEmpty.mock.calls[0][0] as Record; expect(Object.keys(sent)).not.toContain('timezone'); }); it('6.3 saveCard 失败时 code 不为 1000、ok 为 false', async () => { const { saveCard } = loadUseCardEdit(); const updateOwnerCard = jest.fn(async () => { throw new Error('update failed'); }); const result = await saveCard( { id: 8, name: 'Alex Vance', timezone: 'UTC+4' }, { updateOwnerCard } ); expect(result.ok).toBe(false); expect(result.code).not.toBe(1000); expect(result.card).toBeNull(); }); }); describe('7. 模块排序(无服务端排序字段,走客户端顺序)', () => { it('7.1 默认顺序与 normalize / move 可编辑且不伪造 API 字段', () => { const { DEFAULT_MODULE_ORDER, normalizeModuleOrder, moveModuleInOrder, createCardEditState, buildUpdatePayload, formFromState, } = loadUseCardEdit(); expect(DEFAULT_MODULE_ORDER).toEqual(['basic', 'social', 'privacy']); expect(normalizeModuleOrder(null)).toEqual(DEFAULT_MODULE_ORDER); expect(normalizeModuleOrder(['privacy', 'basic', 'extra'])).toEqual([ 'privacy', 'basic', 'social', ]); const moved = moveModuleInOrder( ['basic', 'social', 'privacy'], 0, 1 ); expect(moved).toEqual(['social', 'basic', 'privacy']); expect( moveModuleInOrder(['basic', 'social', 'privacy'], 0, -1) ).toEqual(['basic', 'social', 'privacy']); const state = createCardEditState(); expect(state.moduleOrder).toEqual(DEFAULT_MODULE_ORDER); state.moduleOrder = moved; const form = formFromState(state); expect((form as { moduleOrder?: string[] }).moduleOrder).toBeUndefined(); const payload = buildUpdatePayload({ id: 8, name: 'Alex Vance', slogan: 'Hi', }); expect( Object.prototype.hasOwnProperty.call(payload, 'moduleOrder') ).toBe(false); }); }); }); describe('OmniCard AI 名片主极速编辑页契约 (edit.uvue)', () => { it('页面存在且引 updateOwnerCard 或 saveCard', () => { expect(fs.existsSync(EDIT_PAGE)).toBe(true); expect(fs.existsSync(EDIT_UTS)).toBe(true); const pageSource = fs.readFileSync(EDIT_PAGE, 'utf8'); const logicSource = fs.readFileSync(EDIT_UTS, 'utf8'); const combined = `${pageSource}\n${logicSource}`; expect(pageSource.includes('script setup lang="uts"')).toBe(true); expect( combined.includes('updateOwnerCard') || combined.includes('saveCard') ).toBe(true); expect(combined.includes('fetchOwnerCardInfo') || combined.includes('loadCard')).toBe( true ); expect(logicSource.includes('export {') && logicSource.includes('fetchOwnerCardInfo')).toBe(true); expect(combined.includes('buildUpdatePayload')).toBe(true); expect(combined.includes('extractLockedFields')).toBe(true); expect(combined.includes('isFieldLocked')).toBe(true); expect(combined.includes('lockedFields')).toBe(true); expect(pageSource.includes('v-model="state.name"')).toBe(true); expect(pageSource.includes('v-model="state.title"')).toBe(true); expect(pageSource.includes('v-model="state.company"')).toBe(true); expect(pageSource.includes('v-model="state.slogan"')).toBe(true); expect(pageSource.includes('v-model="state.timezone"')).toBe(true); expect( pageSource.includes('isFieldLocked(state.lockedFields, \'company\')') || pageSource.includes('isFieldLocked(state.lockedFields, "company")') || pageSource.includes('companyLocked') ).toBe(true); expect(pageSource.includes('lockedHint') || pageSource.includes("t('lockedHint')") || pageSource.includes('resolveCardEditText(\'lockedHint\')')).toBe(true); expect(pageSource.includes('v-model="state.phone"')).toBe(true); expect(pageSource.includes('v-model="state.email"')).toBe(true); expect(pageSource.includes('v-model="state.whatsapp"')).toBe(true); expect(pageSource.includes('v-model="state.linkedin"')).toBe(true); expect( pageSource.includes('code === 1000') || logicSource.includes('code === 1000') ).toBe(true); expect(logicSource.includes('UTC+8') || pageSource.includes('UTC+8')).toBe( false ); expect( logicSource.includes('Asia/Shanghai') || pageSource.includes('Asia/Shanghai') ).toBe(false); expect( pageSource.includes('state.moduleOrder') || pageSource.includes('handleMoveModule') || logicSource.includes('moveModuleInOrder') ).toBe(true); expect(pageSource.includes('primary-cta-btn')).toBe(true); expect(pageSource.includes('#0B5CFF')).toBe(true); expect(pageSource.includes('#F7F8FA')).toBe(true); expect(pageSource.includes('#101828')).toBe(true); expect(pageSource.includes('#667085')).toBe(true); expect(pageSource.includes('#E3E7ED')).toBe(true); expect(pageSource.includes('12px')).toBe(true); expect(pageSource.includes('16px')).toBe(true); expect(pageSource.toLowerCase().includes('bento')).toBe(false); expect(pageSource.includes('draggable') || pageSource.includes('@drag')).toBe( false ); }); it('无 emoji、无 any,模板无硬编码中文主文案', () => { const pageSource = fs.readFileSync(EDIT_PAGE, 'utf8'); const logicSource = fs.readFileSync(EDIT_UTS, 'utf8'); const testSource = fs.readFileSync(__filename, 'utf8'); const template = pageSource.split('