重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
umbraco-msw-testing by umbraco/umbraco-cms-backoffice-skills
npx skills add https://github.com/umbraco/umbraco-cms-backoffice-skills --skill umbraco-msw-testingMSW(Mock Service Worker)通过拦截 API 调用并返回模拟响应,实现对 Umbraco 后台扩展的测试。这非常适合在没有运行 Umbraco 实例的情况下测试错误状态、加载状态和边界情况。
Umbraco-CMS/src/Umbraco.Web.UI.Client/src/mocks/handlers/添加到 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",
"msw": "^2.7.0"
},
"scripts": {
"postinstall": "npx msw init . --save",
"test": "web-test-runner",
"test:watch": "web-test-runner --watch"
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
然后运行:
npm install
npx playwright install chromium
postinstall 脚本会将 mockServiceWorker.js 复制到您的项目根目录。没有此文件,MSW 将静默失败。
创建 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',
'@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" />
<!-- 加载 MSW v2 作为 IIFE 以获取 window.MockServiceWorker -->
<script src="/node_modules/msw/lib/iife/index.js"></script>
</head>
<body>
<script type="module" src="${testFramework}"></script>
</body>
</html>`,
};
my-extension/
├── src/
│ ├── my-element.ts
│ ├── my-element.test.ts
│ └── mocks/
│ ├── handlers.ts # MSW 处理程序
│ ├── setup.ts # Worker 设置
│ └── data/
│ └── items.db.ts # 模拟数据库
├── mockServiceWorker.js # 由 postinstall 生成
├── web-test-runner.config.mjs
├── package.json
└── tsconfig.json
Umbraco 使用 MSW v2。关键 API 模式:
| 概念 | MSW v2 语法 |
|---|---|
| HTTP 方法 | http.get(), http.post(), http.put(), http.delete() |
| JSON 响应 | HttpResponse.json(data) |
| 状态码 | HttpResponse.json(data, { status: 201 }) |
| 空响应 | new HttpResponse(null, { status: 204 }) |
| 请求参数 | ({ params }) => { ... } |
| 请求体 | ({ request }) => { const body = await request.json(); } |
| 延迟 | await delay(2000) |
const { http, HttpResponse, delay } = window.MockServiceWorker;
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
// 创建:/umbraco/management/api/v1/document/:id
umbracoPath('/document/:id')
GET 处理程序:
const { http, HttpResponse } = window.MockServiceWorker;
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
http.get(umbracoPath('/document/:id'), ({ params }) => {
const id = params.id as string;
return HttpResponse.json({
id,
name: '测试文档',
documentType: { alias: 'testType' },
});
}),
];
POST 处理程序:
http.post(umbracoPath('/document'), async ({ request }) => {
const body = await request.json();
if (!body.name) {
return HttpResponse.json(
{
type: 'validation',
status: 400,
errors: { name: ['名称是必填项'] },
},
{ status: 400 }
);
}
const newId = crypto.randomUUID();
return HttpResponse.json(
{ id: newId },
{
status: 201,
headers: { 'Umb-Generated-Resource': newId },
}
);
}),
PUT 处理程序:
http.put(umbracoPath('/document/:id'), async ({ params, request }) => {
const id = params.id as string;
const body = await request.json();
mockDb.update(id, body);
return new HttpResponse(null, { status: 200 });
}),
DELETE 处理程序:
http.delete(umbracoPath('/document/:id'), ({ params }) => {
const id = params.id as string;
mockDb.delete(id);
return new HttpResponse(null, { status: 200 });
}),
错误响应:
// 404 未找到
http.get(umbracoPath('/document/:id'), ({ params }) => {
const doc = mockDb.read(params.id as string);
if (!doc) return new HttpResponse(null, { status: 404 });
return HttpResponse.json(doc);
}),
// 500 服务器错误
http.get(umbracoPath('/document/:id'), () => {
return HttpResponse.json(
{ type: 'error', detail: '内部服务器错误' },
{ status: 500 }
);
}),
验证错误:
http.post(umbracoPath('/document'), async ({ request }) => {
const body = await request.json();
if (!body.name) {
return HttpResponse.json(
{
type: 'validation',
errors: {
name: ['名称是必填项'],
title: ['标题必须至少包含 3 个字符'],
},
},
{ status: 400 }
);
}
return new HttpResponse(null, { status: 201 });
}),
延迟响应(加载状态):
http.get(umbracoPath('/slow-endpoint'), async () => {
await delay(2000);
return HttpResponse.json({ data: '已加载' });
}),
// src/mocks/data/items.db.ts
interface Item {
id: string;
name: string;
value: number;
}
class ItemsMockDb {
private data: Item[] = [
{ id: '1', name: '项目 1', value: 100 },
{ id: '2', name: '项目 2', value: 200 },
];
read(id: string) {
return this.data.find((item) => item.id === id);
}
readAll() {
return [...this.data];
}
create(item: Omit<Item, 'id'>) {
const newItem = { ...item, id: crypto.randomUUID() };
this.data.push(newItem);
return newItem.id;
}
update(id: string, updates: Partial<Item>) {
const index = this.data.findIndex((i) => i.id === id);
if (index !== -1) {
this.data[index] = { ...this.data[index], ...updates };
}
}
delete(id: string) {
this.data = this.data.filter((i) => i.id !== id);
}
}
export const itemsDb = new ItemsMockDb();
// src/mocks/setup.ts
const { setupWorker } = window.MockServiceWorker;
import { handlers } from './handlers.js';
const worker = setupWorker(...handlers);
export const startMockServiceWorker = () =>
worker.start({
onUnhandledRequest: 'warn',
quiet: true,
});
在测试文件中:
import { expect, fixture } from '@open-wc/testing';
import { startMockServiceWorker } from './mocks/setup.js';
import './my-element.js';
// 在测试前启动 MSW
before(async () => {
await startMockServiceWorker();
});
describe('MyElement with API', () => {
it('displays data from API', async () => {
const element = await fixture(html`<my-element></my-element>`);
await element.updateComplete;
// 元素应显示模拟数据
expect(element.shadowRoot?.textContent).to.include('Item 1');
});
});
// src/mocks/handlers.ts
const { http, HttpResponse } = window.MockServiceWorker;
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
import { itemsDb } from './data/items.db.js';
export const handlers = [
// 列出项目
http.get(umbracoPath('/my-extension/items'), () => {
const items = itemsDb.readAll();
return HttpResponse.json({ total: items.length, items });
}),
// 获取单个项目
http.get(umbracoPath('/my-extension/items/:id'), ({ params }) => {
const item = itemsDb.read(params.id as string);
if (!item) return new HttpResponse(null, { status: 404 });
return HttpResponse.json(item);
}),
// 创建项目
http.post(umbracoPath('/my-extension/items'), async ({ request }) => {
const body = await request.json();
if (!body.name) {
return HttpResponse.json(
{ type: 'validation', errors: { name: ['必填'] } },
{ status: 400 }
);
}
const id = itemsDb.create(body);
return HttpResponse.json(
{ id },
{
status: 201,
headers: { 'Umb-Generated-Resource': id },
}
);
}),
// 更新项目
http.put(umbracoPath('/my-extension/items/:id'), async ({ params, request }) => {
const id = params.id as string;
if (!itemsDb.read(id)) return new HttpResponse(null, { status: 404 });
itemsDb.update(id, await request.json());
return new HttpResponse(null, { status: 200 });
}),
// 删除项目
http.delete(umbracoPath('/my-extension/items/:id'), ({ params }) => {
const id = params.id as string;
if (!itemsDb.read(id)) return new HttpResponse(null, { status: 404 });
itemsDb.delete(id);
return new HttpResponse(null, { status: 200 });
}),
];
src/mocks/
├── handlers.ts # 聚合所有处理程序
├── setup.ts # Worker 设置
├── handlers/
│ ├── document.handlers.ts
│ ├── media.handlers.ts
│ └── my-extension.handlers.ts
└── data/
├── document.db.ts
└── items.db.ts
// handlers.ts
import { documentHandlers } from './handlers/document.handlers.js';
import { mediaHandlers } from './handlers/media.handlers.js';
import { myExtensionHandlers } from './handlers/my-extension.handlers.js';
export const handlers = [
...documentHandlers,
...mediaHandlers,
...myExtensionHandlers,
];
# 运行所有测试
npm test
# 在监视模式下运行
npm run test:watch
# 运行特定文件
npx web-test-runner src/my-element.test.ts
mockServiceWorker.js 是否存在于项目根目录<script src="/node_modules/msw/lib/iife/index.js"></script>使用全局访问:const { http, HttpResponse } = window.MockServiceWorker;
检查路径是否完全匹配。对于 Umbraco API 路径,请使用 umbracoPath()。
确保设置了 onUnhandledRequest: 'warn',以便在控制台中查看未处理的请求。
如果从 MSW v1 升级,以下是主要变更:
| MSW v1 | MSW v2 |
|---|---|
rest.get() | http.get() |
rest.post() | http.post() |
(req, res, ctx) => res(ctx.json(data)) | () => HttpResponse.json(data) |
res(ctx.status(404)) | new HttpResponse(null, { status: 404 }) |
res(ctx.delay(2000), ctx.json(data)) | await delay(2000); return HttpResponse.json(data) |
req.params.id | ({ params }) => params.id |
await req.json() | ({ request }) => await request.json() |
每周安装量
65
代码仓库
GitHub 星标数
14
首次出现
2026年2月4日
安全审计
已安装于
github-copilot49
opencode20
codex20
cursor19
gemini-cli18
amp18
MSW (Mock Service Worker) enables testing Umbraco backoffice extensions by intercepting API calls and returning mock responses. This is ideal for testing error states, loading states, and edge cases without a running Umbraco instance.
Umbraco-CMS/src/Umbraco.Web.UI.Client/src/mocks/handlers/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",
"msw": "^2.7.0"
},
"scripts": {
"postinstall": "npx msw init . --save",
"test": "web-test-runner",
"test:watch": "web-test-runner --watch"
}
}
Then run:
npm install
npx playwright install chromium
The postinstall script copies mockServiceWorker.js to your project root. Without this file, MSW will fail silently.
Create 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',
'@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" />
<!-- Load MSW v2 as IIFE to get window.MockServiceWorker -->
<script src="/node_modules/msw/lib/iife/index.js"></script>
</head>
<body>
<script type="module" src="${testFramework}"></script>
</body>
</html>`,
};
my-extension/
├── src/
│ ├── my-element.ts
│ ├── my-element.test.ts
│ └── mocks/
│ ├── handlers.ts # MSW handlers
│ ├── setup.ts # Worker setup
│ └── data/
│ └── items.db.ts # Mock database
├── mockServiceWorker.js # Generated by postinstall
├── web-test-runner.config.mjs
├── package.json
└── tsconfig.json
Umbraco uses MSW v2. Key API patterns:
| Concept | MSW v2 Syntax |
|---|---|
| HTTP methods | http.get(), http.post(), http.put(), http.delete() |
| JSON response | HttpResponse.json(data) |
| Status codes | HttpResponse.json(data, { status: 201 }) |
| Empty response | new HttpResponse(null, { status: 204 }) |
const { http, HttpResponse, delay } = window.MockServiceWorker;
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
// Creates: /umbraco/management/api/v1/document/:id
umbracoPath('/document/:id')
GET Handler:
const { http, HttpResponse } = window.MockServiceWorker;
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
http.get(umbracoPath('/document/:id'), ({ params }) => {
const id = params.id as string;
return HttpResponse.json({
id,
name: 'Test Document',
documentType: { alias: 'testType' },
});
}),
];
POST Handler:
http.post(umbracoPath('/document'), async ({ request }) => {
const body = await request.json();
if (!body.name) {
return HttpResponse.json(
{
type: 'validation',
status: 400,
errors: { name: ['Name is required'] },
},
{ status: 400 }
);
}
const newId = crypto.randomUUID();
return HttpResponse.json(
{ id: newId },
{
status: 201,
headers: { 'Umb-Generated-Resource': newId },
}
);
}),
PUT Handler:
http.put(umbracoPath('/document/:id'), async ({ params, request }) => {
const id = params.id as string;
const body = await request.json();
mockDb.update(id, body);
return new HttpResponse(null, { status: 200 });
}),
DELETE Handler:
http.delete(umbracoPath('/document/:id'), ({ params }) => {
const id = params.id as string;
mockDb.delete(id);
return new HttpResponse(null, { status: 200 });
}),
Error Responses:
// 404 Not Found
http.get(umbracoPath('/document/:id'), ({ params }) => {
const doc = mockDb.read(params.id as string);
if (!doc) return new HttpResponse(null, { status: 404 });
return HttpResponse.json(doc);
}),
// 500 Server Error
http.get(umbracoPath('/document/:id'), () => {
return HttpResponse.json(
{ type: 'error', detail: 'Internal server error' },
{ status: 500 }
);
}),
Validation Errors:
http.post(umbracoPath('/document'), async ({ request }) => {
const body = await request.json();
if (!body.name) {
return HttpResponse.json(
{
type: 'validation',
errors: {
name: ['Name is required'],
title: ['Title must be at least 3 characters'],
},
},
{ status: 400 }
);
}
return new HttpResponse(null, { status: 201 });
}),
Delayed Responses (Loading States):
http.get(umbracoPath('/slow-endpoint'), async () => {
await delay(2000);
return HttpResponse.json({ data: 'loaded' });
}),
// src/mocks/data/items.db.ts
interface Item {
id: string;
name: string;
value: number;
}
class ItemsMockDb {
private data: Item[] = [
{ id: '1', name: 'Item 1', value: 100 },
{ id: '2', name: 'Item 2', value: 200 },
];
read(id: string) {
return this.data.find((item) => item.id === id);
}
readAll() {
return [...this.data];
}
create(item: Omit<Item, 'id'>) {
const newItem = { ...item, id: crypto.randomUUID() };
this.data.push(newItem);
return newItem.id;
}
update(id: string, updates: Partial<Item>) {
const index = this.data.findIndex((i) => i.id === id);
if (index !== -1) {
this.data[index] = { ...this.data[index], ...updates };
}
}
delete(id: string) {
this.data = this.data.filter((i) => i.id !== id);
}
}
export const itemsDb = new ItemsMockDb();
// src/mocks/setup.ts
const { setupWorker } = window.MockServiceWorker;
import { handlers } from './handlers.js';
const worker = setupWorker(...handlers);
export const startMockServiceWorker = () =>
worker.start({
onUnhandledRequest: 'warn',
quiet: true,
});
In test file:
import { expect, fixture } from '@open-wc/testing';
import { startMockServiceWorker } from './mocks/setup.js';
import './my-element.js';
// Start MSW before tests
before(async () => {
await startMockServiceWorker();
});
describe('MyElement with API', () => {
it('displays data from API', async () => {
const element = await fixture(html`<my-element></my-element>`);
await element.updateComplete;
// Element should show mocked data
expect(element.shadowRoot?.textContent).to.include('Item 1');
});
});
// src/mocks/handlers.ts
const { http, HttpResponse } = window.MockServiceWorker;
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
import { itemsDb } from './data/items.db.js';
export const handlers = [
// List items
http.get(umbracoPath('/my-extension/items'), () => {
const items = itemsDb.readAll();
return HttpResponse.json({ total: items.length, items });
}),
// Get single item
http.get(umbracoPath('/my-extension/items/:id'), ({ params }) => {
const item = itemsDb.read(params.id as string);
if (!item) return new HttpResponse(null, { status: 404 });
return HttpResponse.json(item);
}),
// Create item
http.post(umbracoPath('/my-extension/items'), async ({ request }) => {
const body = await request.json();
if (!body.name) {
return HttpResponse.json(
{ type: 'validation', errors: { name: ['Required'] } },
{ status: 400 }
);
}
const id = itemsDb.create(body);
return HttpResponse.json(
{ id },
{
status: 201,
headers: { 'Umb-Generated-Resource': id },
}
);
}),
// Update item
http.put(umbracoPath('/my-extension/items/:id'), async ({ params, request }) => {
const id = params.id as string;
if (!itemsDb.read(id)) return new HttpResponse(null, { status: 404 });
itemsDb.update(id, await request.json());
return new HttpResponse(null, { status: 200 });
}),
// Delete item
http.delete(umbracoPath('/my-extension/items/:id'), ({ params }) => {
const id = params.id as string;
if (!itemsDb.read(id)) return new HttpResponse(null, { status: 404 });
itemsDb.delete(id);
return new HttpResponse(null, { status: 200 });
}),
];
src/mocks/
├── handlers.ts # Aggregates all handlers
├── setup.ts # Worker setup
├── handlers/
│ ├── document.handlers.ts
│ ├── media.handlers.ts
│ └── my-extension.handlers.ts
└── data/
├── document.db.ts
└── items.db.ts
// handlers.ts
import { documentHandlers } from './handlers/document.handlers.js';
import { mediaHandlers } from './handlers/media.handlers.js';
import { myExtensionHandlers } from './handlers/my-extension.handlers.js';
export const handlers = [
...documentHandlers,
...mediaHandlers,
...myExtensionHandlers,
];
# Run all tests
npm test
# Run in watch mode
npm run test:watch
# Run specific file
npx web-test-runner src/my-element.test.ts
mockServiceWorker.js exists in project root<script src="/node_modules/msw/lib/iife/index.js"></script>Use global access: const { http, HttpResponse } = window.MockServiceWorker;
Check path matches exactly. Use umbracoPath() for Umbraco API paths.
Ensure onUnhandledRequest: 'warn' is set to see unhandled requests in console.
If upgrading from MSW v1, here are the key changes:
| MSW v1 | MSW v2 |
|---|---|
rest.get() | http.get() |
rest.post() | http.post() |
(req, res, ctx) => res(ctx.json(data)) | () => HttpResponse.json(data) |
res(ctx.status(404)) | new HttpResponse(null, { status: 404 }) |
Weekly Installs
65
Repository
GitHub Stars
14
First Seen
Feb 4, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
github-copilot49
opencode20
codex20
cursor19
gemini-cli18
amp18
代码审查最佳实践指南:完整流程、安全与性能审查清单
12,400 周安装
| Request params | ({ params }) => { ... } |
| Request body | ({ request }) => { const body = await request.json(); } |
| Delay | await delay(2000) |
res(ctx.delay(2000), ctx.json(data)) | await delay(2000); return HttpResponse.json(data) |
req.params.id | ({ params }) => params.id |
await req.json() | ({ request }) => await request.json() |