@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.
@@ -0,0 +1,202 @@
1
+ import {
2
+ createNotificationHarness
3
+ } from "./chunk-GRGR3QZM.js";
4
+
5
+ // src/server.ts
6
+ import { readFile } from "node:fs/promises";
7
+
8
+ // ../adapters/node/dist/serve.js
9
+ import { createServer } from "node:http";
10
+ import { Readable } from "node:stream";
11
+ var serve = async (api, options = {}) => {
12
+ const maxBytes = options.maxBodyBytes ?? 16 * 1024 * 1024;
13
+ const timeoutMs = options.requestTimeoutMs ?? 3e4;
14
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0 || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1)
15
+ throw new Error("invalid request limits");
16
+ const server = createServer(async (req, res) => {
17
+ const aborted = new AbortController();
18
+ let reader;
19
+ let timer;
20
+ const abort = () => {
21
+ aborted.abort();
22
+ void reader?.cancel().catch(() => {
23
+ });
24
+ };
25
+ const fail = (status, message) => {
26
+ if (!res.headersSent && !res.destroyed) {
27
+ res.writeHead(status, { "content-type": "application/json", connection: "close" });
28
+ res.end(JSON.stringify({ error: { type: "mockingbird_transport", message } }));
29
+ }
30
+ abort();
31
+ };
32
+ req.once("aborted", abort);
33
+ res.once("close", abort);
34
+ res.once("error", abort);
35
+ try {
36
+ const address = server.address();
37
+ const port = typeof address === "object" && address !== null ? address.port : 80;
38
+ const url = new URL((req.url ?? "/").replace(/^\/+/, "/"), `http://${req.headers.host ?? `localhost:${port}`}`);
39
+ const headers = new Headers();
40
+ for (const [name, value] of Object.entries(req.headers)) {
41
+ if (Array.isArray(value))
42
+ for (const item of value)
43
+ headers.append(name, item);
44
+ else if (value !== void 0)
45
+ headers.set(name, value);
46
+ }
47
+ const init = {
48
+ method: req.method ?? "GET",
49
+ headers,
50
+ signal: aborted.signal
51
+ };
52
+ if (init.method !== "GET" && init.method !== "HEAD") {
53
+ if (Number(req.headers["content-length"]) > maxBytes) {
54
+ fail(413, "request body exceeds byte limit");
55
+ return;
56
+ }
57
+ let bytes = 0;
58
+ const source = Readable.toWeb(req);
59
+ init.body = source.pipeThrough(new TransformStream({
60
+ transform(chunk, controller) {
61
+ bytes += chunk.byteLength;
62
+ if (bytes > maxBytes) {
63
+ fail(413, "request body exceeds byte limit");
64
+ controller.error(new Error("request body exceeds byte limit"));
65
+ } else
66
+ controller.enqueue(chunk);
67
+ }
68
+ }));
69
+ init.duplex = "half";
70
+ timer = setTimeout(() => fail(408, "request body timed out"), timeoutMs);
71
+ req.once("end", () => clearTimeout(timer));
72
+ }
73
+ const response = await api.fetch(new Request(url, init));
74
+ if (aborted.signal.aborted || res.destroyed) {
75
+ await response.body?.cancel();
76
+ return;
77
+ }
78
+ const output = Object.fromEntries(response.headers);
79
+ const cookies = response.headers.getSetCookie();
80
+ if (cookies.length)
81
+ output["set-cookie"] = cookies;
82
+ res.writeHead(response.status, output);
83
+ if (!response.body) {
84
+ res.end();
85
+ return;
86
+ }
87
+ res.flushHeaders();
88
+ reader = response.body.getReader();
89
+ for (; ; ) {
90
+ const { done, value } = await reader.read();
91
+ if (done || aborted.signal.aborted)
92
+ break;
93
+ if (!res.write(value)) {
94
+ await new Promise((resolve) => {
95
+ const finish = () => {
96
+ res.off("drain", finish);
97
+ res.off("close", finish);
98
+ res.off("error", finish);
99
+ resolve();
100
+ };
101
+ res.once("drain", finish);
102
+ res.once("close", finish);
103
+ res.once("error", finish);
104
+ if (res.destroyed || aborted.signal.aborted)
105
+ finish();
106
+ });
107
+ }
108
+ }
109
+ if (!aborted.signal.aborted)
110
+ res.end();
111
+ } catch (error) {
112
+ if (error.code === "MOCKINGBIRD_DROP")
113
+ req.socket.destroy();
114
+ else if (!aborted.signal.aborted && !res.destroyed) {
115
+ if (res.headersSent)
116
+ res.destroy();
117
+ else {
118
+ res.writeHead(500, { "content-type": "application/json" });
119
+ res.end(JSON.stringify({
120
+ error: {
121
+ type: "mockingbird_internal",
122
+ message: error instanceof Error ? error.message : String(error)
123
+ }
124
+ }));
125
+ }
126
+ }
127
+ } finally {
128
+ clearTimeout(timer);
129
+ reader?.releaseLock();
130
+ req.off("aborted", abort);
131
+ res.off("close", abort);
132
+ res.off("error", abort);
133
+ if (!req.complete)
134
+ req.resume();
135
+ }
136
+ });
137
+ server.requestTimeout = timeoutMs;
138
+ await new Promise((resolve, reject) => {
139
+ server.once("error", reject);
140
+ server.listen(options.port ?? 0, options.host, resolve);
141
+ });
142
+ return server;
143
+ };
144
+
145
+ // ../adapters/node/dist/listen.js
146
+ var listen = async (api, options = {}) => {
147
+ const host = options.host ?? "127.0.0.1";
148
+ const server = await serve(api, {
149
+ port: options.port ?? 0,
150
+ host,
151
+ ...options.maxBodyBytes !== void 0 ? { maxBodyBytes: options.maxBodyBytes } : {}
152
+ });
153
+ const address = server.address();
154
+ const port = typeof address === "object" && address !== null ? address.port : options.port ?? 0;
155
+ const shown = host.includes(":") ? `[${host}]` : host;
156
+ return {
157
+ url: `http://${shown}:${port}`,
158
+ port,
159
+ host,
160
+ server,
161
+ close: () => new Promise((resolve, reject) => {
162
+ server.close((error) => {
163
+ if (error && error.code !== "ERR_SERVER_NOT_RUNNING")
164
+ reject(error);
165
+ else
166
+ resolve();
167
+ });
168
+ server.closeAllConnections?.();
169
+ })
170
+ };
171
+ };
172
+
173
+ // src/server.ts
174
+ var DEFAULT_PORT = 8790;
175
+ var manifestFile = (path) => async () => JSON.parse(await readFile(path, "utf8"));
176
+ var createServer2 = async (options = {}) => {
177
+ const { port, host, manifest, ...rest } = options;
178
+ const harness = createNotificationHarness({
179
+ ...rest,
180
+ ...manifest === void 0 ? {} : { manifest: typeof manifest === "string" ? manifestFile(manifest) : manifest }
181
+ });
182
+ const listening = await listen(harness, {
183
+ port: port ?? 0,
184
+ ...host !== void 0 ? { host } : {}
185
+ });
186
+ return {
187
+ url: listening.url,
188
+ port: listening.port,
189
+ harness,
190
+ close: async () => {
191
+ harness.close();
192
+ await listening.close();
193
+ }
194
+ };
195
+ };
196
+
197
+ export {
198
+ DEFAULT_PORT,
199
+ manifestFile,
200
+ createServer2 as createServer
201
+ };
202
+ //# sourceMappingURL=chunk-JQ6KPT2C.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/server.ts", "../../adapters/node/src/serve.ts", "../../adapters/node/src/listen.ts"],
4
+ "sourcesContent": ["/// <reference types=\"node\" />\nimport { readFile } from \"node:fs/promises\"\nimport { listen } from \"@crvouga/mockingbird-adapter-node\"\nimport type { FleetManifest } from \"./discovery.js\"\nimport {\n createNotificationHarness,\n type HarnessOptions,\n type NotificationHarness,\n} from \"./harness.js\"\n\n/** Port `mockingbird-notifications serve` listens on when none is given. */\nexport const DEFAULT_PORT = 8790\n\nexport type NotificationServerOptions = Omit<HarnessOptions, \"manifest\"> & {\n /** A fleet ready manifest object, or the path of the fleet's `--ready-file` (re-read every sync). */\n manifest?: HarnessOptions[\"manifest\"] | string\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`. */\n host?: string\n}\n\nexport type NotificationServer = {\n url: string\n port: number\n harness: NotificationHarness\n close(): Promise<void>\n}\n\n/** A manifest loader that re-reads the fleet's ready file, so a restarted fleet is picked up. */\nexport const manifestFile = (path: string) => async (): Promise<FleetManifest> =>\n JSON.parse(await readFile(path, \"utf8\")) as FleetManifest\n\nexport const createServer = async (\n options: NotificationServerOptions = {},\n): Promise<NotificationServer> => {\n const { port, host, manifest, ...rest } = options\n const harness = createNotificationHarness({\n ...rest,\n ...(manifest === undefined\n ? {}\n : { manifest: typeof manifest === \"string\" ? manifestFile(manifest) : manifest }),\n })\n const listening = await listen(harness, {\n port: port ?? 0,\n ...(host !== undefined ? { host } : {}),\n })\n return {\n url: listening.url,\n port: listening.port,\n harness,\n close: async () => {\n harness.close()\n await listening.close()\n },\n }\n}\n", "import { createServer, type Server } from \"node:http\"\nimport { Readable } from \"node:stream\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\n\n/** Limits apply incrementally, including chunked uploads. Bodies reach fetch as a stream. */\nexport type NodeServeOptions = {\n port?: number\n host?: string\n maxBodyBytes?: number\n requestTimeoutMs?: number\n}\n\n/** Serve a FetchAPI with bounded streaming uploads and disconnect-aware response backpressure. */\nexport const serve = async (api: FetchAPI, options: NodeServeOptions = {}): Promise<Server> => {\n const maxBytes = options.maxBodyBytes ?? 16 * 1024 * 1024\n const timeoutMs = options.requestTimeoutMs ?? 30_000\n if (\n !Number.isSafeInteger(maxBytes) ||\n maxBytes < 0 ||\n !Number.isSafeInteger(timeoutMs) ||\n timeoutMs < 1\n )\n throw new Error(\"invalid request limits\")\n const server = createServer(async (req, res) => {\n const aborted = new AbortController()\n let reader: ReadableStreamDefaultReader<Uint8Array> | undefined\n let timer: ReturnType<typeof setTimeout> | undefined\n const abort = () => {\n aborted.abort()\n void reader?.cancel().catch(() => {})\n }\n const fail = (status: number, message: string) => {\n if (!res.headersSent && !res.destroyed) {\n res.writeHead(status, { \"content-type\": \"application/json\", connection: \"close\" })\n res.end(JSON.stringify({ error: { type: \"mockingbird_transport\", message } }))\n }\n abort()\n }\n req.once(\"aborted\", abort)\n res.once(\"close\", abort)\n res.once(\"error\", abort)\n try {\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : 80\n const url = new URL(\n (req.url ?? \"/\").replace(/^\\/+/, \"/\"),\n `http://${req.headers.host ?? `localhost:${port}`}`,\n )\n const headers = new Headers()\n for (const [name, value] of Object.entries(req.headers)) {\n if (Array.isArray(value)) for (const item of value) headers.append(name, item)\n else if (value !== undefined) headers.set(name, value)\n }\n const init: RequestInit & { duplex?: \"half\" } = {\n method: req.method ?? \"GET\",\n headers,\n signal: aborted.signal,\n }\n if (init.method !== \"GET\" && init.method !== \"HEAD\") {\n if (Number(req.headers[\"content-length\"]) > maxBytes) {\n fail(413, \"request body exceeds byte limit\")\n return\n }\n let bytes = 0\n const source = Readable.toWeb(req) as unknown as ReadableStream<Uint8Array>\n init.body = source.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n bytes += chunk.byteLength\n if (bytes > maxBytes) {\n fail(413, \"request body exceeds byte limit\")\n controller.error(new Error(\"request body exceeds byte limit\"))\n } else controller.enqueue(chunk)\n },\n }),\n )\n init.duplex = \"half\"\n timer = setTimeout(() => fail(408, \"request body timed out\"), timeoutMs)\n req.once(\"end\", () => clearTimeout(timer))\n }\n const response = await api.fetch(new Request(url, init))\n if (aborted.signal.aborted || res.destroyed) {\n await response.body?.cancel()\n return\n }\n const output: Record<string, string | string[]> = Object.fromEntries(response.headers)\n const cookies = response.headers.getSetCookie()\n if (cookies.length) output[\"set-cookie\"] = cookies\n res.writeHead(response.status, output)\n if (!response.body) {\n res.end()\n return\n }\n res.flushHeaders()\n reader = response.body.getReader()\n for (;;) {\n const { done, value } = await reader.read()\n if (done || aborted.signal.aborted) break\n if (!res.write(value)) {\n await new Promise<void>((resolve) => {\n const finish = () => {\n res.off(\"drain\", finish)\n res.off(\"close\", finish)\n res.off(\"error\", finish)\n resolve()\n }\n res.once(\"drain\", finish)\n res.once(\"close\", finish)\n res.once(\"error\", finish)\n if (res.destroyed || aborted.signal.aborted) finish()\n })\n }\n }\n if (!aborted.signal.aborted) res.end()\n } catch (error) {\n if ((error as { code?: string }).code === \"MOCKINGBIRD_DROP\") req.socket.destroy()\n else if (!aborted.signal.aborted && !res.destroyed) {\n if (res.headersSent) res.destroy()\n else {\n res.writeHead(500, { \"content-type\": \"application/json\" })\n res.end(\n JSON.stringify({\n error: {\n type: \"mockingbird_internal\",\n message: error instanceof Error ? error.message : String(error),\n },\n }),\n )\n }\n }\n } finally {\n clearTimeout(timer)\n reader?.releaseLock()\n req.off(\"aborted\", abort)\n res.off(\"close\", abort)\n res.off(\"error\", abort)\n if (!req.complete) req.resume()\n }\n })\n server.requestTimeout = timeoutMs\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject)\n server.listen(options.port ?? 0, options.host, resolve)\n })\n return server\n}\n", "import type { Server } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\nimport { serve } from \"./serve.js\"\n\nexport type ListenOptions = {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`: a mock should not be reachable off the machine by accident. */\n host?: string\n /** Largest request body accepted. Default 16 MiB. */\n maxBodyBytes?: number\n}\n\n/** A running server, with the address it actually bound. */\nexport type Listening = {\n url: string\n port: number\n host: string\n server: Server\n close(): Promise<void>\n}\n\nexport const listen = async (api: FetchAPI, options: ListenOptions = {}): Promise<Listening> => {\n const host = options.host ?? \"127.0.0.1\"\n const server = await serve(api, {\n port: options.port ?? 0,\n host,\n ...(options.maxBodyBytes !== undefined ? { maxBodyBytes: options.maxBodyBytes } : {}),\n })\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : (options.port ?? 0)\n const shown = host.includes(\":\") ? `[${host}]` : host\n return {\n url: `http://${shown}:${port}`,\n port,\n host,\n server,\n close: () =>\n new Promise<void>((resolve, reject) => {\n // Stop accepting first, then drop keep-alive sockets so close() can finish.\n server.close((error) => {\n if (error && (error as { code?: string }).code !== \"ERR_SERVER_NOT_RUNNING\") reject(error)\n else resolve()\n })\n server.closeAllConnections?.()\n }),\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["createServer"]
7
+ }
@@ -0,0 +1,183 @@
1
+ // src/testids.ts
2
+ var TEST_IDS = {
3
+ /** The UI root. */
4
+ app: "notifications-app",
5
+ // ── Top bar ─────────────────────────────────────────────────────────────────
6
+ topbar: "topbar",
7
+ /** `<select>` of namespaces. */
8
+ namespace: "namespace",
9
+ /** Filter `<input>` (recipient, sender, text). */
10
+ search: "search",
11
+ /** View tabs; `aria-pressed="true"` on the active one. */
12
+ viewDevices: "view-devices",
13
+ viewTimeline: "view-timeline",
14
+ /** "Follow new" checkbox. */
15
+ follow: "follow",
16
+ /** Sound on/off button; `aria-pressed`. */
17
+ sound: "sound-toggle",
18
+ /** Live stream indicator; `data-state="live" | "reconnecting"`. */
19
+ connection: "connection",
20
+ /** One pill per emulator; `data-name`, `data-service`, `data-ok`, `data-count`. */
21
+ source: "source",
22
+ /** Discovery or emulator errors, or the "no emulators" hint; `data-kind="error" | "empty"`. */
23
+ notice: "notice",
24
+ /** Bottom-left message after an action, e.g. "Conversation archived." */
25
+ toast: "toast",
26
+ toastUndo: "toast-undo",
27
+ // ── SMS: Messages ────────────────────────────────────────────────────────────
28
+ /** The phone; `data-screen="threads" | "conversation"`, `data-recipient`, `data-thread`. */
29
+ smsDevice: "sms-device",
30
+ /** Phone number `<select>`; option values are E.164 numbers. */
31
+ smsRecipient: "sms-recipient",
32
+ /** Unread SMS badge next to the column title (absent when 0). */
33
+ smsUnreadCount: "sms-unread-count",
34
+ /** One conversation in the list; `data-from` (the sender address), `data-unread`, `data-pinned`, `data-muted`. */
35
+ smsThread: "sms-thread",
36
+ smsThreadName: "sms-thread-name",
37
+ smsThreadPreview: "sms-thread-preview",
38
+ smsThreadTime: "sms-thread-time",
39
+ smsThreadUnread: "sms-thread-unread",
40
+ /** Swipe actions behind a thread (also reachable with the keyboard: focus the thread, press Delete). */
41
+ smsThreadDelete: "sms-thread-delete",
42
+ smsThreadMute: "sms-thread-mute",
43
+ smsThreadPin: "sms-thread-pin",
44
+ smsThreadToggleUnread: "sms-thread-toggle-unread",
45
+ /** Shown when the phone has no messages. */
46
+ smsEmpty: "sms-empty",
47
+ /** An open conversation; `data-from`. */
48
+ smsConversation: "sms-conversation",
49
+ smsConversationTitle: "sms-conversation-title",
50
+ smsBack: "sms-back",
51
+ /** One message; carries the notification attributes plus `data-code` when it holds a one-time code. */
52
+ smsBubble: "sms-bubble",
53
+ smsBubbleMedia: "sms-bubble-media",
54
+ /** The time revealed by swiping the conversation left. */
55
+ smsBubbleTime: "sms-bubble-time",
56
+ /** A reaction added with the long-press menu; `data-tapback`. */
57
+ smsTapback: "sms-tapback",
58
+ /** Centered date stamp between messages. */
59
+ smsTimestamp: "sms-timestamp",
60
+ /** One-time code suggestion above the keyboard; `data-code`. */
61
+ smsCode: "sms-code",
62
+ /** Long-press menu on a bubble. */
63
+ smsMenu: "sms-menu",
64
+ smsMenuCopy: "sms-menu-copy",
65
+ /** One tapback in the menu; `data-tapback="heart" | "like" | "dislike" | "laugh" | "emphasize" | "question"`. */
66
+ smsMenuTapback: "sms-menu-tapback",
67
+ // ── Push: lock screen (iOS) / shade (Android) ───────────────────────────────
68
+ /** The phone; `data-skin="ios" | "android"`, `data-recipient`, `data-platform`. */
69
+ pushDevice: "push-device",
70
+ /** Device token `<select>`. */
71
+ pushRecipient: "push-recipient",
72
+ /** Skin buttons; `data-value`, `aria-pressed`. */
73
+ pushSkin: "push-skin",
74
+ pushUnreadCount: "push-unread-count",
75
+ /** One notification; notification attributes plus `data-expanded`, `data-platform`, `data-app`, `data-stacked`. */
76
+ pushNotification: "push-notification",
77
+ pushNotificationApp: "push-notification-app",
78
+ pushNotificationTitle: "push-notification-title",
79
+ pushNotificationBody: "push-notification-body",
80
+ pushNotificationTime: "push-notification-time",
81
+ pushNotificationImage: "push-notification-image",
82
+ /** The `data` payload, shown when expanded. */
83
+ pushNotificationData: "push-notification-data",
84
+ /** Service, status, badge and sound, shown when expanded. */
85
+ pushNotificationMeta: "push-notification-meta",
86
+ /** iOS: a group of notifications from one app; `data-app`, `data-count`, `data-open`. */
87
+ pushGroup: "push-group",
88
+ /** iOS: "N more notifications" under a collapsed group (click to open it); `data-count`. */
89
+ pushGroupMore: "push-group-more",
90
+ /** iOS: "Show less" on an open group. */
91
+ pushGroupCollapse: "push-group-collapse",
92
+ /** iOS: swipe-left actions. */
93
+ pushClear: "push-clear",
94
+ pushOptions: "push-options",
95
+ /** iOS long-press preview; Android long-press settings. */
96
+ pushPreview: "push-preview",
97
+ pushSettings: "push-settings",
98
+ /** Android "Clear all". */
99
+ pushClearAll: "push-clear-all",
100
+ pushEmpty: "push-empty",
101
+ // ── Email: web mail ─────────────────────────────────────────────────────────
102
+ /** The browser window; `data-screen="inbox" | "message"`, `data-recipient`. */
103
+ emailDevice: "email-device",
104
+ /** Mailbox `<select>`; option values are lower-case addresses. */
105
+ emailRecipient: "email-recipient",
106
+ emailUnreadCount: "email-unread-count",
107
+ /** One inbox row; notification attributes plus `data-starred`. */
108
+ emailRow: "email-row",
109
+ emailRowFrom: "email-row-from",
110
+ emailRowSubject: "email-row-subject",
111
+ emailRowSnippet: "email-row-snippet",
112
+ emailRowDate: "email-row-date",
113
+ emailRowService: "email-row-service",
114
+ emailRowAttachment: "email-row-attachment",
115
+ emailRowStar: "email-row-star",
116
+ /** Hover actions on a row. */
117
+ emailRowArchive: "email-row-archive",
118
+ emailRowDelete: "email-row-delete",
119
+ emailRowToggleRead: "email-row-toggle-read",
120
+ emailRefresh: "email-refresh",
121
+ emailEmpty: "email-empty",
122
+ /** The open message; notification attributes. */
123
+ emailOpen: "email-open",
124
+ emailBack: "email-back",
125
+ emailArchive: "email-archive",
126
+ emailDelete: "email-delete",
127
+ emailMarkUnread: "email-mark-unread",
128
+ emailSubject: "email-subject",
129
+ emailFromName: "email-from-name",
130
+ emailFromAddress: "email-from-address",
131
+ /** "to me, …" line; click to toggle the details. */
132
+ emailTo: "email-to",
133
+ emailDate: "email-date",
134
+ emailDetails: "email-details",
135
+ /** One row of the details; `data-field="from" | "to" | "cc" | "bcc" | "reply-to" | "date" | "subject" | "mailed-by" | "message-id"`. */
136
+ emailDetail: "email-detail",
137
+ /** The HTML body `<iframe>` (same origin: use `frameLocator`). */
138
+ emailBody: "email-body",
139
+ /** The plain-text body when there is no HTML. */
140
+ emailText: "email-text",
141
+ emailAttachments: "email-attachments",
142
+ /** One attachment; `data-filename`, `data-content-type`, `data-size`. */
143
+ emailAttachment: "email-attachment",
144
+ // ── Timeline + inspector ────────────────────────────────────────────────────
145
+ timeline: "timeline",
146
+ /** One row; notification attributes. Click it to inspect. */
147
+ timelineRow: "timeline-row",
148
+ timelineEmpty: "timeline-empty",
149
+ /** The JSON inspector; notification attributes. Opened by clicking a timeline row or double-clicking any notification. */
150
+ inspector: "inspector",
151
+ inspectorTitle: "inspector-title",
152
+ inspectorClose: "inspector-close",
153
+ inspectorShowOnDevice: "inspector-show-on-device",
154
+ /** One normalized field; `data-field`. */
155
+ inspectorField: "inspector-field",
156
+ /** The full record, normalized fields plus `raw`, as JSON text. */
157
+ inspectorJson: "inspector-json"
158
+ };
159
+ var attr = (value) => JSON.stringify(value);
160
+ var selectors = {
161
+ /** `[data-testid="…"]` */
162
+ testId: (id) => `[data-testid=${attr(id)}]`,
163
+ /** The ready UI root: wait for this before asserting. */
164
+ ready: () => `[data-testid=${attr(TEST_IDS.app)}][data-ready="true"]`,
165
+ /** Any element showing this notification (bubble, thread, card, row, open email, timeline row). */
166
+ notification: (id) => `[data-notification-id=${attr(id)}]`,
167
+ /** A message bubble, optionally from one sender. */
168
+ smsBubble: (filter = {}) => `[data-testid=${attr(TEST_IDS.smsBubble)}]${filter.from ? `[data-from=${attr(filter.from)}]` : ""}${filter.id ? `[data-notification-id=${attr(filter.id)}]` : ""}`,
169
+ /** A conversation in the Messages list. */
170
+ smsThread: (from) => `[data-testid=${attr(TEST_IDS.smsThread)}][data-from=${attr(from)}]`,
171
+ /** A push notification card, optionally by id. */
172
+ pushNotification: (id) => `[data-testid=${attr(TEST_IDS.pushNotification)}]${id ? `[data-notification-id=${attr(id)}]` : ""}`,
173
+ /** An inbox row, optionally by id. */
174
+ emailRow: (id) => `[data-testid=${attr(TEST_IDS.emailRow)}]${id ? `[data-notification-id=${attr(id)}]` : ""}`,
175
+ /** A timeline row, optionally by id. */
176
+ timelineRow: (id) => `[data-testid=${attr(TEST_IDS.timelineRow)}]${id ? `[data-notification-id=${attr(id)}]` : ""}`
177
+ };
178
+
179
+ export {
180
+ TEST_IDS,
181
+ selectors
182
+ };
183
+ //# sourceMappingURL=chunk-LM7R7AR7.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/testids.ts"],
4
+ "sourcesContent": ["/**\n * Every `data-testid` the harness UI renders, and selector helpers for browser tests.\n *\n * Elements that show one notification also carry its fields as attributes, so a test can narrow by\n * what it knows instead of by position:\n *\n * data-notification-id data-channel data-service data-source data-to data-from\n * data-status data-created-at data-unread (\"true\" | \"false\")\n *\n * The UI root (`TEST_IDS.app`) reports its state the same way: `data-ready` (\"true\" once the first\n * read finished), `data-view`, `data-namespace`, `data-count`, `data-version`, `data-connected`,\n * `data-sound` and `data-motion`.\n *\n * This module has no dependencies, so Playwright, Cypress or WebdriverIO specs can import it.\n */\nexport const TEST_IDS = {\n /** The UI root. */\n app: \"notifications-app\",\n\n // \u2500\u2500 Top bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n topbar: \"topbar\",\n /** `<select>` of namespaces. */\n namespace: \"namespace\",\n /** Filter `<input>` (recipient, sender, text). */\n search: \"search\",\n /** View tabs; `aria-pressed=\"true\"` on the active one. */\n viewDevices: \"view-devices\",\n viewTimeline: \"view-timeline\",\n /** \"Follow new\" checkbox. */\n follow: \"follow\",\n /** Sound on/off button; `aria-pressed`. */\n sound: \"sound-toggle\",\n /** Live stream indicator; `data-state=\"live\" | \"reconnecting\"`. */\n connection: \"connection\",\n /** One pill per emulator; `data-name`, `data-service`, `data-ok`, `data-count`. */\n source: \"source\",\n /** Discovery or emulator errors, or the \"no emulators\" hint; `data-kind=\"error\" | \"empty\"`. */\n notice: \"notice\",\n /** Bottom-left message after an action, e.g. \"Conversation archived.\" */\n toast: \"toast\",\n toastUndo: \"toast-undo\",\n\n // \u2500\u2500 SMS: Messages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n /** The phone; `data-screen=\"threads\" | \"conversation\"`, `data-recipient`, `data-thread`. */\n smsDevice: \"sms-device\",\n /** Phone number `<select>`; option values are E.164 numbers. */\n smsRecipient: \"sms-recipient\",\n /** Unread SMS badge next to the column title (absent when 0). */\n smsUnreadCount: \"sms-unread-count\",\n /** One conversation in the list; `data-from` (the sender address), `data-unread`, `data-pinned`, `data-muted`. */\n smsThread: \"sms-thread\",\n smsThreadName: \"sms-thread-name\",\n smsThreadPreview: \"sms-thread-preview\",\n smsThreadTime: \"sms-thread-time\",\n smsThreadUnread: \"sms-thread-unread\",\n /** Swipe actions behind a thread (also reachable with the keyboard: focus the thread, press Delete). */\n smsThreadDelete: \"sms-thread-delete\",\n smsThreadMute: \"sms-thread-mute\",\n smsThreadPin: \"sms-thread-pin\",\n smsThreadToggleUnread: \"sms-thread-toggle-unread\",\n /** Shown when the phone has no messages. */\n smsEmpty: \"sms-empty\",\n /** An open conversation; `data-from`. */\n smsConversation: \"sms-conversation\",\n smsConversationTitle: \"sms-conversation-title\",\n smsBack: \"sms-back\",\n /** One message; carries the notification attributes plus `data-code` when it holds a one-time code. */\n smsBubble: \"sms-bubble\",\n smsBubbleMedia: \"sms-bubble-media\",\n /** The time revealed by swiping the conversation left. */\n smsBubbleTime: \"sms-bubble-time\",\n /** A reaction added with the long-press menu; `data-tapback`. */\n smsTapback: \"sms-tapback\",\n /** Centered date stamp between messages. */\n smsTimestamp: \"sms-timestamp\",\n /** One-time code suggestion above the keyboard; `data-code`. */\n smsCode: \"sms-code\",\n /** Long-press menu on a bubble. */\n smsMenu: \"sms-menu\",\n smsMenuCopy: \"sms-menu-copy\",\n /** One tapback in the menu; `data-tapback=\"heart\" | \"like\" | \"dislike\" | \"laugh\" | \"emphasize\" | \"question\"`. */\n smsMenuTapback: \"sms-menu-tapback\",\n\n // \u2500\u2500 Push: lock screen (iOS) / shade (Android) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n /** The phone; `data-skin=\"ios\" | \"android\"`, `data-recipient`, `data-platform`. */\n pushDevice: \"push-device\",\n /** Device token `<select>`. */\n pushRecipient: \"push-recipient\",\n /** Skin buttons; `data-value`, `aria-pressed`. */\n pushSkin: \"push-skin\",\n pushUnreadCount: \"push-unread-count\",\n /** One notification; notification attributes plus `data-expanded`, `data-platform`, `data-app`, `data-stacked`. */\n pushNotification: \"push-notification\",\n pushNotificationApp: \"push-notification-app\",\n pushNotificationTitle: \"push-notification-title\",\n pushNotificationBody: \"push-notification-body\",\n pushNotificationTime: \"push-notification-time\",\n pushNotificationImage: \"push-notification-image\",\n /** The `data` payload, shown when expanded. */\n pushNotificationData: \"push-notification-data\",\n /** Service, status, badge and sound, shown when expanded. */\n pushNotificationMeta: \"push-notification-meta\",\n /** iOS: a group of notifications from one app; `data-app`, `data-count`, `data-open`. */\n pushGroup: \"push-group\",\n /** iOS: \"N more notifications\" under a collapsed group (click to open it); `data-count`. */\n pushGroupMore: \"push-group-more\",\n /** iOS: \"Show less\" on an open group. */\n pushGroupCollapse: \"push-group-collapse\",\n /** iOS: swipe-left actions. */\n pushClear: \"push-clear\",\n pushOptions: \"push-options\",\n /** iOS long-press preview; Android long-press settings. */\n pushPreview: \"push-preview\",\n pushSettings: \"push-settings\",\n /** Android \"Clear all\". */\n pushClearAll: \"push-clear-all\",\n pushEmpty: \"push-empty\",\n\n // \u2500\u2500 Email: web mail \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n /** The browser window; `data-screen=\"inbox\" | \"message\"`, `data-recipient`. */\n emailDevice: \"email-device\",\n /** Mailbox `<select>`; option values are lower-case addresses. */\n emailRecipient: \"email-recipient\",\n emailUnreadCount: \"email-unread-count\",\n /** One inbox row; notification attributes plus `data-starred`. */\n emailRow: \"email-row\",\n emailRowFrom: \"email-row-from\",\n emailRowSubject: \"email-row-subject\",\n emailRowSnippet: \"email-row-snippet\",\n emailRowDate: \"email-row-date\",\n emailRowService: \"email-row-service\",\n emailRowAttachment: \"email-row-attachment\",\n emailRowStar: \"email-row-star\",\n /** Hover actions on a row. */\n emailRowArchive: \"email-row-archive\",\n emailRowDelete: \"email-row-delete\",\n emailRowToggleRead: \"email-row-toggle-read\",\n emailRefresh: \"email-refresh\",\n emailEmpty: \"email-empty\",\n /** The open message; notification attributes. */\n emailOpen: \"email-open\",\n emailBack: \"email-back\",\n emailArchive: \"email-archive\",\n emailDelete: \"email-delete\",\n emailMarkUnread: \"email-mark-unread\",\n emailSubject: \"email-subject\",\n emailFromName: \"email-from-name\",\n emailFromAddress: \"email-from-address\",\n /** \"to me, \u2026\" line; click to toggle the details. */\n emailTo: \"email-to\",\n emailDate: \"email-date\",\n emailDetails: \"email-details\",\n /** One row of the details; `data-field=\"from\" | \"to\" | \"cc\" | \"bcc\" | \"reply-to\" | \"date\" | \"subject\" | \"mailed-by\" | \"message-id\"`. */\n emailDetail: \"email-detail\",\n /** The HTML body `<iframe>` (same origin: use `frameLocator`). */\n emailBody: \"email-body\",\n /** The plain-text body when there is no HTML. */\n emailText: \"email-text\",\n emailAttachments: \"email-attachments\",\n /** One attachment; `data-filename`, `data-content-type`, `data-size`. */\n emailAttachment: \"email-attachment\",\n\n // \u2500\u2500 Timeline + inspector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n timeline: \"timeline\",\n /** One row; notification attributes. Click it to inspect. */\n timelineRow: \"timeline-row\",\n timelineEmpty: \"timeline-empty\",\n /** The JSON inspector; notification attributes. Opened by clicking a timeline row or double-clicking any notification. */\n inspector: \"inspector\",\n inspectorTitle: \"inspector-title\",\n inspectorClose: \"inspector-close\",\n inspectorShowOnDevice: \"inspector-show-on-device\",\n /** One normalized field; `data-field`. */\n inspectorField: \"inspector-field\",\n /** The full record, normalized fields plus `raw`, as JSON text. */\n inspectorJson: \"inspector-json\",\n} as const\n\nexport type TestId = (typeof TEST_IDS)[keyof typeof TEST_IDS]\n\nconst attr = (value: string) => JSON.stringify(value)\n\n/** CSS selectors for the harness UI. Each one works with `page.locator()` or `querySelector`. */\nexport const selectors = {\n /** `[data-testid=\"\u2026\"]` */\n testId: (id: TestId) => `[data-testid=${attr(id)}]`,\n /** The ready UI root: wait for this before asserting. */\n ready: () => `[data-testid=${attr(TEST_IDS.app)}][data-ready=\"true\"]`,\n /** Any element showing this notification (bubble, thread, card, row, open email, timeline row). */\n notification: (id: string) => `[data-notification-id=${attr(id)}]`,\n /** A message bubble, optionally from one sender. */\n smsBubble: (filter: { from?: string; id?: string } = {}) =>\n `[data-testid=${attr(TEST_IDS.smsBubble)}]${filter.from ? `[data-from=${attr(filter.from)}]` : \"\"}${filter.id ? `[data-notification-id=${attr(filter.id)}]` : \"\"}`,\n /** A conversation in the Messages list. */\n smsThread: (from: string) => `[data-testid=${attr(TEST_IDS.smsThread)}][data-from=${attr(from)}]`,\n /** A push notification card, optionally by id. */\n pushNotification: (id?: string) =>\n `[data-testid=${attr(TEST_IDS.pushNotification)}]${id ? `[data-notification-id=${attr(id)}]` : \"\"}`,\n /** An inbox row, optionally by id. */\n emailRow: (id?: string) =>\n `[data-testid=${attr(TEST_IDS.emailRow)}]${id ? `[data-notification-id=${attr(id)}]` : \"\"}`,\n /** A timeline row, optionally by id. */\n timelineRow: (id?: string) =>\n `[data-testid=${attr(TEST_IDS.timelineRow)}]${id ? `[data-notification-id=${attr(id)}]` : \"\"}`,\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }