@crvouga/mockingbird-notifications 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +304 -2
- package/dist/chunk-GRGR3QZM.js +1066 -0
- package/dist/chunk-GRGR3QZM.js.map +7 -0
- package/dist/chunk-JQ6KPT2C.js +202 -0
- package/dist/chunk-JQ6KPT2C.js.map +7 -0
- package/dist/chunk-LM7R7AR7.js +183 -0
- package/dist/chunk-LM7R7AR7.js.map +7 -0
- package/dist/chunk-QE3TRUI6.js +444 -0
- package/dist/chunk-QE3TRUI6.js.map +7 -0
- package/dist/cli.js +104 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +411 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +7 -0
- package/dist/server.d.ts +177 -0
- package/dist/server.js +13 -0
- package/dist/server.js.map +7 -0
- package/dist/ui.d.ts +445 -0
- package/dist/ui.js +1923 -0
- package/dist/ui.js.map +7 -0
- package/package.json +88 -4
package/README.md
CHANGED
|
@@ -1,3 +1,305 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @crvouga/mockingbird-notifications
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Local emulators. Real API contracts. Part of [Mockingbird](https://github.com/crvouga/mockingbird).
|
|
4
|
+
|
|
5
|
+
A notification test harness for a Mockingbird fleet. It reads the same emulators your application
|
|
6
|
+
sends through and shows every SMS, email and push the way the recipient would see it: a phone's
|
|
7
|
+
Messages app, a phone lock screen (iOS) or notification shade (Android), and a webmail inbox.
|
|
8
|
+
Automated tests get the same data from a JSON API and a small client.
|
|
9
|
+
|
|
10
|
+
| Channel | Emulators read | Shown as |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| SMS | Twilio (Messages and Verify codes), Amazon SNS (direct `PhoneNumber` and `sms` topic subscribers), Customer.io, Mailosaur | Messages app: threads per sender, bubbles, one-time code suggestion |
|
|
13
|
+
| Email | Resend, Amazon SES (`SendEmail` and MIME `SendRawEmail`), Amazon SNS (`email` / `email-json` subscribers), Customer.io, Mailosaur, Twilio Verify email | Webmail: inbox per recipient (to, cc, bcc), rendered HTML, attachments, headers |
|
|
14
|
+
| Push | Firebase Cloud Messaging | iOS lock screen or Android shade, chosen from the device token's platform; title, body, image, data, badge, sound |
|
|
15
|
+
|
|
16
|
+
The [Notifications guide](https://mockingbird.chrisvouga.dev/docs/notifications) on the docs site
|
|
17
|
+
splits this README into pages, and the
|
|
18
|
+
[lab ordering example](https://mockingbird.chrisvouga.dev/examples/medical-testing) runs it live.
|
|
19
|
+
|
|
20
|
+
The harness only reads. It sends `GET` requests to each emulator's admin routes (`/__admin/outbox`
|
|
21
|
+
or `/__admin/state/...`) and never changes emulator state. The device art is generic: no vendor
|
|
22
|
+
logos.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install -D @crvouga/mockingbird-notifications
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Node 22 or newer. Install it next to `@crvouga/mockingbird-fleet` and the emulator packages your
|
|
31
|
+
application uses.
|
|
32
|
+
|
|
33
|
+
## Run it against your fleet
|
|
34
|
+
|
|
35
|
+
Start the fleet as usual, then point the harness at the fleet controller or its ready file:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
mockingbird-fleet serve --config mockingbird.json --ready-file .mockingbird/ready.json
|
|
39
|
+
mockingbird-notifications serve --manifest .mockingbird/ready.json # http://127.0.0.1:8790
|
|
40
|
+
# or, with the controller URL from the ready file:
|
|
41
|
+
mockingbird-notifications serve --fleet http://127.0.0.1:9000/__admin/fleet
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
With `--fleet`, the harness asks the controller what each entry is, so aliases work (`push` with
|
|
45
|
+
`target: "fcm"`), and it follows every namespace the fleet has. With `--manifest`, the file is
|
|
46
|
+
re-read on every sync, so a restarted fleet on new ports is picked up. Entries that are not
|
|
47
|
+
messaging emulators (Stripe, Postgres, …) are skipped. Emulators outside a fleet can be named
|
|
48
|
+
directly:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
mockingbird-notifications serve --service twilio=http://127.0.0.1:8798 --service resend=http://127.0.0.1:8794
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Flag | Meaning |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `--fleet <url>` | Fleet controller `adminBase`. |
|
|
57
|
+
| `--manifest <file>` | The fleet's `--ready-file`. |
|
|
58
|
+
| `--service <name=url>` | One emulator (repeatable). The name is the service (`twilio`, `fcm`, …) or any alias; an alias is identified from the emulator's health route. |
|
|
59
|
+
| `--port`, `--host` | Listener. Default `127.0.0.1:8790`; `--port 0` picks a free port. |
|
|
60
|
+
| `--namespace <name>` | Watch this namespace in the background (repeatable). Default `default`, or every fleet namespace with `--fleet`. Any namespace the UI or an API call asks for is watched from then on. |
|
|
61
|
+
| `--poll <ms>` | Background poll interval, default 750. `0` reads only when asked. |
|
|
62
|
+
| `--admin-key-env <VAR>` | Environment variable with the admin key (default `MOCKINGBIRD_ADMIN_KEY`), sent as `x-mockingbird-admin-key`. |
|
|
63
|
+
| `--ready-json` | Print `{"url": …}` once listening. |
|
|
64
|
+
|
|
65
|
+
## The UI
|
|
66
|
+
|
|
67
|
+
`/` shows three devices side by side. Each has a recipient picker (phone number, push token,
|
|
68
|
+
mailbox) that follows the newest arrival unless **Follow new** is off. The **Timeline** tab lists
|
|
69
|
+
everything in one table; double-click any message, or click a timeline row, to inspect the
|
|
70
|
+
normalized record and the emulator's raw one. Read state is kept per browser.
|
|
71
|
+
|
|
72
|
+
Links open one device for a screenshot, for example in a Playwright test:
|
|
73
|
+
|
|
74
|
+
| Query | Effect |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `?namespace=worker-3` | Show that namespace. |
|
|
77
|
+
| `?view=sms\|push\|email&to=<recipient>` | Show only that device, opened on that recipient (an SMS link opens the newest thread). |
|
|
78
|
+
| `?id=<notification id>` | Open that notification on its device. |
|
|
79
|
+
| `?view=timeline` | Open the timeline. |
|
|
80
|
+
| `?follow=0` | Do not jump to new arrivals. |
|
|
81
|
+
|
|
82
|
+
The devices behave like the real apps, in a generic style with no vendor marks:
|
|
83
|
+
|
|
84
|
+
- **Messages:** swipe a conversation left for Hide Alerts and Delete, or right for Pin and
|
|
85
|
+
Unread. Swipe the open conversation left to see each message's time, and long-press a message
|
|
86
|
+
for tapbacks and Copy. One-time codes show above the keyboard.
|
|
87
|
+
- **Lock screen (iOS):** notifications group by app. Tap a group to open it, swipe a card left for
|
|
88
|
+
Options and Clear, and long-press it for a preview.
|
|
89
|
+
- **Notification shade (Android):** tap a card to expand it, swipe it either way to dismiss it,
|
|
90
|
+
long-press it for its settings, and use Clear all.
|
|
91
|
+
- **Mail:** hover a row for Archive, Delete and Mark as read, then Undo from the toast. Star rows,
|
|
92
|
+
and open a message to read its HTML.
|
|
93
|
+
|
|
94
|
+
Every gesture also works with a click or the keyboard. Dismissing, archiving or deleting only hides
|
|
95
|
+
the message in this browser; the harness keeps every record, and a reload brings it back.
|
|
96
|
+
|
|
97
|
+
**Sound** (off by default; the top bar's speaker toggles it, and `?sound=1` turns it on): new
|
|
98
|
+
arrivals play a synthesized chime in each platform's style, and phones vibrate where the browser
|
|
99
|
+
allows. **Motion:** arrivals, swipes, stacks and screen changes are animated. A viewer's
|
|
100
|
+
reduced-motion setting turns animation off, and so does the `motion: "reduce"` option.
|
|
101
|
+
|
|
102
|
+
## End-to-end tests
|
|
103
|
+
|
|
104
|
+
The UI is built to be driven by Playwright, Cypress or any WebDriver client. It gives tests three
|
|
105
|
+
things:
|
|
106
|
+
|
|
107
|
+
1. **A ready signal.** The root, `[data-testid="notifications-app"]`, gets `data-ready="true"`
|
|
108
|
+
once its first read is drawn. It also reports `data-count`, `data-version`, `data-view`,
|
|
109
|
+
`data-namespace`, `data-connected`, `data-sound` and `data-motion`.
|
|
110
|
+
2. **A `data-testid` on everything a user can see or press.** `TEST_IDS` names them all, and its
|
|
111
|
+
typings say what each one marks.
|
|
112
|
+
3. **The notification's fields on each element that shows it.** Every bubble, thread, card, row,
|
|
113
|
+
open email and timeline row carries `data-notification-id`, `data-channel`, `data-service`,
|
|
114
|
+
`data-source`, `data-to`, `data-from`, `data-status`, `data-created-at` and `data-unread`. A
|
|
115
|
+
bubble that holds a one-time code also has `data-code`. Use these to find an element by what the
|
|
116
|
+
test knows instead of by its position.
|
|
117
|
+
|
|
118
|
+
Wait for the API first, then open a link to the message and assert on what the user would see:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { NotificationClient, selectors, TEST_IDS } from "@crvouga/mockingbird-notifications"
|
|
122
|
+
import { expect, test } from "@playwright/test"
|
|
123
|
+
|
|
124
|
+
const notifications = new NotificationClient("http://127.0.0.1:8790")
|
|
125
|
+
|
|
126
|
+
test("the invitation email reaches Ada's inbox", async ({ page }) => {
|
|
127
|
+
// …the app sends the invitation…
|
|
128
|
+
const [invite] = await notifications.waitFor({ channel: "email", to: "ada@example.com", contains: "invited" })
|
|
129
|
+
if (!invite) throw new Error("no invitation")
|
|
130
|
+
|
|
131
|
+
await page.goto(notifications.link({ id: invite.id })) // opens the inbox on that email
|
|
132
|
+
await page.locator(selectors.ready()).waitFor()
|
|
133
|
+
|
|
134
|
+
await expect(page.getByTestId(TEST_IDS.emailSubject)).toHaveText("You're invited")
|
|
135
|
+
// The HTML body is a same-origin iframe.
|
|
136
|
+
const body = page.frameLocator(selectors.testId(TEST_IDS.emailBody))
|
|
137
|
+
await expect(body.getByRole("link", { name: "Accept" })).toBeVisible()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test("the sign-in code is offered above the keyboard", async ({ page }) => {
|
|
141
|
+
const code = await notifications.waitForCode("+12025550123")
|
|
142
|
+
const [text] = await notifications.list({ channel: "sms", to: "+12025550123", contains: code })
|
|
143
|
+
await page.goto(notifications.link({ id: text?.id }))
|
|
144
|
+
await expect(page.locator(selectors.smsBubble({ id: text?.id }))).toHaveAttribute("data-code", code)
|
|
145
|
+
await expect(page.getByTestId(TEST_IDS.smsCode)).toHaveAttribute("data-code", code)
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`selectors` builds the common CSS selectors: `ready()`, `testId(id)`, `notification(id)`,
|
|
150
|
+
`smsBubble({from, id})`, `smsThread(from)`, `pushNotification(id)`, `emailRow(id)` and
|
|
151
|
+
`timelineRow(id)`.
|
|
152
|
+
|
|
153
|
+
| Device | Main test ids | State attributes |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| Top bar | `namespace`, `search`, `view-devices`, `view-timeline`, `follow`, `sound-toggle`, `connection`, `source`, `notice`, `toast`, `toast-undo` | `source`: `data-name`, `data-service`, `data-ok`, `data-count`. `connection`: `data-state`. |
|
|
156
|
+
| Messages | `sms-device`, `sms-recipient`, `sms-thread` (`-name`, `-preview`, `-time`, `-unread`), `sms-conversation`, `sms-back`, `sms-bubble`, `sms-code`, `sms-menu`, `sms-menu-tapback`, `sms-tapback`, `sms-empty` | `sms-device`: `data-screen` (`threads` or `conversation`), `data-recipient`, `data-thread`. `sms-thread`: `data-pinned`, `data-muted`. |
|
|
157
|
+
| Push | `push-device`, `push-recipient`, `push-skin`, `push-group`, `push-group-more`, `push-notification` (`-title`, `-body`, `-app`, `-time`, `-image`, `-data`, `-meta`), `push-clear`, `push-options`, `push-preview`, `push-settings`, `push-clear-all`, `push-empty` | `push-device`: `data-skin` (`ios` or `android`), `data-platform`. `push-notification`: `data-expanded`, `data-app`, `data-stacked`. |
|
|
158
|
+
| Mail | `email-device`, `email-recipient`, `email-row` (`-from`, `-subject`, `-snippet`, `-date`, `-star`, `-archive`, `-delete`), `email-open`, `email-subject`, `email-from-name`, `email-from-address`, `email-to`, `email-details`, `email-detail`, `email-body`, `email-text`, `email-attachment`, `email-empty` | `email-device`: `data-screen` (`inbox` or `message`). `email-detail`: `data-field`. `email-attachment`: `data-filename`, `data-content-type`, `data-size`. |
|
|
159
|
+
| Timeline | `timeline`, `timeline-row`, `inspector`, `inspector-json`, `inspector-field`, `inspector-close`, `inspector-show-on-device` | `inspector-json` holds the whole record, `raw` included, as JSON. |
|
|
160
|
+
|
|
161
|
+
Controls are real buttons, selects and inputs with accessible names, so `getByRole` works as well.
|
|
162
|
+
New arrivals are drawn live, so a test can keep a page open while the app sends. When several tests
|
|
163
|
+
share a harness, give each one its own namespace (`?namespace=`) or open a deep link to one
|
|
164
|
+
recipient: a link with `to=` stays on that recipient instead of following new arrivals. Set
|
|
165
|
+
`?follow=0` on any other link that should stay put.
|
|
166
|
+
|
|
167
|
+
[`e2e/ui.e2e.ts`](https://github.com/crvouga/mockingbird/blob/main/packages/notifications/e2e/ui.e2e.ts)
|
|
168
|
+
in this repository is a complete, passing example with real Twilio, Resend and FCM emulators.
|
|
169
|
+
|
|
170
|
+
## Embed the UI
|
|
171
|
+
|
|
172
|
+
`@crvouga/mockingbird-notifications/ui` mounts the same UI on any element, for example as a tab in
|
|
173
|
+
your own development shell. It reads an in-process harness directly, so the emulators, the harness
|
|
174
|
+
and the UI can all run in one browser tab with no server:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { createNotificationHarness } from "@crvouga/mockingbird-notifications"
|
|
178
|
+
import { mountNotifications } from "@crvouga/mockingbird-notifications/ui"
|
|
179
|
+
import { createRuntime as resend } from "@crvouga/mockingbird-service-resend"
|
|
180
|
+
import { createRuntime as twilio } from "@crvouga/mockingbird-service-twilio"
|
|
181
|
+
|
|
182
|
+
const harness = createNotificationHarness({ services: { twilio: twilio(), resend: resend() } })
|
|
183
|
+
const panel = document.getElementById("notifications") as HTMLElement
|
|
184
|
+
const ui = mountNotifications(panel, { harness, title: "My app notifications" })
|
|
185
|
+
await ui.ready
|
|
186
|
+
// ui.show({ channel: "sms", to: "+12025550123" }); ui.setSound(true); ui.setNamespace("worker-3")
|
|
187
|
+
ui.destroy()
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Its styles are scoped to the UI's root (`.mb-notifications`) and added once to the document, or to
|
|
191
|
+
the shadow root that holds `panel`. Options: `harness`, or `fetch` and `baseUrl` for a harness on
|
|
192
|
+
another origin; `channels` (which devices to show); `chrome` (show the top bar); `sound`; `motion`
|
|
193
|
+
(`auto`, `full` or `reduce`); `onArrive`; and `syncUrl`, which reads and writes `?view=`-style
|
|
194
|
+
links on the page. The [lab ordering example](https://github.com/crvouga/mockingbird/tree/main/packages/examples/medical-testing)
|
|
195
|
+
mounts it as its Notifications tab.
|
|
196
|
+
|
|
197
|
+
## Usage
|
|
198
|
+
|
|
199
|
+
`NotificationClient` talks to a running harness:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { NotificationClient } from "@crvouga/mockingbird-notifications"
|
|
203
|
+
|
|
204
|
+
const notifications = new NotificationClient("http://127.0.0.1:8790", { namespace: "worker-3" })
|
|
205
|
+
|
|
206
|
+
// The app texts a sign-in code; the test reads it the way the user would.
|
|
207
|
+
const code = await notifications.waitForCode("+12025550123")
|
|
208
|
+
|
|
209
|
+
// The app emails an invitation.
|
|
210
|
+
const [invite] = await notifications.waitFor({ channel: "email", to: "ada@example.com", contains: "invited" })
|
|
211
|
+
console.log(invite?.email?.subject, invite?.email?.html)
|
|
212
|
+
|
|
213
|
+
// A link to the inbox opened on that email, e.g. for Playwright's `page.goto(url)` and a screenshot.
|
|
214
|
+
const url = notifications.link({ id: invite?.id })
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Or run the harness inside the test process, against servers or in-process runtimes:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { createNotificationHarness } from "@crvouga/mockingbird-notifications"
|
|
221
|
+
import { createRuntime as twilio } from "@crvouga/mockingbird-service-twilio"
|
|
222
|
+
|
|
223
|
+
const harness = createNotificationHarness({ services: { twilio: twilio() }, pollIntervalMs: 0 })
|
|
224
|
+
// …the app sends through the same runtime…
|
|
225
|
+
const [sms] = await harness.waitFor({ channel: "sms", to: "+12025550123" })
|
|
226
|
+
harness.close()
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`createServer` from `@crvouga/mockingbird-notifications/server` serves the same harness over
|
|
230
|
+
`node:http` and takes the CLI's options (`fleet`, `manifest` as a path or object, `services`,
|
|
231
|
+
`namespaces`, `pollIntervalMs`, `adminKey`, `port`, `host`).
|
|
232
|
+
|
|
233
|
+
A read always reflects sends that finished before it started: `list`, `waitFor` and every API read
|
|
234
|
+
sync the emulators first instead of waiting for the next poll.
|
|
235
|
+
|
|
236
|
+
## HTTP API
|
|
237
|
+
|
|
238
|
+
All routes are `GET`. Filters: `channel` (`sms`, `email`, `push`), `service`, `source` (the fleet
|
|
239
|
+
entry name), `namespace` (default `default`), `to`, `contains` (case-insensitive text), `since`
|
|
240
|
+
(ISO-8601 or epoch ms).
|
|
241
|
+
|
|
242
|
+
| Route | Answer |
|
|
243
|
+
| --- | --- |
|
|
244
|
+
| `/api/notifications` | `{namespace, version, notifications}`, oldest first (`order=newest` reverses). |
|
|
245
|
+
| `/api/notifications/latest` | The newest match, or 404. |
|
|
246
|
+
| `/api/notifications/{id}` | One notification. |
|
|
247
|
+
| `/api/wait?timeoutMs=&count=` | `{notifications}` once `count` (default 1) match; 408 with what was found after `timeoutMs` (default 10000, at most 120000). |
|
|
248
|
+
| `/api/events?namespace=` | Server-sent events: `hello`, `change` (`{added, version}`) and `sources` every 2 s. |
|
|
249
|
+
| `/api/sources` | Each emulator: name, service, admin URL, `ok`, last error and count. |
|
|
250
|
+
| `/api/namespaces` | Namespaces watched. |
|
|
251
|
+
|
|
252
|
+
A notification:
|
|
253
|
+
|
|
254
|
+
```jsonc
|
|
255
|
+
{
|
|
256
|
+
"id": "twilio:default:SMb6c1…", // <fleet entry>:<namespace>:<provider id>[#mailbox]
|
|
257
|
+
"providerId": "SMb6c1…",
|
|
258
|
+
"channel": "sms", // sms | email | push
|
|
259
|
+
"source": "twilio", "service": "twilio", "namespace": "default",
|
|
260
|
+
"to": "+12025550123", // phone, mailbox (lower case), or push token
|
|
261
|
+
"from": { "name": null, "address": "+15005550006" },
|
|
262
|
+
"createdAt": "2026-11-10T06:39:06.886Z", // on the emulator's clock
|
|
263
|
+
"preview": "Your code is 123456",
|
|
264
|
+
"status": "queued",
|
|
265
|
+
"sms": { "body": "Your code is 123456", "code": null, "media": [] },
|
|
266
|
+
"email": null, // {subject, html, text, cc, bcc, replyTo, attachments, headers}
|
|
267
|
+
"push": null, // {title, body, imageUrl, app, platform, data, badge, sound}
|
|
268
|
+
"raw": { /* the emulator's own record */ }
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
An email to several recipients appears once in each mailbox (`to`, `cc` and `bcc`). When Resend
|
|
273
|
+
forwards to Mailosaur (`--forward-to-inbox`), the Mailosaur copy is hidden so the message shows once.
|
|
274
|
+
|
|
275
|
+
## API
|
|
276
|
+
|
|
277
|
+
| Export | Kind | Description |
|
|
278
|
+
| --- | --- | --- |
|
|
279
|
+
| `createNotificationHarness` | function | The harness as a Fetch handler: `fetch`, `sync(namespace?)`, `list(filter)`, `waitFor(filter, {timeoutMs, count})`, `sources()`, `close()`. Options: `fleet`, `manifest`, `services`, `namespaces`, `pollIntervalMs`, `adminKey`, `fetch`, `title`. |
|
|
280
|
+
| `NotificationClient` | class | HTTP client for a running harness: `list`, `latest`, `waitFor`, `waitForCode`, `sources`, `link`. Options: `namespace`, `fetch`. |
|
|
281
|
+
| `matches` | function | `(notification, filter)` → whether it matches, as the API filters. |
|
|
282
|
+
| `parseMime` | function | The MIME reader used for SES raw email: headers, subject, addresses, text, HTML, attachments. |
|
|
283
|
+
| `SERVICE_KINDS`, `SERVICE_LABELS` | values | The emulators the harness reads, and their display names. |
|
|
284
|
+
| `TEST_IDS`, `selectors` | values | Every `data-testid` the UI renders, and CSS selector builders for browser tests. Also exported from `./ui`. |
|
|
285
|
+
| `mountNotifications` (`./ui`) | function | `(element, {harness, …})` → `{ready, show, setNamespace, setSound, render, destroy}`. Mounts the UI on an element. |
|
|
286
|
+
| `createApp`, `STYLES`, `ROOT_CLASS` (`./ui`) | browser | The UI without the style injection and embedded defaults, its scoped stylesheet, and its root class. |
|
|
287
|
+
| `createServer`, `manifestFile`, `DEFAULT_PORT` (`./server`) | Node | Serve the harness over `node:http`; `manifestFile(path)` re-reads a fleet ready file. Port 8790. |
|
|
288
|
+
|
|
289
|
+
## Not covered
|
|
290
|
+
|
|
291
|
+
- Customer.io renders templates on its side and the emulator keeps no body, so its email shows the
|
|
292
|
+
template id and `message_data`. In-app (`inbox_message`) sends are not shown.
|
|
293
|
+
- SNS mobile push (platform endpoints) is not emulated, and SNS topic subscribers are matched by
|
|
294
|
+
topic only.
|
|
295
|
+
- Twilio MMS media is not in the emulator's outbox, so bubbles show text only.
|
|
296
|
+
- Replying from a device. The harness shows what was sent; inbound traffic goes through each
|
|
297
|
+
emulator's own admin routes.
|
|
298
|
+
|
|
299
|
+
## Develop
|
|
300
|
+
|
|
301
|
+
```sh
|
|
302
|
+
bun run --cwd packages/notifications dev # a fleet of every messaging emulator, sample traffic, harness on :8790
|
|
303
|
+
bun run --cwd packages/notifications e2e # the browser tests (bunx playwright install chromium first)
|
|
304
|
+
bun run --cwd packages/notifications storybook # every UI state, from fixtures
|
|
305
|
+
```
|