Skip to main content
umbraco-msw-testing MSW (Mock Service Worker) patterns for testing Umbraco backoffice extensions with mocked APIs
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/umbraco/Umbraco-CMS-Backoffice-Skills --skill umbraco-msw-testing命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... 相关职业
SOC
name umbraco-msw-testing description MSW (Mock Service Worker) patterns for testing Umbraco backoffice extensions with mocked APIs version 2.0.0 location managed allowed-tools Read, Write, Edit, WebFetch
Umbraco MSW Testing
What is it?
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.
When to Use
Testing API error handling (404, 500, validation errors)
Testing loading spinners and skeleton states
Testing network retry behavior
Testing edge cases without backend setup
Adding API mocking to unit tests
Related Skills
umbraco-testing - Master skill for testing overview
umbraco-unit-testing - Unit testing patterns (combine with MSW)
Documentation
MSW Docs : https://mswjs.io/docs/
Reference handlers : Umbraco-CMS/src/Umbraco.Web.UI.Client/src/mocks/handlers/
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" ,
"msw" : "^2.7.0"
} ,
"scripts" : {
"postinstall" : "npx msw init . --save" ,
"test" : "web-test-runner" ,
"test:watch" : "web-test-runner --watch"
}
}
npm install
npx playwright install chromium
The postinstall script copies mockServiceWorker.js to your project root. Without this file, MSW will fail silently.
Configuration 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>` ,
};
Directory Structure 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
Patterns
MSW v2 Syntax 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 })Request params ({ params }) => { ... }Request body ({ request }) => { const body = await request.json(); }Delay await delay(2000)
Global MSW Access const { http, HttpResponse , delay } = window .MockServiceWorker ;
umbracoPath Helper import { umbracoPath } from '@umbraco-cms/backoffice/utils' ;
umbracoPath ('/document/:id' )
Basic Handlers 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' },
});
}),
];
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 },
}
);
}),
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 });
}),
http.delete (umbracoPath ('/document/:id' ), ({ params } ) => {
const id = params.id as string ;
mockDb.delete (id);
return new HttpResponse (null , { status : 200 });
}),
Simulating States
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);
}),
http.get (umbracoPath ('/document/:id' ), () => {
return HttpResponse .json (
{ type : 'error' , detail : 'Internal server error' },
{ status : 500 }
);
}),
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' });
}),
Mock Database Pattern
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 ();
Worker Setup
const { setupWorker } = window .MockServiceWorker ;
import { handlers } from './handlers.js' ;
const worker = setupWorker (...handlers);
export const startMockServiceWorker = ( ) =>
worker.start ({
onUnhandledRequest : 'warn' ,
quiet : true ,
});
Integration with Tests import { expect, fixture } from '@open-wc/testing' ;
import { startMockServiceWorker } from './mocks/setup.js' ;
import './my-element.js' ;
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' );
});
});
Examples
Complete Handler File
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 : ['Required' ] } },
{ 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 });
}),
];
Handler Organization 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
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,
];
Running Tests
npm test
npm run test :watch
npx web-test-runner src/my-element.test.ts
Troubleshooting
MSW not intercepting requests
Check mockServiceWorker.js exists in project root
Verify MSW script is loaded in test HTML: <script src="/node_modules/msw/lib/iife/index.js"></script>
Ensure worker is started before tests run
"http is not defined" Use global access: const { http, HttpResponse } = window.MockServiceWorker;
Handler not matching Check path matches exactly. Use umbracoPath() for Umbraco API paths.
Requests still hitting real server Ensure onUnhandledRequest: 'warn' is set to see unhandled requests in console.
Migration from MSW v1 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 })res(ctx.delay(2000), ctx.json(data))await delay(2000); return HttpResponse.json(data)req.params.id({ params }) => params.idawait req.json()({ request }) => await request.json()