From umbraco-cms-backoffice-testing-skills
Generates complete, testable example extensions for Umbraco backoffice using TypeScript and LitElement, runs them via Vite dev server with hot reload and mocked APIs.
npx claudepluginhub umbraco/umbraco-cms-backoffice-skills --plugin umbraco-cms-backoffice-testing-skillsThis skill uses the workspace's default tool permissions.
---
examples/workspace-feature-toggle/README.mdexamples/workspace-feature-toggle/feature-toggle-action.tsexamples/workspace-feature-toggle/feature-toggle-context.test.tsexamples/workspace-feature-toggle/feature-toggle-context.tsexamples/workspace-feature-toggle/feature-toggle-footer.element.tsexamples/workspace-feature-toggle/feature-toggle-view.element.tsexamples/workspace-feature-toggle/index.tsexamples/workspace-feature-toggle/package-lock.jsonexamples/workspace-feature-toggle/package.jsonexamples/workspace-feature-toggle/tests/feature-toggle.spec.tsexamples/workspace-feature-toggle/tests/playwright.config.tsexamples/workspace-feature-toggle/tsconfig.jsonexamples/workspace-feature-toggle/web-test-runner.config.mjsRuns Umbraco backoffice UI with mocked APIs for visual extension testing without .NET backend. Enables rapid iteration, demos, and CI/CD via MSW handlers or mock repository.
Provides complete working blueprints and mandatory workflows for building Umbraco backoffice extensions combining sections, menus, trees, dashboards, and workspaces.
Monitors deployed URLs for regressions after deploys, merges, or upgrades by checking HTTP status, console errors, network failures, performance (LCP/CLS/INP), content, and API health.
Share bugs, ideas, or general feedback.
Generate complete, testable example extensions for the Umbraco backoffice and run them using the Umbraco source's dev infrastructure.
git clone https://github.com/umbraco/Umbraco-CMS
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
npm install
my-extension/
├── index.ts # REQUIRED - exports manifests
└── my-element.ts # Your element(s)
import './my-element.js';
export const manifests = [
{
type: 'dashboard',
alias: 'My.Dashboard',
name: 'My Dashboard',
element: 'my-element',
meta: { label: 'My Dashboard', pathname: 'my-dashboard' },
conditions: [{ alias: 'Umb.Condition.SectionAlias', match: 'Umb.Section.Content' }]
}
];
// my-element.ts
import { LitElement, html, customElement } from '@umbraco-cms/backoffice/external/lit';
@customElement('my-element')
export class MyElement extends LitElement {
render() {
return html`<uui-box headline="Hello">It works!</uui-box>`;
}
}
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
VITE_EXTERNAL_EXTENSION=/full/path/to/my-extension npm run dev:external
Open http://localhost:5173 - your extension appears in the Content section.
The Umbraco source (Umbraco-CMS/src/Umbraco.Web.UI.Client) provides two ways to load extensions:
npm run example)Examples placed in the examples/ folder inside the Umbraco source.
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
npm run example
# Select from list of examples
How it works: Sets VITE_EXAMPLE_PATH and imports ./examples/{name}/index.ts
npm run dev:external)Extensions from any location on your filesystem - perfect for developing packages.
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
VITE_EXTERNAL_EXTENSION=/path/to/your/extension npm run dev:external
How it works:
VITE_UMBRACO_USE_MSW=on (mocked APIs)@external-extension alias pointing to your extension path@external-extension/index.ts and registers exports with umbExtensionsRegistry@umbraco-cms/backoffice/* imports from the main project (avoids duplicate element registrations)// From Umbraco-CMS/src/Umbraco.Web.UI.Client/index.ts
if (import.meta.env.VITE_EXTERNAL_EXTENSION) {
const js = await import('@external-extension/index.ts');
if (js) {
Object.keys(js).forEach((key) => {
const value = js[key];
if (Array.isArray(value)) {
umbExtensionsRegistry.registerMany(value);
} else if (typeof value === 'object') {
umbExtensionsRegistry.register(value);
}
});
}
}
Key point: Your index.ts must export manifests (arrays or objects) that get registered automatically.
Clone and set up the Umbraco source:
git clone https://github.com/umbraco/Umbraco-CMS
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
npm install
Your extension needs this minimal structure:
my-extension/
├── index.ts # Exports manifests array (REQUIRED)
├── my-element.ts # Your element(s)
├── my-context.ts # Context (if needed)
├── package.json # Optional - for IDE support and tests
├── tsconfig.json # Optional - for IDE support
└── README.md # Documentation
Your index.ts must export manifests that will be registered:
import './my-dashboard.element.js';
export const manifests = [
{
type: 'dashboard',
alias: 'My.Dashboard',
name: 'My Dashboard',
element: 'my-dashboard',
weight: 100,
meta: {
label: 'My Dashboard',
pathname: 'my-dashboard'
},
conditions: [
{
alias: 'Umb.Condition.SectionAlias',
match: 'Umb.Section.Content'
}
]
}
];
{
"name": "my-extension",
"type": "module",
"devDependencies": {
"@umbraco-cms/backoffice": "^17.0.0",
"typescript": "~5.8.0"
}
}
Important: The @umbraco-cms/backoffice dependency is only for IDE TypeScript support. At runtime, imports are resolved from the main Umbraco project.
cd /path/to/Umbraco-CMS/src/Umbraco.Web.UI.Client
VITE_EXTERNAL_EXTENSION=/absolute/path/to/my-extension npm run dev:external
Navigate to http://localhost:5173 - your extension is loaded automatically.
Changes to your extension files trigger hot reload - no restart needed.
// my-dashboard.element.ts
import { LitElement, html, css, customElement } from '@umbraco-cms/backoffice/external/lit';
@customElement('my-dashboard')
export class MyDashboardElement extends LitElement {
static override styles = css`
:host {
display: block;
padding: var(--uui-size-layout-1);
}
`;
override render() {
return html`
<uui-box headline="My Extension">
<p>Running in the mocked backoffice!</p>
</uui-box>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'my-dashboard': MyDashboardElement;
}
}
import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { EXAMPLE_MY_CONTEXT } from './my-context.js';
@customElement('example-my-feature-view')
export class ExampleMyFeatureViewElement extends UmbLitElement {
@state()
private _value?: string;
constructor() {
super();
this.consumeContext(EXAMPLE_MY_CONTEXT, (context) => {
this.observe(context.value, (value) => {
this._value = value;
});
});
}
override render() {
return html`
<uui-box headline="My Feature Example">
<p>Current value: ${this._value ?? 'Loading...'}</p>
</uui-box>
`;
}
}
export default ExampleMyFeatureViewElement;
declare global {
interface HTMLElementTagNameMap {
'example-my-feature-view': ExampleMyFeatureViewElement;
}
}
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class ExampleMyContext extends UmbContextBase {
#value = new UmbStringState('initial');
readonly value = this.#value.asObservable();
constructor(host: UmbControllerHost) {
super(host, EXAMPLE_MY_CONTEXT);
}
setValue(value: string) {
this.#value.setValue(value);
}
getValue() {
return this.#value.getValue();
}
public override destroy(): void {
this.#value.destroy();
super.destroy();
}
}
export const EXAMPLE_MY_CONTEXT = new UmbContextToken<ExampleMyContext>(
'ExampleMyContext'
);
export { ExampleMyContext as api };
Add unit tests using @open-wc/testing. See umbraco-unit-testing skill for full setup.
npm install --save-dev @open-wc/testing @web/test-runner @web/test-runner-playwright
Add E2E tests that run against the mocked backoffice. See umbraco-mocked-backoffice skill for patterns.
npm install --save-dev @playwright/test
npx playwright install chromium
Location: ./examples/workspace-feature-toggle/
A complete standalone example demonstrating:
UmbArrayStatecd examples/workspace-feature-toggle
npm install
npm test # Unit tests
npm run test:e2e # E2E tests (requires mocked backoffice running)
Location: Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/
27 official examples covering all extension types. Run any example:
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
npm run example
# Select from list
| Item | Convention | Example |
|---|---|---|
| Directory | kebab-case describing feature | workspace-context-counter |
| Alias prefix | example. | example.workspaceView.counter |
| Element prefix | example- | example-counter-view |
| Context token | EXAMPLE_ + SCREAMING_CASE | EXAMPLE_COUNTER_CONTEXT |
index.ts exports a manifests arrayVITE_EXTERNAL_EXTENSION is absolute📦 Loading external extension from: messageImports should use @umbraco-cms/backoffice/*. The Vite plugin resolves these from the main project.
Your extension's node_modules is being used instead of the main project's. The external-extension-resolver plugin should handle this, but ensure:
npm run dev:external@umbraco-cms/backoffice/* not relative paths to node_modulesEnsure the file is within the path specified by VITE_EXTERNAL_EXTENSION. Only files in that directory tree are watched.