fix: address phase two review and theme benchmark page
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import {describe,it,expect,vi} from 'vitest'
|
||||
// @vitest-environment jsdom
|
||||
import {webcrypto} from 'node:crypto'
|
||||
import {describe,it,expect,vi,afterEach} from 'vitest'
|
||||
vi.mock('./apiClient',()=>({apiClient:{post:vi.fn(),get:vi.fn()}}))
|
||||
vi.mock('./mermaidService',()=>({renderMermaid:vi.fn()}))
|
||||
import {renderMermaid} from './mermaidService'
|
||||
import {apiClient} from './apiClient'
|
||||
import {exportService} from './exportService'
|
||||
describe('export snapshot contract',()=>{
|
||||
@@ -16,3 +20,45 @@ describe('export snapshot contract',()=>{
|
||||
expect(apiClient.post).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
const reviewOptions={theme_id:'light',include_title:true,page_size:'A4'}
|
||||
const queued={job_id:'review-job',status:'queued',warnings:[],file:null,error:null}
|
||||
afterEach(()=>{vi.restoreAllMocks();vi.unstubAllGlobals();vi.clearAllMocks()})
|
||||
it('cancels a created server job after an in-flight submission is aborted',async()=>{
|
||||
let finish!:(value:unknown)=>void
|
||||
vi.mocked(apiClient.post).mockImplementationOnce(()=>new Promise(resolve=>{finish=resolve}) as never).mockResolvedValue({status:'completed'})
|
||||
vi.mocked(apiClient.get).mockResolvedValue({...queued,status:'cancelled'})
|
||||
const controller=new AbortController()
|
||||
const pending=exportService.create('# snapshot','review','html',reviewOptions,controller.signal)
|
||||
controller.abort();finish(queued)
|
||||
await expect(pending).rejects.toMatchObject({name:'AbortError'})
|
||||
expect(apiClient.post).toHaveBeenLastCalledWith('/api/exports/review-job/cancel')
|
||||
})
|
||||
it('does not report cancellation when the server job already completed',async()=>{
|
||||
let finish!:(value:unknown)=>void
|
||||
vi.mocked(apiClient.post).mockImplementationOnce(()=>new Promise(resolve=>{finish=resolve}) as never).mockResolvedValue({status:'completed'})
|
||||
vi.mocked(apiClient.get).mockResolvedValue({...queued,status:'completed'})
|
||||
const controller=new AbortController()
|
||||
const pending=exportService.create('# snapshot','review','html',reviewOptions,controller.signal)
|
||||
controller.abort();finish(queued)
|
||||
await expect(pending).rejects.toThrow('导出已完成,无法取消')
|
||||
})
|
||||
it('surfaces a server cancellation failure instead of claiming it was cancelled',async()=>{
|
||||
let finish!:(value:unknown)=>void
|
||||
vi.mocked(apiClient.post).mockImplementationOnce(()=>new Promise(resolve=>{finish=resolve}) as never).mockRejectedValue(new Error('network failure'))
|
||||
const controller=new AbortController()
|
||||
const pending=exportService.create('# snapshot','review','html',reviewOptions,controller.signal)
|
||||
controller.abort();finish(queued)
|
||||
await expect(pending).rejects.toThrow('network failure')
|
||||
})
|
||||
it.each(['mermaid','Mermaid','mermaid title="Flow"'])('prepares a static asset for %s',async language=>{
|
||||
vi.stubGlobal('crypto',webcrypto)
|
||||
vi.stubGlobal('Image',class {src='';decode(){return Promise.resolve()}})
|
||||
vi.spyOn(HTMLCanvasElement.prototype,'getContext').mockReturnValue({fillStyle:'',fillRect:vi.fn(),drawImage:vi.fn()} as never)
|
||||
vi.spyOn(HTMLCanvasElement.prototype,'toDataURL').mockReturnValue('data:image/png;base64,YWJj')
|
||||
vi.mocked(renderMermaid).mockResolvedValue({svg:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 10"></svg>',warnings:[]} as never)
|
||||
vi.mocked(apiClient.post).mockResolvedValue(queued)
|
||||
await exportService.create('```'+language+'\nflowchart LR\n A-->B\n```','review','html',reviewOptions)
|
||||
expect(renderMermaid).toHaveBeenCalledWith('flowchart LR\n A-->B',{mode:'raster',theme:'light'})
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/exports',expect.objectContaining({assets:[expect.objectContaining({kind:'mermaid',png_base64:'YWJj',source_hash:expect.stringMatching(/^[a-f0-9]{64}$/)})]}))
|
||||
})
|
||||
|
||||
@@ -44,7 +44,7 @@ export const exportService = {
|
||||
async create(markdown: string, title: string, format: ExportFormat, options: { theme_id: string; include_title: boolean; page_size: string }, signal?: AbortSignal, filePath?: string) {
|
||||
const blocks: string[] = []
|
||||
const parser = new Marked()
|
||||
parser.walkTokens(parser.lexer(markdown), token => { if (token.type === 'code' && token.lang === 'mermaid') blocks.push(token.text) })
|
||||
parser.walkTokens(parser.lexer(markdown), token => { if (token.type === 'code' && token.lang?.trim().split(/\s+/)[0]?.toLowerCase() === 'mermaid') blocks.push(token.text) })
|
||||
const assets = []
|
||||
for (const source of [...new Set(blocks)]) {
|
||||
signal?.throwIfAborted()
|
||||
@@ -54,7 +54,17 @@ export const exportService = {
|
||||
assets.push({ kind: 'mermaid', source_hash: await hashSource(source), png_base64: await rasterize(result.svg, signal) })
|
||||
}
|
||||
signal?.throwIfAborted()
|
||||
return mapJob(await apiClient.post<JobWire>('/api/exports', { source: { type: 'markdown', markdown, file_path: filePath }, title, format, options, assets }))
|
||||
// Keep the response handle when cancellation arrives during submission:
|
||||
// aborting HTTP alone could leave an undiscoverable running server job.
|
||||
const job = mapJob(await apiClient.post<JobWire>('/api/exports', { source: { type: 'markdown', markdown, file_path: filePath }, title, format, options, assets }))
|
||||
if (signal?.aborted) {
|
||||
await apiClient.post(`/api/exports/${encodeURIComponent(job.id)}/cancel`)
|
||||
const current = await apiClient.get<JobWire>(`/api/exports/${encodeURIComponent(job.id)}`)
|
||||
if (current.status === 'completed') throw new Error('导出已完成,无法取消;请在任务列表中下载。')
|
||||
if (current.status === 'failed') throw new Error(current.error || '导出任务已失败,请查看任务列表。')
|
||||
signal.throwIfAborted()
|
||||
}
|
||||
return job
|
||||
},
|
||||
async get(id: string) { return mapJob(await apiClient.get<JobWire>(`/api/exports/${encodeURIComponent(id)}`)) },
|
||||
async list() { const response = await apiClient.get<{ items: JobWire[] }>('/api/exports'); return response.items.map(mapJob) },
|
||||
|
||||
Reference in New Issue
Block a user