# 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 | `""` | 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 | `""` | `` | | **Happy Path** | YouTube short | `""` | Same as above | | **Happy Path** | YouTube markdown link | `"[Video](https://youtube.com/watch?v=abc123def45)"` | Converted | | **Happy Path** | Rumble embed | `""` | `` | | **Edge** | Extra params | `""` | Only video-id extracted | | **Edge** | Non-video link | `""` | Unchanged | | **Edge** | Mixed content | `"Text 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 #world"` | | **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)"` | `@Alice` | | **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**"` | `bold` | | **Happy Path** | Italic | `"*italic*"` | `italic` | | **Happy Path** | Inline code | `` "`code`" `` | `code` | | **Happy Path** | External link | `"[text](https://x.com)"` | `` | | **Happy Path** | Internal link | `"[profile](/profile)"` | `` (no target) | | **Happy Path** | Headers H1-H6 | `"# Title"` ... `"###### Sub"` | Corresponding h1-h6 tags | | **Happy Path** | Blockquote | `"> quote"` | `
quote
` | | **Happy Path** | Paragraph break | `"Para1\n\nPara2"` | `

` | | **Happy Path** | Line break | `"Line1\nLine2"` | `
` | | **Happy Path** | Video embed | `` | `