
Umbraco E2e Testing
- 276 installs
- 26 repo stars
- Updated August 1, 2026
- umbraco/umbraco-cms-backoffice-skills
Helps with testing & qa tasks.
About
umbraco-e2e-testing is a Claude Code skill for testing & qa. It helps solo builders move faster with AI-assisted coding.
- umbraco-e2e-testing
- Testing & QA
- AI-coding skill
Umbraco E2e Testing by the numbers
- 276 all-time installs (skills.sh)
- +12 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #741 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/umbraco/umbraco-cms-backoffice-skills --skill umbraco-e2e-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 276 |
|---|---|
| repo stars | ★ 26 |
| Last updated | August 1, 2026 |
| Repository | umbraco/umbraco-cms-backoffice-skills ↗ |
What it does
Helps with testing & qa tasks.
Files
Umbraco E2E Testing
End-to-end testing for Umbraco backoffice extensions using Playwright and @umbraco/playwright-testhelpers. This approach tests against a real running Umbraco instance, validating complete user workflows.
Critical: Use Testhelpers for Core Umbraco
Use @umbraco/playwright-testhelpers for core Umbraco operations:
| Package | Purpose | Why Required |
|---|---|---|
@umbraco/playwright-testhelpers | UI and API helpers | Handles auth, navigation, core entity CRUD |
@umbraco/json-models-builders | Test data builders | Creates valid Umbraco entities with correct structure |
Why use testhelpers for core Umbraco?
- Umbraco uses
data-markinstead ofdata-testid- testhelpers handle this - Auth token management is complex - testhelpers manage
STORAGE_STAGE_PATH - API setup/teardown requires specific payload formats - builders ensure correctness
- Selectors change between versions - testhelpers abstract these away
// WRONG - Raw Playwright for core Umbraco (brittle)
await page.goto('/umbraco');
await page.fill('[name="email"]', 'admin@example.com');
// CORRECT - Testhelpers for core Umbraco
import { test } from '@umbraco/playwright-testhelpers';
test('my test', async ({ umbracoApi, umbracoUi }) => {
await umbracoUi.goToBackOffice();
await umbracoUi.login.enterEmail('admin@example.com');
});When to Use Raw Playwright
For custom extensions, use umbracoUi.page (raw Playwright) because testhelpers don't know about your custom elements:
test('my custom extension', async ({ umbracoUi }) => {
// Testhelpers for core navigation
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.settings);
// Raw Playwright for YOUR custom elements
await umbracoUi.page.getByRole('link', { name: 'My Custom Item' }).click();
await expect(umbracoUi.page.locator('my-custom-workspace')).toBeVisible();
});| Use Testhelpers For | Use umbracoUi.page For |
|---|---|
| Login/logout | Custom tree items |
| Navigate to ANY section (including custom) | Custom workspace elements |
| Create/edit documents via API | Custom entity actions |
| Built-in UI interactions | Custom UI components |
When to Use
- Testing complete user workflows
- Testing data persistence
- Testing authentication/authorization
- Acceptance testing before release
- Integration testing with real API responses
Related Skills
- umbraco-testing - Master skill for testing overview
- umbraco-playwright-testhelpers - Full reference for the testhelpers package
- umbraco-test-builders - JsonModels.Builders for test data
- umbraco-mocked-backoffice - Test without real backend (faster)
Documentation
- Playwright: https://playwright.dev/docs/intro
- Reference tests:
Umbraco-CMS/tests/Umbraco.Tests.AcceptanceTest
---
Setup
Dependencies
Add to package.json:
{
"devDependencies": {
"@playwright/test": "^1.56",
"@umbraco/playwright-testhelpers": "^17.0.15",
"@umbraco/json-models-builders": "^2.0.42",
"dotenv": "^16.3.1"
},
"scripts": {
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
}
}Then run:
npm install
npx playwright install chromiumVersion Compatibility: Match testhelpers to your Umbraco version:
| Umbraco | Testhelpers |
|---|---|
| 17.1.x (pre-release) | 17.1.0-beta.x |
| 17.0.x | ^17.0.15 |
| 14.x | ^14.x |
Configuration
Create playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export const STORAGE_STATE = join(__dirname, 'tests/e2e/.auth/user.json');
// CRITICAL: Testhelpers read auth tokens from this file
process.env.STORAGE_STAGE_PATH = STORAGE_STATE;
export default defineConfig({
testDir: './tests/e2e',
timeout: 30 * 1000,
expect: { timeout: 5000 },
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: process.env.CI ? 'line' : 'html',
use: {
baseURL: process.env.UMBRACO_URL || 'https://localhost:44325',
trace: 'retain-on-failure',
ignoreHTTPSErrors: true,
// CRITICAL: Umbraco uses 'data-mark' not 'data-testid'
testIdAttribute: 'data-mark',
},
projects: [
{
name: 'setup',
testMatch: '**/*.setup.ts',
},
{
name: 'e2e',
testMatch: '**/*.spec.ts',
dependencies: ['setup'],
use: {
...devices['Desktop Chrome'],
ignoreHTTPSErrors: true,
storageState: STORAGE_STATE,
},
},
],
});Critical Settings
| Setting | Value | Why Required |
|---|---|---|
testIdAttribute | 'data-mark' | Umbraco uses data-mark, not data-testid |
STORAGE_STAGE_PATH | Path to user.json | Testhelpers read auth tokens from this file |
ignoreHTTPSErrors | true | For local dev with self-signed certs |
Without `testIdAttribute: 'data-mark'`, all `getByTestId()` calls will fail.
Authentication Setup
Create tests/e2e/auth.setup.ts:
import { test as setup } from '@playwright/test';
import { STORAGE_STATE } from '../../playwright.config';
import { ConstantHelper, UiHelpers } from '@umbraco/playwright-testhelpers';
setup('authenticate', async ({ page }) => {
const umbracoUi = new UiHelpers(page);
await umbracoUi.goToBackOffice();
await umbracoUi.login.enterEmail(process.env.UMBRACO_USER_LOGIN!);
await umbracoUi.login.enterPassword(process.env.UMBRACO_USER_PASSWORD!);
await umbracoUi.login.clickLoginButton();
await umbracoUi.login.goToSection(ConstantHelper.sections.settings);
await page.context().storageState({ path: STORAGE_STATE });
});Environment Variables
Create .env (add to .gitignore):
UMBRACO_URL=https://localhost:44325
UMBRACO_USER_LOGIN=admin@example.com
UMBRACO_USER_PASSWORD=yourpassword
UMBRACO_DATA_PATH=/path/to/Umbraco.Web.UI/App_Data # Optional: for data reset| Variable | Required | Purpose |
|---|---|---|
UMBRACO_URL | Yes | Backoffice URL |
UMBRACO_USER_LOGIN | Yes | Admin email |
UMBRACO_USER_PASSWORD | Yes | Admin password |
UMBRACO_DATA_PATH | No | App_Data path for test data reset (see "Testing with Persistent Data") |
Directory Structure
my-extension/
├── src/
│ └── ...
├── tests/
│ └── e2e/
│ ├── .auth/
│ │ └── user.json # Auth state (gitignored)
│ ├── auth.setup.ts # Authentication
│ └── my-extension.spec.ts
├── playwright.config.ts
├── .env # Gitignored
├── .env.example
└── package.json---
Patterns
Test Fixtures
import { test } from '@umbraco/playwright-testhelpers';
test('my test', async ({ umbracoApi, umbracoUi }) => {
// umbracoApi - API helpers for setup/teardown
// umbracoUi - UI helpers for backoffice interaction
});AAA Pattern (Arrange-Act-Assert)
test('can create content', async ({ umbracoApi, umbracoUi }) => {
// Arrange - Setup via API
await umbracoApi.documentType.createDefaultDocumentType('TestDocType');
// Act - Perform user actions via UI
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.clickActionsMenuAtRoot();
// Assert - Verify results
expect(await umbracoApi.document.doesNameExist('TestContent')).toBeTruthy();
});Idempotent Cleanup
test.afterEach(async ({ umbracoApi }) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});API Helpers (umbracoApi)
Document Types:
await umbracoApi.documentType.createDefaultDocumentType('TestDocType');
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(
'TestDocType', 'Textstring', dataTypeData.id
);
await umbracoApi.documentType.ensureNameNotExists('TestDocType');Documents:
await umbracoApi.document.createDefaultDocument('TestContent', docTypeId);
await umbracoApi.document.createDocumentWithTextContent(
'TestContent', docTypeId, 'value', 'Textstring'
);
await umbracoApi.document.publish(contentId);
await umbracoApi.document.ensureNameNotExists('TestContent');Data Types:
const dataType = await umbracoApi.dataType.getByName('Textstring');
await umbracoApi.dataType.create('MyType', 'Umbraco.TextBox', 'Umb.PropertyEditorUi.TextBox', []);Using Builders for Complex Data
For complex test data, use @umbraco/json-models-builders:
import { DocumentTypeBuilder, DocumentBuilder } from '@umbraco/json-models-builders';
test('create complex document type', async ({ umbracoApi }) => {
// Build a document type with multiple properties
const docType = new DocumentTypeBuilder()
.withName('Article')
.withAlias('article')
.addGroup()
.withName('Content')
.addTextBoxProperty()
.withAlias('title')
.withLabel('Title')
.done()
.addRichTextProperty()
.withAlias('body')
.withLabel('Body')
.done()
.done()
.build();
await umbracoApi.documentType.create(docType);
});Why use builders?
- Fluent API makes complex structures readable
- Ensures valid payload structure for Umbraco API
- Handles required fields and defaults
- Type-safe in TypeScript
UI Helpers (umbracoUi)
Navigation:
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName('My Page');Testing Custom Trees in Sidebar
When testing custom tree extensions (e.g., in Settings), use this pattern to handle async loading and scrolling:
test('should click custom tree item', async ({ umbracoUi }) => {
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.settings);
// 1. Wait for your tree heading (custom trees often at bottom of sidebar)
await umbracoUi.page.getByRole('heading', { name: 'My Tree' }).waitFor({ timeout: 15000 });
// 2. Scroll into view (important - sidebar may be long)
await umbracoUi.page.getByRole('heading', { name: 'My Tree' }).scrollIntoViewIfNeeded();
// 3. Wait for tree items to load (async from API)
const item1Link = umbracoUi.page.getByRole('link', { name: 'Item 1' });
await item1Link.waitFor({ timeout: 15000 });
// 4. Click the item
await item1Link.click();
// Assert workspace loads
await expect(umbracoUi.page.locator('my-tree-workspace-editor')).toBeVisible({ timeout: 15000 });
});Why this pattern?
- Custom trees are often at the bottom of the Settings sidebar
- Tree items load asynchronously from your API
- Using
getByRole('link', { name: '...' })is more reliable than genericumb-tree-itemselectors - Built-in trees (Document Types, etc.) also use
umb-tree-item, causing selector conflicts
Content Actions:
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType('TestDocType');
await umbracoUi.content.enterContentName('My Page');
await umbracoUi.content.enterTextstring('My text value');
await umbracoUi.content.clickSaveButton();Constants:
import { ConstantHelper } from '@umbraco/playwright-testhelpers';
ConstantHelper.sections.content
ConstantHelper.sections.settings
ConstantHelper.buttons.save
ConstantHelper.buttons.saveAndPublish---
Examples
Complete Test
import { expect } from '@playwright/test';
import { ConstantHelper, NotificationConstantHelper, test } from '@umbraco/playwright-testhelpers';
const contentName = 'TestContent';
const documentTypeName = 'TestDocType';
const dataTypeName = 'Textstring';
const contentText = 'Test content text';
test.afterEach(async ({ umbracoApi }) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test('can create content', { tag: '@smoke' }, async ({ umbracoApi, umbracoUi }) => {
// Arrange
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(
documentTypeName, dataTypeName, dataTypeData.id
);
// Act
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.enterTextstring(contentText);
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.waitForContentToBeCreated();
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value).toBe(contentText);
});
test('can publish content', async ({ umbracoApi, umbracoUi }) => {
// Arrange
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
const docTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(
documentTypeName, dataTypeName, dataTypeData.id
);
await umbracoApi.document.createDocumentWithTextContent(
contentName, docTypeId, contentText, dataTypeName
);
// Act
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.clickActionsMenuForContent(contentName);
await umbracoUi.content.clickPublishActionMenuOption();
await umbracoUi.content.clickConfirmToPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(
NotificationConstantHelper.success.published
);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe('Published');
});Working Example: tree-example
The tree-example demonstrates E2E testing for a custom tree extension:
Location: umbraco-backoffice/examples/tree-example/Client/
# Run E2E tests (requires running Umbraco)
URL=https://localhost:44325 \
UMBRACO_USER_LOGIN=admin@example.com \
UMBRACO_USER_PASSWORD=yourpassword \
npm run test:e2e # 7 testsKey files:
tests/playwright.e2e.config.ts- E2E configuration with auth setuptests/auth.setup.ts- Authentication using testhelperstests/tree-e2e.spec.ts- Tests for custom tree in Settings sidebar
Working Example: notes-wiki (Full-Stack with Data Reset)
The notes-wiki demonstrates E2E testing with persistent data and CRUD operations:
Location: umbraco-backoffice/examples/notes-wiki/Client/
# Run E2E tests (with data reset)
URL=https://localhost:44325 \
UMBRACO_USER_LOGIN=admin@example.com \
UMBRACO_USER_PASSWORD=yourpassword \
UMBRACO_DATA_PATH=/path/to/App_Data \
npm run test:e2e # 16 testsKey files:
tests/playwright.e2e.config.ts- Config withglobalSetupfor data resettests/global-setup.ts- Resets data to seed state before teststests/test-seed-data.json- Known test data (notes, folders)tests/notes-wiki-e2e.spec.ts- CRUD and navigation tests
What it demonstrates:
- Testing a custom section using
goToSection('notes') - Resetting file-based data before each test run
- Testing tree navigation, folders, and workspaces
- Entity actions via "View actions" button (more reliable than right-click)
- Dashboard and workspace view testing
---
Testing Extensions with Persistent Data
When your extension persists data (JSON files, database, etc.), tests need predictable starting state.
Global Setup Pattern
Add globalSetup to reset data before tests:
playwright.e2e.config.ts:
export default defineConfig({
// ... other config
globalSetup: './global-setup.ts',
});global-setup.ts:
import { FullConfig } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
async function globalSetup(config: FullConfig) {
const dataPath = process.env.UMBRACO_DATA_PATH;
if (!dataPath) {
console.warn('⚠️ UMBRACO_DATA_PATH not set. Skipping data reset.');
return;
}
const targetFile = path.join(dataPath, 'MyExtension/data.json');
const seedFile = path.join(__dirname, 'test-seed-data.json');
// Ensure directory exists
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
// Copy seed data to target
fs.copyFileSync(seedFile, targetFile);
console.log('🌱 Reset data to seed state');
}
export default globalSetup;test-seed-data.json:
{
"items": [
{ "id": "test-1", "name": "Test Item 1" },
{ "id": "test-2", "name": "Test Item 2" }
]
}Environment Variable
Add UMBRACO_DATA_PATH to locate your Umbraco's App_Data folder:
UMBRACO_DATA_PATH=/path/to/Umbraco.Web.UI/App_Data npm run test:e2e---
Testing Custom Sections
Custom sections work with testhelpers' goToSection() method - pass the section pathname:
// Section pathname - matches what you defined in section/constants.ts
const MY_SECTION = 'my-section';
// Helper to navigate to custom section using testhelpers
async function goToMySection(umbracoUi: any) {
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(MY_SECTION);
await umbracoUi.page.waitForTimeout(500);
}
test('should navigate to custom section', async ({ umbracoUi }) => {
await goToMySection(umbracoUi);
// Assert - your dashboard or tree should be visible
await expect(umbracoUi.page.getByText('Welcome')).toBeVisible({ timeout: 15000 });
});
// To verify the section exists in the section bar:
test('should display my section', async ({ umbracoUi }) => {
await umbracoUi.goToBackOffice();
await expect(umbracoUi.page.getByRole('tab', { name: 'My Section' })).toBeVisible({ timeout: 15000 });
});---
Context Menu (Entity Actions) Testing
Testing entity actions on tree items. Uses umbracoUi.page since testhelpers don't cover custom entity actions.
Important: Entity actions in Umbraco are rendered as buttons inside the dropdown menu, not as menuitem roles directly. The most reliable approach is to use the "View actions" button rather than right-click:
test('should show delete action via actions button', async ({ umbracoUi }) => {
await goToMySection(umbracoUi);
// Wait for tree item
const itemLink = umbracoUi.page.getByRole('link', { name: 'My Item' });
await itemLink.waitFor({ timeout: 15000 });
// Hover to reveal action buttons
await itemLink.hover();
// Click the "View actions" button to open dropdown
const actionsButton = umbracoUi.page.getByRole('button', { name: "View actions for 'My Item'" });
await actionsButton.click();
// Wait for dropdown and check for actions (actions are BUTTONS, not menuitems!)
await umbracoUi.page.waitForTimeout(500);
const deleteButton = umbracoUi.page.getByRole('button', { name: 'Delete' });
const renameButton = umbracoUi.page.getByRole('button', { name: 'Rename' });
// Assert - at least one action should be visible
await expect(deleteButton.or(renameButton)).toBeVisible({ timeout: 5000 });
});
test('should delete item via actions menu', async ({ umbracoUi }) => {
await goToMySection(umbracoUi);
const itemLink = umbracoUi.page.getByRole('link', { name: 'Item to Delete' });
await itemLink.waitFor({ timeout: 15000 });
// Hover and open actions menu
await itemLink.hover();
await umbracoUi.page.getByRole('button', { name: "View actions for 'Item to Delete'" }).click();
// Click delete button
await umbracoUi.page.getByRole('button', { name: 'Delete' }).click();
// Confirm deletion (if modal appears)
const confirmButton = umbracoUi.page.getByRole('button', { name: /Confirm|Delete/i });
if (await confirmButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await confirmButton.click();
}
// Assert - item should be gone
await expect(itemLink).not.toBeVisible({ timeout: 15000 });
});Alternative: Right-Click Context Menu
Right-click also works but the actions button approach is more reliable:
// Right-click approach (less reliable than actions button)
await itemLink.click({ button: 'right' });
await umbracoUi.page.waitForTimeout(500);
await umbracoUi.page.getByRole('button', { name: 'Delete' }).click();---
CRUD Testing Patterns
For custom extensions, use umbracoUi.page for UI interactions. For core Umbraco content, prefer umbracoApi helpers for setup/teardown.
Create via Actions Menu (Custom Extension)
test('should create new item', async ({ umbracoUi }) => {
await goToMySection(umbracoUi);
// Hover over parent folder and use Create button
const folderLink = umbracoUi.page.getByRole('link', { name: 'Parent Folder' });
await folderLink.hover();
// Scope to specific menu to avoid ambiguity with multiple items
const folderMenu = umbracoUi.page.getByRole('menu').filter({ hasText: 'Parent Folder' });
const createButton = folderMenu.getByRole('button', { name: 'Create Note' });
await createButton.click();
// Assert - workspace should open with "New" indicator
await expect(umbracoUi.page.locator('my-workspace-editor')).toBeVisible({ timeout: 15000 });
});Scoping to Specific Tree Items
When multiple tree items exist with similar elements, scope selectors to avoid ambiguity:
// WRONG - ambiguous when multiple folders have "Create" buttons
const createButton = page.getByRole('button', { name: 'Create' });
// CORRECT - scoped to specific folder's menu
const folderMenu = page.getByRole('menu').filter({ hasText: 'My Folder' });
const createButton = folderMenu.getByRole('button', { name: 'Create' });Update and Save
test('should update item', async ({ umbracoUi }) => {
await goToMySection(umbracoUi);
// Navigate to item
await umbracoUi.page.getByRole('link', { name: 'Test Item' }).click();
await umbracoUi.page.locator('my-workspace-editor').waitFor({ timeout: 15000 });
// Update field
const titleInput = umbracoUi.page.locator('uui-input#title');
await titleInput.clear();
await titleInput.fill('Updated Title');
// Save
await umbracoUi.page.getByRole('button', { name: /Save/i }).click();
// Wait for save to complete
await umbracoUi.page.waitForTimeout(2000);
// Assert - header should reflect change
await expect(umbracoUi.page.getByText('Updated Title')).toBeVisible();
});---
Running Tests
# Run all E2E tests
npm run test:e2e
# Run with UI mode (visual debugging)
npm run test:e2e:ui
# Run specific test file
npx playwright test tests/e2e/my-extension.spec.ts
# Run with specific tag
npx playwright test --grep "@smoke"
# Run in debug mode
npx playwright test --debug---
Troubleshooting
getByTestId() not finding elements
Ensure testIdAttribute: 'data-mark' is set in playwright.config.ts.
Authentication fails
- Check
.envcredentials are correct - Ensure Umbraco instance is running
- Verify
STORAGE_STAGE_PATHis set
Tests timeout
- Increase timeouts in config
- Ensure Umbraco is responsive
- Check for JS errors in browser console
Tests fail in CI
- Ensure Umbraco instance is accessible
- Set environment variables in CI
- Use
npx playwright install chromium
---
Alternative: MSW Mode (No Backend Required)
For faster testing without a real Umbraco backend, use the mocked backoffice approach.
Invoke: skill: umbraco-mocked-backoffice
| Aspect | Real Backend (this skill) | MSW Mode |
|---|---|---|
| Setup | Running Umbraco instance | Clone Umbraco-CMS, npm install |
| Auth | Required | Not required |
| Speed | Slower | Faster |
| Use case | Integration/acceptance | UI/component testing |
# Dependencies
node_modules/
# Playwright
playwright/.auth/
playwright-report/
test-results/
{
"name": "@example/e2e-document-type-crud",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@example/e2e-document-type-crud",
"version": "1.0.0",
"dependencies": {
"@umbraco/json-models-builders": "^2.0.42",
"@umbraco/playwright-testhelpers": "17.1.0-beta.4",
"tslib": "^2.4.0"
},
"devDependencies": {
"@playwright/test": "^1.49.0",
"typescript": "^5.7.2"
}
},
"node_modules/@playwright/test": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
"integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.57.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@umbraco/json-models-builders": {
"version": "2.0.42",
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.42.tgz",
"integrity": "sha512-5Zh/dSBGSKD9s0soemNnd5qT2h4gsKfTmQou/X34kqELSln333XMMfg+rbHfMleDwSBxh4dWAulntQFsfX0VtA==",
"license": "MIT",
"dependencies": {
"camelize": "^1.0.1"
}
},
"node_modules/@umbraco/playwright-testhelpers": {
"version": "17.1.0-beta.4",
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.1.0-beta.4.tgz",
"integrity": "sha512-WIPLQkzLYUpRPIdnienQKaNThAzQHUG6UnVylRJuRK3IMLPBBt+zUh43KJTTAwsvIg8Rb31EvR5CMH33mvixlw==",
"license": "MIT",
"dependencies": {
"@umbraco/json-models-builders": "2.0.42",
"node-fetch": "^2.6.7"
}
},
"node_modules/camelize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/playwright": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.57.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
}
}
}
{
"name": "@example/e2e-document-type-crud",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Example demonstrating E2E testing for Umbraco with Playwright",
"scripts": {
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:debug": "playwright test --debug",
"test:report": "playwright show-report"
},
"dependencies": {
"@umbraco/json-models-builders": "^2.0.42",
"@umbraco/playwright-testhelpers": "17.1.0-beta.4",
"tslib": "^2.4.0"
},
"devDependencies": {
"@playwright/test": "^1.49.0",
"typescript": "^5.7.2"
},
"keywords": [
"umbraco",
"playwright",
"e2e",
"testing"
]
}
import { defineConfig, devices } from '@playwright/test';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
/**
* Playwright Configuration for Umbraco E2E Tests
*
* Uses @umbraco/playwright-testhelpers which provides:
* - umbracoApi fixture for API operations
* - umbracoUi fixture for UI interactions
* - ConstantHelper for common values
*
* Authentication:
* - The 'setup' project logs in and saves auth state
* - Other projects reuse this state via storageState
*/
// ESM equivalent of __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Storage state file for authenticated session
export const STORAGE_STATE = join(__dirname, 'playwright/.auth/user.json');
// Set the storage state path for testhelpers to read tokens from
process.env.STORAGE_STAGE_PATH = STORAGE_STATE;
export default defineConfig({
testDir: './tests',
timeout: 60000, // 60 second timeout for E2E tests
expect: {
timeout: 10000,
},
fullyParallel: false, // Sequential to avoid conflicts
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1, // Single worker for Umbraco tests
reporter: [['html'], ['list']],
outputDir: './test-results',
use: {
// Base URL for Umbraco instance
baseURL: process.env.URL || 'https://localhost:44325',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
ignoreHTTPSErrors: true, // For local dev with self-signed certs
actionTimeout: 0,
// Umbraco uses 'data-mark' as the test ID attribute
testIdAttribute: 'data-mark',
},
projects: [
// Setup project - authenticates and saves state
{
name: 'setup',
testMatch: '**/*.setup.ts',
},
// Main tests - depend on setup, use saved auth state
{
name: 'chromium',
testMatch: '**/*.spec.ts',
dependencies: ['setup'],
use: {
...devices['Desktop Chrome'],
ignoreHTTPSErrors: true,
storageState: STORAGE_STATE,
},
},
],
});
Document Type CRUD - E2E Testing Example
This example demonstrates how to write E2E tests for Umbraco backoffice using Playwright with @umbraco/playwright-testhelpers.
What This Example Shows
This example demonstrates:
- Test Setup - Using
@umbraco/playwright-testhelpersfixtures - API Helpers - Fast test data creation via
umbracoApi - UI Helpers - Page interactions via
umbracoUi - AAA Pattern - Arrange-Act-Assert test structure
- Idempotent Cleanup - Using
ensureNameNotExistsfor reliable teardown - Authentication Setup - Separate setup project for login
Files Included
| File | Description |
|---|---|
tests/auth.setup.ts | Authentication setup (runs first) |
tests/document-type.spec.ts | Document type CRUD tests |
playwright.config.ts | Playwright configuration with auth |
umbraco.config.ts | Umbraco instance configuration |
package.json | Dependencies and scripts |
Project Structure
document-type-crud/
├── tests/
│ ├── auth.setup.ts # Login and save auth state
│ └── document-type.spec.ts # Document type tests
├── playwright/
│ └── .auth/
│ └── user.json # Saved auth state (generated)
├── playwright.config.ts
├── umbraco.config.ts
├── package.json
└── README.mdPrerequisites
1. Running Umbraco instance - Tests run against a real Umbraco backoffice 2. Admin credentials - Valid username/password for the instance 3. Matching testhelpers version - The @umbraco/playwright-testhelpers version must match your Umbraco version
Environment Variables
| Variable | Description | Default |
|---|---|---|
URL | Umbraco backoffice URL | https://localhost:44325 |
UMBRACO_USER_LOGIN | Admin email | admin@example.com |
UMBRACO_USER_PASSWORD | Admin password | 1234567890 |
Running the Tests
# Install dependencies
npm install
# Install Playwright browsers
npx playwright install chromium
# Run tests (with environment variables)
URL=https://localhost:44325 \
UMBRACO_USER_LOGIN=admin@example.com \
UMBRACO_USER_PASSWORD=yourpassword \
npm test
# Run with UI mode for debugging
npm run test:ui
# Run in debug mode
npm run test:debugCritical Configuration
Test ID Attribute
Umbraco uses data-mark as its test ID attribute (not the Playwright default data-testid). This must be configured in playwright.config.ts:
export default defineConfig({
use: {
// CRITICAL: Umbraco uses 'data-mark' for test IDs
testIdAttribute: 'data-mark',
ignoreHTTPSErrors: true,
},
});Without this setting, the testhelpers' getByTestId() calls will fail to find elements.
Storage State Path
The testhelpers need to know where the auth state is stored. Set the STORAGE_STAGE_PATH environment variable in your config:
export const STORAGE_STATE = join(__dirname, 'playwright/.auth/user.json');
process.env.STORAGE_STAGE_PATH = STORAGE_STATE;Test Patterns
Authentication Setup
The auth.setup.ts file uses testhelpers to log in and save the session:
import { test as setup } from '@playwright/test';
import { STORAGE_STATE } from '../playwright.config';
import { ConstantHelper, UiHelpers } from '@umbraco/playwright-testhelpers';
setup('authenticate', async ({ page }) => {
const umbracoUi = new UiHelpers(page);
await umbracoUi.goToBackOffice();
await umbracoUi.login.enterEmail(process.env.UMBRACO_USER_LOGIN);
await umbracoUi.login.enterPassword(process.env.UMBRACO_USER_PASSWORD);
await umbracoUi.login.clickLoginButton();
await umbracoUi.login.goToSection(ConstantHelper.sections.settings);
await umbracoUi.page.context().storageState({ path: STORAGE_STATE });
});Test with Fixtures
import { ConstantHelper, test } from '@umbraco/playwright-testhelpers';
import { expect } from '@playwright/test';
const documentTypeName = 'TestDocumentType';
test.beforeEach(async ({ umbracoApi, umbracoUi }) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoUi.goToBackOffice();
});
test.afterEach(async ({ umbracoApi }) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test('can create a document type', { tag: '@smoke' }, async ({ umbracoApi, umbracoUi }) => {
// Arrange
await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings);
// Act
await umbracoUi.documentType.clickActionsMenuAtRoot();
await umbracoUi.documentType.clickCreateActionMenuOption();
await umbracoUi.documentType.clickCreateDocumentTypeButton();
await umbracoUi.documentType.enterDocumentTypeName(documentTypeName);
await umbracoUi.documentType.clickSaveButton();
// Assert
await umbracoUi.documentType.waitForDocumentTypeToBeCreated();
expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy();
});Idempotent Cleanup
test.afterEach(async ({ umbracoApi }) => {
// Won't fail if item doesn't exist
await umbracoApi.documentType.ensureNameNotExists('TestDocType');
});Version Compatibility
The @umbraco/playwright-testhelpers package is versioned to match Umbraco:
| Umbraco Version | Testhelpers Version |
|---|---|
| Umbraco 17.1.x | @umbraco/playwright-testhelpers@17.1.0-beta.x |
| Umbraco 17.0.x | @umbraco/playwright-testhelpers@^17.0.x |
| Umbraco 14.x | @umbraco/playwright-testhelpers@^14.x |
Important: Use the testhelpers version that matches your Umbraco instance. For pre-release Umbraco versions, use the corresponding beta testhelpers.
Troubleshooting
"Error refreshing access token"
This usually means there's a version mismatch between the testhelpers and your Umbraco instance, or the authentication state is invalid. Try:
1. Delete playwright/.auth/user.json and re-run tests 2. Verify your Umbraco instance is running 3. Check that credentials are correct 4. Ensure STORAGE_STAGE_PATH is set correctly
"Invalid character in header content"
This is a known issue with certain testhelpers versions. Ensure you're using a compatible version for your Umbraco instance.
Tests timeout waiting for selectors
The testhelpers use specific selectors that may change between Umbraco versions. If you see locator timeouts:
1. Check `testIdAttribute` - Must be set to 'data-mark' in playwright.config.ts 2. Check the Umbraco UI has loaded correctly 3. Verify you're using the correct testhelpers version 4. Check the Umbraco acceptance tests in the source for current patterns
"element(s) not found" errors
Most likely the testIdAttribute is not set correctly. Umbraco uses data-mark, not data-testid.
Reference
- Umbraco Acceptance Tests:
Umbraco-CMS/tests/Umbraco.Tests.AcceptanceTest - Testhelpers Repo: https://github.com/umbraco/Umbraco.Playwright.Testhelpers
- Playwright Docs: https://playwright.dev/docs/intro
Skills Referenced
| Skill | What It Covers |
|---|---|
umbraco-e2e-testing | E2E test patterns |
umbraco-playwright-testhelpers | Full testhelpers reference |
umbraco-test-builders | Test data builders |
/**
* Authentication Setup
*
* This setup project runs before all other tests to:
* 1. Log in to Umbraco backoffice using testhelpers
* 2. Save authentication state to a file
* 3. Other tests reuse this state (no repeated login)
*/
import { test as setup } from '@playwright/test';
import { STORAGE_STATE } from '../playwright.config';
import { ConstantHelper, UiHelpers } from '@umbraco/playwright-testhelpers';
setup('authenticate', async ({ page }) => {
const umbracoUi = new UiHelpers(page);
await umbracoUi.goToBackOffice();
await umbracoUi.login.enterEmail(process.env.UMBRACO_USER_LOGIN);
await umbracoUi.login.enterPassword(process.env.UMBRACO_USER_PASSWORD);
await umbracoUi.login.clickLoginButton();
await umbracoUi.login.goToSection(ConstantHelper.sections.settings);
await umbracoUi.page.context().storageState({ path: STORAGE_STATE });
});
/**
* Document Type E2E Tests
*
* Demonstrates:
* - AAA pattern (Arrange-Act-Assert)
* - umbracoApi for fast test setup
* - umbracoUi for UI interactions
* - Idempotent cleanup with ensureNameNotExists
*
* Based on: Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentType.spec.ts
*/
import { ConstantHelper, test } from '@umbraco/playwright-testhelpers';
import { expect } from '@playwright/test';
const documentTypeName = 'E2ETestDocumentType';
test.beforeEach(async ({ umbracoUi, umbracoApi }) => {
// Ensure clean state before each test
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoUi.goToBackOffice();
});
test.afterEach(async ({ umbracoApi }) => {
// Cleanup after each test (idempotent)
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test('can create a document type', { tag: '@smoke' }, async ({ umbracoApi, umbracoUi }) => {
// Arrange
await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings);
// Act
await umbracoUi.documentType.clickActionsMenuAtRoot();
await umbracoUi.documentType.clickCreateActionMenuOption();
await umbracoUi.documentType.clickCreateDocumentTypeButton();
await umbracoUi.documentType.enterDocumentTypeName(documentTypeName);
await umbracoUi.documentType.clickSaveButton();
// Assert
await umbracoUi.documentType.waitForDocumentTypeToBeCreated();
expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy();
await umbracoUi.documentType.reloadTree('Document Types');
await umbracoUi.documentType.isDocumentTreeItemVisible(documentTypeName);
});
test('can create an element type', { tag: '@smoke' }, async ({ umbracoApi, umbracoUi }) => {
// Arrange
await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings);
// Act
await umbracoUi.documentType.clickActionsMenuAtRoot();
await umbracoUi.documentType.clickCreateActionMenuOption();
await umbracoUi.documentType.clickCreateElementTypeButton();
await umbracoUi.documentType.enterDocumentTypeName(documentTypeName);
await umbracoUi.documentType.clickSaveButton();
// Assert
await umbracoUi.documentType.waitForDocumentTypeToBeCreated();
expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy();
// Verify isElement flag is true
const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName);
expect(documentTypeData.isElement).toBeTruthy();
});
test('can rename a document type', { tag: '@smoke' }, async ({ umbracoApi, umbracoUi }) => {
// Arrange
const wrongName = 'WrongDocumentTypeName';
await umbracoApi.documentType.ensureNameNotExists(wrongName);
await umbracoApi.documentType.createDefaultDocumentType(wrongName);
await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings);
// Act
await umbracoUi.documentType.goToDocumentType(wrongName);
await umbracoUi.documentType.enterDocumentTypeName(documentTypeName);
await umbracoUi.documentType.clickSaveButton();
// Assert
await umbracoUi.documentType.isSuccessStateVisibleForSaveButton();
expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy();
await umbracoUi.documentType.isDocumentTreeItemVisible(wrongName, false);
await umbracoUi.documentType.isDocumentTreeItemVisible(documentTypeName);
});
test('can delete a document type', { tag: '@smoke' }, async ({ umbracoApi, umbracoUi }) => {
// Arrange
await umbracoApi.documentType.createDefaultDocumentType(documentTypeName);
await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings);
expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy();
// Act
await umbracoUi.documentType.clickRootFolderCaretButton();
await umbracoUi.documentType.clickActionsMenuForDocumentType(documentTypeName);
await umbracoUi.documentType.clickDeleteAndConfirmButton();
// Assert
await umbracoUi.documentType.waitForDocumentTypeToBeDeleted();
expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeFalsy();
});
test('can add an icon for a document type', async ({ umbracoApi, umbracoUi }) => {
// Arrange
const bugIcon = 'icon-bug';
await umbracoApi.documentType.createDefaultDocumentType(documentTypeName);
await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings);
// Act
await umbracoUi.documentType.goToDocumentType(documentTypeName);
await umbracoUi.waitForTimeout(500);
await umbracoUi.documentType.updateIcon(bugIcon);
await umbracoUi.documentType.clickSaveButton();
// Assert
await umbracoUi.documentType.isSuccessStateVisibleForSaveButton();
const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName);
expect(documentTypeData.icon).toBe(bugIcon);
});
{
"compilerOptions": {
"target": "ES2021",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2021", "DOM"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["tests/**/*.ts", "playwright.config.ts"]
}
/**
* Umbraco Configuration for E2E Tests
*
* Configure the URL and credentials for the Umbraco instance to test against.
* Uses environment variables with fallback defaults.
*/
const umbracoConfig = {
environment: {
// Local Umbraco instance URL
baseUrl: process.env.URL || 'https://localhost:44325',
},
user: {
// Admin credentials - update these to match your instance
login: process.env.UMBRACO_USER_LOGIN || 'admin@example.com',
password: process.env.UMBRACO_USER_PASSWORD || '1234567890',
},
};
export { umbracoConfig };