.claude/skills/pom-patterns/SKILL.md
Page Object Model patterns for Cypress tests with hierarchical structure. Covers BasePOM, DashboardEntityPOM, entity POMs, and selector integration. Use this skill when creating new POMs or extending existing ones.
npx skillsauth add NextSpark-js/nextspark pom-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Patterns and tools for creating Page Object Models (POMs) for Cypress tests.
contents/themes/{theme}/tests/cypress/src/
├── core/
│ ├── BasePOM.ts # Base class for all POMs
│ └── DashboardEntityPOM.ts # Entity POM base (CRUD operations)
├── entities/
│ ├── TasksPOM.ts # Entity-specific POM
│ ├── PostsPOM.ts # Entity-specific POM
│ └── {Entity}POM.ts # Pattern: extend DashboardEntityPOM
├── features/
│ └── {Feature}POM.ts # Non-entity feature POMs
├── selectors.ts # Re-exports from lib/selectors.ts
└── fixtures/
└── entities.json # Auto-generated entity config
BasePOM
│
├── DashboardEntityPOM (entities with CRUD)
│ ├── TasksPOM
│ ├── PostsPOM
│ └── {Entity}POM
│
└── {Feature}POM (non-entity features)
├── AuthPOM
├── OnboardingPOM
└── SettingsPOM
Location: contents/themes/{theme}/tests/cypress/src/core/BasePOM.ts
Provides:
{index}, {id})this)cy() helper method for cleaner selectors// Key methods
export abstract class BasePOM {
// Get Cypress chainable by selector
protected cy(selector: string): Cypress.Chainable<JQuery<HTMLElement>> {
return cy.get(`[data-cy="${selector}"]`)
}
// Replace placeholders in selector patterns
protected replacePattern(pattern: string, replacements: Record<string, string>): string {
let result = pattern
for (const [key, value] of Object.entries(replacements)) {
result = result.replace(`{${key}}`, value)
}
return result
}
}
Location: contents/themes/{theme}/tests/cypress/src/core/DashboardEntityPOM.ts
Extends BasePOM with:
visitList(), visitCreate(), visitEdit())getTableRow(), selectRow(), selectAllRows())getField(), fillField())interceptList(), interceptCreate(), interceptUpdate(), interceptDelete())executeBulkAction())changeStatus())export class DashboardEntityPOM extends BasePOM {
protected entitySlug: string
constructor(entitySlug: string) {
super()
this.entitySlug = entitySlug
}
// Navigation
visitList(): this { /* ... */ return this }
visitCreate(): this { /* ... */ return this }
visitEdit(id: string): this { /* ... */ return this }
// Table operations
getTableRow(index: number): Cypress.Chainable { /* ... */ }
clickTableRow(index: number): this { /* ... */ return this }
// API Interceptors
interceptList(alias: string = 'getList'): this { /* ... */ return this }
interceptCreate(alias: string = 'postCreate'): this { /* ... */ return this }
// Form operations
fillField(fieldName: string, value: string): this { /* ... */ return this }
}
Location: contents/themes/{theme}/tests/cypress/src/entities/{Entity}POM.ts
import { DashboardEntityPOM } from '../core/DashboardEntityPOM'
import { cySelector } from '../selectors'
import entitiesConfig from '../fixtures/entities.json'
// Form interface based on entity fields
interface TaskFormData {
title?: string
description?: string
priority?: string
status?: string
}
export class TasksPOM extends DashboardEntityPOM {
constructor() {
// Use entity slug from config
super(entitiesConfig.entities.tasks.slug)
}
// Factory pattern (MANDATORY)
static create(): TasksPOM {
return new TasksPOM()
}
// Entity-specific selectors
get elements() {
return {
// Use cySelector for all selectors
priorityFilter: cySelector('entities.tasks.filters.priority'),
statusBadge: cySelector('entities.tasks.list.statusBadge'),
}
}
// Entity-specific form filling
fillTaskForm(data: TaskFormData): this {
if (data.title) this.fillField('title', data.title)
if (data.description) this.fillField('description', data.description)
if (data.priority) this.selectField('priority', data.priority)
if (data.status) this.selectField('status', data.status)
return this
}
// Entity-specific workflows
createTask(data: TaskFormData): this {
return this
.visitCreate()
.interceptCreate()
.fillTaskForm(data)
.submitForm()
.waitForCreate()
}
}
CRITICAL: Always use cySelector() from the selectors module, never hardcode selectors.
import { cySelector, sel } from '../selectors'
class MyPOM extends BasePOM {
get elements() {
return {
// ✅ CORRECT - Use cySelector
loginForm: cySelector('auth.login.form'),
submitButton: cySelector('auth.login.submit'),
// For dynamic selectors with replacements
tableRow: (id: string) => cySelector('entities.tasks.row', { id }),
faqItem: (index: number) => cySelector('blocks.faqAccordion.item', { index: String(index) }),
}
}
// ❌ WRONG - Never hardcode selectors
// get loginForm() { return cy.get('[data-cy="login-form"]') }
}
// Core selectors (from CORE_SELECTORS)
cySelector('auth.login.form') // Login form
cySelector('auth.login.submit') // Submit button
cySelector('dashboard.sidebar') // Sidebar
cySelector('common.toast.success') // Success toast
// Entity selectors (from CORE_SELECTORS)
cySelector('entities.{entity}.list.table') // Entity table
cySelector('entities.{entity}.list.row') // Table row (needs {id})
cySelector('entities.{entity}.form.submit') // Form submit
// Block selectors (from BLOCK_SELECTORS)
cySelector('blocks.hero.container') // Hero block
cySelector('blocks.faqAccordion.item', { index: '0' }) // FAQ item
CRITICAL: Use interceptors for deterministic waits, never cy.wait(timeout).
class TasksPOM extends DashboardEntityPOM {
// Intercept before action
interceptList(alias: string = 'getTasksList'): this {
cy.intercept('GET', `/api/v1/entities/${this.entitySlug}*`).as(alias)
return this
}
// Wait after action
waitForList(alias: string = 'getTasksList'): this {
cy.wait(`@${alias}`)
return this
}
// Combined pattern
loadList(): this {
return this
.interceptList()
.visitList()
.waitForList()
}
}
All methods that perform actions should return this for chaining.
class TasksPOM extends DashboardEntityPOM {
// ✅ CORRECT - Returns this for chaining
fillTitle(value: string): this {
cy.get(this.elements.titleInput).type(value)
return this
}
// ❌ WRONG - Breaks the chain
fillTitle(value: string): void {
cy.get(this.elements.titleInput).type(value)
}
}
// Usage with fluent interface
TasksPOM.create()
.visitCreate()
.interceptCreate()
.fillTitle('My Task')
.fillDescription('Description')
.submitForm()
.waitForCreate()
.assertSuccessToast()
Location: contents/themes/{theme}/tests/cypress/fixtures/entities.json
Auto-generated fixture containing entity configurations:
{
"entities": {
"tasks": {
"slug": "tasks",
"singular": "Task",
"plural": "Tasks",
"tableName": "tasks",
"fields": {
"title": { "type": "text", "required": true },
"description": { "type": "textarea" },
"priority": { "type": "select", "options": ["low", "medium", "high"] },
"status": { "type": "select", "options": ["draft", "active", "completed"] }
},
"filters": ["status", "priority"]
}
}
}
Usage in POM:
import entitiesConfig from '../fixtures/entities.json'
class TasksPOM extends DashboardEntityPOM {
constructor() {
super(entitiesConfig.entities.tasks.slug)
}
get entityConfig() {
return entitiesConfig.entities.tasks
}
}
# Generate a new entity POM
python3 .claude/skills/pom-patterns/scripts/generate-pom.py \
--entity products \
--theme default
# Preview without writing
python3 .claude/skills/pom-patterns/scripts/generate-pom.py \
--entity products \
--theme default \
--dry-run
# Specify custom fields
python3 .claude/skills/pom-patterns/scripts/generate-pom.py \
--entity products \
--theme default \
--fields "title,description,price,category,status"
| Method | Description |
|--------|-------------|
| visitList() | Navigate to entity list page |
| visitCreate() | Navigate to create form |
| visitEdit(id) | Navigate to edit form |
| visitView(id) | Navigate to detail view |
| Method | Description |
|--------|-------------|
| getTableRow(index) | Get row by index |
| clickTableRow(index) | Click row by index |
| selectRow(index) | Select row checkbox |
| selectAllRows() | Select all rows |
| getRowCount() | Get number of rows |
| Method | Description |
|--------|-------------|
| fillField(name, value) | Fill text field |
| selectField(name, value) | Select dropdown option |
| checkField(name) | Check checkbox |
| submitForm() | Click submit button |
| Method | Description |
|--------|-------------|
| assertSuccessToast() | Assert success toast visible |
| assertErrorToast() | Assert error toast visible |
| assertOnListPage() | Assert on list page |
| assertOnCreatePage() | Assert on create page |
| assertRowExists(text) | Assert table row with text |
| Method | Description |
|--------|-------------|
| interceptList() | Intercept GET list request |
| interceptCreate() | Intercept POST create request |
| interceptUpdate() | Intercept PATCH update request |
| interceptDelete() | Intercept DELETE request |
| waitForList() | Wait for list response |
| waitForCreate() | Wait for create response |
// ❌ NEVER: Hardcoded selectors
cy.get('[data-cy="task-title"]')
// ✅ CORRECT: Use cySelector
cy.get(cySelector('entities.tasks.form.title'))
// ❌ NEVER: Fixed timeouts
cy.wait(3000)
// ✅ CORRECT: Use interceptors
this.interceptCreate().submitForm().waitForCreate()
// ❌ NEVER: Direct page visits without interceptor
visitList() {
cy.visit('/dashboard/tasks')
}
// ✅ CORRECT: Intercept before navigation
loadList() {
return this.interceptList().visitList().waitForList()
}
// ❌ NEVER: Breaking fluent interface
fillTitle(value: string): void { ... }
// ✅ CORRECT: Return this
fillTitle(value: string): this { ... return this }
// ❌ NEVER: Missing factory pattern
const pom = new TasksPOM()
// ✅ CORRECT: Use factory
const pom = TasksPOM.create()
Before finalizing a POM:
static create())this (fluent interface)cySelector() for all selectors (no hardcoded strings){Entity}POM.tsdevelopment
Zod validation patterns for this Next.js application. Covers schema definition, API validation, form integration, error formatting, and type inference. Use this skill when implementing validation for APIs, forms, or entity schemas.
development
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
testing
Test coverage metrics and registry system for this Next.js application. Covers FEATURE_REGISTRY, FLOW_REGISTRY, TAGS_REGISTRY, and coverage metrics interpretation. Use this skill when evaluating test coverage, identifying gaps, or planning testing priorities.
development
TanStack Query (React Query) patterns for data fetching in this Next.js application. Covers useQuery, useMutation, optimistic updates, cache invalidation, and anti-patterns. Use this skill when implementing data fetching or state management with server data.