forked from mirror/crumbs-original
Replace plain text toolbar labels with Lucide SVG icons. Group heading, list, and alignment options into dropdown menus to reduce toolbar width. Replace window.prompt link insertion with an inline popover (input, apply, open, remove). Add underline and text-align TipTap extensions. Remove task list from TipTap editor (broken styling). Fix task list bullet rendering in note card preview. Increase editor min-height for markdown mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
154 lines
6.8 KiB
TypeScript
154 lines
6.8 KiB
TypeScript
import { test, expect } from './helpers/fixtures.js';
|
|
import type { Page, Locator } from '@playwright/test';
|
|
|
|
/** Run a TipTap command chain via the exposed editor instance on the DOM element */
|
|
async function runTiptapCommand(page: Page, commandFn: string) {
|
|
await page.getByTestId('tiptap-editor').evaluate(
|
|
(el, fn) => {
|
|
const editor = (el as any).__tiptapEditor;
|
|
if (!editor) throw new Error('TipTap editor not found on element');
|
|
new Function('editor', fn)(editor);
|
|
},
|
|
commandFn
|
|
);
|
|
}
|
|
|
|
/** Type content into the TipTap editor via markdown mode (reliable for e2e) */
|
|
async function typeViaMarkdown(page: Page, content: string) {
|
|
await page.getByTestId('markdown-toggle').click();
|
|
await page.getByTestId('note-content-input').fill(content);
|
|
await page.getByTestId('markdown-toggle').click();
|
|
}
|
|
|
|
test.describe('Rich text formatting', () => {
|
|
test('Scenario: Bold shortcut formats selected text in the editor', async ({ authenticatedPage: page }) => {
|
|
// Given the user is editing a new note with content "hello world"
|
|
await page.getByTestId('new-note-btn').click();
|
|
await typeViaMarkdown(page, 'hello world');
|
|
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
|
|
|
|
// When the user selects "world" and applies bold formatting
|
|
await runTiptapCommand(page, 'editor.chain().focus().setTextSelection({from:7,to:12}).toggleBold().run()');
|
|
|
|
// Then the selected text appears bold in the editor
|
|
await expect(editor.locator('strong')).toHaveText('world');
|
|
|
|
// And the markdown content contains bold syntax
|
|
await page.getByTestId('markdown-toggle').click();
|
|
await expect(page.getByTestId('note-content-input')).toHaveValue('hello **world**');
|
|
});
|
|
|
|
test('Scenario: Italic shortcut formats selected text in the editor', async ({ authenticatedPage: page }) => {
|
|
// Given the user is editing a new note with content "hello world"
|
|
await page.getByTestId('new-note-btn').click();
|
|
await typeViaMarkdown(page, 'hello world');
|
|
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
|
|
|
|
// When the user selects "world" and applies italic formatting
|
|
await runTiptapCommand(page, 'editor.chain().focus().setTextSelection({from:7,to:12}).toggleItalic().run()');
|
|
|
|
// Then the selected text appears italic in the editor
|
|
await expect(editor.locator('em')).toHaveText('world');
|
|
});
|
|
|
|
test('Scenario: Formatting toolbar applies bold to selected text', async ({ authenticatedPage: page }) => {
|
|
// Given the user is editing a note
|
|
await page.getByTestId('new-note-btn').click();
|
|
await typeViaMarkdown(page, 'hello world');
|
|
|
|
// When the user selects "world" and clicks the bold button
|
|
await runTiptapCommand(page, 'editor.chain().focus().setTextSelection({from:7,to:12}).run()');
|
|
await page.getByTestId('format-bold').click();
|
|
|
|
// Then the selected text appears bold
|
|
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
|
|
await expect(editor.locator('strong')).toHaveText('world');
|
|
});
|
|
|
|
test('Scenario: Formatting toolbar is always visible when editing', async ({ authenticatedPage: page }) => {
|
|
// When the user opens a new note
|
|
await page.getByTestId('new-note-btn').click();
|
|
|
|
// Then the formatting toolbar is visible immediately
|
|
await expect(page.getByTestId('formatting-toolbar')).toBeVisible();
|
|
});
|
|
|
|
test('Scenario: Inline code shortcut wraps selected text', async ({ authenticatedPage: page }) => {
|
|
// Given the user is editing a note with content "use npm install"
|
|
await page.getByTestId('new-note-btn').click();
|
|
await typeViaMarkdown(page, 'use npm install');
|
|
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
|
|
|
|
// When the user selects "npm install" and applies code formatting
|
|
await runTiptapCommand(page, 'editor.chain().focus().setTextSelection({from:5,to:16}).toggleCode().run()');
|
|
|
|
// Then the selected text appears as inline code
|
|
await expect(editor.locator('code')).toHaveText('npm install');
|
|
});
|
|
|
|
test('Scenario: Existing note shows rich text in TipTap editor', async ({ authenticatedPage: page }) => {
|
|
// Given a note with bold markdown content exists
|
|
await page.getByTestId('new-note-btn').click();
|
|
await page.getByTestId('note-title-input').fill('Rich Text Note');
|
|
await typeViaMarkdown(page, 'Hello **bold** world');
|
|
await page.getByTestId('close-editor-btn').click();
|
|
|
|
// When the user reopens the note
|
|
await page.getByTestId('note-card').click();
|
|
|
|
// Then the content is rendered as rich text in the TipTap editor
|
|
await expect(page.getByTestId('tiptap-editor')).toBeVisible();
|
|
await expect(page.getByTestId('tiptap-editor').locator('strong')).toHaveText('bold');
|
|
});
|
|
|
|
test('Scenario: Toolbar dropdown closes when clicking outside', async ({ authenticatedPage: page }) => {
|
|
// Given the user has opened the heading dropdown
|
|
await page.getByTestId('new-note-btn').click();
|
|
await page.getByTestId('format-heading').click();
|
|
await expect(page.getByTestId('format-h1')).toBeVisible();
|
|
|
|
// When the user clicks inside the editor area
|
|
await page.getByTestId('tiptap-editor').click();
|
|
|
|
// Then the dropdown closes
|
|
await expect(page.getByTestId('format-h1')).not.toBeVisible();
|
|
});
|
|
|
|
test('Scenario: Link popover allows inserting a link inline', async ({ authenticatedPage: page }) => {
|
|
// Given the user is editing a note with selected text
|
|
await page.getByTestId('new-note-btn').click();
|
|
await typeViaMarkdown(page, 'visit example');
|
|
await runTiptapCommand(page, 'editor.chain().focus().setTextSelection({from:7,to:14}).run()');
|
|
|
|
// When the user opens the link popover and enters a URL
|
|
await page.getByTestId('format-link').click();
|
|
await expect(page.getByTestId('format-link-input')).toBeVisible();
|
|
await page.getByTestId('format-link-input').fill('https://example.com');
|
|
await page.getByTestId('format-link-apply').click();
|
|
|
|
// Then the selected text becomes a link
|
|
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
|
|
const link = editor.locator('a');
|
|
await expect(link).toHaveText('example');
|
|
await expect(link).toHaveAttribute('href', 'https://example.com');
|
|
});
|
|
|
|
test('Scenario: Markdown toggle shows raw markdown content', async ({ authenticatedPage: page }) => {
|
|
// Given the user is editing a note with bold content
|
|
await page.getByTestId('new-note-btn').click();
|
|
await typeViaMarkdown(page, 'Hello **bold** world');
|
|
|
|
// Verify the rich text renders correctly first
|
|
const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
|
|
await expect(editor.locator('strong')).toHaveText('bold');
|
|
|
|
// When the user toggles markdown mode
|
|
await page.getByTestId('markdown-toggle').click();
|
|
|
|
// Then the raw markdown textarea is shown with the markdown source
|
|
await expect(page.getByTestId('note-content-input')).toBeVisible();
|
|
await expect(page.getByTestId('tiptap-editor')).not.toBeVisible();
|
|
await expect(page.getByTestId('note-content-input')).toHaveValue('Hello **bold** world');
|
|
});
|
|
});
|