Revert "docs: add TipTap migration testing strategy"

This reverts commit 64b6e60951dbefee64a54e688396099f370ee150.
This commit is contained in:
mahula 2026-01-15 18:33:05 +01:00
parent 317ec72c7e
commit 8e5c6a0907
2 changed files with 376 additions and 752 deletions

376
TESTING_STRATEGY.md Normal file
View File

@ -0,0 +1,376 @@
# Teststrategie für TipTap Markdown-Migration
## Zusammenfassung der Analyse
Die TipTap-Migration umfasst folgende Kernkomponenten:
| Komponente | Beschreibung | Komplexität |
|------------|--------------|-------------|
| `lib/src/Components/TipTap/utils/preprocessMarkdown.ts` | 6-stufige Preprocessing-Pipeline | Hoch |
| `lib/src/Components/TipTap/utils/simpleMarkdownToHtml.tsx` | Statische HTML-Konvertierung | Mittel |
| `lib/src/Components/TipTap/extensions/Hashtag.tsx` | Custom Extension mit Tokenizer | Mittel |
| `lib/src/Components/TipTap/extensions/ItemMention.tsx` | Custom Extension mit Tokenizer | Mittel |
| `lib/src/Components/TipTap/extensions/VideoEmbed.tsx` | Block-Element für Videos | Mittel |
| `lib/src/Components/Input/RichTextEditor.tsx` | Haupt-Editor-Komponente | Hoch |
| `lib/src/Components/Map/Subcomponents/ItemPopupComponents/TextView.tsx` | Read-Only Editor | Mittel |
| `lib/src/Components/Map/Subcomponents/ItemPopupComponents/TextViewStatic.tsx` | Lightweight Static Renderer | Niedrig |
---
## Empfohlene Teststrategie: Testing Pyramid
```
┌─────────────────┐
│ E2E Tests │ ← Wenige, kritische User Journeys
│ (Cypress) │
└────────┬────────┘
┌────────┴────────┐
│ Integration │ ← TipTap + Extensions zusammen
│ Tests (Vitest) │
└────────┬────────┘
┌───────────────────┴───────────────────┐
│ Unit Tests (Vitest) │ ← Utility-Funktionen isoliert
│ preprocessMarkdown, simpleMarkdownToHtml │
└────────────────────────────────────────┘
```
### Begründung der Strategie
1. **Unit Tests für Utility-Funktionen (Hauptfokus)**
- `preprocessMarkdown.ts` und `simpleMarkdownToHtml.tsx` sind **pure Funktionen** ohne Abhängigkeiten
- Extrem schnelle Ausführung, hohe Coverage möglich
- Einfach zu warten und zu debuggen
- Hier liegt die meiste **Geschäftslogik** der Markdown-Verarbeitung
2. **Integration Tests für TipTap Extensions**
- Extensions benötigen einen Editor-Kontext
- Testen der Markdown ↔ JSON ↔ HTML Roundtrips
- Mäßiger Aufwand, gute Fehlererkennung
3. **E2E Tests nur für kritische User Journeys**
- Hoher Wartungsaufwand
- Langsame Ausführung
- Für Smoke Tests und Regressionsschutz
---
## Detaillierte Testfälle
### 1. Unit Tests für `preprocessMarkdown.ts`
#### A) `convertNakedUrls`
| Testfall | Input | Expected Output |
|----------|-------|-----------------|
| **Happy Path** | `Check https://example.com out` | `Check [example.com](https://example.com) out` |
| **www entfernen** | `https://www.example.com` | `[example.com](https://example.com)` |
| **URL in Markdown-Link (Skip)** | `[link](https://example.com)` | Unverändert |
| **URL in Autolink (Skip)** | `<https://example.com>` | Unverändert |
| **Mehrere URLs** | `https://a.com and https://b.com` | Beide konvertiert |
| **URL am Satzende mit Punkt** | `Visit https://example.com.` | Punkt nicht Teil der URL |
| **URL mit Klammern** | `(https://example.com)` | Klammern korrekt behandelt |
| **URL mit Query-Params** | `https://example.com?a=1&b=2` | Vollständig konvertiert |
#### B) `preprocessVideoLinks`
| Testfall | Input | Expected Output |
|----------|-------|-----------------|
| **YouTube Standard** | `<https://www.youtube.com/watch?v=abc123>` | `<video-embed provider="youtube" video-id="abc123">` |
| **YouTube Short URL** | `<https://youtu.be/abc123>` | Korrekt konvertiert |
| **YouTube Markdown Link** | `[Video](https://youtube.com/watch?v=abc123)` | Korrekt konvertiert |
| **Rumble Embed** | `<https://rumble.com/embed/xyz789>` | `<video-embed provider="rumble"...>` |
| **URL mit Extra-Params** | `<https://youtube.com/watch?v=abc&t=120>` | Nur Video-ID extrahiert |
| **Kein Video-Link** | `<https://example.com>` | Unverändert |
| **Gemischter Content** | `Text <https://youtu.be/x> more` | Nur Video konvertiert |
#### C) `preprocessHashtags`
| Testfall | Input | Expected Output |
|----------|-------|-----------------|
| **Einfacher Hashtag** | `Hello #world` | `Hello <span data-hashtag...>#world</span>` |
| **Hashtag mit Umlauten** | `#München` | Korrekt erkannt |
| **Hashtag mit Zahlen** | `#test123` | Korrekt erkannt |
| **Hashtag in Link (Skip)** | `[#tag](#anchor)` | Unverändert |
| **Hashtag nach Klammer (Skip)** | `(#section)` | Unverändert |
| **Mehrere Hashtags** | `#one #two #three` | Alle konvertiert |
| **Ungültiger Hashtag** | `#` | Unverändert (kein Text) |
| **Hashtag mit Underscore** | `#my_tag` | Korrekt erkannt |
#### D) `preprocessItemMentions`
| Testfall | Input | Expected Output |
|----------|-------|-----------------|
| **Standard Format** | `[@Person](/item/uuid-123)` | `<span data-item-mention...>@Person</span>` |
| **Mit Layer (Legacy)** | `[@Name](/item/layer/uuid)` | Korrekt konvertiert |
| **Relativer Pfad** | `[@Name](item/uuid)` | Korrekt konvertiert |
| **Mehrere Mentions** | `[@A](/item/1) und [@B](/item/2)` | Beide konvertiert |
| **Kein Item-Link** | `[@Name](/other/path)` | Unverändert |
| **UUID Case-Insensitive** | `[@Name](/item/ABC-def-123)` | Korrekt erkannt |
| **Label mit Sonderzeichen** | `[@Max Müller](/item/uuid)` | Korrekt konvertiert |
#### E) `truncateMarkdown`
| Testfall | Input | Limit | Expected |
|----------|-------|-------|----------|
| **Unter Limit** | `Short text` | 100 | Unverändert |
| **Über Limit (Plain)** | `A very long text...` | 10 | `A very lo...` |
| **Hashtag nicht schneiden** | `Text #verylonghashtag more` | 15 | Vollständiger Hashtag oder davor abschneiden |
| **Mention nicht schneiden** | `Hi [@Person](/item/x) bye` | 10 | Vollständige Mention oder davor |
| **Link nicht schneiden** | `See [link](url) more` | 8 | Vollständiger Link oder davor |
| **Newlines nicht zählen** | `Line1\n\nLine2` | 10 | Newlines ignoriert bei Zählung |
| **Gemischter Content** | `#tag [@m](/item/1) text` | 20 | Tokens atomar |
#### F) `removeMarkdownSyntax`
| Testfall | Input | Expected |
|----------|-------|----------|
| **Bold** | `**bold**` | `bold` |
| **Italic** | `*italic*` | `italic` |
| **Headers** | `# Heading` | `Heading` |
| **Links** | `[text](url)` | `text` |
| **Item Mentions erhalten** | `[@Name](/item/x)` | Erhalten |
| **Bilder entfernen** | `![alt](img.png)` | Leer |
| **Code** | `` `code` `` | `code` |
---
### 2. Unit Tests für `simpleMarkdownToHtml.tsx`
| Testfall | Input | Expected HTML |
|----------|-------|---------------|
| **Bold** | `**bold**` | `<strong>bold</strong>` |
| **Italic** | `*italic*` | `<em>italic</em>` |
| **Link** | `[text](url)` | `<a href="url">text</a>` |
| **External Link** | `[text](https://ext.com)` | `<a href="..." target="_blank"...>` |
| **Header H1** | `# Title` | `<h1>Title</h1>` |
| **Header H2-H6** | `## ... ######` | Entsprechende h-Tags |
| **Inline Code** | `` `code` `` | `<code>code</code>` |
| **Blockquote** | `> quote` | `<blockquote>quote</blockquote>` |
| **Video Embed** | `<video-embed provider="youtube"...>` | `<iframe src="youtube-nocookie...">` |
| **Hashtag mit Farbe** | Preprocessed Hashtag + Tag mit color | Style mit korrekter Farbe |
| **Item Mention** | Preprocessed Mention + Item | Link mit korrekter Farbe |
| **XSS Prevention** | `<script>alert('xss')</script>` | Escaped, kein Script-Tag |
| **Newlines** | `Line1\n\nLine2` | `</p><p>` Trennung |
---
### 3. Integration Tests für TipTap Extensions
Diese Tests benötigen einen TipTap Editor-Kontext. Setup via `@tiptap/core`:
```typescript
// Test-Setup Beispiel
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { Markdown } from '@tiptap/markdown'
import { Hashtag } from './Hashtag'
```
#### A) Hashtag Extension
| Testfall | Beschreibung |
|----------|--------------|
| **Markdown → JSON** | `#tag` wird zu `{ type: 'hashtag', attrs: { label: 'tag' } }` |
| **JSON → Markdown** | Hashtag-Node wird zu `#tag` serialisiert |
| **HTML Parse** | `<span data-hashtag data-label="x">#x</span>` wird erkannt |
| **HTML Render** | Node rendert korrekte HTML-Struktur |
| **Tokenizer Start** | `/(?<!\[)#[a-zA-Z]/` matched korrekt |
| **Click Handler (read-only)** | `onTagClick` wird aufgerufen |
| **No Click (editable)** | Kein Click-Handler im Edit-Modus |
#### B) ItemMention Extension
| Testfall | Beschreibung |
|----------|--------------|
| **Markdown → JSON** | `[@Name](/item/uuid)` wird zu korrektem Node |
| **JSON → Markdown** | Node wird korrekt serialisiert |
| **Mit Layer** | Legacy-Format wird korrekt geparst |
| **UUID Case-Insensitive** | Groß-/Kleinschreibung egal |
| **Farbe aus Item** | `getItemColor` wird korrekt verwendet |
#### C) VideoEmbed Extension
| Testfall | Beschreibung |
|----------|--------------|
| **YouTube Parse** | Autolink zu Video-Node |
| **Rumble Parse** | Embed-URL zu Video-Node |
| **Iframe Render** | Korrekter `youtube-nocookie.com` Embed |
| **Paste Handler** | Video-URL wird beim Einfügen erkannt |
#### D) Roundtrip Tests
| Testfall | Flow |
|----------|------|
| **Markdown Roundtrip** | Markdown → Editor → `getMarkdown()` → identisch |
| **Komplexer Content** | Text + #tag + @mention + Video → Roundtrip |
| **Preserve Formatting** | Bold, Italic, Listen bleiben erhalten |
---
### 4. Component Tests (React Testing Library)
#### RichTextEditor
| Testfall | Beschreibung |
|----------|--------------|
| **Render mit Default** | Editor rendert mit initialem Content |
| **onChange Callback** | `updateFormValue` erhält Markdown |
| **Placeholder** | Placeholder wird angezeigt |
| **Hashtag Suggestion** | `#` triggert Suggestion Popup |
| **Item Mention Suggestion** | `@` triggert Suggestion Popup |
| **Keyboard Navigation** | Arrow Keys in Suggestions |
| **Suggestion Select** | Enter fügt Tag ein |
#### TextView
| Testfall | Beschreibung |
|----------|--------------|
| **Read-Only** | Editor ist nicht editierbar |
| **Truncation** | Langer Text wird gekürzt + `...` |
| **Hashtag Click** | `addFilterTag` wird aufgerufen |
| **Internal Link** | React Router Navigation |
| **External Link** | Neuer Tab |
#### TextViewStatic
| Testfall | Beschreibung |
|----------|--------------|
| **HTML Render** | Markdown wird zu HTML |
| **Hashtag Farbe** | Farbe aus Tags-Array |
| **Item Mention Link** | Korrekter `/item/` Link |
| **Video Embed** | Iframe wird gerendert |
---
### 5. E2E Tests (Cypress)
Nur **kritische User Journeys** - minimaler Scope für maximale Stabilität:
#### A) Editor Flow
```typescript
describe('Rich Text Editor', () => {
it('should create and save content with hashtags and mentions', () => {
// 1. Neues Item erstellen
// 2. Text eingeben mit #tag und @mention
// 3. Speichern
// 4. Popup öffnen und Rendering prüfen
})
it('should handle video embeds', () => {
// YouTube URL einfügen → Video-Embed sichtbar
})
})
```
#### B) Display Flow
```typescript
describe('Text Display', () => {
it('should render hashtags clickable in popup', () => {
// Item mit Hashtag öffnen
// Hashtag klicken
// Filter wird aktiviert
})
it('should navigate to mentioned items', () => {
// Item mit @mention öffnen
// Mention klicken
// Navigation zum verlinkten Item
})
})
```
---
## Priorisierung der Implementierung
### Phase 1: Unit Tests (Höchste Priorität)
| Datei | Geschätzte Tests | Grund |
|-------|------------------|-------|
| `preprocessMarkdown.spec.ts` | ~40 Tests | Pure Functions, schnell, hohe Coverage |
| `simpleMarkdownToHtml.spec.ts` | ~25 Tests | Pure Function, XSS-kritisch |
### Phase 2: Integration Tests
| Datei | Geschätzte Tests | Grund |
|-------|------------------|-------|
| `Hashtag.spec.ts` | ~15 Tests | Custom Extension mit Tokenizer |
| `ItemMention.spec.ts` | ~15 Tests | Custom Extension mit Tokenizer |
| `VideoEmbed.spec.ts` | ~10 Tests | Block-Element |
### Phase 3: Component Tests
| Datei | Geschätzte Tests | Grund |
|-------|------------------|-------|
| `RichTextEditor.spec.tsx` | ~15 Tests | Haupt-Editor |
| `TextView.spec.tsx` | ~10 Tests | Read-Only Variante |
| `TextViewStatic.spec.tsx` | ~8 Tests | Lightweight Renderer |
### Phase 4: E2E Tests (Niedrigste Priorität)
| Datei | Geschätzte Tests | Grund |
|-------|------------------|-------|
| `editor-flow.cy.ts` | 3-5 Tests | Kritische User Journey |
---
## Edge Cases und Error Handling
### Besonders wichtige Grenzfälle
1. **Leerer Input** - Alle Funktionen sollten mit `''`, `null`, `undefined` umgehen
2. **Sehr langer Text** - Performance bei >10.000 Zeichen
3. **Verschachtelte Syntax** - `**#bold-hashtag**`, `[[@mention](/item/x)](url)`
4. **Unicode** - Emojis, RTL-Text, Sonderzeichen
5. **Malformed Markdown** - Ungeschlossene Tags: `**bold`, `[link(`
6. **XSS Vectors** - `<script>`, Event-Handler in Links
7. **Concurrent Tokens** - `#tag1#tag2` (ohne Leerzeichen)
8. **URLs in Code-Blöcken** - Sollten nicht konvertiert werden
---
## Test-Setup Empfehlungen
### Vitest Setup erweitern (`lib/setupTest.ts`)
```typescript
import '@testing-library/jest-dom'
import { vi } from 'vitest'
// TipTap DOM-Mocks für Vitest (basierend auf Community-Empfehlungen)
Range.prototype.getBoundingClientRect = () => ({
bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0,
toJSON: vi.fn(),
})
Range.prototype.getClientRects = () => ({
item: () => null, length: 0, [Symbol.iterator]: vi.fn(),
})
Document.prototype.elementFromPoint = vi.fn()
```
---
## Fazit
Die empfohlene Strategie fokussiert auf **Unit Tests für die Markdown-Utility-Funktionen**, da hier:
- Die meiste Geschäftslogik liegt
- Pure Functions einfach testbar sind
- Hohe Coverage mit geringem Aufwand erreichbar ist
E2E Tests sollten auf ein Minimum beschränkt bleiben und nur kritische User Journeys abdecken. Dies folgt der Testing Pyramid Best Practice und sorgt für:
- **Schnelles Feedback** (Unit Tests <1s)
- **Hohe Wartbarkeit** (keine flaky UI-Tests)
- **Gute Fehlerlokalisierung** (isolierte Tests)
---
## Quellen
- [TipTap Testing Discussion #4008](https://github.com/ueberdosis/tiptap/discussions/4008)
- [TipTap Jest Issue #5108](https://github.com/ueberdosis/tiptap/issues/5108)
- [Testing TipTap CodeSandbox](https://codesandbox.io/s/testing-tiptap-p0oomz)
- [TipTap Contributing Guide](https://tiptap.dev/docs/resources/contributing)

View File

@ -1,752 +0,0 @@
# TipTap Migration Testing Strategy
## Overview
This document outlines the comprehensive testing strategy for the TipTap Markdown migration. The strategy uses a combination of **Vitest** for pure function unit tests and **Cypress Component Testing** for TipTap-dependent components, leveraging the project's existing test infrastructure.
---
## Component Overview
The TipTap migration includes the following core components:
| Component | Description | Test Tool |
|-----------|-------------|-----------|
| `lib/src/Components/TipTap/utils/preprocessMarkdown.ts` | 6-stage preprocessing pipeline | Vitest |
| `lib/src/Components/TipTap/utils/simpleMarkdownToHtml.tsx` | Static HTML conversion | Vitest |
| `lib/src/Components/TipTap/extensions/Hashtag.tsx` | Custom extension with tokenizer | Cypress Component |
| `lib/src/Components/TipTap/extensions/ItemMention.tsx` | Custom extension with tokenizer | Cypress Component |
| `lib/src/Components/TipTap/extensions/VideoEmbed.tsx` | Block element for videos | Cypress Component |
| `lib/src/Components/Input/RichTextEditor.tsx` | Main editor component | Cypress Component |
| `lib/src/Components/Map/Subcomponents/ItemPopupComponents/TextView.tsx` | Read-only editor | Cypress Component |
| `lib/src/Components/Map/Subcomponents/ItemPopupComponents/TextViewStatic.tsx` | Lightweight static renderer | Vitest |
| `lib/src/Utils/ReplaceURLs.ts` | URL/email processing utilities | Vitest |
---
## Testing Pyramid Architecture
```
┌─────────────────────┐
│ E2E Tests │ 3-5 critical user journeys
│ (Cypress E2E) │
└──────────┬──────────┘
┌───────────────┴───────────────┐
│ Cypress Component Tests │ TipTap extensions + editors
│ Real browser, no mocking │ Contract tests
└───────────────┬───────────────┘
┌──────────────────────────┴──────────────────────────┐
│ Vitest Unit Tests │
│ preprocessMarkdown, simpleMarkdownToHtml │
│ Pure functions, security tests │
└──────────────────────────────────────────────────────┘
```
### Rationale
1. **Vitest Unit Tests for Pure Functions (Primary Focus)**
- `preprocessMarkdown.ts` and `simpleMarkdownToHtml.tsx` are **pure functions** without DOM dependencies
- Extremely fast execution, high coverage achievable
- Easy to maintain and debug
- Contains most of the **business logic** for markdown processing
- Includes **security/XSS tests** for HTML output
2. **Cypress Component Tests for TipTap Extensions**
- TipTap requires a **real browser environment** (jsdom mocking is fragile and incomplete)
- The project already has Cypress Component Testing configured (`lib/cypress.config.ts`)
- Real browser provides native support for `Range`, `Selection`, `ResizeObserver`, etc.
- Test Markdown ↔ JSON ↔ HTML roundtrips with actual TipTap editor
- **Contract tests** verify preprocessing output is valid TipTap input
3. **E2E Tests Only for Critical User Journeys**
- Uses existing Cypress E2E setup (`cypress/`)
- Leverages existing custom commands (`cy.clickMarker()`, `cy.waitForPopup()`)
- For smoke tests and regression protection
---
## Component Usage Context
Understanding where each component is used guides test priority:
| Context | Component | Rendering | Priority |
|---------|-----------|-----------|----------|
| **Map Popup** | `TextViewStatic` | `simpleMarkdownToHtml` (static HTML) | P0 - most visible |
| **Item Card** | `TextViewStatic` | `simpleMarkdownToHtml` (static HTML) | P0 - list views |
| **Item Profile** | `TextView` | TipTap editor (read-only) | P1 - detail view |
| **Item Edit Form** | `RichTextEditor` | TipTap editor (editable) | P0 - data integrity |
---
## Test Setup
### Vitest Configuration (`lib/setupTest.ts`)
For **pure function unit tests**, no TipTap-specific mocks are needed:
```typescript
import '@testing-library/jest-dom'
```
> **Note:** TipTap editor tests use Cypress Component Testing instead of Vitest to avoid fragile jsdom mocks. This provides a real browser environment where `Range`, `Selection`, `ResizeObserver`, and other DOM APIs work natively.
### Cypress Component Testing (`lib/cypress.config.ts`)
Already configured in the project:
```typescript
import { defineConfig } from 'cypress'
export default defineConfig({
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
specPattern: ['**/**/*.cy.{ts,tsx}'],
},
})
```
### Running Tests
```bash
# Vitest unit tests
cd lib && npm run test:unit
# Cypress component tests (interactive)
cd lib && npx cypress open --component
# Cypress component tests (headless)
cd lib && npx cypress run --component
# Cypress E2E tests
cd cypress && npm test
```
---
## Detailed Test Cases
### 1. Unit Tests: `preprocessMarkdown.ts`
#### A) `convertNakedUrls` (internal function)
| Category | Test Case | Input | Expected Output |
|----------|-----------|-------|-----------------|
| **Happy Path** | Basic URL | `"Check https://example.com out"` | `"Check [example.com](https://example.com) out"` |
| **Happy Path** | Remove www | `"https://www.example.com"` | `"[example.com](https://example.com)"` |
| **Happy Path** | Multiple URLs | `"https://a.com and https://b.com"` | Both converted |
| **Happy Path** | URL with query params | `"https://example.com?a=1&b=2"` | Full URL preserved in link |
| **Skip** | URL in markdown link | `"[link](https://example.com)"` | Unchanged |
| **Skip** | URL in autolink | `"<https://example.com>"` | Unchanged |
| **Edge** | URL at sentence end | `"Visit https://example.com."` | Dot NOT part of URL |
| **Edge** | URL in parentheses | `"(https://example.com)"` | Parentheses handled correctly |
| **Edge** | URL at line start | `"https://example.com is good"` | Converted correctly |
#### B) `preprocessVideoLinks`
| Category | Test Case | Input | Expected Output |
|----------|-----------|-------|-----------------|
| **Happy Path** | YouTube standard | `"<https://www.youtube.com/watch?v=abc123def45>"` | `<video-embed provider="youtube" video-id="abc123def45">` |
| **Happy Path** | YouTube short | `"<https://youtu.be/abc123def45>"` | Same as above |
| **Happy Path** | YouTube markdown link | `"[Video](https://youtube.com/watch?v=abc123def45)"` | Converted |
| **Happy Path** | Rumble embed | `"<https://rumble.com/embed/v1abc>"` | `<video-embed provider="rumble"...>` |
| **Edge** | Extra params | `"<https://youtube.com/watch?v=abc&t=120>"` | Only video-id extracted |
| **Edge** | Non-video link | `"<https://example.com>"` | Unchanged |
| **Edge** | Mixed content | `"Text <https://youtu.be/x> more"` | Only video converted |
| **Error** | Invalid video ID | `"[V](https://youtube.com/watch?v=)"` | Unchanged (no match) |
#### C) `preprocessHashtags`
| Category | Test Case | Input | Expected Output |
|----------|-----------|-------|-----------------|
| **Happy Path** | Simple hashtag | `"Hello #world"` | `"Hello <span data-hashtag data-label=\"world\">#world</span>"` |
| **Happy Path** | Multiple hashtags | `"#one #two #three"` | All converted |
| **Happy Path** | With numbers | `"#test123"` | Converted |
| **Happy Path** | With underscore | `"#my_tag"` | Converted |
| **Happy Path** | Unicode (umlauts) | `"#München"` | `data-label="München"` |
| **Happy Path** | Unicode (accents) | `"#café"` | Converted |
| **Skip** | Hashtag in link text | `"[#tag](#anchor)"` | Unchanged |
| **Skip** | Hashtag in link URL | `"[section](#section)"` | Unchanged |
| **Edge** | Concurrent hashtags | `"#tag1#tag2"` | Only `#tag1` converted (no space) |
| **Edge** | Hashtag only `#` | `"Just #"` | Unchanged |
| **Edge** | Hashtag with hyphen | `"#my-tag"` | Converted |
#### D) `preprocessItemMentions`
| Category | Test Case | Input | Expected Output |
|----------|-----------|-------|-----------------|
| **Happy Path** | Standard format | `"[@Alice](/item/abc-123)"` | `<span data-item-mention data-label="Alice" data-id="abc-123">@Alice</span>` |
| **Happy Path** | With layer (legacy) | `"[@Bob](/item/people/def-456)"` | `data-id="def-456"` extracted |
| **Happy Path** | Relative path | `"[@Name](item/uuid)"` | Converted |
| **Happy Path** | Multiple mentions | `"[@A](/item/1) and [@B](/item/2)"` | Both converted |
| **Happy Path** | UUID case-insensitive | `"[@Name](/item/ABC-DEF-123)"` | Converted |
| **Happy Path** | Label with spaces | `"[@Max Müller](/item/uuid)"` | Converted |
| **Skip** | Non-item link | `"[@Name](/other/path)"` | Unchanged |
| **Skip** | Regular link | `"[Name](/item/123)"` | Unchanged (no @) |
#### E) `truncateMarkdown`
| Category | Test Case | Input | Limit | Expected |
|----------|-----------|-------|-------|----------|
| **Happy Path** | Under limit | `"Short text"` | 100 | Unchanged |
| **Happy Path** | At limit | `"A".repeat(150)` | 100 | `"A".repeat(100) + "..."` |
| **Atomic** | Preserve hashtag | `"A".repeat(95) + " #tag"` | 100 | Complete `#tag` or cut before |
| **Atomic** | Preserve mention | `"A".repeat(90) + " [@X](/item/1)"` | 100 | Complete mention or cut before |
| **Atomic** | Preserve link | `"See [link](url) more"` | 8 | Complete link or cut before |
| **Edge** | Newlines don't count | `"Line1\n\nLine2"` | 10 | Newlines not counted |
| **Edge** | Empty text | `""` | 100 | `""` |
| **Edge** | Limit 0 | `"Text"` | 0 | `"..."` |
| **Error** | Negative limit | `"Text"` | -1 | No throw |
#### F) `removeMarkdownSyntax`
| Category | Test Case | Input | Expected |
|----------|-----------|-------|----------|
| **Happy Path** | Bold | `"**bold**"` | `"bold"` |
| **Happy Path** | Italic | `"*italic*"` | `"italic"` |
| **Happy Path** | Headers | `"# Heading"` | `"Heading"` |
| **Happy Path** | Links | `"[text](url)"` | `"text"` |
| **Happy Path** | Images | `"![alt](img.png)"` | `""` |
| **Happy Path** | Inline code | `` "`code`" `` | `"code"` |
| **Preserve** | Item mentions | `"[@Name](/item/x)"` | Preserved (contains @) |
| **Preserve** | Hashtags | `"#tag"` | Preserved |
#### G) Full Pipeline `preprocessMarkdown`
| Category | Test Case | Input | Expected Behavior |
|----------|-----------|-------|-------------------|
| **Happy Path** | Complete content | `"Check https://x.com #tag [@A](/item/1)"` | All transformations applied |
| **Edge** | Empty string | `""` | Returns `""` |
| **Edge** | Null input | `null` | Returns `""` (no throw) |
| **Edge** | Undefined input | `undefined` | Returns `""` (no throw) |
| **Edge** | Only whitespace | `" "` | Whitespace preserved |
| **Edge** | Very long text | `"A".repeat(10000)` | Completes without timeout |
| **Error** | Malformed markdown | `"[unclosed link"` | No throw |
| **Error** | Malformed URL | `"http:/broken"` | No throw |
---
### 2. Unit Tests: `simpleMarkdownToHtml.tsx`
| Category | Test Case | Input | Expected HTML |
|----------|-----------|-------|---------------|
| **Happy Path** | Bold | `"**bold**"` | `<strong>bold</strong>` |
| **Happy Path** | Italic | `"*italic*"` | `<em>italic</em>` |
| **Happy Path** | Inline code | `` "`code`" `` | `<code>code</code>` |
| **Happy Path** | External link | `"[text](https://x.com)"` | `<a href="..." target="_blank" rel="noopener noreferrer">` |
| **Happy Path** | Internal link | `"[profile](/profile)"` | `<a href="/profile">` (no target) |
| **Happy Path** | Headers H1-H6 | `"# Title"` ... `"###### Sub"` | Corresponding h1-h6 tags |
| **Happy Path** | Blockquote | `"> quote"` | `<blockquote>quote</blockquote>` |
| **Happy Path** | Paragraph break | `"Para1\n\nPara2"` | `</p><p>` |
| **Happy Path** | Line break | `"Line1\nLine2"` | `<br>` |
| **Happy Path** | Video embed | `<video-embed provider="youtube" video-id="abc">` | `<iframe src="...youtube-nocookie.com/embed/abc"` |
| **Happy Path** | Hashtag with color | Preprocessed hashtag + tag with `color: #ff0000` | `style="color: #ff0000"` |
| **Happy Path** | Item mention | Preprocessed mention + item in list | `<a href="/item/..." class="item-mention"` |
| **Edge** | Empty string | `""` | `""` |
| **Edge** | Unknown tag | Hashtag for unknown tag | `color: inherit` |
| **Edge** | Unknown item | Mention for unknown item | Fallback color |
| **Edge** | Consecutive newlines | `"\n\n\n\n"` | No excessive empty elements |
| **Security** | XSS script tag | `"<script>alert(1)</script>"` | `&lt;script&gt;` escaped |
| **Security** | XSS event handler | `"<img onerror=alert(1)>"` | Escaped |
| **Security** | Already escaped | `"&amp;"` | Preserved correctly |
---
### 3. Unit Tests: Dependency Functions (`ReplaceURLs.ts`)
The preprocessing pipeline depends on `fixUrls` and `mailRegex` from `lib/src/Utils/ReplaceURLs.ts`. These must be tested:
#### A) `fixUrls`
| Category | Test Case | Input | Expected Output |
|----------|-----------|-------|-----------------|
| **Happy Path** | Add https to naked domain | `"Visit example.com today"` | `"Visit https://example.com today"` |
| **Happy Path** | Preserve existing https | `"https://example.com"` | Unchanged |
| **Happy Path** | Preserve existing http | `"http://example.com"` | Unchanged |
| **Happy Path** | Multiple domains | `"a.com and b.org"` | Both get https:// |
| **Edge** | Domain with path | `"example.com/page"` | `"https://example.com/page"` |
| **Edge** | Domain with subdomain | `"sub.example.com"` | `"https://sub.example.com"` |
| **Skip** | Inside markdown link | `"[link](example.com)"` | Behavior depends on implementation |
#### B) `mailRegex`
| Category | Test Case | Input | Should Match |
|----------|-----------|-------|--------------|
| **Happy Path** | Simple email | `"test@example.com"` | ✓ |
| **Happy Path** | With subdomain | `"user@mail.example.com"` | ✓ |
| **Happy Path** | With plus | `"user+tag@example.com"` | ✓ |
| **Happy Path** | With dots | `"first.last@example.com"` | ✓ |
| **Happy Path** | Country TLD | `"user@example.co.uk"` | ✓ |
| **Edge** | Invalid - no @ | `"not-an-email"` | ✗ |
| **Edge** | Invalid - no domain | `"user@"` | ✗ |
| **Edge** | Invalid - no local | `"@example.com"` | ✗ |
---
### 4. Unit Tests: XSS Security (`xss.spec.ts`)
**Critical:** The `simpleMarkdownToHtml` function uses a tag restoration pattern that could be vulnerable to XSS. A dedicated security test suite is required.
#### XSS Attack Vectors
```typescript
const XSS_VECTORS = [
// Basic XSS
'<script>alert(1)</script>',
'<img src=x onerror=alert(1)>',
'<svg onload=alert(1)>',
'<body onload=alert(1)>',
// URL-based XSS
'[click](javascript:alert(1))',
'[click](data:text/html,<script>alert(1)</script>)',
'[click](vbscript:alert(1))',
// Tag restoration bypass attempts
'&lt;span data-hashtag onclick=alert(1)',
'&lt;video-embed onload=alert(1)',
'<span data-hashtag data-label="x" onclick="alert(1)">#x</span>',
// Attribute injection
'#tag" onclick="alert(1)',
'[@Name" onclick="alert(1)](/item/123)',
// Unicode/encoding escapes
'\\u003cscript\\u003ealert(1)\\u003c/script\\u003e',
'%3Cscript%3Ealert(1)%3C/script%3E',
]
```
| Category | Test Case | Verification |
|----------|-----------|--------------|
| **Script Tags** | `<script>alert(1)</script>` | No `<script` in output |
| **Event Handlers** | `onerror=`, `onload=`, `onclick=` | No `on*=` in output |
| **JavaScript URLs** | `javascript:alert(1)` | No `javascript:` in href |
| **Data URLs** | `data:text/html,...` | No `data:` in href |
| **Tag Restoration Bypass** | `&lt;span data-hashtag onclick=` | No `onclick` in output |
| **Attribute Injection** | `#tag" onclick="alert(1)` | Quotes properly escaped |
---
### 5. Cypress Component Tests: TipTap Extensions
> **Note:** TipTap extension tests use **Cypress Component Testing** instead of Vitest to leverage a real browser environment. This eliminates the need for fragile jsdom mocks.
#### Test Wrapper Pattern
```typescript
/// <reference types="cypress" />
import { mount } from 'cypress/react'
import { EditorContent, useEditor } from '@tiptap/react'
import { StarterKit } from '@tiptap/starter-kit'
import { Markdown } from '@tiptap/markdown'
import { Hashtag } from './Hashtag'
interface TestEditorProps {
content: string
tags?: { name: string; color: string }[]
onTagClick?: (tag: any) => void
editable?: boolean
}
function TestEditor({ content, tags = [], onTagClick, editable = false }: TestEditorProps) {
const editor = useEditor({
extensions: [
StarterKit,
Markdown,
Hashtag.configure({ tags, onTagClick }),
],
content,
contentType: 'markdown',
editable,
})
return <EditorContent editor={editor} />
}
```
#### A) Hashtag Extension
| Category | Test Case | Verification |
|----------|-----------|--------------|
| **Parse** | Markdown → JSON | `"#tag"``{ type: 'hashtag', attrs: { label: 'tag' } }` |
| **Serialize** | JSON → Markdown | Hashtag node → `"#tag"` |
| **HTML Parse** | `<span data-hashtag data-label="x">#x</span>` | Recognized as hashtag node |
| **HTML Render** | Node → HTML | Contains `data-hashtag`, `class="hashtag"` |
| **Tokenizer** | Start hint | `/(?<!\[)#[a-zA-Z]/` matches correctly |
| **Behavior** | Click in view mode | `onTagClick` callback fired |
| **Behavior** | Click in edit mode | No callback fired |
| **Styling** | Known tag | Applies tag color |
| **Styling** | Unknown tag | Uses `inherit` |
#### B) ItemMention Extension
| Category | Test Case | Verification |
|----------|-----------|--------------|
| **Parse** | Markdown → JSON | `"[@Name](/item/uuid)"` → correct node |
| **Parse** | With layer path | `"[@Name](/item/layer/uuid)"` → extracts uuid |
| **Parse** | Case-insensitive UUID | `"[@X](/item/ABC-DEF)"` → works |
| **Serialize** | JSON → Markdown | Node → `"[@Name](/item/uuid)"` |
| **Styling** | Known item | Uses `getItemColor()` |
| **Styling** | Unknown item | Uses `var(--color-primary)` |
| **Behavior** | Click in view mode | Navigates to `/item/{id}` |
| **Behavior** | Click in edit mode | No navigation |
#### C) VideoEmbed Extension
| Category | Test Case | Verification |
|----------|-----------|--------------|
| **Parse** | YouTube autolink | `"<https://youtube.com/watch?v=abc>"` → videoEmbed node |
| **Parse** | YouTube short | `"<https://youtu.be/abc>"` → videoEmbed node |
| **Parse** | Rumble | `"<https://rumble.com/embed/xyz>"` → videoEmbed node |
| **Serialize** | YouTube node → Markdown | `"<https://www.youtube.com/watch?v=abc>"` |
| **Render** | YouTube | iframe with `youtube-nocookie.com` |
| **Render** | Rumble | iframe with `rumble.com/embed/` |
| **Paste** | Paste YouTube URL | Video embed node inserted |
| **Paste** | Paste non-video URL | Normal text paste |
#### D) Roundtrip Tests (Critical)
| Test Case | Flow |
|-----------|------|
| **Simple text** | Markdown → Editor → `getMarkdown()` → identical |
| **With hashtag** | `"Hello #world"` → roundtrip → identical |
| **With mention** | `"Thanks [@Alice](/item/123)"` → roundtrip → identical |
| **With video** | `"<https://youtu.be/abc>"` → roundtrip → identical |
| **Complex** | Text + hashtag + mention + video → roundtrip → identical |
| **Formatting** | Bold, italic, lists → roundtrip → preserved |
#### E) Contract Tests (Critical)
Contract tests verify that the **output of `preprocessMarkdown()` is valid input for TipTap**. These catch integration failures that unit tests miss.
```typescript
// lib/src/Components/TipTap/__tests__/contracts.cy.tsx
/// <reference types="cypress" />
import { mount } from 'cypress/react'
import { EditorContent, useEditor } from '@tiptap/react'
import { StarterKit } from '@tiptap/starter-kit'
import { Markdown } from '@tiptap/markdown'
import { Hashtag, ItemMention, VideoEmbed } from '../extensions'
import { preprocessMarkdown } from '../utils/preprocessMarkdown'
function ContractTestEditor({ rawContent }: { rawContent: string }) {
const preprocessed = preprocessMarkdown(rawContent)
const editor = useEditor({
extensions: [StarterKit, Markdown, Hashtag.configure({ tags: [] }), ItemMention, VideoEmbed],
content: preprocessed,
})
return <EditorContent editor={editor} data-testid="editor" />
}
describe('Preprocessing → TipTap Contract', () => {
it('preprocessed hashtag renders correctly', () => {
mount(<ContractTestEditor rawContent="#nature" />)
cy.get('.hashtag').should('contain', '#nature')
})
it('preprocessed mention renders correctly', () => {
mount(<ContractTestEditor rawContent="[@Alice](/item/123-abc)" />)
cy.get('.item-mention').should('contain', '@Alice')
})
it('preprocessed video renders as iframe', () => {
mount(<ContractTestEditor rawContent="<https://youtu.be/abc123>" />)
cy.get('iframe').should('have.attr', 'src').and('include', 'youtube-nocookie.com')
})
it('complex content renders all elements', () => {
mount(<ContractTestEditor rawContent="Hello #world with [@Bob](/item/456) and <https://youtu.be/xyz>" />)
cy.get('.hashtag').should('exist')
cy.get('.item-mention').should('exist')
cy.get('iframe').should('exist')
})
})
```
| Test Case | Verification |
|-----------|--------------|
| **Hashtag contract** | Preprocessed `#tag` → TipTap renders `.hashtag` element |
| **Mention contract** | Preprocessed `[@Name](/item/id)` → TipTap renders `.item-mention` |
| **Video contract** | Preprocessed `<https://youtu.be/x>` → TipTap renders `iframe` |
| **Complex contract** | All three together render correctly |
| **Empty content** | Empty string doesn't crash |
| **Malformed content** | Unclosed markdown doesn't crash |
---
### 6. Cypress Component Tests: Editor Components
#### RichTextEditor (`RichTextEditor.cy.tsx`)
| Category | Test Case | Verification |
|----------|-----------|--------------|
| **Render** | With default value | Editor displays content |
| **Callback** | Type text | `updateFormValue` receives markdown |
| **Placeholder** | Empty editor | Placeholder visible |
| **Suggestion** | Type `#` | Hashtag suggestion popup appears |
| **Suggestion** | Type `@` | Item mention suggestion popup appears |
| **Keyboard** | Arrow keys in suggestions | Navigation works |
| **Keyboard** | Enter in suggestions | Item selected |
| **Keyboard** | Escape | Popup closes |
| **New tag** | Select "Create #newTag" | `addTag` called, node inserted |
#### TextView (`TextView.cy.tsx`)
| Category | Test Case | Verification |
|----------|-----------|--------------|
| **Render** | With text | TipTap editor in read-only mode |
| **Empty** | `text=""` | Returns `null` |
| **Null** | `text=null` | Returns `null` |
| **Undefined** | `text=undefined` | Shows login prompt |
| **Truncation** | `truncate=true` | Text ends with `...` |
| **Hashtag** | Click hashtag | `addFilterTag` called |
| **Link** | Click internal link | React Router navigation |
| **Link** | Click external link | Opens new tab |
---
### 7. Vitest Component Tests: Static Renderer
#### TextViewStatic (`TextViewStatic.spec.ts`)
> **Note:** `TextViewStatic` does not use TipTap - it renders HTML directly via `simpleMarkdownToHtml`. Therefore it uses **Vitest** instead of Cypress Component Testing.
| Category | Test Case | Verification |
|----------|-----------|--------------|
| **Render** | With text | HTML rendered via `dangerouslySetInnerHTML` |
| **Empty** | `text=""` | Returns `null` |
| **Undefined** | `text=undefined` | Shows login prompt |
| **Truncation** | `truncate=true` | Truncated to ~100 chars |
| **Hashtag** | Click hashtag | `addFilterTag` called |
| **Hashtag** | Color applied | Tag color from tags array |
| **Mention** | Rendered as link | `<a href="/item/...">` |
| **Video** | Rendered as iframe | YouTube nocookie embed |
| **Security** | XSS vectors | All escaped (see XSS test suite) |
---
### 8. E2E Tests (Minimal - Critical Journeys Only)
```typescript
// cypress/e2e/tiptap/rich-text.cy.ts
describe('Rich Text Editor - Critical Flows', () => {
beforeEach(() => {
cy.login()
cy.visit('/')
cy.waitForMapReady()
})
it('creates item with hashtags and mentions, verifies rendering', () => {
// 1. Create new item
cy.get('[data-cy="create-item-button"]').click()
// 2. Enter rich content
cy.get('.ProseMirror').type('Project about #nature with [@Alice]')
cy.get('[data-cy="suggestion-list"]').contains('Alice').click()
// 3. Save
cy.get('[data-cy="save-button"]').click()
cy.wait('@saveItem')
// 4. Verify popup rendering
cy.get('[data-cy="item-popup"]').should('be.visible')
cy.get('.hashtag').should('contain', '#nature')
cy.get('.item-mention').should('contain', '@Alice')
})
it('embeds video from pasted URL', () => {
cy.get('[data-cy="create-item-button"]').click()
cy.get('.ProseMirror').type('Check this: ')
// Paste video URL
cy.get('.ProseMirror').invoke('val', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ')
.trigger('paste')
cy.get('.video-embed-wrapper iframe').should('be.visible')
})
it('filters by hashtag when clicked in popup', () => {
// Navigate to item with hashtag
cy.clickMarker()
cy.waitForPopup()
// Click hashtag
cy.get('.hashtag').first().click()
// Verify filter applied
cy.get('[data-cy="active-filters"]').should('be.visible')
})
it('navigates to mentioned item when clicked', () => {
// Navigate to item with mention
cy.clickMarker()
cy.waitForPopup()
// Click mention
cy.get('.item-mention').first().click()
// Verify navigation
cy.url().should('include', '/item/')
})
it('preserves markdown through edit-save cycle', () => {
// Create item with formatting
cy.get('[data-cy="create-item-button"]').click()
cy.get('.ProseMirror').type('**Bold** and *italic* with #tag')
cy.get('[data-cy="save-button"]').click()
// Edit item
cy.get('[data-cy="edit-button"]').click()
// Verify markdown preserved
cy.get('.ProseMirror strong').should('contain', 'Bold')
cy.get('.ProseMirror em').should('contain', 'italic')
cy.get('.ProseMirror .hashtag').should('contain', '#tag')
})
})
```
---
## Edge Cases & Error Handling
### Critical Edge Cases
All functions should handle these edge cases gracefully:
1. **Empty/Null Input** - `''`, `null`, `undefined` should not throw
2. **Very Long Text** - Performance check with 10,000+ characters
3. **Nested Syntax** - `**#bold-hashtag**`, `*[@mention](/item/x)*`
4. **Concurrent Tokens** - `#tag1#tag2` (no space between)
5. **Unicode** - Emojis 🎉, RTL text, umlauts (München), accents (café)
6. **Malformed Markdown** - Unclosed tags: `**bold`, `[link(`
7. **URLs in Code Blocks** - Should NOT be converted
8. **XSS Vectors** - `<script>`, event handlers in links, `javascript:` URLs
9. **Special Characters** - `<`, `>`, `&`, `"`, `'` in content must be escaped
---
## Implementation Plan
### Phase 1: Vitest Unit Tests - Highest Priority
| File | Test Count | Purpose |
|------|------------|---------|
| `preprocessMarkdown.spec.ts` | ~45 tests | Pure function preprocessing pipeline |
| `simpleMarkdownToHtml.spec.ts` | ~25 tests | HTML conversion, basic security |
| `xss.spec.ts` | ~15 tests | Comprehensive XSS attack vectors |
| `ReplaceURLs.spec.ts` | ~10 tests | Dependency functions (fixUrls, mailRegex) |
**Deliverable:** Core transformation logic + security fully covered
### Phase 2: Cypress Component Tests - TipTap Extensions
| File | Test Count | Purpose |
|------|------------|---------|
| `Hashtag.cy.tsx` | ~12 tests | Parse, style, behavior |
| `ItemMention.cy.tsx` | ~12 tests | Parse, style, behavior |
| `VideoEmbed.cy.tsx` | ~8 tests | Parse, render |
| `contracts.cy.tsx` | ~6 tests | Preprocessing → TipTap integration |
**Deliverable:** All TipTap extensions verified in real browser
### Phase 3: Component Tests - Editors
| File | Tool | Test Count | Purpose |
|------|------|------------|---------|
| `RichTextEditor.cy.tsx` | Cypress | ~10 tests | Full editor with suggestions |
| `TextView.cy.tsx` | Cypress | ~6 tests | Read-only TipTap |
| `TextViewStatic.spec.ts` | Vitest | ~12 tests | Static HTML renderer (no TipTap) |
**Deliverable:** All render paths tested
### Phase 4: E2E Tests - Lowest Priority
| File | Test Count | Purpose |
|------|------------|---------|
| `rich-text.cy.ts` | 5 tests | Critical user journeys |
**Deliverable:** End-to-end user flows verified
---
## File Structure
```
lib/src/Components/TipTap/
├── utils/
│ ├── preprocessMarkdown.ts
│ ├── preprocessMarkdown.spec.ts # Vitest - pure functions
│ ├── simpleMarkdownToHtml.tsx
│ ├── simpleMarkdownToHtml.spec.ts # Vitest - pure functions
│ └── xss.spec.ts # Vitest - security tests
├── extensions/
│ ├── Hashtag.tsx
│ ├── Hashtag.cy.tsx # Cypress Component
│ ├── ItemMention.tsx
│ ├── ItemMention.cy.tsx # Cypress Component
│ ├── VideoEmbed.tsx
│ └── VideoEmbed.cy.tsx # Cypress Component
└── __tests__/
└── contracts.cy.tsx # Cypress Component - contract tests
lib/src/Components/Input/
├── RichTextEditor.tsx
└── RichTextEditor.cy.tsx # Cypress Component
lib/src/Components/Map/Subcomponents/ItemPopupComponents/
├── TextView.tsx
├── TextView.cy.tsx # Cypress Component
├── TextViewStatic.tsx
└── TextViewStatic.spec.ts # Vitest - no TipTap dependency
lib/src/Utils/
└── ReplaceURLs.spec.ts # Vitest - dependency tests
cypress/e2e/tiptap/
└── rich-text.cy.ts # Cypress E2E
```
---
## Summary
### Test Distribution
| Test Type | Tool | Test Count | Coverage Target |
|-----------|------|------------|-----------------|
| **Vitest Unit Tests** | Vitest | ~95 | >90% for utility functions |
| **Cypress Component Tests** | Cypress | ~54 | >80% for TipTap components |
| **Vitest Component Tests** | Vitest | ~12 | >80% for TextViewStatic |
| **E2E Tests** | Cypress | 5 | Critical paths only |
### Key Principles
1. **Pure functions in Vitest** - `preprocessMarkdown` and `simpleMarkdownToHtml` are fast, isolated tests
2. **TipTap components in Cypress** - Real browser avoids fragile jsdom mocks
3. **Contract tests are critical** - Verify preprocessing output works with TipTap
4. **Roundtrip tests are critical** - Markdown → Editor → Markdown must be lossless
5. **Minimal E2E** - Only critical user journeys to avoid flaky tests
6. **XSS prevention** - Dedicated security test suite is mandatory
### Benefits of This Strategy
- **Fast Feedback** - Vitest unit tests execute in <1 second
- **Reliable TipTap Tests** - Cypress Component uses real browser (no mocking)
- **High Maintainability** - Clear separation between Vitest and Cypress responsibilities
- **Good Error Localization** - Isolated tests pinpoint failures
- **Security Coverage** - Dedicated XSS test suite catches vulnerabilities
---
## References
- [Cypress Component Testing](https://docs.cypress.io/guides/component-testing/overview)
- [TipTap Testing Discussion #4008](https://github.com/ueberdosis/tiptap/discussions/4008)
- [TipTap Jest Issue #5108](https://github.com/ueberdosis/tiptap/issues/5108)
- [TipTap Markdown Extension Docs](https://tiptap.dev/docs/extensions/extensions/markdown)