utopia-ui/docs/Test_Strategy.md

32 KiB

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:

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:

import { defineConfig } from 'cypress'

export default defineConfig({
  component: {
    devServer: {
      framework: 'react',
      bundler: 'vite',
    },
    specPattern: ['**/**/*.cy.{ts,tsx}'],
  },
})

Running Tests

# 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
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

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

/// <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.

// 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)

// 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