ワンクリックで
ui4-convert-tests
// Use when UI changes are complete and e2e tests need updating. Analyzes what changed in UI components and systematically finds/fixes affected tests.
// Use when UI changes are complete and e2e tests need updating. Analyzes what changed in UI components and systematically finds/fixes affected tests.
Manually invoked skill for reskinning Payload UI components. Requires Figma URL. Usage: /ui4
Review UI4 CSS migrations for proper token usage. Checks that CSS variables are used instead of hardcoded values.
Use when CI tests fail on main branch after PR merge, when investigating flaky test failures, or when user provides a PR URL/number to aggregate all failing tests
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.
Use when fixing dependency vulnerabilities, running pnpm audit, or when the audit-dependencies CI check fails
Use when new translation keys are added to packages to generate new translations strings
| name | ui4-convert-tests |
| description | Use when UI changes are complete and e2e tests need updating. Analyzes what changed in UI components and systematically finds/fixes affected tests. |
After completing UI changes, this skill systematically identifies and fixes affected e2e tests. It analyzes the diff to understand what kind of changes were made (not just which files), then finds tests that need updates.
Goal: Understand the nature of your changes to predict test impact.
# Get changed UI files
git diff main --name-only -- 'packages/ui/src/**/*.tsx' 'packages/ui/src/**/*.css'
For each changed file, categorize the changes:
git diff main -- <file> | grep -E '^\-.*className|^\-.*id=|^\+.*className|^\+.*id='
Look for components being:
git diff main -- <file> | grep -E 'Popup|PopupList|Drawer|Dropdown'
# Translation keys
git diff main -- <file> | grep -E "t\('|i18n\.t\("
# Hardcoded text
git diff main -- <file> | grep -E 'placeholder=|aria-label='
Build a change summary:
| Change Type | What Changed | Test Impact |
|---|---|---|
| Selector | .btn:has-text("Create") → #create-new-doc | Update locators |
| Structure | Button moved into popup | Add popup open step |
| Text | "Search by ID" → "Search" | Update assertions |
Search strategy: Cast a wide net, then narrow down.
# Search for component name references (not just selectors)
grep -rn "QueryPreset\|query-preset\|preset" test/**/*.ts --include="*.spec.ts" --include="*.ts"
# Search for specific selectors from Step 1
grep -rn "\.list-header\|Create New\|#create-new" test/**/*.ts
Key test locations:
| Pattern | Where to Look |
|---|---|
| Component-specific | test/<feature>/e2e.spec.ts |
| Shared helpers | test/<feature>/helpers/*.ts |
| Cross-cutting | test/__helpers/e2e/*.ts |
| Multiple features using same component | Search ALL test dirs |
Don't just search for exact selectors! Also search for:
QueryPreset, ListHeader)preset, filter, search)"Create New", "Search by")Before fixing, understand the test:
If multiple tests use the same selector, create/update a helper:
// test/<feature>/helpers/togglePreset.ts
export async function openCreatePreset(page: Page) {
await page.click('#select-preset') // Open popup first
await page.click('#create-new-preset')
}
This centralizes the fix and prevents future duplication.
| Change Type | Fix Strategy |
|---|---|
| Selector renamed | Direct string replacement |
| Element moved to popup | Add click to open popup before clicking element |
| Element moved to drawer | Add drawer open/close handling |
| Text simplified | Update assertion to match new text |
| Element removed | Rework test logic or delete test |
| Props changed | Update attribute assertions |
| Conditional rendering | May need to set up state before element appears |
Run tests BEFORE making fixes to confirm they actually fail:
# Use isolated port to avoid conflicts
PORT=3150 pnpm test:e2e <suite> --max-failures=1
# Run specific test by name
PORT=3150 pnpm test:e2e <suite> -g "test name" --max-failures=1
Document failure patterns:
Timeout waiting for locator('.old-selector') → Selector changedlocator resolved to 0 elements → Element moved or removedexpected "New Text" received "Old Text" → Text content changedPriority: Fix helpers first, then individual tests.
// Before
await page.click('.list-header .btn:has-text("Create")')
// After - prefer IDs when available
await page.click('#create-new-doc')
// Before - direct click
await page.click('#edit-preset')
// After - open popup first
await page.click('#select-preset') // Opens the popup
await page.click('#edit-preset') // Now visible in popup
// Before - specific placeholder text
await expect(input).toHaveAttribute('placeholder', /(Search by ID)/)
// After - simplified text
await expect(input).toHaveAttribute('placeholder', 'Search')
When the same interaction is needed in multiple tests:
// test/<feature>/helpers/interactions.ts
export async function openEditPreset(page: Page) {
await page.click('#select-preset')
await page.click('#edit-preset')
}
// In tests - import and use
import { openEditPreset } from './helpers/interactions.js'
await openEditPreset(page)
# Run same tests that failed
PORT=3150 pnpm test:e2e <suite> --max-failures=1
Only commit after tests pass.
Symptom: Test times out waiting for button that used to be directly visible.
Detection: Check if buttons were wrapped in <Popup> or <PopupList>:
git diff main -- <file> | grep -E 'PopupList|Popup'
Fix: Add popup trigger click before clicking the button:
// Before: Button was directly in toolbar
await page.click('#edit-preset')
// After: Button is now inside a popup
await page.click('#select-preset') // Opens popup
await page.click('#edit-preset') // Now visible
Bonus: If multiple tests need this, create a helper function.
Symptom: .some-class or :has-text("Button Text") no longer finds element.
Detection: Component added id= attribute:
git diff main -- <file> | grep -E '^\+.*id='
Fix: Use the more stable ID:
// Before: Fragile class + text selector
await page.click('.list-header .btn:has-text("Create New")')
// After: Stable ID selector
await page.click('#create-new-doc')
Symptom: Assertion fails with expected "New Text" received "Old Text".
Detection: Translation key or hardcoded text changed:
git diff main -- <file> | grep -E 'placeholder=|t\('
Fix: Update assertion to match new text:
// Before: Verbose placeholder
await expect(input).toHaveAttribute('placeholder', /(Search by ID, Title)/)
// After: Simplified
await expect(input).toHaveAttribute('placeholder', 'Search')
Symptom: Several tests in different suites fail with similar selector issues.
Detection:
# Find all tests using the old selector
grep -rn "old-selector\|.old-class" test/**/*.ts
Fix:
test/<feature>/helpers/When you change a shared component (like ListControls, QueryPresetBar), multiple test suites may be affected.
Detection:
# Find component name references across all tests
grep -rn "QueryPreset\|ListControl" test/**/*.ts | cut -d: -f1 | sort -u
Common cross-suite components:
ListControls → affects any list view testsQueryPresetBar → query-presets, group-by, admin testsSearch → i18n, admin, most collection testsButton → nearly everything| Component | Common Selectors |
|---|---|
| Search | .search-filter__input, #search-filter-input |
| List View | .collection-list, tbody tr, .table-row |
| Popup | .popup__content, .popup-button-list__button |
| Modal | dialog, [id^=doc-drawer_], [id^=list-drawer_] |
| Buttons | .btn, button[type="button"] |
| Query Presets | #select-preset, .query-preset-bar__* |
# Run all e2e tests for a test suite (auto-starts dev server)
PORT=3150 pnpm test:e2e <suite-name>
# Run specific test file
PORT=3150 pnpm test:e2e test/<suite>/e2e.spec.ts
# Run with headed browser (see what's happening)
PORT=3150 pnpm test:e2e:headed test/<suite>/e2e.spec.ts
# Run in debug mode (step through)
PORT=3150 pnpm test:e2e:debug test/<suite>/e2e.spec.ts
# Run specific test by name pattern
PORT=3150 pnpm test:e2e test/<suite>/e2e.spec.ts -g "test name pattern"
# Stop on first failure (useful during debugging)
PORT=3150 pnpm test:e2e test/<suite>/e2e.spec.ts --max-failures=1
Note: The pnpm test:e2e command automatically:
Pick a unique port in the 3100-3199 range:
# Run tests on an isolated port (MongoDB auto-starts its own in-memory server)
PORT=3150 pnpm test:e2e query-presets --max-failures=1
That's it for MongoDB (default). Each test run starts its own in-memory MongoDB server, so no database conflicts occur.
For Postgres tests, use a unique database per worktree/repo:
# Create a unique database for this worktree
PGPASSWORD=payload psql -h localhost -p 5433 -U payload -c "CREATE DATABASE payload_worktree1;"
# Run tests against that database
POSTGRES_URL="postgres://payload:payload@localhost:5433/payload_worktree1" \
PORT=3150 pnpm test:e2e query-presets --max-failures=1
Or use the custom schema approach (no separate DB needed):
# Tests will use a separate schema within the same database
PAYLOAD_DATABASE=postgres-custom-schema PORT=3150 pnpm test:e2e query-presets
| Scenario | Port Conflict? | DB Conflict? |
|---|---|---|
| MongoDB in-memory | Yes (same port) | No (each run has own server) |
| Postgres | Yes (same port) | Yes (same tables) |
| Multiple worktrees | Yes | Yes (Postgres only) |
Solution: Always set PORT to avoid port conflicts. For Postgres, also isolate the database.
Dev server not running or wrong port:
Tests read PORT env var (default 3000). Two approaches:
Option A: Use isolated port (preferred for parallel test suites)
# Run tests on custom port - script handles everything
PORT=3105 pnpm test:e2e test/query-presets/e2e.spec.ts
Option B: Kill existing ports and use default
# Kill all dev server ports
lsof -ti:3000,3001,3002,3003,3004,3005,3006,3007,3008,3009 | xargs kill -9 2>/dev/null
# Tests use 3000 by default
pnpm test:e2e test/query-presets/e2e.spec.ts
Wrong test suite running: Each test suite (fields, query-presets, localization, etc.) has its own Payload config. Tests will fail or behave unexpectedly if the wrong dev server is running.
Not running tests before fixing: Always verify tests actually fail before making changes. If a test passes, don't change it.
Not checking helper files:
Test helpers in test/*/helpers/ often contain shared selectors that affect multiple tests.
Missing popup interactions: When elements move into popups, tests need to open the popup first.
Forgetting confirmation dialogs: Delete actions often add confirmation modals - tests need to handle the confirm step.
Placeholder text changes: Search placeholders, button labels, and other text content may change.
Modal slug mismatches:
When deleting/confirming actions, the modal slug may change. Check the component code for the actual slug prop passed to <Modal> or drawer components.
Old structure (chips):
// Direct buttons visible
await page.click('#create-new-preset')
await page.click('#edit-preset')
await page.click('#delete-preset')
await page.click('.chip__remove') // clear
New structure (popup dropdown):
// Open popup first
await page.click('#select-preset')
// Then click menu items
await page.click('.popup-button-list__button:has-text("Create New")')
await page.click('.popup-button-list__button:has-text("Edit")')
await page.click('.popup-button-list__button:has-text("Delete")')
// Clear uses dedicated button
await page.click('.query-preset-bar__clear')