Epic 6: Stories 6-2 (Markdown roundtrip) + 6-3 (Brainstorm PPTX + Canvas)
Story 6-2 — Markdown roundtrip export/import: - lib/editor/markdown-export.ts: tiptapHTMLToMarkdown, markdownToHTML, looksLikeMarkdown - lib/editor/markdown-paste-extension.ts: TipTap extension paste Markdown → blocs - note-editor-toolbar.tsx: export .md + import .md (file picker) - rich-text-editor.tsx: intégration MarkdownPasteExtension - 40 tests unitaires markdown-export.test.ts Story 6-3 — Brainstorm PPTX + Canvas: - lib/brainstorm/export-pptx.ts: génération PPTX 5 slides (pptxgenjs) - app/api/brainstorm/[sessionId]/export-pptx/route.ts: route POST protégée - brainstorm-page.tsx: bouton PPTX, auto-select session, fix emoji, fix router.replace - wave-canvas.tsx: fitTrigger recentrage, légende bas-droite Onboarding activation wizard (Story 6-1): - components/onboarding/: wizard multi-étapes, hints éditeur - app/api/onboarding/: route PATCH onboarding - prisma/migrations: champs onboarding user Locales: 15 langues mises à jour (brainstorm, markdown, onboarding keys) Sprint: 6-1 done, 6-2 review, 6-3 review Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
218
memento-note/tests/unit/markdown-export.test.ts
Normal file
218
memento-note/tests/unit/markdown-export.test.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { describe, test, expect } from 'vitest'
|
||||
import {
|
||||
tiptapHTMLToMarkdown,
|
||||
markdownToHTML,
|
||||
looksLikeMarkdown,
|
||||
extractMarkdownTitle,
|
||||
} from '../../lib/editor/markdown-export'
|
||||
|
||||
describe('looksLikeMarkdown', () => {
|
||||
test('detects H1 heading', () => {
|
||||
expect(looksLikeMarkdown('# Hello World')).toBe(true)
|
||||
})
|
||||
|
||||
test('detects unordered list', () => {
|
||||
expect(looksLikeMarkdown('- Item one\n- Item two')).toBe(true)
|
||||
expect(looksLikeMarkdown('* Item one')).toBe(true)
|
||||
})
|
||||
|
||||
test('detects ordered list', () => {
|
||||
expect(looksLikeMarkdown('1. First\n2. Second')).toBe(true)
|
||||
})
|
||||
|
||||
test('detects blockquote', () => {
|
||||
expect(looksLikeMarkdown('> This is a quote')).toBe(true)
|
||||
})
|
||||
|
||||
test('detects code fence', () => {
|
||||
expect(looksLikeMarkdown('```\nconst x = 1\n```')).toBe(true)
|
||||
})
|
||||
|
||||
test('detects inline code', () => {
|
||||
expect(looksLikeMarkdown('Use `console.log()` for debugging')).toBe(true)
|
||||
})
|
||||
|
||||
test('detects bold', () => {
|
||||
expect(looksLikeMarkdown('This is **bold** text')).toBe(true)
|
||||
})
|
||||
|
||||
test('detects italic', () => {
|
||||
expect(looksLikeMarkdown('This is *italic* text')).toBe(true)
|
||||
})
|
||||
|
||||
test('detects table', () => {
|
||||
expect(looksLikeMarkdown('| Col1 | Col2 |\n|------|------|')).toBe(true)
|
||||
})
|
||||
|
||||
test('detects link', () => {
|
||||
expect(looksLikeMarkdown('See [TipTap docs](https://tiptap.dev)')).toBe(true)
|
||||
})
|
||||
|
||||
test('does NOT flag plain prose as Markdown', () => {
|
||||
expect(looksLikeMarkdown('This is a normal sentence without any markdown.')).toBe(false)
|
||||
expect(looksLikeMarkdown('Hello world, this is plain text.')).toBe(false)
|
||||
})
|
||||
|
||||
test('does NOT flag very short text', () => {
|
||||
expect(looksLikeMarkdown('Hi')).toBe(false)
|
||||
expect(looksLikeMarkdown('')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('tiptapHTMLToMarkdown', () => {
|
||||
test('converts H1 to # heading', () => {
|
||||
const md = tiptapHTMLToMarkdown('<h1>Hello</h1>')
|
||||
expect(md).toBe('# Hello')
|
||||
})
|
||||
|
||||
test('converts H2 to ## heading', () => {
|
||||
const md = tiptapHTMLToMarkdown('<h2>Section</h2>')
|
||||
expect(md).toBe('## Section')
|
||||
})
|
||||
|
||||
test('converts H3 to ### heading', () => {
|
||||
const md = tiptapHTMLToMarkdown('<h3>Sub</h3>')
|
||||
expect(md).toBe('### Sub')
|
||||
})
|
||||
|
||||
test('converts bold text', () => {
|
||||
const md = tiptapHTMLToMarkdown('<p>This is <strong>bold</strong> text.</p>')
|
||||
expect(md).toContain('**bold**')
|
||||
})
|
||||
|
||||
test('converts italic text', () => {
|
||||
const md = tiptapHTMLToMarkdown('<p>This is <em>italic</em> text.</p>')
|
||||
expect(md).toContain('_italic_')
|
||||
})
|
||||
|
||||
test('converts unordered list', () => {
|
||||
const md = tiptapHTMLToMarkdown('<ul><li>Item 1</li><li>Item 2</li></ul>')
|
||||
expect(md).toContain('Item 1')
|
||||
expect(md).toContain('Item 2')
|
||||
expect(md).toMatch(/^[-*+]\s/m)
|
||||
})
|
||||
|
||||
test('converts ordered list', () => {
|
||||
const md = tiptapHTMLToMarkdown('<ol><li>First</li><li>Second</li></ol>')
|
||||
expect(md).toContain('1. First')
|
||||
expect(md).toContain('2. Second')
|
||||
})
|
||||
|
||||
test('converts code block', () => {
|
||||
const md = tiptapHTMLToMarkdown('<pre><code>const x = 1;</code></pre>')
|
||||
expect(md).toContain('```')
|
||||
expect(md).toContain('const x = 1;')
|
||||
})
|
||||
|
||||
test('converts blockquote', () => {
|
||||
const md = tiptapHTMLToMarkdown('<blockquote><p>Quote text</p></blockquote>')
|
||||
expect(md).toContain('> Quote text')
|
||||
})
|
||||
|
||||
test('converts inline code', () => {
|
||||
const md = tiptapHTMLToMarkdown('<p>Use <code>console.log()</code> here.</p>')
|
||||
expect(md).toContain('`console.log()`')
|
||||
})
|
||||
|
||||
test('converts hyperlink', () => {
|
||||
const md = tiptapHTMLToMarkdown('<p><a href="https://example.com">Example</a></p>')
|
||||
expect(md).toContain('[Example](https://example.com)')
|
||||
})
|
||||
|
||||
test('handles empty HTML', () => {
|
||||
expect(tiptapHTMLToMarkdown('')).toBe('')
|
||||
expect(tiptapHTMLToMarkdown(' ')).toBe('')
|
||||
})
|
||||
|
||||
test('preserves liveBlock as HTML comment', () => {
|
||||
const html = '<div data-live-block="true" sourceNoteId="abc" blockId="def"></div>'
|
||||
const md = tiptapHTMLToMarkdown(html)
|
||||
expect(md).toContain('<!-- live-block:')
|
||||
})
|
||||
|
||||
test('preserves structuredViewBlock as HTML comment', () => {
|
||||
const html = '<div data-structured-view-block="true" data-sv-mode="table"></div>'
|
||||
const md = tiptapHTMLToMarkdown(html)
|
||||
expect(md).toContain('<!-- structured-view:')
|
||||
})
|
||||
})
|
||||
|
||||
describe('markdownToHTML', () => {
|
||||
test('converts # H1 to h1 element', () => {
|
||||
const html = markdownToHTML('# Hello')
|
||||
expect(html.toLowerCase()).toContain('<h1>hello</h1>')
|
||||
})
|
||||
|
||||
test('converts ## H2 to h2 element', () => {
|
||||
const html = markdownToHTML('## Section')
|
||||
expect(html.toLowerCase()).toContain('<h2>section</h2>')
|
||||
})
|
||||
|
||||
test('converts bold markdown to <strong>', () => {
|
||||
const html = markdownToHTML('This is **bold** text.')
|
||||
expect(html).toContain('<strong>bold</strong>')
|
||||
})
|
||||
|
||||
test('converts italic markdown to <em>', () => {
|
||||
const html = markdownToHTML('This is *italic* text.')
|
||||
expect(html).toContain('<em>italic</em>')
|
||||
})
|
||||
|
||||
test('converts unordered list to <ul><li>', () => {
|
||||
const html = markdownToHTML('- Item 1\n- Item 2')
|
||||
expect(html).toContain('<ul>')
|
||||
expect(html).toContain('<li>Item 1</li>')
|
||||
})
|
||||
|
||||
test('converts ordered list to <ol><li>', () => {
|
||||
const html = markdownToHTML('1. First\n2. Second')
|
||||
expect(html).toContain('<ol>')
|
||||
expect(html).toContain('<li>First</li>')
|
||||
})
|
||||
|
||||
test('converts fenced code block to <pre><code>', () => {
|
||||
const html = markdownToHTML('```\nconst x = 1;\n```')
|
||||
expect(html).toContain('<pre>')
|
||||
expect(html).toContain('<code>')
|
||||
expect(html).toContain('const x = 1;')
|
||||
})
|
||||
|
||||
test('converts GFM table to <table>', () => {
|
||||
const md = '| Name | Age |\n|------|-----|\n| Alice | 30 |'
|
||||
const html = markdownToHTML(md)
|
||||
expect(html).toContain('<table>')
|
||||
expect(html).toContain('<th>Name</th>')
|
||||
expect(html).toContain('Alice')
|
||||
})
|
||||
|
||||
test('handles empty markdown', () => {
|
||||
expect(markdownToHTML('')).toBe('')
|
||||
expect(markdownToHTML(' ')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractMarkdownTitle', () => {
|
||||
test('extracts first H1', () => {
|
||||
const md = '# My Note\n\nSome content'
|
||||
expect(extractMarkdownTitle(md)).toBe('My Note')
|
||||
})
|
||||
|
||||
test('extracts H1 even with preceding content', () => {
|
||||
const md = 'Some preamble\n# The Title\n\nContent'
|
||||
expect(extractMarkdownTitle(md)).toBe('The Title')
|
||||
})
|
||||
|
||||
test('returns null if no H1', () => {
|
||||
const md = '## Not an H1\n\nContent'
|
||||
expect(extractMarkdownTitle(md)).toBeNull()
|
||||
})
|
||||
|
||||
test('returns null for empty string', () => {
|
||||
expect(extractMarkdownTitle('')).toBeNull()
|
||||
})
|
||||
|
||||
test('handles H1 with leading spaces in title', () => {
|
||||
const md = '# Spaced Title '
|
||||
expect(extractMarkdownTitle(md)).toBe('Spaced Title')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user