import { _electron as electron, expect, test } from '@playwright/test' import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' test.describe.configure({ retries: 2 }) test('REPORT-TEMPLATE-MARKET-E2E-01 reads, installs, restores, renders, and switches published templates', async () => { test.setTimeout(120_000) const userData = mkdtempSync(join(tmpdir(), 'tracememo-template-market-user-')) const outputDir = mkdtempSync(join(tmpdir(), 'tracememo-template-market-output-')) const launch = (): ReturnType => electron.launch({ args: [resolve('out/main/reportTemplateTest.js')], env: { ...process.env, TRACEMEMO_TEMPLATE_TEST_USER_DATA: userData, TRACEMEMO_REPORT_OUTPUT_DIR: outputDir } }) const report = { overview: '虚构远端模板日报概览', hero: { headline: '今日群聊重点', summary: '虚构的结构化日报内容', keyTakeaway: '保留结构化快照并切换版式', pendingNote: '', statusLine: '' }, topics: [ { title: '模板市场联调', timeRange: '09:00-10:00', heat: '高' as const, participants: ['小明'], summary: '验证安装、重启恢复和导出', keywords: ['模板', '日报'], messages: [] } ], resources: [], importantMessages: [], quotes: [], qa: [], todos: [], unresolved: [], storylines: [], reversals: [], participantChains: [], analytics: { topicHeat: [{ topic: '模板市场联调', score: 1 }], activeTimeline: '09:00-10:00', topSpeakers: [{ name: '小明', count: 1 }], voiceLeaderboard: [] }, keywords: ['模板', '日报'], media: { gallery: [], voiceHighlights: [], funBadges: [] } } const metadata = { groupName: '虚构市场验收群', reportDate: '2026-09-07', dateRange: '今日', messageCount: 1, activeUsers: 1, timeSpan: '09:00-10:00', generatedAt: '2026-09-07 10:00', recordNote: 'fixture', footerNote: 'market fixture', heroParticipants: ['小明'], avatars: {} } let app: Awaited> | null = null try { app = await launch() let page = await app.firstWindow() await page.waitForLoadState('domcontentloaded') const catalogResult = await page.evaluate(() => window.api.listReportTemplateCatalog()) expect(catalogResult.success, catalogResult.error).toBe(true) const catalog = catalogResult.catalog expect(catalog?.status).toBe('published') expect(catalog?.templates.length).toBeGreaterThanOrEqual(3) const sourceCommit = catalog?.source?.commit expect(sourceCommit).toMatch(/^[a-f0-9]{40}$/) const entries = catalog!.templates for (const requiredId of [ 'community.github.tracememo.quickread', 'community.github.tracememo.paperdaily', 'community.github.tracememo.teamboard' ]) expect(entries.some((entry) => entry.id === requiredId)).toBe(true) for (const entry of entries) { expect(entry.interfaceVersion).toBe('1') expect(entry.status).toBe('published') expect(entry.download).toContain(`/Wxw-Gu/TraceMemo-Templates/${sourceCommit}/`) expect(entry.preview).toContain(`/Wxw-Gu/TraceMemo-Templates/${sourceCommit}/`) } const exportsById = new Map< string, { htmlPath: string; pngPath: string; imageDataUrl: string; version: string } >() for (const entry of entries) { const installed = await page.evaluate( async ({ id, version }) => window.api.installReportTemplateFromCatalog(id, version), { id: entry.id, version: entry.version } ) expect(installed.success, installed.error).toBe(true) expect(installed.catalogEntry?.sha256).toBe(entry.sha256) expect(installed.template?.id).toBe(entry.id) expect(installed.template?.version).toBe(entry.version) const exported = await page.evaluate( async ({ id, version, reportValue, metadataValue }) => window.api.exportGroupReport({ templateRef: { id, version }, report: reportValue, metadata: metadataValue }), { id: entry.id, version: entry.version, reportValue: report, metadataValue: metadata } ) expect(exported.success, exported.error).toBe(true) expect(exported.htmlPath).toBeTruthy() expect(exported.pngPath).toBeTruthy() expect(readFileSync(exported.htmlPath!, 'utf8')).toContain('虚构的结构化日报内容') const png = readFileSync(exported.pngPath!) expect(png.length).toBeGreaterThan(1000) const renderedWidth = png.readUInt32BE(16) expect(renderedWidth).toBeGreaterThanOrEqual(entry.platform === 'desktop' ? 1000 : 400) expect(png.readUInt32BE(20)).toBeGreaterThan(100) exportsById.set(entry.id, { htmlPath: exported.htmlPath!, pngPath: exported.pngPath!, imageDataUrl: exported.imageDataUrl!, version: entry.version }) } await app.close() app = await launch() page = await app.firstWindow() await page.waitForLoadState('domcontentloaded') const restored = await page.evaluate(() => window.api.listReportTemplates()) for (const entry of entries) { expect( restored.some((template) => template.id === entry.id && template.version === entry.version) ).toBe(true) } const quickread = entries.find((entry) => entry.id.endsWith('.quickread'))! const paperdaily = entries.find((entry) => entry.id.endsWith('.paperdaily'))! const firstExport = exportsById.get(quickread.id)! const saved = await page.evaluate( async ({ firstExportValue, reportValue, metadataValue, templateId }) => window.api.saveGeneratedReport({ contactId: 'fixture-market-group', contactName: metadataValue.groupName, source: 'manual', dateRange: metadataValue.dateRange, messageCount: metadataValue.messageCount, generatedAt: '2026-09-07T10:00:00.000Z', reportDate: metadataValue.reportDate, generatedImage: firstExportValue.imageDataUrl, htmlPath: firstExportValue.htmlPath, pngPath: firstExportValue.pngPath, reportSnapshot: reportValue, reportMetadata: metadataValue, templateId }), { firstExportValue: firstExport, reportValue: report, metadataValue: metadata, templateId: `external:${quickread.id}@${quickread.version}` } ) expect(saved.success, saved.error).toBe(true) const reportId = saved.record!.id const switchedExport = await page.evaluate( async ({ id, version, reportValue, metadataValue }) => window.api.exportGroupReport({ templateRef: { id, version }, report: reportValue, metadata: metadataValue }), { id: paperdaily.id, version: paperdaily.version, reportValue: report, metadataValue: metadata } ) expect(switchedExport.success, switchedExport.error).toBe(true) const switched = await page.evaluate( async ({ reportIdValue, id, version, exported }) => window.api.updateGeneratedReportTemplate({ reportId: reportIdValue, templateId: `external:${id}@${version}`, templateRef: { id, version }, generatedImage: exported.imageDataUrl, htmlPath: exported.htmlPath, pngPath: exported.pngPath }), { reportIdValue: reportId, id: paperdaily.id, version: paperdaily.version, exported: { imageDataUrl: switchedExport.imageDataUrl!, htmlPath: switchedExport.htmlPath!, pngPath: switchedExport.pngPath! } } ) expect(switched.success, switched.error).toBe(true) expect(switched.record?.id).toBe(reportId) expect(switched.record?.templateId).toBe(`external:${paperdaily.id}@${paperdaily.version}`) expect(switched.record?.reportSnapshot).toEqual(report) expect(readFileSync(switched.record!.htmlPath!, 'utf8')).toContain('虚构的结构化日报内容') const history = await page.evaluate(() => window.api.listGeneratedReports()) expect(history.success).toBe(true) expect(history.reports?.find((record) => record.id === reportId)?.templateId).toBe( `external:${paperdaily.id}@${paperdaily.version}` ) const removed = await page.evaluate( async ({ id, version }) => window.api.uninstallReportTemplate(id, version), { id: paperdaily.id, version: paperdaily.version } ) expect(removed.success, removed.error).toBe(true) const afterUninstall = await page.evaluate( async ({ id, version, reportValue, metadataValue }) => window.api.exportGroupReport({ templateRef: { id, version }, report: reportValue, metadata: metadataValue }), { id: paperdaily.id, version: paperdaily.version, reportValue: report, metadataValue: metadata } ) expect(afterUninstall.success).toBe(false) expect(afterUninstall.error).toContain('模板不存在') const legacyHtml = join(outputDir, 'legacy-without-structured-data.html') writeFileSync(legacyHtml, '

旧日报

') const legacy = await page.evaluate( async ({ htmlPath, pngPath, imageDataUrl }) => window.api.saveGeneratedReport({ contactId: 'fixture-legacy-group', contactName: '虚构旧日报', dateRange: '今日', messageCount: 1, generatedAt: '2026-09-07T11:00:00.000Z', htmlPath, pngPath, generatedImage: imageDataUrl }), { htmlPath: legacyHtml, pngPath: firstExport.pngPath, imageDataUrl: firstExport.imageDataUrl } ) expect(legacy.success).toBe(true) const legacySwitch = await page.evaluate( async ({ reportIdValue, id, version, imageDataUrl, htmlPath }) => window.api.updateGeneratedReportTemplate({ reportId: reportIdValue, templateId: `external:${id}@${version}`, templateRef: { id, version }, generatedImage: imageDataUrl, htmlPath, pngPath: htmlPath }), { reportIdValue: legacy.record!.id, id: quickread.id, version: quickread.version, imageDataUrl: firstExport.imageDataUrl, htmlPath: legacyHtml } ) expect(legacySwitch.success).toBe(false) expect(legacySwitch.error).toContain('旧报告未保存结构化数据') } finally { if (app) await app.close() rmSync(outputDir, { recursive: true, force: true }) rmSync(userData, { recursive: true, force: true }) } })