Loading...
Loading...
MSW (Mock Service Worker) patterns for testing Umbraco backoffice extensions with mocked APIs
npx skill4agent add umbraco/umbraco-cms-backoffice-skills umbraco-msw-testingUmbraco-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"
}
}npm install
npx playwright install chromiumpostinstallmockServiceWorker.jsweb-test-runner.config.mjsimport { 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| Concept | MSW v2 Syntax |
|---|---|
| HTTP methods | |
| JSON response | |
| Status codes | |
| Empty response | |
| Request params | |
| Request body | |
| Delay | |
const { http, HttpResponse, delay } = window.MockServiceWorker;import { umbracoPath } from '@umbraco-cms/backoffice/utils';
// Creates: /umbraco/management/api/v1/document/:id
umbracoPath('/document/:id')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 });
}),// 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 }
);
}),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 });
}),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,
});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.tsmockServiceWorker.js<script src="/node_modules/msw/lib/iife/index.js"></script>const { http, HttpResponse } = window.MockServiceWorker;umbracoPath()onUnhandledRequest: 'warn'| MSW v1 | MSW v2 |
|---|---|
| |
| |
| |
| |
| |
| |
| |