umbraco-unit-testing by umbraco/umbraco-cms-backoffice-skills
npx skills add https://github.com/umbraco/umbraco-cms-backoffice-skills --skill umbraco-unit-testing使用 @open-wc/testing 对 Umbraco 后台扩展进行单元测试 - 这是一个专为 Web Components 和 Lit 元素设计的测试框架。这是最快且最隔离的测试方法。
添加到 package.json:
{
"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"
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
然后运行:
npm install
npx playwright install chromium
在项目根目录创建 web-test-runner.config.mjs:
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',
// 关键:使用 dist-cms,而不是 dist/packages
'@umbraco-cms/backoffice/lit-element':
'/node_modules/@umbraco-cms/backoffice/dist-cms/packages/core/lit-element/index.js',
// 关键:库位于 dist-cms/libs/,而不是 dist-cms/packages/
'@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>`,
};
| 类型 | 位置 | 示例 |
|---|---|---|
| Libs (底层 API) | dist-cms/libs/ | element-api, observable-api |
| Packages (功能模块) | dist-cms/packages/ | core/lit-element, core/class-api |
常见错误:使用 dist/packages 而不是 dist-cms 会导致 404 错误。
对于不需要完整 Umbraco 上下文系统的更简单的单元测试,可以完全模拟 Umbraco 导入。这种方法:
// web-test-runner.config.mjs
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 导入映射到本地模拟
'@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',
// 根据需要添加其他映射
},
},
},
}),
],
};
创建 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 = []; }
}
创建 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) => {};
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');
});
});
| 场景 | 方法 |
|---|---|
| 测试类型、常量、纯函数 | 基于模拟(更简单) |
| 测试可观察状态模式 | 基于模拟(更简单) |
| 测试带有 shadow DOM 的 Lit 元素 | 完整的 Umbraco 导入 |
| 测试元素间的上下文消费 | 完整的 Umbraco 导入 |
| 测试 UUI 组件 | 完整的 Umbraco 导入 |
查看 umbraco-backoffice/examples/tree-example/Client/ 中的 tree-example:
web-test-runner.config.mjs - 基于模拟的配置src/__mocks__/ - 模拟实现src/**/*.test.ts - 使用模拟的单元测试my-extension/
├── src/
│ ├── my-context.ts
│ ├── my-context.test.ts # 与源代码一起测试
│ ├── my-element.ts
│ └── my-element.test.ts
├── web-test-runner.config.mjs
├── package.json
└── tsconfig.json
import { expect, fixture, defineCE } from '@open-wc/testing';
import { html } from 'lit';
describe('MyFeature', () => {
beforeEach(async () => {
// 每个测试前的设置
});
afterEach(() => {
// 每个测试后的清理
});
it('should do something', async () => {
// 准备,执行,断言
});
});
fixture() - 创建并等待元素:
const element = await fixture(html`<my-element></my-element>`);
// 带有父节点(用于上下文消费)
const element = await fixture(html`<my-element></my-element>`, {
parentNode: hostElement,
});
defineCE() - 使用唯一标签定义自定义元素:
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
class TestHostElement extends UmbLitElement {}
const testHostTag = defineCE(TestHostElement);
const host = await fixture(`<${testHostTag}></${testHostTag}>`);
expect() - Chai 断言:
expect(value).to.equal(5);
expect(value).to.be.true;
expect(array).to.have.length(3);
expect(element.shadowRoot?.textContent).to.include('Hello');
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();
}
});
});
});
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');
});
});
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 () => {
// 1. 创建宿主元素
hostElement = await fixture(`<${testHostTag}></${testHostTag}>`);
// 2. 在宿主上创建上下文
context = new MyContext(hostElement);
// 3. 创建元素作为宿主的子元素
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');
});
});
UUI 组件使用 shadow DOM,因此事件需要 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');
});
// 切换 uui-toggle
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');
});
重要:状态对象仅在值发生变化时发出:
// 这将发出两次(值不同)
state.setValue(0);
state.setValue(1);
// 这发出一次(相同值 - 无第二次发出)
state.setValue(0);
state.setValue(0);
测试无操作:
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(); // 尝试低于 0
setTimeout(() => {
expect(callCount).to.equal(1); // 无第二次发出
done();
}, 50);
}
});
});
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');
});
});
# 运行所有单元测试
npm test
# 在监视模式下运行
npm run test:watch
# 运行特定文件
npx web-test-runner src/my-element.test.ts
# 运行覆盖率测试
npx web-test-runner --coverage
检查导入映射路径。对于 API 使用 dist-cms/libs/,对于功能模块使用 dist-cms/packages/。
确保在测试中使用元素之前导入元素文件:
import './my-element.js'; // 副作用导入会注册元素
元素必须是具有上下文的宿主的子元素:
element = await fixture(html`<my-element></my-element>`, {
parentNode: hostElement, // 宿主必须具有上下文
});
对异步订阅使用 done() 回调:
it('test', (done) => {
observable.subscribe((value) => {
expect(value).to.equal(expected);
done(); // 发出完成信号
});
});
确保等待它:
element.value = 'new';
await element.updateComplete; // 必须等待
expect(element.shadowRoot?.textContent).to.include('new');
每周安装量
72
仓库
GitHub 星标
17
首次出现
2026年2月4日
安全审计
安装于
github-copilot53
opencode23
codex22
cursor21
gemini-cli20
amp20
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.
Add to package.json:
{
"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"
}
}
Then run:
npm install
npx playwright install chromium
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',
// CRITICAL: Use dist-cms, NOT dist/packages
'@umbraco-cms/backoffice/lit-element':
'/node_modules/@umbraco-cms/backoffice/dist-cms/packages/core/lit-element/index.js',
// CRITICAL: libs are at dist-cms/libs/, NOT dist-cms/packages/
'@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',
// Add other imports as needed
},
},
},
}),
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>`,
};
| Type | Location | Example |
|---|---|---|
| Libs (low-level APIs) | dist-cms/libs/ | element-api, observable-api |
| Packages (features) | dist-cms/packages/ | core/lit-element, core/class-api |
Common mistake : Using dist/packages instead of dist-cms causes 404 errors.
For simpler unit tests that don't need the full Umbraco context system, mock the Umbraco imports entirely. This approach:
// web-test-runner.config.mjs
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: {
// Map Umbraco imports to local mocks
'@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',
// Add others as needed
},
},
},
}),
],
};
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) => {};
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');
});
});
| 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 |
See tree-example in umbraco-backoffice/examples/tree-example/Client/:
web-test-runner.config.mjs - Mock-based configurationsrc/__mocks__/ - Mock implementationssrc/**/*.test.ts - Unit tests using mocksmy-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
import { expect, fixture, defineCE } from '@open-wc/testing';
import { html } from 'lit';
describe('MyFeature', () => {
beforeEach(async () => {
// Setup for each test
});
afterEach(() => {
// Cleanup after each test
});
it('should do something', async () => {
// Arrange, Act, Assert
});
});
fixture() - Create and wait for element:
const element = await fixture(html`<my-element></my-element>`);
// With parent node (for context consumption)
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');
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();
}
});
});
});
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');
});
});
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 () => {
// 1. Create host element
hostElement = await fixture(`<${testHostTag}></${testHostTag}>`);
// 2. Create context on host
context = new MyContext(hostElement);
// 3. Create element as child of host
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');
});
});
UUI components use shadow DOM, so events need composed: true:
// Clicking buttons
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');
});
// Toggling uui-toggle
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');
});
Important : State objects only emit when values change:
// This WILL emit twice (values different)
state.setValue(0);
state.setValue(1);
// This emits ONCE (same value - no second emission)
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(); // Try to go below 0
setTimeout(() => {
expect(callCount).to.equal(1); // No second emission
done();
}, 50);
}
});
});
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');
});
});
# Run all unit tests
npm test
# Run in watch mode
npm run test:watch
# Run specific file
npx web-test-runner src/my-element.test.ts
# Run with coverage
npx web-test-runner --coverage
Check import map paths. Use dist-cms/libs/ for APIs and dist-cms/packages/ for features.
Ensure you import the element file before using it in tests:
import './my-element.js'; // Side effect import registers element
Element must be child of host with context:
element = await fixture(html`<my-element></my-element>`, {
parentNode: hostElement, // Host must have context
});
Use done() callback for async subscriptions:
it('test', (done) => {
observable.subscribe((value) => {
expect(value).to.equal(expected);
done(); // Signal completion
});
});
Ensure you await it:
element.value = 'new';
await element.updateComplete; // Must await
expect(element.shadowRoot?.textContent).to.include('new');
Weekly Installs
72
Repository
GitHub Stars
17
First Seen
Feb 4, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
github-copilot53
opencode23
codex22
cursor21
gemini-cli20
amp20
Vue 3 调试指南:解决响应式、计算属性与监听器常见错误
11,900 周安装
执行加速器:克服分析瘫痪,快速决策与发布的速度框架 | 创业生产力工具
150 周安装
Docker容器化开发指南:镜像构建、安全实践与编排最佳实践
150 周安装
散文风格诊断技能:识别小说句子写作问题,提升写作质量与故事表现力
150 周安装
GitHub Copilot CLI 委托技能:非交互式命令、模型选择与安全权限指南
153 周安装
Claude Code 多智能体编排系统 - 基于 Git 的轻量级任务追踪与工作流管理
150 周安装
Next.js useSearchParams Suspense 模式详解:解决URL参数读取与服务器端渲染问题
151 周安装