Claude
Skills
Sign in
Back

umbraco-msw-testing

Included with Lifetime
$97 forever

MSW (Mock Service Worker) patterns for testing Umbraco backoffice extensions with mocked APIs

General

What this skill does

# 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`:

```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:
```bash
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`:

```javascript
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

```typescript
const { http, HttpResponse, delay } = window.MockServiceWorker;
```

### umbracoPath Helper

```typescript
import { umbracoPath } from '@umbraco-cms/backoffice/utils';

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

### Basic Handlers

**GET Handler:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
// 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:**
```typescript
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):**
```typescript
http.get(umbracoPath('/slow-endpoint'), async () => {
  await delay(2000);
  return HttpResponse.json({ data: 'loaded' });
}),
```

### Mock Database Pattern

```typescript
// 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];

Related in General