CRM / ERP (Enterprise)
The biggest example. Parent-child features, cross-domain composition, and the patterns that hold up when your suite hits 500+ tests.
Folder structure
examples/crm-erp/
├── features/
│ ├── customers/
│ │ ├── list/{4 files}
│ │ ├── create/{4 files}
│ │ ├── edit/{4 files}
│ │ └── shared/ # cross-sub-feature components
│ ├── invoices/
│ │ ├── list/{4 files}
│ │ ├── generate/{4 files}
│ │ └── status-flow/{4 files}
│ └── reports/
│ ├── ar-aging/{4 files}
│ └── monthly-summary/{4 files}
├── tests/
│ └── integration/ # cross-feature scenarios
├── utils/ # money, dates, ID generators
└── types/ # shared domain typesEach leaf folder (list/, create/, etc.) is a complete CDAT slice. Sub-features live under their parent domain.
Parent-child composition
A common enterprise scenario: “create a customer, generate an invoice for them, mark it paid”. Three features collaborate:
import { CustomerCreateActions } from '../features/customers/create/actions';
import { InvoiceGenerateActions } from '../features/invoices/generate/actions';
import { InvoiceStatusFlowActions } from '../features/invoices/status-flow/actions';
test('full customer-to-paid-invoice flow', async ({ page }) => {
const customerCreate = new CustomerCreateActions(page);
const invoiceGen = new InvoiceGenerateActions(page);
const invoiceStatus = new InvoiceStatusFlowActions(page);
const customerId = await customerCreate.createCustomer(SAMPLE_CUSTOMER);
const invoiceId = await invoiceGen.generateForCustomer(customerId, INVOICE_DATA);
await invoiceStatus.markAsPaid(invoiceId);
await expect(page).toHaveURL(new RegExp(`invoices/${invoiceId}`));
});Notice: each Action returns the data the next step needs (customerId, invoiceId). Actions don’t mutate hidden state; they pass values explicitly.
Domain types live in types/
examples/crm-erp/types/ holds types used across multiple features:
// types/customer.ts
export interface Customer {
id: string;
name: string;
email: string;
taxId?: string;
}
// features/customers/create/data.ts
import type { Customer } from '../../../types/customer';
export type { Customer };
export const SAMPLE_CUSTOMER: Omit<Customer, 'id'> = {
name: 'ACME Corp',
email: '[email protected]',
taxId: '1234567890',
};data.ts per feature re-exports the cross-feature type and adds feature-specific fixtures. No duplication, but each feature still owns its scenarios.
Cross-feature shared/ folders
Inside customers/, the shared/ folder holds components used by multiple sub-features:
features/customers/
├── list/
├── create/
├── edit/
└── shared/
├── CustomerNameField.ts # used by create + edit
└── CustomerSearchBox.ts # used by list + create + editUse shared/ sparingly. If you’re putting more than 3 components in there, split into a real sub-feature.
Run it locally
git clone https://github.com/darco81/cdat-pattern.git
cd cdat-pattern/examples/crm-erp
pnpm install
pnpm exec playwright install
pnpm testWhen to use this layout
- >50 features in one application
- 3+ teams contributing tests to the same repo
- Parent-child domain hierarchy mirrors the application’s bounded contexts
For smaller projects, /examples/e-commerce is enough.
Next steps
- /docs/architecture - when to nest features
- /docs/anti-patterns - global state, shared/ overuse, locator drift