Skip to main content
umbraco-unit-testing Unit and component testing for Umbraco backoffice extensions using @open-wc/testing
Ir a la instalación Skills Marketplace Descubre y explora habilidades de IA creadas por la comunidad.
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Copiar promptMostrar detalles del prompt Un comando directo omite el prompt de revisión. Revisa el origen antes de ejecutarlo.
npx skills add https://github.com/umbraco/Umbraco-CMS-Backoffice-Skills --skill umbraco-unit-testingEl comando permanece en una sola línea. Desplázate horizontalmente para revisarlo antes de copiarlo.
¿Prefieres una copia local? Descarga los archivos que SkillsMP tiene disponibles ahora.
Descargar Zip Descargando... Ocupaciones relacionadas
SOC
Basado en la clasificación ocupacional SOC
Explorador de archivos
10 archivos name umbraco-unit-testing description Unit and component testing for Umbraco backoffice extensions using @open-wc/testing version 1.0.0 location managed allowed-tools Read, Write, Edit, WebFetch
Umbraco Unit Testing
What is it?
Unit testing for Umbraco backoffice extensions using @open-wc/testing - a testing framework designed for Web Components and Lit elements. This is the fastest and most isolated testing approach.
When to Use
Testing context logic and state management
Testing Lit element rendering and shadow DOM
Testing observable subscriptions and state changes
Testing controllers and utility functions
Fast feedback during development
Related Skills
umbraco-testing - Master skill for testing overview
umbraco-msw-testing - Add API mocking to unit tests
Documentation
Setup
Dependencies {
"devDependencies" : {
"@open-wc/testing" : "^4.0.0" ,
"@web/dev-server-esbuild" : "^1.0.0" ,
"@web/dev-server-import-maps" : "^0.2.0" ,
"@web/test-runner" : "^0.18.0" ,
"@web/test-runner-playwright" : "^0.11.0"
} ,
"scripts" : {
"test" : "web-test-runner" ,
"test:watch" : "web-test-runner --watch"
}
}
npm install
npx playwright install chromium
Configuration Create web-test-runner.config.mjs in the project root:
import { esbuildPlugin } from '@web/dev-server-esbuild' ;
import { playwrightLauncher } from '@web/test-runner-playwright' ;
import { importMapsPlugin } from '@web/dev-server-import-maps' ;
export default {
rootDir : '.' ,
files : ['./src/**/*.test.ts' , '!**/node_modules/**' ],
nodeResolve : {
exportConditions : ['development' ],
preferBuiltins : false ,
browser : false ,
},
browsers : [playwrightLauncher ({ product : 'chromium' })],
plugins : [
importMapsPlugin ({
inject : {
importMap : {
imports : {
'@umbraco-cms/backoffice/external/lit' : '/node_modules/lit/index.js' ,
'@umbraco-cms/backoffice/lit-element' :
'/node_modules/@umbraco-cms/backoffice/dist-cms/packages/core/lit-element/index.js' ,
'@umbraco-cms/backoffice/element-api' :
'/node_modules/@umbraco-cms/backoffice/dist-cms/libs/element-api/index.js' ,
'@umbraco-cms/backoffice/observable-api' :
'/node_modules/@umbraco-cms/backoffice/dist-cms/libs/observable-api/index.js' ,
'@umbraco-cms/backoffice/context-api' :
'/node_modules/@umbraco-cms/backoffice/dist-cms/libs/context-api/index.js' ,
'@umbraco-cms/backoffice/controller-api' :
'/node_modules/@umbraco-cms/backoffice/dist-cms/libs/controller-api/index.js' ,
'@umbraco-cms/backoffice/class-api' :
'/node_modules/@umbraco-cms/backoffice/dist-cms/packages/core/class-api/index.js' ,
},
},
},
}),
esbuildPlugin ({
ts : true ,
tsconfig : './tsconfig.json' ,
target : 'auto' ,
json : true ,
}),
],
testRunnerHtml : (testFramework ) =>
`<html lang="en-us">
<head>
<meta charset="UTF-8" />
</head>
<body>
<script type="module" src="${testFramework} "></script>
</body>
</html>` ,
};
Import Path Reference Type Location Example Libs (low-level APIs)dist-cms/libs/element-api, observable-apiPackages (features)dist-cms/packages/core/lit-element, core/class-api
Common mistake : Using dist/packages instead of dist-cms causes 404 errors.
Alternative: Mock-Based Approach (Simpler) For simpler unit tests that don't need the full Umbraco context system, mock the Umbraco imports entirely. This approach:
Avoids complex import map configuration
Runs faster (no loading Umbraco packages)
Tests logic in true isolation
Works well for testing types, constants, and observable patterns
Simplified Configuration
import { esbuildPlugin } from '@web/dev-server-esbuild' ;
import { importMapsPlugin } from '@web/dev-server-import-maps' ;
import { playwrightLauncher } from '@web/test-runner-playwright' ;
export default {
files : 'src/**/*.test.ts' ,
nodeResolve : true ,
browsers : [playwrightLauncher ({ product : 'chromium' })],
plugins : [
esbuildPlugin ({ ts : true }),
importMapsPlugin ({
inject : {
importMap : {
imports : {
'@umbraco-cms/backoffice/external/lit' : '/src/__mocks__/lit.js' ,
'@umbraco-cms/backoffice/observable-api' : '/src/__mocks__/observable-api.js' ,
'@umbraco-cms/backoffice/class-api' : '/src/__mocks__/class-api.js' ,
},
},
},
}),
],
};
Mock Files Create src/__mocks__/observable-api.js:
export class UmbStringState {
#value;
#subscribers = [];
constructor (initialValue ) {
this .#value = initialValue;
}
getValue ( ) { return this .#value; }
setValue (value ) {
this .#value = value;
this .#subscribers.forEach (cb => cb (value));
}
asObservable ( ) {
return {
subscribe : (callback ) => {
this .#subscribers.push (callback);
callback (this .#value);
return { unsubscribe : () => {
const idx = this .#subscribers.indexOf (callback);
if (idx > -1 ) this .#subscribers.splice (idx, 1 );
}};
}
};
}
destroy ( ) { this .#subscribers = []; }
}
Create src/__mocks__/lit.js:
export const html = (strings, ...values ) => ({ strings, values });
export const css = (strings, ...values ) => ({ strings, values });
export const nothing = Symbol ('nothing' );
export const customElement = (name ) => (target ) => target;
export const state = ( ) => (target, propertyKey ) => {};
Testing with Mocks import { expect } from '@open-wc/testing' ;
import { OUR_ENTITY_TYPE } from './types.js' ;
describe ('Entity Types' , () => {
it ('should define entity type' , () => {
expect (OUR_ENTITY_TYPE ).to .equal ('our-entity' );
});
});
When to Use Each Approach Scenario Approach Testing types, constants, pure functions Mock-based (simpler) Testing observable state patterns Mock-based (simpler) Testing Lit elements with shadow DOM Full Umbraco imports Testing context consumption between elements Full Umbraco imports Testing with UUI components Full Umbraco imports
Working Example See tree-example in umbraco-backoffice/examples/tree-example/Client/:
web-test-runner.config.mjs - Mock-based configuration
src/__mocks__/ - Mock implementations
src/**/*.test.ts - Unit tests using mocks
Directory Structure my-extension/
├── src/
│ ├── my-context.ts
│ ├── my-context.test.ts # Test alongside source
│ ├── my-element.ts
│ └── my-element.test.ts
├── web-test-runner.config.mjs
├── package.json
└── tsconfig.json
Patterns
Basic Test Structure import { expect, fixture, defineCE } from '@open-wc/testing' ;
import { html } from 'lit' ;
describe ('MyFeature' , () => {
beforeEach (async () => {
});
afterEach (() => {
});
it ('should do something' , async () => {
});
});
Key Utilities fixture() - Create and wait for element:
const element = await fixture (html`<my-element > </my-element > ` );
const element = await fixture (html`<my-element > </my-element > ` , {
parentNode : hostElement,
});
defineCE() - Define custom element with unique tag:
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element' ;
class TestHostElement extends UmbLitElement {}
const testHostTag = defineCE (TestHostElement );
const host = await fixture (`<${testHostTag} ></${testHostTag} >` );
expect() - Chai assertions:
expect (value).to .equal (5 );
expect (value).to .be .true ;
expect (array).to .have .length (3 );
expect (element.shadowRoot ?.textContent ).to .include ('Hello' );
Testing Contexts import { expect, fixture, defineCE } from '@open-wc/testing' ;
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element' ;
import { MyContext } from './my-context.js' ;
class TestHostElement extends UmbLitElement {}
const testHostTag = defineCE (TestHostElement );
describe ('MyContext' , () => {
let hostElement : UmbLitElement ;
let context : MyContext ;
beforeEach (async () => {
hostElement = await fixture (`<${testHostTag} ></${testHostTag} >` );
context = new MyContext (hostElement);
});
it ('initializes with default value' , (done ) => {
context.value .subscribe ((value ) => {
expect (value).to .equal (0 );
done ();
});
});
it ('increments value' , (done ) => {
let callCount = 0 ;
context.value .subscribe ((value ) => {
callCount++;
if (callCount === 1 ) {
expect (value).to .equal (0 );
context.increment ();
} else if (callCount === 2 ) {
expect (value).to .equal (1 );
done ();
}
});
});
});
Testing Lit Elements import { expect, fixture } from '@open-wc/testing' ;
import { html } from 'lit' ;
import './my-element.js' ;
import type { MyElement } from './my-element.js' ;
describe ('MyElement' , () => {
let element : MyElement ;
beforeEach (async () => {
element = await fixture (html`<my-element > </my-element > ` );
});
it ('renders with default content' , async () => {
expect (element.shadowRoot ?.textContent ).to .include ('Default Value' );
});
it ('updates display when property changes' , async () => {
element.value = 'New Value' ;
await element.updateComplete ;
expect (element.shadowRoot ?.textContent ).to .include ('New Value' );
});
});
Testing Elements with Context import { expect, fixture, defineCE } from '@open-wc/testing' ;
import { html } from 'lit' ;
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element' ;
import { MyContext } from './my-context.js' ;
import './my-view.js' ;
class TestHostElement extends UmbLitElement {}
const testHostTag = defineCE (TestHostElement );
describe ('MyView' , () => {
let element : MyViewElement ;
let context : MyContext ;
let hostElement : UmbLitElement ;
beforeEach (async () => {
hostElement = await fixture (`<${testHostTag} ></${testHostTag} >` );
context = new MyContext (hostElement);
element = await fixture (html`<my-view > </my-view > ` , {
parentNode : hostElement,
});
await element.updateComplete ;
});
it ('displays value from context' , async () => {
expect (element.shadowRoot ?.textContent ).to .include ('Value: 0' );
});
it ('updates when context changes' , async () => {
context.increment ();
await element.updateComplete ;
expect (element.shadowRoot ?.textContent ).to .include ('Value: 1' );
});
});
Testing UI Interactions UUI components use shadow DOM, so events need composed: true:
it ('button click triggers action' , async () => {
const button = element.shadowRoot ?.querySelector ('uui-button' ) as HTMLElement ;
button.dispatchEvent (new MouseEvent ('click' , { bubbles : true , composed : true }));
await element.updateComplete ;
expect (element.shadowRoot ?.textContent ).to .include ('clicked' );
});
it ('toggle changes state' , async () => {
const toggle = element.shadowRoot ?.querySelector ('uui-toggle' ) as HTMLElement ;
toggle.dispatchEvent (new Event ('change' , { bubbles : true }));
await element.updateComplete ;
expect (element.shadowRoot ?.textContent ).to .include ('toggled' );
});
Observable State Behavior Important : State objects only emit when values change:
state.setValue (0 );
state.setValue (1 );
state.setValue (0 );
state.setValue (0 );
Testing no-op operations:
it ('does not go below 0' , (done ) => {
let callCount = 0 ;
context.count .subscribe ((value ) => {
callCount++;
if (callCount === 1 ) {
expect (value).to .equal (0 );
context.decrement ();
setTimeout (() => {
expect (callCount).to .equal (1 );
done ();
}, 50 );
}
});
});
Examples
Complete Context Test import { expect, fixture, defineCE } from '@open-wc/testing' ;
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element' ;
import { html } from '@umbraco-cms/backoffice/external/lit' ;
import { CounterContext } from './counter-context.js' ;
import './counter-view.js' ;
class TestHostElement extends UmbLitElement {}
const testHostTag = defineCE (TestHostElement );
describe ('CounterContext' , () => {
let element : UmbLitElement ;
let context : CounterContext ;
beforeEach (async () => {
element = await fixture (`<${testHostTag} ></${testHostTag} >` );
context = new CounterContext (element);
});
it ('initializes with 0' , (done ) => {
context.counter .subscribe ((value ) => {
expect (value).to .equal (0 );
done ();
});
});
it ('increments' , (done ) => {
let callCount = 0 ;
context.counter .subscribe ((value ) => {
callCount++;
if (callCount === 1 ) {
context.increment ();
} else if (callCount === 2 ) {
expect (value).to .equal (1 );
done ();
}
});
});
it ('resets to 0' , (done ) => {
let callCount = 0 ;
context.counter .subscribe ((value ) => {
callCount++;
if (callCount === 1 ) {
context.increment ();
context.increment ();
} else if (callCount === 3 ) {
context.reset ();
} else if (callCount === 4 ) {
expect (value).to .equal (0 );
done ();
}
});
});
});
describe ('CounterView' , () => {
let element : CounterViewElement ;
let context : CounterContext ;
let hostElement : UmbLitElement ;
beforeEach (async () => {
hostElement = await fixture (`<${testHostTag} ></${testHostTag} >` );
context = new CounterContext (hostElement);
element = await fixture (html`<counter-view > </counter-view > ` , {
parentNode : hostElement,
});
await element.updateComplete ;
});
it ('shows initial value' , async () => {
expect (element.shadowRoot ?.textContent ).to .include ('Count: 0' );
});
it ('reflects changes' , async () => {
context.increment ();
await element.updateComplete ;
expect (element.shadowRoot ?.textContent ).to .include ('Count: 1' );
});
});
Running Tests
npm test
npm run test :watch
npx web-test-runner src/my-element.test.ts
npx web-test-runner --coverage
Troubleshooting
404 errors for imports Check import map paths. Use dist-cms/libs/ for APIs and dist-cms/packages/ for features.
Element not defined Ensure you import the element file before using it in tests:
import './my-element.js' ;
Context not available Element must be child of host with context:
element = await fixture (html`<my-element > </my-element > ` , {
parentNode : hostElement,
});
Observable tests hang Use done() callback for async subscriptions:
it ('test' , (done ) => {
observable.subscribe ((value ) => {
expect (value).to .equal (expected);
done ();
});
});
updateComplete not waiting element.value = 'new' ;
await element.updateComplete ;
expect (element.shadowRoot ?.textContent ).to .include ('new' );