-
Notifications
You must be signed in to change notification settings - Fork 48
fix(comark): surface parse failures instead of rendering an empty document #412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ee9e0cc
bd19e88
b21fa0b
8b0e947
a559f08
af469f1
d009890
f80b24f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { describe, expect, it } from 'vitest' | ||
| import { describe, expect, it, vi } from 'vitest' | ||
| import { render } from 'vitest-browser-svelte' | ||
| import { parseMarkdown } from 'comark' | ||
| import type { ComarkPlugin } from 'comark' | ||
| import Markdown from '../src/components/Markdown.svelte' | ||
| import MarkdownDocument from '../src/components/MarkdownDocument.svelte' | ||
| import Alert from './test-components/Alert.svelte' | ||
|
|
@@ -186,3 +187,29 @@ describe('streaming with MarkdownDocument', () => { | |
| await expect.element(screen.getByText('Second')).toBeInTheDocument() | ||
| }) | ||
| }) | ||
|
|
||
| describe('parse failures', () => { | ||
| it('ignores a stale parse when a newer one rejects', async () => { | ||
| let release!: () => void | ||
| const gate = new Promise<void>((resolve) => (release = resolve)) | ||
| const slow: ComarkPlugin = { name: 'slow', post: () => gate } | ||
| const failing: ComarkPlugin = { | ||
| name: 'failing', | ||
| post() { | ||
| throw new Error('plugin exploded') | ||
| }, | ||
| } | ||
| vi.spyOn(console, 'error').mockImplementation(() => {}) | ||
|
|
||
| const screen = await render(Markdown, { value: 'Good' }) | ||
| await expect.element(screen.getByText('Good')).toBeInTheDocument() | ||
|
|
||
| await screen.rerender({ value: 'Stale', plugins: [slow] }) | ||
| await screen.rerender({ value: 'Newer', plugins: [failing] }) | ||
| release() | ||
| await new Promise((resolve) => setTimeout(resolve, 50)) | ||
|
|
||
| expect(screen.container.textContent).toContain('Good') | ||
| expect(screen.container.textContent).not.toContain('Stale') | ||
|
Comment on lines
+212
to
+213
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Prevent a stale parse from replacing the last successful document.
Apply a successful result only when 🤖 Prompt for AI Agents |
||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { describe, expect, it } from 'vitest' | ||
| import { createSSRApp, h, onErrorCaptured } from 'vue' | ||
| import { renderToString } from '@vue/server-renderer' | ||
| import type { ComarkPlugin } from 'comark' | ||
| import { Markdown } from '../src/components/Markdown.ts' | ||
|
|
||
| describe('Markdown parse errors', () => { | ||
| it('surfaces an initial parse failure instead of rendering an empty document', async () => { | ||
| const failing: ComarkPlugin = { | ||
| name: 'failing', | ||
| post() { | ||
| throw new Error('plugin exploded') | ||
| }, | ||
| } | ||
|
|
||
| const captured: unknown[] = [] | ||
| const app = createSSRApp({ | ||
| setup() { | ||
| onErrorCaptured((error) => { | ||
| captured.push(error) | ||
| return false | ||
| }) | ||
| return () => h(Markdown, { value: '# Hello', plugins: [failing] }) | ||
| }, | ||
| }) | ||
|
|
||
| const html = await renderToString(app as any) | ||
|
|
||
| expect(captured).toHaveLength(1) | ||
| expect((captured[0] as Error).message).toBe('plugin exploded') | ||
| expect(html).not.toContain('comark-content') | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { describe, it, expect } from 'vitest' | ||
| import { createSerializedTask } from '../src/utils/helpers.ts' | ||
|
|
||
| describe('createSerializedTask', () => { | ||
| it('rejects the caller instead of resolving null', async () => { | ||
| const task = createSerializedTask(async () => { | ||
| throw new Error('boom') | ||
| }) | ||
|
|
||
| await expect(task()).rejects.toThrow('boom') | ||
| }) | ||
|
|
||
| it('keeps running after a rejection', async () => { | ||
| let calls = 0 | ||
| const task = createSerializedTask(async () => { | ||
| calls++ | ||
| if (calls === 1) throw new Error('boom') | ||
| return calls | ||
| }) | ||
|
|
||
| await expect(task()).rejects.toThrow('boom') | ||
| await expect(task()).resolves.toBe(2) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Let the initial parse rejection propagate.
ngOnChangesstarts the first string parse, and malformed input can rejectcreateSerializedMarkdownParser. The unconditional.catch()logs and resolves that rejection, so configured Angular error handling cannot receive it. Attach this recovery handler only to later parses; those parses can retain the last good document.🤖 Prompt for AI Agents