@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/dist/server.d.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** The three kinds of device a notification lands on. */
|
|
2
|
+
type Channel = "sms" | "email" | "push";
|
|
3
|
+
/** Emulators the harness knows how to read. */
|
|
4
|
+
type ServiceKind = "twilio" | "resend" | "fcm" | "ses" | "sns" | "customerio" | "mailosaur";
|
|
5
|
+
type Address = {
|
|
6
|
+
name: string | null;
|
|
7
|
+
address: string;
|
|
8
|
+
};
|
|
9
|
+
type SmsContent = {
|
|
10
|
+
body: string;
|
|
11
|
+
/** One-time code the sender included (Twilio Verify), when known. */
|
|
12
|
+
code: string | null;
|
|
13
|
+
media: string[];
|
|
14
|
+
};
|
|
15
|
+
type EmailAttachment = {
|
|
16
|
+
filename: string;
|
|
17
|
+
contentType: string | null;
|
|
18
|
+
size: number | null;
|
|
19
|
+
};
|
|
20
|
+
type EmailContent = {
|
|
21
|
+
subject: string;
|
|
22
|
+
html: string | null;
|
|
23
|
+
text: string | null;
|
|
24
|
+
cc: string[];
|
|
25
|
+
bcc: string[];
|
|
26
|
+
replyTo: string[];
|
|
27
|
+
attachments: EmailAttachment[];
|
|
28
|
+
headers: Record<string, string>;
|
|
29
|
+
};
|
|
30
|
+
type PushPlatform = "android" | "ios" | "web" | "unknown";
|
|
31
|
+
type PushContent = {
|
|
32
|
+
title: string | null;
|
|
33
|
+
body: string | null;
|
|
34
|
+
imageUrl: string | null;
|
|
35
|
+
/** The app the notification belongs to (the FCM project, for example). */
|
|
36
|
+
app: string;
|
|
37
|
+
platform: PushPlatform;
|
|
38
|
+
data: Record<string, string>;
|
|
39
|
+
badge: number | null;
|
|
40
|
+
sound: string | null;
|
|
41
|
+
};
|
|
42
|
+
/** One message as a recipient would receive it, normalized across emulators. */
|
|
43
|
+
type Notification = {
|
|
44
|
+
/** Stable across polls: `<service entry>:<namespace>:<provider id>`. */
|
|
45
|
+
id: string;
|
|
46
|
+
/** The provider's own id (Twilio SID, Resend id, FCM message name, …). */
|
|
47
|
+
providerId: string;
|
|
48
|
+
channel: Channel;
|
|
49
|
+
/** The fleet entry name (`twilio`, or an alias such as `push`). */
|
|
50
|
+
source: string;
|
|
51
|
+
service: ServiceKind;
|
|
52
|
+
namespace: string;
|
|
53
|
+
/** Phone number, email address, or push registration token. */
|
|
54
|
+
to: string;
|
|
55
|
+
from: Address;
|
|
56
|
+
/** ISO-8601, on the emulator's clock. */
|
|
57
|
+
createdAt: string;
|
|
58
|
+
/** Short plain-text preview. */
|
|
59
|
+
preview: string;
|
|
60
|
+
/** Provider delivery state when it reports one (`queued`, `delivered`, `suppressed`, …). */
|
|
61
|
+
status: string | null;
|
|
62
|
+
sms: SmsContent | null;
|
|
63
|
+
email: EmailContent | null;
|
|
64
|
+
push: PushContent | null;
|
|
65
|
+
/** The record exactly as the emulator reported it. */
|
|
66
|
+
raw: unknown;
|
|
67
|
+
};
|
|
68
|
+
type NotificationFilter = {
|
|
69
|
+
channel?: Channel | undefined;
|
|
70
|
+
service?: ServiceKind | undefined;
|
|
71
|
+
source?: string | undefined;
|
|
72
|
+
namespace?: string | undefined;
|
|
73
|
+
/** Exact recipient (case-insensitive for email). */
|
|
74
|
+
to?: string | undefined;
|
|
75
|
+
/** Case-insensitive text search across sender, subject, title and body. */
|
|
76
|
+
contains?: string | undefined;
|
|
77
|
+
/** Only notifications created at or after this instant (ISO-8601 or epoch ms). */
|
|
78
|
+
since?: string | number | undefined;
|
|
79
|
+
};
|
|
80
|
+
type SourceStatus = {
|
|
81
|
+
name: string;
|
|
82
|
+
service: ServiceKind | null;
|
|
83
|
+
url: string;
|
|
84
|
+
ok: boolean;
|
|
85
|
+
error: string | null;
|
|
86
|
+
count: number;
|
|
87
|
+
lastSyncAt: string | null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Where the emulators are. The harness reads the same fleet the application uses: the live fleet
|
|
92
|
+
* controller (`adminBase`), the fleet's ready manifest, or explicit URLs and in-process runtimes.
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
type FetchLike = (request: Request) => Response | Promise<Response>;
|
|
96
|
+
/** A service emulator: its base URL, an `{ url, adminUrl?, service? }` entry, or an in-process runtime. */
|
|
97
|
+
type ServiceInput = string | {
|
|
98
|
+
url: string;
|
|
99
|
+
adminUrl?: string;
|
|
100
|
+
service?: ServiceKind;
|
|
101
|
+
} | {
|
|
102
|
+
fetch: FetchLike;
|
|
103
|
+
service?: ServiceKind;
|
|
104
|
+
adminPrefix?: string;
|
|
105
|
+
};
|
|
106
|
+
/** The subset of a fleet ready manifest (`--ready-file`) the harness reads. */
|
|
107
|
+
type FleetManifest = {
|
|
108
|
+
adminBase?: string;
|
|
109
|
+
services: Record<string, {
|
|
110
|
+
protocol?: string;
|
|
111
|
+
url?: string;
|
|
112
|
+
adminUrl?: string;
|
|
113
|
+
target?: string;
|
|
114
|
+
} | undefined>;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
type HarnessOptions = {
|
|
118
|
+
/** A running fleet controller's `adminBase` (`http://127.0.0.1:9000/__admin/fleet`). */
|
|
119
|
+
fleet?: string;
|
|
120
|
+
/** A fleet ready manifest, or a function that reads the current one (re-read on every sync). */
|
|
121
|
+
manifest?: FleetManifest | (() => FleetManifest | Promise<FleetManifest>);
|
|
122
|
+
/** Explicit emulators by name: base URLs, `{ url, adminUrl }`, or in-process runtimes. */
|
|
123
|
+
services?: Record<string, ServiceInput>;
|
|
124
|
+
/**
|
|
125
|
+
* Namespaces to watch in the background. `"all"` follows the fleet's namespace list.
|
|
126
|
+
* Any namespace an API call or the UI asks for is watched from then on. Default `["default"]`,
|
|
127
|
+
* or `"all"` with `fleet`.
|
|
128
|
+
*/
|
|
129
|
+
namespaces?: "all" | string[];
|
|
130
|
+
/** Background polling interval. `0` turns it off; reads still sync on demand. Default 750 ms. */
|
|
131
|
+
pollIntervalMs?: number;
|
|
132
|
+
/** Sent as `x-mockingbird-admin-key` to the fleet and every emulator. */
|
|
133
|
+
adminKey?: string;
|
|
134
|
+
/** Transport for URL endpoints. Default: global `fetch`. */
|
|
135
|
+
fetch?: FetchLike;
|
|
136
|
+
/** Page title. */
|
|
137
|
+
title?: string;
|
|
138
|
+
};
|
|
139
|
+
type WaitOptions = {
|
|
140
|
+
timeoutMs?: number;
|
|
141
|
+
count?: number;
|
|
142
|
+
};
|
|
143
|
+
type NotificationHarness = {
|
|
144
|
+
/** The harness UI and JSON API as a Fetch handler. */
|
|
145
|
+
fetch(request: Request): Promise<Response>;
|
|
146
|
+
/** Read every emulator now. Resolves after a read that started after this call. */
|
|
147
|
+
sync(namespace?: string): Promise<void>;
|
|
148
|
+
/** Notifications matching `filter`, oldest first, after a fresh sync. */
|
|
149
|
+
list(filter?: NotificationFilter): Promise<Notification[]>;
|
|
150
|
+
/** Resolve once at least `count` (default 1) notifications match; reject after `timeoutMs`. */
|
|
151
|
+
waitFor(filter: NotificationFilter, options?: WaitOptions): Promise<Notification[]>;
|
|
152
|
+
sources(): SourceStatus[];
|
|
153
|
+
close(): void;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** Port `mockingbird-notifications serve` listens on when none is given. */
|
|
157
|
+
declare const DEFAULT_PORT = 8790;
|
|
158
|
+
type NotificationServerOptions = Omit<HarnessOptions, "manifest"> & {
|
|
159
|
+
/** A fleet ready manifest object, or the path of the fleet's `--ready-file` (re-read every sync). */
|
|
160
|
+
manifest?: HarnessOptions["manifest"] | string;
|
|
161
|
+
/** Default `0`: the OS picks a free port. */
|
|
162
|
+
port?: number;
|
|
163
|
+
/** Default `127.0.0.1`. */
|
|
164
|
+
host?: string;
|
|
165
|
+
};
|
|
166
|
+
type NotificationServer = {
|
|
167
|
+
url: string;
|
|
168
|
+
port: number;
|
|
169
|
+
harness: NotificationHarness;
|
|
170
|
+
close(): Promise<void>;
|
|
171
|
+
};
|
|
172
|
+
/** A manifest loader that re-reads the fleet's ready file, so a restarted fleet is picked up. */
|
|
173
|
+
declare const manifestFile: (path: string) => () => Promise<FleetManifest>;
|
|
174
|
+
declare const createServer: (options?: NotificationServerOptions) => Promise<NotificationServer>;
|
|
175
|
+
|
|
176
|
+
export { DEFAULT_PORT, createServer, manifestFile };
|
|
177
|
+
export type { NotificationServer, NotificationServerOptions };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PORT,
|
|
3
|
+
createServer,
|
|
4
|
+
manifestFile
|
|
5
|
+
} from "./chunk-JQ6KPT2C.js";
|
|
6
|
+
import "./chunk-GRGR3QZM.js";
|
|
7
|
+
import "./chunk-QE3TRUI6.js";
|
|
8
|
+
export {
|
|
9
|
+
DEFAULT_PORT,
|
|
10
|
+
createServer,
|
|
11
|
+
manifestFile
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=server.js.map
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/** The three kinds of device a notification lands on. */
|
|
2
|
+
type Channel = "sms" | "email" | "push";
|
|
3
|
+
/** Emulators the harness knows how to read. */
|
|
4
|
+
type ServiceKind = "twilio" | "resend" | "fcm" | "ses" | "sns" | "customerio" | "mailosaur";
|
|
5
|
+
type Address = {
|
|
6
|
+
name: string | null;
|
|
7
|
+
address: string;
|
|
8
|
+
};
|
|
9
|
+
type SmsContent = {
|
|
10
|
+
body: string;
|
|
11
|
+
/** One-time code the sender included (Twilio Verify), when known. */
|
|
12
|
+
code: string | null;
|
|
13
|
+
media: string[];
|
|
14
|
+
};
|
|
15
|
+
type EmailAttachment = {
|
|
16
|
+
filename: string;
|
|
17
|
+
contentType: string | null;
|
|
18
|
+
size: number | null;
|
|
19
|
+
};
|
|
20
|
+
type EmailContent = {
|
|
21
|
+
subject: string;
|
|
22
|
+
html: string | null;
|
|
23
|
+
text: string | null;
|
|
24
|
+
cc: string[];
|
|
25
|
+
bcc: string[];
|
|
26
|
+
replyTo: string[];
|
|
27
|
+
attachments: EmailAttachment[];
|
|
28
|
+
headers: Record<string, string>;
|
|
29
|
+
};
|
|
30
|
+
type PushPlatform = "android" | "ios" | "web" | "unknown";
|
|
31
|
+
type PushContent = {
|
|
32
|
+
title: string | null;
|
|
33
|
+
body: string | null;
|
|
34
|
+
imageUrl: string | null;
|
|
35
|
+
/** The app the notification belongs to (the FCM project, for example). */
|
|
36
|
+
app: string;
|
|
37
|
+
platform: PushPlatform;
|
|
38
|
+
data: Record<string, string>;
|
|
39
|
+
badge: number | null;
|
|
40
|
+
sound: string | null;
|
|
41
|
+
};
|
|
42
|
+
/** One message as a recipient would receive it, normalized across emulators. */
|
|
43
|
+
type Notification = {
|
|
44
|
+
/** Stable across polls: `<service entry>:<namespace>:<provider id>`. */
|
|
45
|
+
id: string;
|
|
46
|
+
/** The provider's own id (Twilio SID, Resend id, FCM message name, …). */
|
|
47
|
+
providerId: string;
|
|
48
|
+
channel: Channel;
|
|
49
|
+
/** The fleet entry name (`twilio`, or an alias such as `push`). */
|
|
50
|
+
source: string;
|
|
51
|
+
service: ServiceKind;
|
|
52
|
+
namespace: string;
|
|
53
|
+
/** Phone number, email address, or push registration token. */
|
|
54
|
+
to: string;
|
|
55
|
+
from: Address;
|
|
56
|
+
/** ISO-8601, on the emulator's clock. */
|
|
57
|
+
createdAt: string;
|
|
58
|
+
/** Short plain-text preview. */
|
|
59
|
+
preview: string;
|
|
60
|
+
/** Provider delivery state when it reports one (`queued`, `delivered`, `suppressed`, …). */
|
|
61
|
+
status: string | null;
|
|
62
|
+
sms: SmsContent | null;
|
|
63
|
+
email: EmailContent | null;
|
|
64
|
+
push: PushContent | null;
|
|
65
|
+
/** The record exactly as the emulator reported it. */
|
|
66
|
+
raw: unknown;
|
|
67
|
+
};
|
|
68
|
+
type NotificationFilter = {
|
|
69
|
+
channel?: Channel | undefined;
|
|
70
|
+
service?: ServiceKind | undefined;
|
|
71
|
+
source?: string | undefined;
|
|
72
|
+
namespace?: string | undefined;
|
|
73
|
+
/** Exact recipient (case-insensitive for email). */
|
|
74
|
+
to?: string | undefined;
|
|
75
|
+
/** Case-insensitive text search across sender, subject, title and body. */
|
|
76
|
+
contains?: string | undefined;
|
|
77
|
+
/** Only notifications created at or after this instant (ISO-8601 or epoch ms). */
|
|
78
|
+
since?: string | number | undefined;
|
|
79
|
+
};
|
|
80
|
+
type SourceStatus = {
|
|
81
|
+
name: string;
|
|
82
|
+
service: ServiceKind | null;
|
|
83
|
+
url: string;
|
|
84
|
+
ok: boolean;
|
|
85
|
+
error: string | null;
|
|
86
|
+
count: number;
|
|
87
|
+
lastSyncAt: string | null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type WaitOptions = {
|
|
91
|
+
timeoutMs?: number;
|
|
92
|
+
count?: number;
|
|
93
|
+
};
|
|
94
|
+
type NotificationHarness = {
|
|
95
|
+
/** The harness UI and JSON API as a Fetch handler. */
|
|
96
|
+
fetch(request: Request): Promise<Response>;
|
|
97
|
+
/** Read every emulator now. Resolves after a read that started after this call. */
|
|
98
|
+
sync(namespace?: string): Promise<void>;
|
|
99
|
+
/** Notifications matching `filter`, oldest first, after a fresh sync. */
|
|
100
|
+
list(filter?: NotificationFilter): Promise<Notification[]>;
|
|
101
|
+
/** Resolve once at least `count` (default 1) notifications match; reject after `timeoutMs`. */
|
|
102
|
+
waitFor(filter: NotificationFilter, options?: WaitOptions): Promise<Notification[]>;
|
|
103
|
+
sources(): SourceStatus[];
|
|
104
|
+
close(): void;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The harness UI. Plain DOM: each device re-renders from `state` when the harness reports a change.
|
|
109
|
+
*
|
|
110
|
+
* `createApp` mounts it on any element. Live, it reads a harness through `fetch` (a Fetch handler
|
|
111
|
+
* for an in-process harness, or HTTP); otherwise it draws the notifications it is given, which is
|
|
112
|
+
* how the Storybook catalog shows every state.
|
|
113
|
+
*
|
|
114
|
+
* Every element a test might look for has a `data-testid` from `../testids.ts`, and every element
|
|
115
|
+
* that shows a notification carries its id, channel, service, recipient, sender and status as
|
|
116
|
+
* `data-*` attributes. Every gesture (swipe, long press) also has a click or key equivalent.
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
type MotionPreference = "auto" | "full" | "reduce";
|
|
120
|
+
type AppOptions = {
|
|
121
|
+
/** Read a harness and follow its event stream. Default true. */
|
|
122
|
+
live?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* How to reach the harness: an in-process harness's `fetch`, or any Fetch-compatible function.
|
|
125
|
+
* Default: the global `fetch` against `baseUrl`.
|
|
126
|
+
*/
|
|
127
|
+
fetch?: (request: Request) => Promise<Response>;
|
|
128
|
+
/** Where the harness API lives. Default: the page's own URL (standalone), or a placeholder origin with `fetch`. */
|
|
129
|
+
baseUrl?: string;
|
|
130
|
+
/** Read `?view=…` from the page URL and write `?namespace=` back to it. Default: true only for the standalone page. */
|
|
131
|
+
syncUrl?: boolean;
|
|
132
|
+
/** Keep read state in `localStorage`. Default: same as `live`. */
|
|
133
|
+
storage?: boolean;
|
|
134
|
+
/** Play alert sounds (and vibrate) on arrivals. Default false; the top bar toggles it. */
|
|
135
|
+
sound?: boolean;
|
|
136
|
+
/** Animations. `auto` follows the viewer's reduced-motion setting. Default `auto`. */
|
|
137
|
+
motion?: MotionPreference;
|
|
138
|
+
/** Fill the host element and scroll inside it, for a tab or panel in another app. */
|
|
139
|
+
embedded?: boolean;
|
|
140
|
+
/** Which devices to show. Default all three. */
|
|
141
|
+
channels?: Channel[];
|
|
142
|
+
/** Show the top bar (namespace, filter, view tabs, sound, emulator status). Default true. */
|
|
143
|
+
chrome?: boolean;
|
|
144
|
+
/** Called with each batch of new notifications. */
|
|
145
|
+
onArrive?: (added: Notification[]) => void;
|
|
146
|
+
/** Fixed notifications to draw when not live. */
|
|
147
|
+
items?: Notification[];
|
|
148
|
+
sources?: SourceStatus[];
|
|
149
|
+
namespaces?: string[];
|
|
150
|
+
namespace?: string;
|
|
151
|
+
discoveryError?: string | null;
|
|
152
|
+
/** Live-stream indicator when not live. Default true. */
|
|
153
|
+
connected?: boolean;
|
|
154
|
+
/** Ids already read (when not live there is no browser storage). */
|
|
155
|
+
seen?: string[];
|
|
156
|
+
/** Ids drawn with their arrival animation. */
|
|
157
|
+
arrived?: string[];
|
|
158
|
+
/** Fixed clock in epoch ms, so relative times and the status bar never move. */
|
|
159
|
+
now?: number;
|
|
160
|
+
title?: string;
|
|
161
|
+
/** A query string such as `?view=sms&to=…`, as a deep link would carry. */
|
|
162
|
+
search?: string;
|
|
163
|
+
initial?: {
|
|
164
|
+
view?: View;
|
|
165
|
+
only?: Channel | null;
|
|
166
|
+
search?: string;
|
|
167
|
+
follow?: boolean;
|
|
168
|
+
inspect?: string | null;
|
|
169
|
+
mailDetailsOpen?: boolean;
|
|
170
|
+
sms?: {
|
|
171
|
+
to?: string | null;
|
|
172
|
+
thread?: string | null;
|
|
173
|
+
menu?: string | null;
|
|
174
|
+
};
|
|
175
|
+
push?: {
|
|
176
|
+
to?: string | null;
|
|
177
|
+
skin?: PushSkin;
|
|
178
|
+
expanded?: string | null;
|
|
179
|
+
preview?: string | null;
|
|
180
|
+
settings?: string | null;
|
|
181
|
+
openGroups?: string[];
|
|
182
|
+
};
|
|
183
|
+
email?: {
|
|
184
|
+
to?: string | null;
|
|
185
|
+
open?: string | null;
|
|
186
|
+
};
|
|
187
|
+
toast?: string | null;
|
|
188
|
+
local?: {
|
|
189
|
+
dismissed?: string[];
|
|
190
|
+
pinned?: string[];
|
|
191
|
+
muted?: string[];
|
|
192
|
+
starred?: string[];
|
|
193
|
+
silent?: string[];
|
|
194
|
+
tapbacks?: Record<string, Tapback>;
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
type AppHandle = {
|
|
199
|
+
render(): void;
|
|
200
|
+
state: {
|
|
201
|
+
items: Notification[];
|
|
202
|
+
};
|
|
203
|
+
/** Resolves once the first read has been drawn (`data-ready="true"` on the root). */
|
|
204
|
+
ready: Promise<void>;
|
|
205
|
+
/** Open one notification on its device, or a recipient on a device. */
|
|
206
|
+
show(target: {
|
|
207
|
+
id?: string;
|
|
208
|
+
channel?: Channel;
|
|
209
|
+
to?: string;
|
|
210
|
+
}): void;
|
|
211
|
+
setNamespace(namespace: string): void;
|
|
212
|
+
setSound(on: boolean): void;
|
|
213
|
+
/** Stop streams and timers, remove listeners and empty the host. */
|
|
214
|
+
destroy(): void;
|
|
215
|
+
};
|
|
216
|
+
type View = "devices" | "timeline";
|
|
217
|
+
type PushSkin = "auto" | "ios" | "android";
|
|
218
|
+
type Tapback = "heart" | "like" | "dislike" | "laugh" | "emphasize" | "question";
|
|
219
|
+
declare function createApp(root: HTMLElement, options?: AppOptions): AppHandle;
|
|
220
|
+
|
|
221
|
+
/** The class on the UI root. Every rule below is scoped to it. */
|
|
222
|
+
declare const ROOT_CLASS = "mb-notifications";
|
|
223
|
+
/** The stylesheet, every rule scoped to `.mb-notifications`. */
|
|
224
|
+
declare const STYLES: string;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Every `data-testid` the harness UI renders, and selector helpers for browser tests.
|
|
228
|
+
*
|
|
229
|
+
* Elements that show one notification also carry its fields as attributes, so a test can narrow by
|
|
230
|
+
* what it knows instead of by position:
|
|
231
|
+
*
|
|
232
|
+
* data-notification-id data-channel data-service data-source data-to data-from
|
|
233
|
+
* data-status data-created-at data-unread ("true" | "false")
|
|
234
|
+
*
|
|
235
|
+
* The UI root (`TEST_IDS.app`) reports its state the same way: `data-ready` ("true" once the first
|
|
236
|
+
* read finished), `data-view`, `data-namespace`, `data-count`, `data-version`, `data-connected`,
|
|
237
|
+
* `data-sound` and `data-motion`.
|
|
238
|
+
*
|
|
239
|
+
* This module has no dependencies, so Playwright, Cypress or WebdriverIO specs can import it.
|
|
240
|
+
*/
|
|
241
|
+
declare const TEST_IDS: {
|
|
242
|
+
/** The UI root. */
|
|
243
|
+
readonly app: "notifications-app";
|
|
244
|
+
readonly topbar: "topbar";
|
|
245
|
+
/** `<select>` of namespaces. */
|
|
246
|
+
readonly namespace: "namespace";
|
|
247
|
+
/** Filter `<input>` (recipient, sender, text). */
|
|
248
|
+
readonly search: "search";
|
|
249
|
+
/** View tabs; `aria-pressed="true"` on the active one. */
|
|
250
|
+
readonly viewDevices: "view-devices";
|
|
251
|
+
readonly viewTimeline: "view-timeline";
|
|
252
|
+
/** "Follow new" checkbox. */
|
|
253
|
+
readonly follow: "follow";
|
|
254
|
+
/** Sound on/off button; `aria-pressed`. */
|
|
255
|
+
readonly sound: "sound-toggle";
|
|
256
|
+
/** Live stream indicator; `data-state="live" | "reconnecting"`. */
|
|
257
|
+
readonly connection: "connection";
|
|
258
|
+
/** One pill per emulator; `data-name`, `data-service`, `data-ok`, `data-count`. */
|
|
259
|
+
readonly source: "source";
|
|
260
|
+
/** Discovery or emulator errors, or the "no emulators" hint; `data-kind="error" | "empty"`. */
|
|
261
|
+
readonly notice: "notice";
|
|
262
|
+
/** Bottom-left message after an action, e.g. "Conversation archived." */
|
|
263
|
+
readonly toast: "toast";
|
|
264
|
+
readonly toastUndo: "toast-undo";
|
|
265
|
+
/** The phone; `data-screen="threads" | "conversation"`, `data-recipient`, `data-thread`. */
|
|
266
|
+
readonly smsDevice: "sms-device";
|
|
267
|
+
/** Phone number `<select>`; option values are E.164 numbers. */
|
|
268
|
+
readonly smsRecipient: "sms-recipient";
|
|
269
|
+
/** Unread SMS badge next to the column title (absent when 0). */
|
|
270
|
+
readonly smsUnreadCount: "sms-unread-count";
|
|
271
|
+
/** One conversation in the list; `data-from` (the sender address), `data-unread`, `data-pinned`, `data-muted`. */
|
|
272
|
+
readonly smsThread: "sms-thread";
|
|
273
|
+
readonly smsThreadName: "sms-thread-name";
|
|
274
|
+
readonly smsThreadPreview: "sms-thread-preview";
|
|
275
|
+
readonly smsThreadTime: "sms-thread-time";
|
|
276
|
+
readonly smsThreadUnread: "sms-thread-unread";
|
|
277
|
+
/** Swipe actions behind a thread (also reachable with the keyboard: focus the thread, press Delete). */
|
|
278
|
+
readonly smsThreadDelete: "sms-thread-delete";
|
|
279
|
+
readonly smsThreadMute: "sms-thread-mute";
|
|
280
|
+
readonly smsThreadPin: "sms-thread-pin";
|
|
281
|
+
readonly smsThreadToggleUnread: "sms-thread-toggle-unread";
|
|
282
|
+
/** Shown when the phone has no messages. */
|
|
283
|
+
readonly smsEmpty: "sms-empty";
|
|
284
|
+
/** An open conversation; `data-from`. */
|
|
285
|
+
readonly smsConversation: "sms-conversation";
|
|
286
|
+
readonly smsConversationTitle: "sms-conversation-title";
|
|
287
|
+
readonly smsBack: "sms-back";
|
|
288
|
+
/** One message; carries the notification attributes plus `data-code` when it holds a one-time code. */
|
|
289
|
+
readonly smsBubble: "sms-bubble";
|
|
290
|
+
readonly smsBubbleMedia: "sms-bubble-media";
|
|
291
|
+
/** The time revealed by swiping the conversation left. */
|
|
292
|
+
readonly smsBubbleTime: "sms-bubble-time";
|
|
293
|
+
/** A reaction added with the long-press menu; `data-tapback`. */
|
|
294
|
+
readonly smsTapback: "sms-tapback";
|
|
295
|
+
/** Centered date stamp between messages. */
|
|
296
|
+
readonly smsTimestamp: "sms-timestamp";
|
|
297
|
+
/** One-time code suggestion above the keyboard; `data-code`. */
|
|
298
|
+
readonly smsCode: "sms-code";
|
|
299
|
+
/** Long-press menu on a bubble. */
|
|
300
|
+
readonly smsMenu: "sms-menu";
|
|
301
|
+
readonly smsMenuCopy: "sms-menu-copy";
|
|
302
|
+
/** One tapback in the menu; `data-tapback="heart" | "like" | "dislike" | "laugh" | "emphasize" | "question"`. */
|
|
303
|
+
readonly smsMenuTapback: "sms-menu-tapback";
|
|
304
|
+
/** The phone; `data-skin="ios" | "android"`, `data-recipient`, `data-platform`. */
|
|
305
|
+
readonly pushDevice: "push-device";
|
|
306
|
+
/** Device token `<select>`. */
|
|
307
|
+
readonly pushRecipient: "push-recipient";
|
|
308
|
+
/** Skin buttons; `data-value`, `aria-pressed`. */
|
|
309
|
+
readonly pushSkin: "push-skin";
|
|
310
|
+
readonly pushUnreadCount: "push-unread-count";
|
|
311
|
+
/** One notification; notification attributes plus `data-expanded`, `data-platform`, `data-app`, `data-stacked`. */
|
|
312
|
+
readonly pushNotification: "push-notification";
|
|
313
|
+
readonly pushNotificationApp: "push-notification-app";
|
|
314
|
+
readonly pushNotificationTitle: "push-notification-title";
|
|
315
|
+
readonly pushNotificationBody: "push-notification-body";
|
|
316
|
+
readonly pushNotificationTime: "push-notification-time";
|
|
317
|
+
readonly pushNotificationImage: "push-notification-image";
|
|
318
|
+
/** The `data` payload, shown when expanded. */
|
|
319
|
+
readonly pushNotificationData: "push-notification-data";
|
|
320
|
+
/** Service, status, badge and sound, shown when expanded. */
|
|
321
|
+
readonly pushNotificationMeta: "push-notification-meta";
|
|
322
|
+
/** iOS: a group of notifications from one app; `data-app`, `data-count`, `data-open`. */
|
|
323
|
+
readonly pushGroup: "push-group";
|
|
324
|
+
/** iOS: "N more notifications" under a collapsed group (click to open it); `data-count`. */
|
|
325
|
+
readonly pushGroupMore: "push-group-more";
|
|
326
|
+
/** iOS: "Show less" on an open group. */
|
|
327
|
+
readonly pushGroupCollapse: "push-group-collapse";
|
|
328
|
+
/** iOS: swipe-left actions. */
|
|
329
|
+
readonly pushClear: "push-clear";
|
|
330
|
+
readonly pushOptions: "push-options";
|
|
331
|
+
/** iOS long-press preview; Android long-press settings. */
|
|
332
|
+
readonly pushPreview: "push-preview";
|
|
333
|
+
readonly pushSettings: "push-settings";
|
|
334
|
+
/** Android "Clear all". */
|
|
335
|
+
readonly pushClearAll: "push-clear-all";
|
|
336
|
+
readonly pushEmpty: "push-empty";
|
|
337
|
+
/** The browser window; `data-screen="inbox" | "message"`, `data-recipient`. */
|
|
338
|
+
readonly emailDevice: "email-device";
|
|
339
|
+
/** Mailbox `<select>`; option values are lower-case addresses. */
|
|
340
|
+
readonly emailRecipient: "email-recipient";
|
|
341
|
+
readonly emailUnreadCount: "email-unread-count";
|
|
342
|
+
/** One inbox row; notification attributes plus `data-starred`. */
|
|
343
|
+
readonly emailRow: "email-row";
|
|
344
|
+
readonly emailRowFrom: "email-row-from";
|
|
345
|
+
readonly emailRowSubject: "email-row-subject";
|
|
346
|
+
readonly emailRowSnippet: "email-row-snippet";
|
|
347
|
+
readonly emailRowDate: "email-row-date";
|
|
348
|
+
readonly emailRowService: "email-row-service";
|
|
349
|
+
readonly emailRowAttachment: "email-row-attachment";
|
|
350
|
+
readonly emailRowStar: "email-row-star";
|
|
351
|
+
/** Hover actions on a row. */
|
|
352
|
+
readonly emailRowArchive: "email-row-archive";
|
|
353
|
+
readonly emailRowDelete: "email-row-delete";
|
|
354
|
+
readonly emailRowToggleRead: "email-row-toggle-read";
|
|
355
|
+
readonly emailRefresh: "email-refresh";
|
|
356
|
+
readonly emailEmpty: "email-empty";
|
|
357
|
+
/** The open message; notification attributes. */
|
|
358
|
+
readonly emailOpen: "email-open";
|
|
359
|
+
readonly emailBack: "email-back";
|
|
360
|
+
readonly emailArchive: "email-archive";
|
|
361
|
+
readonly emailDelete: "email-delete";
|
|
362
|
+
readonly emailMarkUnread: "email-mark-unread";
|
|
363
|
+
readonly emailSubject: "email-subject";
|
|
364
|
+
readonly emailFromName: "email-from-name";
|
|
365
|
+
readonly emailFromAddress: "email-from-address";
|
|
366
|
+
/** "to me, …" line; click to toggle the details. */
|
|
367
|
+
readonly emailTo: "email-to";
|
|
368
|
+
readonly emailDate: "email-date";
|
|
369
|
+
readonly emailDetails: "email-details";
|
|
370
|
+
/** One row of the details; `data-field="from" | "to" | "cc" | "bcc" | "reply-to" | "date" | "subject" | "mailed-by" | "message-id"`. */
|
|
371
|
+
readonly emailDetail: "email-detail";
|
|
372
|
+
/** The HTML body `<iframe>` (same origin: use `frameLocator`). */
|
|
373
|
+
readonly emailBody: "email-body";
|
|
374
|
+
/** The plain-text body when there is no HTML. */
|
|
375
|
+
readonly emailText: "email-text";
|
|
376
|
+
readonly emailAttachments: "email-attachments";
|
|
377
|
+
/** One attachment; `data-filename`, `data-content-type`, `data-size`. */
|
|
378
|
+
readonly emailAttachment: "email-attachment";
|
|
379
|
+
readonly timeline: "timeline";
|
|
380
|
+
/** One row; notification attributes. Click it to inspect. */
|
|
381
|
+
readonly timelineRow: "timeline-row";
|
|
382
|
+
readonly timelineEmpty: "timeline-empty";
|
|
383
|
+
/** The JSON inspector; notification attributes. Opened by clicking a timeline row or double-clicking any notification. */
|
|
384
|
+
readonly inspector: "inspector";
|
|
385
|
+
readonly inspectorTitle: "inspector-title";
|
|
386
|
+
readonly inspectorClose: "inspector-close";
|
|
387
|
+
readonly inspectorShowOnDevice: "inspector-show-on-device";
|
|
388
|
+
/** One normalized field; `data-field`. */
|
|
389
|
+
readonly inspectorField: "inspector-field";
|
|
390
|
+
/** The full record, normalized fields plus `raw`, as JSON text. */
|
|
391
|
+
readonly inspectorJson: "inspector-json";
|
|
392
|
+
};
|
|
393
|
+
type TestId = (typeof TEST_IDS)[keyof typeof TEST_IDS];
|
|
394
|
+
/** CSS selectors for the harness UI. Each one works with `page.locator()` or `querySelector`. */
|
|
395
|
+
declare const selectors: {
|
|
396
|
+
/** `[data-testid="…"]` */
|
|
397
|
+
testId: (id: TestId) => string;
|
|
398
|
+
/** The ready UI root: wait for this before asserting. */
|
|
399
|
+
ready: () => string;
|
|
400
|
+
/** Any element showing this notification (bubble, thread, card, row, open email, timeline row). */
|
|
401
|
+
notification: (id: string) => string;
|
|
402
|
+
/** A message bubble, optionally from one sender. */
|
|
403
|
+
smsBubble: (filter?: {
|
|
404
|
+
from?: string;
|
|
405
|
+
id?: string;
|
|
406
|
+
}) => string;
|
|
407
|
+
/** A conversation in the Messages list. */
|
|
408
|
+
smsThread: (from: string) => string;
|
|
409
|
+
/** A push notification card, optionally by id. */
|
|
410
|
+
pushNotification: (id?: string) => string;
|
|
411
|
+
/** An inbox row, optionally by id. */
|
|
412
|
+
emailRow: (id?: string) => string;
|
|
413
|
+
/** A timeline row, optionally by id. */
|
|
414
|
+
timelineRow: (id?: string) => string;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* The harness UI as a component, for an app that wants its notifications on a tab or panel of its
|
|
419
|
+
* own page: in the browser, against an in-process harness, with no server and no network.
|
|
420
|
+
*
|
|
421
|
+
* const harness = createNotificationHarness({ services: { twilio, resend, fcm } })
|
|
422
|
+
* const ui = mountNotifications(panel, { harness })
|
|
423
|
+
* await ui.ready
|
|
424
|
+
* ui.destroy()
|
|
425
|
+
*
|
|
426
|
+
* Every rule of the stylesheet is scoped to the UI root (`.mb-notifications`), so it can sit next
|
|
427
|
+
* to the host app's own styles.
|
|
428
|
+
*/
|
|
429
|
+
|
|
430
|
+
type MountOptions = Omit<AppOptions, "fetch"> & {
|
|
431
|
+
/** An in-process harness (anything with a Fetch handler). Omit it to read `baseUrl` over HTTP. */
|
|
432
|
+
harness?: Pick<NotificationHarness, "fetch">;
|
|
433
|
+
/** Any Fetch-compatible transport to the harness API. Ignored when `harness` is given. */
|
|
434
|
+
fetch?: NonNullable<AppOptions["fetch"]>;
|
|
435
|
+
/** Add the stylesheet to the host's document or shadow root. Default true. */
|
|
436
|
+
injectStyles?: boolean;
|
|
437
|
+
};
|
|
438
|
+
/**
|
|
439
|
+
* Mount the live harness UI inside `host`. It fills the element and scrolls inside it
|
|
440
|
+
* (`embedded`), and leaves the page URL alone unless `syncUrl` is set.
|
|
441
|
+
*/
|
|
442
|
+
declare const mountNotifications: (host: HTMLElement, options?: MountOptions) => AppHandle;
|
|
443
|
+
|
|
444
|
+
export { ROOT_CLASS, STYLES, TEST_IDS, createApp, mountNotifications, selectors };
|
|
445
|
+
export type { AppHandle, AppOptions, MotionPreference, MountOptions, PushSkin, TestId, View };
|