|
| 1 | +/** |
| 2 | + * Tests for remark-restore-time-colons plugin |
| 3 | + * |
| 4 | + * Verifies that numeric-only directive nodes created by remark-directive |
| 5 | + * (e.g. `:47` from "2:47 AM") are restored to plain text while leaving |
| 6 | + * real directives like :::video intact. |
| 7 | + */ |
| 8 | +import { describe, it, expect } from 'vitest' |
| 9 | +import { unified } from 'unified' |
| 10 | +import remarkParse from 'remark-parse' |
| 11 | +import remarkDirective from 'remark-directive' |
| 12 | +import remarkRehype from 'remark-rehype' |
| 13 | +import rehypeStringify from 'rehype-stringify' |
| 14 | +import remarkRestoreTimeColons from '@lib/markdown/plugins/remark-restore-time-colons' |
| 15 | + |
| 16 | +/** |
| 17 | + * Process markdown through remarkDirective + remarkRestoreTimeColons and |
| 18 | + * return the rendered HTML string. |
| 19 | + */ |
| 20 | +async function processToHtml(markdown: string): Promise<string> { |
| 21 | + const result = await unified() |
| 22 | + .use(remarkParse) |
| 23 | + .use(remarkDirective) |
| 24 | + .use(remarkRestoreTimeColons) |
| 25 | + .use(remarkRehype) |
| 26 | + .use(rehypeStringify) |
| 27 | + .process(markdown) |
| 28 | + |
| 29 | + return String(result) |
| 30 | +} |
| 31 | + |
| 32 | +/** |
| 33 | + * Process markdown through remarkDirective WITHOUT the fix, to confirm |
| 34 | + * the baseline is broken. |
| 35 | + */ |
| 36 | +async function processWithoutFix(markdown: string): Promise<string> { |
| 37 | + const result = await unified() |
| 38 | + .use(remarkParse) |
| 39 | + .use(remarkDirective) |
| 40 | + .use(remarkRehype) |
| 41 | + .use(rehypeStringify) |
| 42 | + .process(markdown) |
| 43 | + |
| 44 | + return String(result) |
| 45 | +} |
| 46 | + |
| 47 | +describe('remark-restore-time-colons', () => { |
| 48 | + describe('baseline: confirms remarkDirective corrupts time strings', () => { |
| 49 | + it('corrupts H:MM time patterns without the fix', async () => { |
| 50 | + const output = await processWithoutFix('At 2:47 AM the alert fired.') |
| 51 | + expect(output).toContain('<div></div>') |
| 52 | + expect(output).not.toContain('2:47') |
| 53 | + }) |
| 54 | + |
| 55 | + it('corrupts HH:MM:SS time patterns without the fix', async () => { |
| 56 | + const output = await processWithoutFix('Reset at 11:59:59 PM.') |
| 57 | + expect(output).toContain('<div></div>') |
| 58 | + expect(output).not.toContain('11:59:59') |
| 59 | + }) |
| 60 | + }) |
| 61 | + |
| 62 | + describe('single time strings', () => { |
| 63 | + it('preserves H:MM AM format (2:47 AM)', async () => { |
| 64 | + const output = await processToHtml('At 2:47 AM the alert fired.') |
| 65 | + expect(output).toContain('2:47 AM') |
| 66 | + expect(output).not.toContain('<div></div>') |
| 67 | + }) |
| 68 | + |
| 69 | + it('preserves H:MM PM format (3:30 PM)', async () => { |
| 70 | + const output = await processToHtml('The time was 3:30 PM.') |
| 71 | + expect(output).toContain('3:30 PM') |
| 72 | + expect(output).not.toContain('<div></div>') |
| 73 | + }) |
| 74 | + |
| 75 | + it('preserves HH:MM format (12:00)', async () => { |
| 76 | + const output = await processToHtml('Starts at 12:00 sharp.') |
| 77 | + expect(output).toContain('12:00') |
| 78 | + expect(output).not.toContain('<div></div>') |
| 79 | + }) |
| 80 | + |
| 81 | + it('preserves HH:MM:SS format (11:59:59)', async () => { |
| 82 | + const output = await processToHtml('At 11:59:59 PM things changed.') |
| 83 | + expect(output).toContain('11:59:59') |
| 84 | + expect(output).not.toContain('<div></div>') |
| 85 | + }) |
| 86 | + |
| 87 | + it('preserves HH:MM:SS format (12:00:01)', async () => { |
| 88 | + const output = await processToHtml('Reset at 12:00:01 exactly.') |
| 89 | + expect(output).toContain('12:00:01') |
| 90 | + expect(output).not.toContain('<div></div>') |
| 91 | + }) |
| 92 | + }) |
| 93 | + |
| 94 | + describe('multiple time strings in one paragraph', () => { |
| 95 | + it('preserves both times in boundary burst example', async () => { |
| 96 | + const input = |
| 97 | + 'A client can make 95 requests at 11:59:59 and 95 more at 12:00:01.' |
| 98 | + const output = await processToHtml(input) |
| 99 | + expect(output).toContain('11:59:59') |
| 100 | + expect(output).toContain('12:00:01') |
| 101 | + expect(output).not.toContain('<div></div>') |
| 102 | + }) |
| 103 | + |
| 104 | + it('preserves two different H:MM times', async () => { |
| 105 | + const input = 'At 2:47 AM the alert fired. By 3:15 PM it was resolved.' |
| 106 | + const output = await processToHtml(input) |
| 107 | + expect(output).toContain('2:47 AM') |
| 108 | + expect(output).toContain('3:15 PM') |
| 109 | + expect(output).not.toContain('<div></div>') |
| 110 | + }) |
| 111 | + }) |
| 112 | + |
| 113 | + describe('non-time directives are unaffected', () => { |
| 114 | + it('does not interfere with alphabetic text directives', async () => { |
| 115 | + // A real text directive like :abbr[HTML] should not be touched |
| 116 | + const output = await processToHtml('Use the :abbr[HTML] standard.') |
| 117 | + // The directive node should still be present (not converted to text) |
| 118 | + expect(output).not.toContain(':abbr') |
| 119 | + }) |
| 120 | + |
| 121 | + it('does not interfere with container directives', async () => { |
| 122 | + const input = ':::video{src="/videos/demo.mp4"}\nVideo caption\n:::' |
| 123 | + const output = await processToHtml(input) |
| 124 | + // Container directive should still be processed, not restored to text |
| 125 | + expect(output).not.toContain(':::video') |
| 126 | + }) |
| 127 | + }) |
| 128 | + |
| 129 | + describe('output HTML structure', () => { |
| 130 | + it('produces a clean paragraph without broken elements', async () => { |
| 131 | + const input = |
| 132 | + 'A client can make 95 requests at 11:59:59 and 95 more at 12:00:01.' |
| 133 | + const output = await processToHtml(input) |
| 134 | + // Should be a single, unbroken paragraph |
| 135 | + expect(output).toBe( |
| 136 | + '<p>A client can make 95 requests at 11:59:59 and 95 more at 12:00:01.</p>' |
| 137 | + ) |
| 138 | + }) |
| 139 | + |
| 140 | + it('wraps surrounding text correctly', async () => { |
| 141 | + const output = await processToHtml( |
| 142 | + 'At 2:47 AM, all inter-service communication fails.' |
| 143 | + ) |
| 144 | + expect(output).toBe( |
| 145 | + '<p>At 2:47 AM, all inter-service communication fails.</p>' |
| 146 | + ) |
| 147 | + }) |
| 148 | + }) |
| 149 | +}) |
0 commit comments