Mock data
The frontend can run without the BFF. With ?mock=true in the URL, MSW intercepts every GraphQL request and every request for embedded service owner content, and answers from mock data in packages/frontend/src/mocks.
Playwright runs all integration tests this way. You can open the same scenarios by hand under pnpm dev.
Intent
Section titled “Intent”- One scenario per story. A story is a folder in
src/mocks/data/stories/. It gives a test a known set of profile, parties, dialogs and feature flags, and the same data on every run. - Base plus overrides.
src/mocks/data/base/is a broad default data set. A story only contains the files it changes, so a story shows exactly what is special about its scenario. - A dialog is complete where it is defined. Everything the app can ask about a dialog is on the dialog object: search fields, attachments, activities, transmissions, GUI actions, main content and logs. To change what a dialog shows, you edit the dialog, and nothing else.
Earlier, the dialog page was assembled from the dialog id: helpers in helper.ts looked up activities, transmissions, logs and actions with if (id === …). A dialog’s content was spread over several files, and the inbox and the dialog page could disagree, for example on the number of attachments.
Do not bring id lookups back. If a scenario needs something special, put it on the dialog.
How it fits together
Section titled “How it fits together”flowchart LR
URL["?mock=true&playwrightId=story"] --> main["main.tsx starts the MSW worker"]
main --> data["data.ts loads stories/story/type.ts, falling back to base/type.ts"]
data --> store["handlers.ts keeps the data in an in-memory store"]
store --> search["search queries: filterDialogs + toSearchDialog"]
store --> details["getDialogById: toDialogById"]
store --> rest["logs, forwarding, story markdown: read from the dialog"]
src/main.tsxstarts the MSW worker fromsrc/mocks/browser.tswhen the app runs in dev and the URL hasmock=true.src/mocks/data.tsloads each data type (profile,parties,dialogs,features,searches,services) fromdata/stories/<playwrightId>/<type>.ts. If the story has no such file, it usesdata/base/<type>.ts. Saved searches default to an empty list, and organizations always come from base.src/mocks/handlers.tsputs the data in an in-memory store and answers every request from it:- The inbox queries filter the dialogs with
filterDialogsand return each one throughtoSearchDialog. getDialogByIdreturnstoDialogById(dialog).- Notification logs, the label assignment log, forwarding and story markdown are read from the same dialog.
- Mutations (system labels, saved searches, profile settings and so on) change the store. The change lasts until the page is loaded again.
- The inbox queries filter the dialogs with
- Embedded content (main content and transmission content) points to
https://dialogporten-serviceprovider.net/fce-*URLs, and the same handlers answer them.
The dialog model
Section titled “The dialog model”A dialog is a MockDialog, built with mockDialog() from src/mocks/data/base/mockDialog.ts. You pass id and content, plus whatever the scenario needs. Everything else gets the same default for every dialog:
| Field | Default |
|---|---|
Search fields (party, org, status, dates, endUserContext …) | Person party urn:altinn:person:identifier-no:1, org: 'ok', RequiresAttention, Default label, seen, correspondenceservice |
attachments | One kvittering.pdf (mockAttachment(id)) |
activities | Dialog created, and one information activity |
transmissions | None. A transmission without navigationalActions gets an empty list |
guiActions | An unauthorized “Til skjema” action |
additionalInfo | A standard sentence |
mainContentReference | Markdown main content. Use mainContentMarkdown to serve story-specific markdown instead |
notificationLogs | None (mockNotificationLog() builds one) |
labelAssignmentLog | Left out: derived from the dialog’s system labels |
forwarding | 'allowed'. Also 'notAllowed' and 'attachmentsExceedMaxLimit' |
Some values are derived, so the inbox and the dialog page always agree:
guiAttachmentCountis the number of attachments with a GUI URL.- The received and sent counts are the transmissions from the service owner and from the party.
- On the dialog page,
isContentSeenis false while the dialog has theMarkedAsUnopenedlabel.
Adding a scenario
Section titled “Adding a scenario”-
Create
src/mocks/data/stories/<my-story>/dialogs.ts. Each file exports a constant named after the file (dialogs,parties,profileand so on).import { ActorType, TransmissionType } from 'bff-types-generated';import { dialogs as baseDialogs } from '../../base/dialogs.ts';import { type MockDialog, mockDialog } from '../../base/mockDialog.ts';export const dialogs: MockDialog[] = [mockDialog({id: '019241f7-0000-7000-8000-000000000001',content: {title: { mediaType: 'text/plain', value: [{ value: 'Min dialog', languageCode: 'nb' }] },summary: { mediaType: 'text/plain', value: [{ value: 'Kort oppsummering', languageCode: 'nb' }] },senderName: null,extendedStatus: null,},attachments: [],transmissions: [{id: 'my-story-t1',isAuthorized: true,createdAt: '2024-08-15T10:00:00.000Z',type: TransmissionType.Information,sender: { actorType: ActorType.ServiceOwner, actorId: null, actorName: null },relatedTransmissionId: null,content: {title: { mediaType: 'text/plain', value: [{ value: 'Melding fra etaten', languageCode: 'nb' }] },summary: null,contentReference: null,},attachments: [],},],}),...baseDialogs,];Leave out
...baseDialogsif the scenario should only contain your own dialogs. -
Add
parties.ts,profile.ts,features.ts,services.tsorsearches.tsonly if the scenario needs them. -
Open the story in a spec with
await page.goto(appUrlWithPlaywrightId('my-story'))(fromtests/index.ts). By hand, openhttp://localhost:5173/?mock=true&playwrightId=my-storyunderpnpm dev, orhttps://app.localhost/?mock=true&playwrightId=my-storywithmake dev.
Where things go
Section titled “Where things go”- Attachment:
attachments.mockAttachment(dialogId, index)gives the standardkvittering.pdf/vedlegg-N.pdffiles. AnexpiresAtin the past gives an expired attachment, and aurlofurn:dialogporten:unauthorizedgives one without access. See theattachment-statesstory. - Transmission:
transmissions. The received and sent counts follow automatically. - Transmission button:
navigationalActionson the transmission. Aurlofurn:dialogporten:unauthorizedorurn:dialogporten:expiredgives a disabled button. See thetransmission-navigational-actionsstory. - Activity:
activities.activities: []gives an empty activity log. - Notification:
notificationLogs, built withmockNotificationLog(notificationId, overrides). - Label history:
labelAssignmentLog. - Action:
guiActions. - Main content:
mainContentReference, with the content helpers inbase/helper.ts, ormainContentMarkdownfor text written in the story.getMockedUnauthorizedFCEContent()gives content without access, here or as a transmission’scontentReference. See thefce-unauthorizedstory. - Forwarding by email:
forwarding. The attachments decide what the forward dialog offers. - The same dialog in two stories: export it from one story and spread it in the other with
mockDialog({ ...otherDialog, ... }).activity-historydoes this with the dialog fromtransmissions.
See also Playwright pitfalls, for example why mock pages need the Vite dev server and why page.reload() leaves them blank.