
Umbraco Mocked Backoffice
- 280 installs
- 26 repo stars
- Updated August 1, 2026
- umbraco/umbraco-cms-backoffice-skills
Helps with ai & agent building tasks.
About
umbraco-mocked-backoffice is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- umbraco-mocked-backoffice
- AI & Agent Building
- AI-coding skill
Umbraco Mocked Backoffice by the numbers
- 280 all-time installs (skills.sh)
- +12 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #2,417 of 16,546 AI & Agent Building 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-mocked-backofficeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 280 |
|---|---|
| repo stars | ★ 26 |
| Last updated | August 1, 2026 |
| Repository | umbraco/umbraco-cms-backoffice-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Umbraco Mocked Backoffice
Status: This skill is currently awaiting an update from Umbraco to allow external extensions to use the mocked backoffice. The patterns documented here work when running from within the Umbraco-CMS source repository.
Run the full Umbraco backoffice UI with all API calls mocked - no .NET backend required.
When to Use
- Visually test extensions during development
- Rapid iteration without backend deployment
- Test extensions in realistic UI environment
- Demonstrate extensions without infrastructure
- CI/CD testing without backend setup
Related Skills
- umbraco-example-generator - Set up extensions for mocked backoffice (start here)
- umbraco-testing - Master skill for testing overview
- umbraco-unit-testing - Test extension logic in isolation
- umbraco-e2e-testing - Test against a real Umbraco instance
---
Two Mocking Approaches
Extensions with custom APIs can use two mocking approaches:
| Approach | Use Case | Best For |
|---|---|---|
| [MSW Handlers](patterns/external-msw-handlers.md) | Network-level API mocking | Testing error handling, loading states, retries |
| [Mock Repository](patterns/mock-repository-pattern.md) | Application-level mocking | Testing UI with predictable data (recommended) |
Both approaches require MSW to be enabled (VITE_UMBRACO_USE_MSW=on) for core Umbraco APIs.
---
Setup
Create Your Extension
Use the umbraco-example-generator skill to set up your extension:
Invoke: skill: umbraco-example-generator
This covers:
- Cloning Umbraco-CMS repository
- Extension structure and
src/index.tsrequirements - Running with
VITE_EXAMPLE_PATHandnpm run dev
Add Testing Dependencies
{
"devDependencies": {
"@playwright/test": "^1.56"
},
"scripts": {
"test:mock-repo": "playwright test --config=tests/mock-repo/playwright.config.ts",
"test:msw": "playwright test --config=tests/msw/playwright.config.ts"
}
}npm install
npx playwright install chromiumDirectory Structure
my-extension/Client/
├── src/
│ ├── index.ts # Entry point (loads manifests, registers MSW handlers)
│ ├── manifests.ts # Production manifests
│ ├── feature/
│ │ ├── my-element.ts
│ │ └── types.ts
│ └── msw/ # MSW handlers (loaded from index.ts)
│ └── handlers.ts
├── tests/
│ ├── mock-repo/ # Mock repository tests
│ │ ├── playwright.config.ts
│ │ ├── my-extension.spec.ts
│ │ └── mock/
│ │ ├── index.ts # Mock manifests (replaces repository)
│ │ ├── mock-repository.ts
│ │ └── mock-data.ts
│ └── msw/ # MSW tests
│ ├── playwright.config.ts
│ └── my-extension.spec.ts
├── package.json
└── tsconfig.json---
Entry Point (src/index.ts)
The entry point conditionally loads MSW handlers or mock manifests based on environment:
// Entry point for external extension loading
// Run from Umbraco.Web.UI.Client with:
// VITE_EXAMPLE_PATH=/path/to/extension/Client VITE_UMBRACO_USE_MSW=on npm run dev
// VITE_EXAMPLE_PATH=/path/to/extension/Client VITE_USE_MOCK_REPO=on VITE_UMBRACO_USE_MSW=on npm run dev
// Register MSW handlers when running in MSW mode (but not mock-repo mode)
if (import.meta.env.VITE_UMBRACO_USE_MSW === 'on' && import.meta.env.VITE_USE_MOCK_REPO !== 'on') {
import('./msw/handlers.js').then(({ createHandlers }) => {
const { addMockHandlers } = (window as any).MockServiceWorker;
addMockHandlers(...createHandlers());
});
}
// Export manifests - use mock repository if VITE_USE_MOCK_REPO is set
export const manifests = import.meta.env.VITE_USE_MOCK_REPO === 'on'
? (await import('../tests/mock-repo/mock/index.js')).manifests
: (await import('./manifests.js')).manifests;---
Running Tests
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
VITE_EXAMPLE_PATH | /path/to/extension/Client | Path to extension directory |
VITE_UMBRACO_USE_MSW | on | Enable MSW for core Umbraco APIs |
VITE_USE_MOCK_REPO | on | Use mock repository instead of MSW handlers |
UMBRACO_CLIENT_PATH | /path/to/Umbraco.Web.UI.Client | Path to Umbraco client (for Playwright) |
Manual Dev Server
cd /path/to/Umbraco-CMS/src/Umbraco.Web.UI.Client
# MSW mode (uses your handlers for custom APIs)
VITE_EXAMPLE_PATH=/path/to/extension/Client VITE_UMBRACO_USE_MSW=on npm run dev
# Mock repository mode (uses mock repository for custom APIs)
VITE_EXAMPLE_PATH=/path/to/extension/Client VITE_USE_MOCK_REPO=on VITE_UMBRACO_USE_MSW=on npm run devRun Tests
cd /path/to/extension/Client
# Set path to Umbraco client
export UMBRACO_CLIENT_PATH=/path/to/Umbraco-CMS/src/Umbraco.Web.UI.Client
# Run MSW tests
npm run test:msw
# Run mock repository tests
npm run test:mock-repo---
Playwright Config Example
Create tests/msw/playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const EXTENSION_PATH = resolve(__dirname, '../..');
const UMBRACO_CLIENT_PATH = process.env.UMBRACO_CLIENT_PATH;
if (!UMBRACO_CLIENT_PATH) {
throw new Error('UMBRACO_CLIENT_PATH environment variable is required');
}
const DEV_SERVER_PORT = 5176;
export default defineConfig({
testDir: '.',
testMatch: ['*.spec.ts'],
timeout: 60000,
expect: { timeout: 15000 },
fullyParallel: false,
workers: 1,
// Start dev server with extension and MSW enabled
webServer: {
command: `VITE_EXAMPLE_PATH=${EXTENSION_PATH} VITE_UMBRACO_USE_MSW=on npm run dev -- --port ${DEV_SERVER_PORT}`,
cwd: UMBRACO_CLIENT_PATH,
port: DEV_SERVER_PORT,
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
use: {
baseURL: `http://localhost:${DEV_SERVER_PORT}`,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});For mock-repo tests, change the command to include VITE_USE_MOCK_REPO=on:
command: `VITE_EXAMPLE_PATH=${EXTENSION_PATH} VITE_USE_MOCK_REPO=on VITE_UMBRACO_USE_MSW=on npm run dev -- --port ${DEV_SERVER_PORT}`,---
Test Patterns
Navigation Helper
import { type Page } from '@playwright/test';
async function navigateToSettings(page: Page) {
await page.goto('/section/settings');
await page.waitForLoadState('domcontentloaded');
await page.waitForSelector('umb-section-sidebar', { timeout: 30000 });
}Testing Tree Items
test('should display root tree items', async ({ page }) => {
await navigateToSettings(page);
await page.waitForSelector('umb-tree-item', { timeout: 15000 });
const treeItems = page.locator('umb-tree-item');
await expect(treeItems.first()).toBeVisible();
});
test('should expand tree item to show children', async ({ page }) => {
await navigateToSettings(page);
const expandableItem = page.locator('umb-tree-item').filter({ hasText: 'Group A' });
const expandButton = expandableItem.locator('button[aria-label="toggle child items"]');
await expandButton.click();
const childItem = page.locator('umb-tree-item').filter({ hasText: 'Child 1' });
await expect(childItem).toBeVisible({ timeout: 15000 });
});MSW Mock Document URLs
| Document Name | URL Path |
|---|---|
| The Simplest Document | /section/content/workspace/document/edit/the-simplest-document-id |
| All properties | /section/content/workspace/document/edit/all-property-editors-document-id |
---
Troubleshooting
Extension not appearing
- Check that your extension exports a
manifestsarray fromsrc/index.ts - Check browser console for errors
- Verify
VITE_EXAMPLE_PATHpoints to theClientdirectory
Tests timeout waiting for elements
- Ensure the dev server is running with your extension loaded
- Check the browser console for extension loading errors
- Use longer timeouts (15000ms+) for initial element appearance
MSW handlers not intercepting requests
- Check console for
[MSW]logs showing handler registration - Verify handler URL patterns match the actual API calls
- Use browser DevTools Network tab to see actual request URLs
---
Working Example
See tree-example in umbraco-backoffice-skills/examples/tree-example/Client/:
| Path | Description |
|---|---|
src/index.ts | Entry point with conditional manifest loading |
src/msw/handlers.ts | MSW handlers for custom API |
tests/mock-repo/ | Mock repository tests |
tests/msw/ | MSW tests |
cd tree-example/Client
export UMBRACO_CLIENT_PATH=/path/to/Umbraco-CMS/src/Umbraco.Web.UI.Client
npm run test:msw # Run MSW tests
npm run test:mock-repo # Run mock repository tests---
What's Mocked?
MSW provides mock data for all backoffice APIs:
- Documents, media, members
- Document types, media types, member types
- Data types, templates, stylesheets
- Users, user groups, permissions
- Languages, cultures, dictionary items
External MSW Handlers
Add MSW handlers in src/msw/handlers.ts and load them from your entry point when VITE_UMBRACO_USE_MSW=on. Your production code stays unchanged.
Note: For extensions using hey-api/OpenAPI clients, consider the Mock Repository Pattern instead - it's simpler and avoids cross-origin issues.
Limitations
1. Cross-Origin Requests: MSW can only intercept same-origin requests. If your API client uses an absolute baseUrl (e.g., https://localhost:44348), you must configure it to use relative URLs in mock mode.
2. TypeScript Source Loading: When loading TypeScript source directly via VITE_EXAMPLE_PATH, dynamic imports with .js extensions may not resolve. This affects extensions with lazy-loaded repositories.
3. hey-api Client: The generated client uses CommonJS files that need an ESM wrapper for Vite.
Directory Structure
my-extension/Client/
├── src/
│ ├── index.ts # Entry point (loads handlers when MSW enabled)
│ ├── manifests.ts # Your extension manifests
│ ├── api/
│ │ ├── client.gen.ts # hey-api generated client
│ │ └── client/
│ │ ├── index.cjs # CommonJS (generated)
│ │ └── index.js # ESM wrapper (you create this)
│ ├── msw/ # MSW handlers
│ │ └── handlers.ts # Exports createHandlers() function
│ └── ...
└── tests/
└── msw/
├── playwright.config.ts
└── my-extension.spec.tsStep 1: Create ESM Wrapper for hey-api Client
If using hey-api, create src/api/client/index.js:
// ESM wrapper for hey-api client (CommonJS)
import cjs from './index.cjs';
export const {
createClient,
createConfig,
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} = cjs;
export default cjs;Step 2: Configure Client for Mock Mode
In your entrypoint, detect mock mode and use relative URLs:
import { UMB_AUTH_CONTEXT } from "@umbraco-cms/backoffice/auth";
import type { UmbEntryPointOnInit } from "@umbraco-cms/backoffice/extension-api";
import { client } from "../api";
// Detect mock mode - not on the real backend port
const isMockMode = () => {
const isBackendPort = window.location.port === '44348' ||
window.location.port === '443' ||
window.location.port === '';
return !isBackendPort;
};
export const onInit: UmbEntryPointOnInit = (_host) => {
if (isMockMode()) {
// Use relative URLs so MSW can intercept same-origin requests
client.setConfig({ baseUrl: '' });
console.log('🎭 Mock mode - using relative URLs');
return;
}
// Production mode - use auth context
_host.consumeContext(UMB_AUTH_CONTEXT, (_auth) => {
if (!_auth) return;
const config = _auth.getOpenApiConfiguration();
client.setConfig({
baseUrl: config.base,
credentials: config.credentials,
});
// ... add auth interceptor
});
};Step 3: Create MSW Handlers
Create src/msw/handlers.ts:
// Handler factory - gets http/HttpResponse from Umbraco's MSW instance at runtime
export function createHandlers() {
const { http, HttpResponse } = (window as any).MockServiceWorker;
// Mock data
const items = [
{ id: 'item-1', name: '[MSW] Item 1', icon: 'icon-document' },
{ id: 'item-2', name: '[MSW] Item 2', icon: 'icon-folder' },
];
// Use relative paths (same-origin)
const API_PATH = '/umbraco/myextension/api/v1';
return [
http.get(`${API_PATH}/items`, () => {
return HttpResponse.json({ total: items.length, items });
}),
http.post(`${API_PATH}/items`, async ({ request }: { request: Request }) => {
const body = await request.json();
return HttpResponse.json({ id: 'new-id', ...body }, { status: 201 });
}),
];
}Step 4: Load Handlers from Entry Point
Update src/index.ts to register handlers when MSW is enabled:
// Register MSW handlers when running in MSW mode
if (import.meta.env.VITE_UMBRACO_USE_MSW === 'on') {
import('./msw/handlers.js').then(({ createHandlers }) => {
const { addMockHandlers } = (window as any).MockServiceWorker;
addMockHandlers(...createHandlers());
});
}
// Export manifests
export const manifests = (await import('./manifests.js')).manifests;Running
cd /path/to/Umbraco-CMS/src/Umbraco.Web.UI.Client
# Point to the Client directory containing src/index.ts
VITE_EXAMPLE_PATH=/path/to/my-extension/Client VITE_UMBRACO_USE_MSW=on npm run devEnvironment Variables
| Variable | Value | Purpose |
|---|---|---|
VITE_EXAMPLE_PATH | /path/to/extension/Client | Path to extension directory |
VITE_UMBRACO_USE_MSW | on | Enable MSW for core Umbraco APIs and your handlers |
You'll see in the browser console:
[MSW] Custom handlers registeredWorking Example
See tree-example in umbraco-backoffice/examples/tree-example/Client/:
| Path | Description |
|---|---|
src/index.ts | Entry point with MSW handler registration |
src/msw/handlers.ts | MSW handlers using createHandlers() pattern |
tests/msw/playwright.config.ts | Playwright config with webServer |
tests/msw/tree.spec.ts | Test suite (6 tests) |
Running the Tree Example Tests
cd /path/to/tree-example/Client
export UMBRACO_CLIENT_PATH=/path/to/Umbraco-CMS/src/Umbraco.Web.UI.Client
npm run test:mswMock Repository Pattern
Replace the API-calling repository with a mock version that returns data directly. This approach:
- Bypasses the API client entirely for your extension's custom API
- Tests UI rendering and interactions without network calls
- Still requires MSW for core Umbraco management APIs (authentication, user data, etc.)
Alternative: For testing with MSW intercepting all HTTP requests, see External MSW Handlers.
When to Use Each Approach
| Scenario | Recommended Approach |
|---|---|
| Want to test UI without any custom API calls | Mock Repository Pattern |
| Want to test full HTTP request/response cycle | MSW Handlers |
| Need fine-grained API control (errors, delays) | MSW Handlers |
Directory Structure
my-extension/Client/
├── src/
│ ├── index.ts # Entry point (loads mock or real manifests)
│ ├── manifests.ts # Production manifests
│ └── ... # Production code
└── tests/
└── mock-repo/
├── playwright.config.ts
├── my-extension.spec.ts
└── mock/
├── index.ts # Mock manifests (replaces repository)
├── mock-repository.ts # Repository returning mock data
└── mock-data.ts # Test dataStep 1: Create Entry Point
Update src/index.ts to conditionally load mock manifests:
// Entry point for external extension loading
// Run from Umbraco.Web.UI.Client with:
// VITE_EXAMPLE_PATH=/path/to/extension/Client VITE_UMBRACO_USE_MSW=on npm run dev
// VITE_EXAMPLE_PATH=/path/to/extension/Client VITE_USE_MOCK_REPO=on VITE_UMBRACO_USE_MSW=on npm run dev
// Register MSW handlers when running in MSW mode (but not mock-repo mode)
if (import.meta.env.VITE_UMBRACO_USE_MSW === 'on' && import.meta.env.VITE_USE_MOCK_REPO !== 'on') {
import('./msw/handlers.js').then(({ createHandlers }) => {
const { addMockHandlers } = (window as any).MockServiceWorker;
addMockHandlers(...createHandlers());
});
}
// Export manifests - use mock repository if VITE_USE_MOCK_REPO is set
export const manifests = import.meta.env.VITE_USE_MOCK_REPO === 'on'
? (await import('../tests/mock-repo/mock/index.js')).manifests
: (await import('./manifests.js')).manifests;Step 2: Create Mock Manifests
Create tests/mock-repo/mock/index.ts:
import { manifests as productionManifests } from '../../../src/manifests.js';
// Mock repository manifest (replaces the API-calling one)
const mockRepositoryManifest: UmbExtensionManifest = {
type: 'repository',
alias: 'MyExtension.Repository', // Same alias as original
name: 'MyExtension Repository (Mock)',
api: () => import('./mock-repository.js'),
};
// Filter out original repository, keep everything else
const filteredManifests = productionManifests.filter(
(m) => m.alias !== 'MyExtension.Repository'
);
export const manifests: Array<UmbExtensionManifest> = [
mockRepositoryManifest,
...filteredManifests,
];Step 3: Create Mock Repository
Create tests/mock-repo/mock/mock-repository.ts:
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import { UmbTreeRepositoryBase } from '@umbraco-cms/backoffice/tree';
// Import types from original src files
import type { MyItemModel, MyRootModel } from '../../../src/feature/types.js';
import { rootItems, childrenByParent } from './mock-data.js';
class MockTreeDataSource {
constructor(_host: UmbControllerHost) {}
async getRootItems(args: { skip: number; take: number }) {
const items = rootItems.slice(args.skip, args.skip + args.take);
return { data: { total: rootItems.length, items: this.#mapItems(items) } };
}
async getChildrenOf(args: { parent: { unique: string | null } }) {
if (!args.parent?.unique) {
return this.getRootItems({ skip: 0, take: 100 });
}
const children = childrenByParent[args.parent.unique] || [];
return { data: { total: children.length, items: this.#mapItems(children) } };
}
async getAncestorsOf() {
return { data: [] };
}
#mapItems(items: any[]): MyItemModel[] {
return items.map((item) => ({
unique: item.id,
name: item.name,
hasChildren: item.hasChildren,
icon: item.icon,
// ... map other fields
}));
}
}
export class MockTreeRepository
extends UmbTreeRepositoryBase<MyItemModel, MyRootModel>
implements UmbApi
{
constructor(host: UmbControllerHost) {
super(host, MockTreeDataSource);
}
async requestTreeRoot() {
return {
data: {
unique: null,
name: 'My Tree Root',
icon: 'icon-star',
hasChildren: true,
isFolder: true,
},
};
}
}
export { MockTreeRepository as api };Step 4: Create Mock Data
Create tests/mock-repo/mock/mock-data.ts:
export interface MockTreeItem {
id: string;
name: string;
icon: string;
hasChildren: boolean;
}
export const rootItems: MockTreeItem[] = [
{ id: 'item-1', name: '[Mock Repo] Group A', icon: 'icon-folder', hasChildren: true },
{ id: 'item-2', name: '[Mock Repo] Group B', icon: 'icon-folder', hasChildren: false },
];
export const childrenByParent: Record<string, MockTreeItem[]> = {
'item-1': [
{ id: 'item-1-1', name: '[Mock Repo] Child 1', icon: 'icon-document', hasChildren: false },
],
};Important: Keep mock data in a separate file that's NOT exported from index.ts. If arrays are exported directly, Umbraco will try to register them as extensions.
Step 5: Create Playwright Config
Create tests/mock-repo/playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const EXTENSION_PATH = resolve(__dirname, '../..');
const UMBRACO_CLIENT_PATH = process.env.UMBRACO_CLIENT_PATH;
if (!UMBRACO_CLIENT_PATH) {
throw new Error('UMBRACO_CLIENT_PATH environment variable is required');
}
const DEV_SERVER_PORT = 5175;
export default defineConfig({
testDir: '.',
testMatch: ['*.spec.ts'],
timeout: 60000,
expect: { timeout: 15000 },
fullyParallel: false,
workers: 1,
// Start dev server with mock repository AND MSW (for core Umbraco APIs)
webServer: {
command: `VITE_EXAMPLE_PATH=${EXTENSION_PATH} VITE_USE_MOCK_REPO=on VITE_UMBRACO_USE_MSW=on npm run dev -- --port ${DEV_SERVER_PORT}`,
cwd: UMBRACO_CLIENT_PATH,
port: DEV_SERVER_PORT,
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
use: {
baseURL: `http://localhost:${DEV_SERVER_PORT}`,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});Running
# Set the path to Umbraco.Web.UI.Client
export UMBRACO_CLIENT_PATH=/path/to/Umbraco-CMS/src/Umbraco.Web.UI.Client
# Run mock-repo tests
npm run test:mock-repoOr run the dev server manually:
cd /path/to/Umbraco-CMS/src/Umbraco.Web.UI.Client
VITE_EXAMPLE_PATH=/path/to/my-extension/Client VITE_USE_MOCK_REPO=on VITE_UMBRACO_USE_MSW=on npm run devImportant Notes
1. MSW is still required - The mock repository only replaces your extension's custom API. Core Umbraco management APIs (authentication, user data, sections, etc.) still need MSW to be enabled.
2. Use consistent env var values - Both VITE_USE_MOCK_REPO=on and VITE_UMBRACO_USE_MSW=on use =on (not =true).
3. Vite's `/@fs/` prefix - External extensions are loaded via Vite's /@fs/ URL prefix for absolute paths.
Working Example
See tree-example in umbraco-backoffice-skills/examples/tree-example/Client/:
| Path | Description |
|---|---|
src/index.ts | Entry point with conditional manifest loading |
tests/mock-repo/mock/ | Mock repository implementation |
tests/mock-repo/playwright.config.ts | Playwright test config |
cd tree-example/Client
export UMBRACO_CLIENT_PATH=/path/to/Umbraco-CMS/src/Umbraco.Web.UI.Client
npm run test:mock-repo