E2E Selector Drift After Design Token CSS Refactor
E2E Selector Drift After Design Token CSS Refactor
Problem
After a design-token refactor that replaced hardcoded Tailwind classes with semantic CSS custom properties, 52 out of 142 E2E tests failed. Every spec file was affected.
Symptoms:
waitForAppReady()hangs indefinitely (looking fortext=Pagesheading that no longer exists)- Selectors like
.bg-blue-600,.bg-gray-800,.text-red-500find 0 elements textarea[placeholder="Start writing..."]finds nothing (editor changed from textarea to TipTap/ProseMirror)- Tests expecting a “Back” button fail (two-panel layout replaced single-panel navigation)
Investigation
Steps Tried
- Ran full E2E suite - 52 failures across all spec files. Not a single-test issue.
- Compared selectors to actual UI - Read each component’s source to map old selectors to new ones. Found systematic
CSS class renaming:
bg-blue-600→bg-accent-basebg-gray-800→bg-bg-elevatedtext-red-500→text-status-dangertext=Pagesheading → removed entirely (sidebar usesdata-tourattributes)
- Fixed centralized selectors first - Updated
test-base.tsselectors object, which fixed most tests - Found hardcoded selectors in spec files - Some tests had inline selectors that bypassed the central
selectorsobject - Found structural UI changes - Editor changed from
<textarea>to ProseMirror, layout changed from single-panel with Back button to persistent two-panel
Root Cause
Design-token refactor changed CSS class names without updating E2E selectors. The refactor (commit 097d2f0)
replaced raw Tailwind utility classes with semantic design tokens:
| Old (Tailwind literal) | New (Design token) |
|---|---|
bg-blue-600 | bg-accent-base |
bg-gray-800 | bg-bg-elevated |
text-red-500 | text-status-danger |
bg-black | bg-black/50 (with opacity) |
Additionally, structural UI changes weren’t reflected in tests:
- Editor:
<textarea>→ TipTap ProseMirror (.ProseMirror) - Navigation: Back button → persistent sidebar (two-panel layout)
- Sidebar:
<h2>Pages</h2>heading → no heading, usesdata-tourattributes
Solution
Central Selector Updates (test-base.ts)
// Beforesidebar: 'text=Pages',pageTree: 'text=Pages',commandPaletteItem: '.bg-blue-600, [class*="bg-blue"]',commandPaletteBackdrop: '.fixed.inset-0.bg-black',importDialog: '.bg-gray-800.rounded-lg:has(h2:has-text("Import"))',errorMessage: 'text=Error, .text-red-500',
// Aftersidebar: '[data-tour="new-page"]',pageTree: '[data-tour="page-tree"]',commandPaletteItem: '[class*="bg-accent-base"]:not([data-tour])',commandPaletteBackdrop: '.fixed.inset-0[class*="bg-black"]',importDialog: '.bg-bg-elevated:has(h2:has-text("Import"))',errorMessage: 'text=Error, [class*="text-status-danger"]',Key Technique: :not() Exclusion for Ambiguous Selectors
The [class*="bg-accent-base"] selector matched both command palette results AND the sidebar’s ”+ New Page” button.
Fixed with :not([data-tour]) since only sidebar elements have data-tour attributes.
Structural Test Updates (page.spec.ts)
// Before: textarea-based editorconst editor = page.locator('textarea[placeholder="Start writing..."]');await editor.fill("content");
// After: TipTap/ProseMirror editorconst editor = page.locator('.ProseMirror');await editor.click();await page.keyboard.type("content");
// Before: single-panel with Back buttonconst backButton = page.locator('button:has-text("Back")');await backButton.click();
// After: two-panel layout (sidebar always visible)const pageTree = page.locator('[data-tour="page-tree"]');await expect(pageTree).toBeVisible();Implementation Notes
[class*="..."]attribute selectors match Tailwind utility classes even after purgingdata-tourattributes serve double duty: tour targeting AND stable test selectorswaitForAppReady()changed fromtext=Pagesto[data-tour="new-page"]- a structural element guaranteed to exist when the app is ready- ProseMirror requires
click()+keyboard.type()instead of.fill()for text input
Prevention
Best Practices
- Use
data-tourordata-testidattributes as primary E2E selectors. These are stable across CSS refactors. Thedata-tourattributes in this codebase already serve this purpose. - Centralize all selectors in
test-base.ts. Never hardcode selectors in spec files. The 52→9 failure reduction came from fixingtest-base.ts; the remaining 9 were hardcoded in specs. - Run E2E tests after any CSS/design refactor. Even “cosmetic” changes can break test selectors.
- Use
[class*="..."]for Tailwind class matching instead of exact class selectors (.bg-blue-600). This is more resilient to class ordering changes, though still breaks on renames.
Warning Signs
- E2E selectors that reference raw Tailwind classes (
bg-blue-600,text-red-500) waitForAppReady()relying on text content instead of structural elements- Tests referencing specific UI patterns (textarea, Back button) that could change with component library updates
- Selectors in spec files that aren’t using the centralized
selectorsobject
Selector Stability Hierarchy (Most → Least Stable)
data-testid/data-tourattributes- ARIA roles (
getByRole) - Semantic HTML (
h1,input[type="text"]) - CSS attribute selectors (
[class*="bg-accent"]) - Exact CSS classes (
.bg-blue-600) - avoid - Text content (
text=Pages) - avoid for structural elements
Additional Patterns: Mock Command & Dialog State
Command Name Mismatch
Mock commands must match actual Tauri invoke() calls exactly. Read the UI source to verify:
// Wrong: mock uses different name than UIcase "analyze_import_source": ...
// Right: matches the actual invoke callcase "analyze_import": ...Warning sign: [Tauri Mock] Unknown command in test logs.
Multi-Step Dialog Selectors
Dialogs that change state (e.g., “Import Content” → “Import Preview”) need separate selectors per step:
importDialog: '.bg-bg-elevated:has(h2:has-text("Import"))',importPreviewDialog: '.bg-bg-elevated:has(h2:has-text("Import Preview"))',Wait for the new state after triggering a transition:
await analyzeButton.click();const previewDialog = page.locator(selectors.importPreviewDialog);await expect(previewDialog).toBeVisible({ timeout: 5000 });References
- Commit:
aa298b6on branchmatt/ink-50-create-page-does-not-persist-to-filesystem - Design token refactor: commit
097d2f0 - Linear: INK-51
- Files:
test-base.ts,command-palette.spec.ts,first-launch.spec.ts,page.spec.ts
Path Canonicalization for Safe Cleanup Operations Next
E2E Test Robustness Patterns - Condition-Based Waits and Error Simulation
Was this page helpful?
Thanks for your feedback!