umbraco-msw-testing

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "umbraco-msw-testing" with this command: npx skills add umbraco/umbraco-cms-backoffice-skills/umbraco-umbraco-cms-backoffice-skills-umbraco-msw-testing

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

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.

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';

// Creates: /umbraco/management/api/v1/document/:id umbracoPath('/document/:id')

Basic Handlers

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 }); }),

Simulating States

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' }); }),

Mock Database Pattern

// 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();

Worker Setup

// 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, });

Integration with Tests

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&#x3C;my-element>&#x3C;/my-element>); await element.updateComplete;

// Element should show mocked data
expect(element.shadowRoot?.textContent).to.include('Item 1');

}); });

Examples

Complete Handler File

// 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 }); }), ];

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

// 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, ];

Running Tests

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

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.id

await req.json()

({ request }) => await request.json()

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

umbraco-backoffice

No summary provided by upstream source.

Repository SourceNeeds Review
General

umbraco-dashboard

No summary provided by upstream source.

Repository SourceNeeds Review
General

umbraco-extension-template

No summary provided by upstream source.

Repository SourceNeeds Review
General

umbraco-quickstart

No summary provided by upstream source.

Repository SourceNeeds Review