@gravito/signal 3.1.0 → 3.1.2

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.
Files changed (68) hide show
  1. package/dist/index.cjs +55126 -81925
  2. package/dist/index.d.ts +513 -1
  3. package/dist/index.js +61492 -0
  4. package/dist/index.mjs +55301 -82110
  5. package/package.json +12 -1
  6. package/CHANGELOG.md +0 -74
  7. package/build.ts +0 -133
  8. package/dist/index.cjs.map +0 -712
  9. package/dist/index.mjs.map +0 -710
  10. package/doc/ADVANCED_RENDERING.md +0 -71
  11. package/doc/DISTRIBUTED_MESSAGING.md +0 -79
  12. package/doc/OPTIMIZATION_PLAN.md +0 -496
  13. package/package.json.bak +0 -75
  14. package/scripts/check-coverage.ts +0 -64
  15. package/src/Mailable.ts +0 -674
  16. package/src/OrbitSignal.ts +0 -451
  17. package/src/Queueable.ts +0 -9
  18. package/src/TypedMailable.ts +0 -96
  19. package/src/dev/DevMailbox.ts +0 -146
  20. package/src/dev/DevServer.ts +0 -192
  21. package/src/dev/storage/FileMailboxStorage.ts +0 -66
  22. package/src/dev/storage/MailboxStorage.ts +0 -15
  23. package/src/dev/storage/MemoryMailboxStorage.ts +0 -36
  24. package/src/dev/ui/mailbox.ts +0 -77
  25. package/src/dev/ui/preview.ts +0 -103
  26. package/src/dev/ui/shared.ts +0 -60
  27. package/src/errors.ts +0 -69
  28. package/src/events.ts +0 -72
  29. package/src/index.ts +0 -41
  30. package/src/renderers/HtmlRenderer.ts +0 -41
  31. package/src/renderers/MjmlRenderer.ts +0 -73
  32. package/src/renderers/ReactMjmlRenderer.ts +0 -94
  33. package/src/renderers/ReactRenderer.ts +0 -66
  34. package/src/renderers/Renderer.ts +0 -67
  35. package/src/renderers/TemplateRenderer.ts +0 -84
  36. package/src/renderers/VueMjmlRenderer.ts +0 -99
  37. package/src/renderers/VueRenderer.ts +0 -71
  38. package/src/renderers/mjml-templates.ts +0 -50
  39. package/src/transports/BaseTransport.ts +0 -148
  40. package/src/transports/LogTransport.ts +0 -55
  41. package/src/transports/MemoryTransport.ts +0 -55
  42. package/src/transports/SesTransport.ts +0 -129
  43. package/src/transports/SmtpTransport.ts +0 -184
  44. package/src/transports/Transport.ts +0 -45
  45. package/src/types.ts +0 -309
  46. package/src/utils/html.ts +0 -43
  47. package/src/webhooks/SendGridWebhookDriver.ts +0 -80
  48. package/src/webhooks/SesWebhookDriver.ts +0 -44
  49. package/tests/DevMailbox.test.ts +0 -54
  50. package/tests/FileMailboxStorage.test.ts +0 -56
  51. package/tests/MjmlLayout.test.ts +0 -28
  52. package/tests/MjmlRenderer.test.ts +0 -53
  53. package/tests/OrbitSignalWebhook.test.ts +0 -56
  54. package/tests/ReactMjmlRenderer.test.ts +0 -33
  55. package/tests/SendGridWebhookDriver.test.ts +0 -69
  56. package/tests/SesWebhookDriver.test.ts +0 -46
  57. package/tests/VueMjmlRenderer.test.ts +0 -35
  58. package/tests/dev-server.test.ts +0 -66
  59. package/tests/log-transport.test.ts +0 -21
  60. package/tests/mailable-extra.test.ts +0 -68
  61. package/tests/mailable.test.ts +0 -77
  62. package/tests/orbit-signal.test.ts +0 -43
  63. package/tests/renderers.test.ts +0 -58
  64. package/tests/template-renderer.test.ts +0 -24
  65. package/tests/transports.test.ts +0 -52
  66. package/tests/ui.test.ts +0 -37
  67. package/tsconfig.build.json +0 -24
  68. package/tsconfig.json +0 -9
package/src/types.ts DELETED
@@ -1,309 +0,0 @@
1
- import type { GravitoContext } from '@gravito/core'
2
- import type { Transport } from './transports/Transport'
3
-
4
- /**
5
- * Interface for Webhook Drivers.
6
- */
7
- export interface WebhookDriver {
8
- handle(c: GravitoContext): Promise<{ event: string; payload: any }[] | null>
9
- }
10
-
11
- /**
12
- * Transport interface for sending email messages.
13
- *
14
- * Defines the contract for different delivery mechanisms (SMTP, SES, etc.).
15
- */
16
- export type { Transport }
17
-
18
- /**
19
- * Representation of an email address with optional display name.
20
- *
21
- * Defines the structure for email addresses used throughout the mail system.
22
- * Supports both simple string addresses and formatted addresses with display names.
23
- *
24
- * @example
25
- * ```typescript
26
- * // Simple address
27
- * const addr1: Address = { address: 'user@example.com' }
28
- *
29
- * // Address with display name
30
- * const addr2: Address = {
31
- * name: 'John Doe',
32
- * address: 'john@example.com'
33
- * }
34
- * ```
35
- *
36
- * @see {@link Envelope} For the email envelope structure
37
- * @see {@link Message} For the complete message structure
38
- *
39
- * @public
40
- * @since 3.0.0
41
- */
42
- export interface Address {
43
- /** The display name of the recipient/sender, e.g., "John Doe". */
44
- name?: string
45
- /** The actual email address string. */
46
- address: string
47
- }
48
-
49
- /**
50
- * Configuration for an email attachment.
51
- *
52
- * Defines file attachments for email messages. Supports both regular attachments
53
- * and inline attachments (e.g., embedded images referenced via Content-ID).
54
- *
55
- * @example
56
- * ```typescript
57
- * // Regular file attachment
58
- * const attachment: Attachment = {
59
- * filename: 'document.pdf',
60
- * content: Buffer.from('...'),
61
- * contentType: 'application/pdf'
62
- * }
63
- *
64
- * // Inline image attachment
65
- * const inlineImage: Attachment = {
66
- * filename: 'logo.png',
67
- * content: Buffer.from('...'),
68
- * contentType: 'image/png',
69
- * cid: 'logo@example.com' // Reference in HTML: <img src="cid:logo@example.com">
70
- * }
71
- * ```
72
- *
73
- * @see {@link Envelope} For adding attachments to emails
74
- *
75
- * @public
76
- * @since 3.0.0
77
- */
78
- export interface Attachment {
79
- /** The filename of the attachment. */
80
- filename: string
81
- /** The content of the attachment as a string or Buffer. */
82
- content: string | Buffer // Buffer for Node.js
83
- /** Optional MIME type of the content. */
84
- contentType?: string
85
- /** Optional Content-ID for referencing within HTML content (inline images). */
86
- cid?: string // Content-ID for inline images
87
- /** Optional content encoding. */
88
- encoding?: string
89
- }
90
-
91
- /**
92
- * The envelope containing metadata for an email message.
93
- *
94
- * Used during the construction phase of a Mailable. All fields are optional
95
- * at this stage and will be validated when converting to a Message.
96
- *
97
- * @example
98
- * ```typescript
99
- * const envelope: Envelope = {
100
- * from: { name: 'App', address: 'noreply@app.com' },
101
- * to: [{ address: 'user@example.com' }],
102
- * subject: 'Welcome!',
103
- * priority: 'high',
104
- * replyTo: { address: 'support@app.com' }
105
- * }
106
- * ```
107
- *
108
- * @see {@link Mailable} For building envelopes fluently
109
- * @see {@link Message} For the validated, finalized message structure
110
- *
111
- * @public
112
- * @since 3.0.0
113
- */
114
- export interface Envelope {
115
- /** The sender's address. */
116
- from?: Address | undefined
117
- /** Primary recipients. */
118
- to?: Address[] | undefined
119
- /** Carbon copy recipients. */
120
- cc?: Address[] | undefined
121
- /** Blind carbon copy recipients. */
122
- bcc?: Address[] | undefined
123
- /** Reply-to address. */
124
- replyTo?: Address | undefined
125
- /** Email subject line. */
126
- subject?: string | undefined
127
- /** Importance level of the email. */
128
- priority?: 'high' | 'normal' | 'low' | undefined
129
- /** List of file attachments. */
130
- attachments?: Attachment[] | undefined
131
- }
132
-
133
- /**
134
- * A fully finalized email message ready to be sent by a transport.
135
- *
136
- * Requires mandatory fields that were optional in the Envelope.
137
- * This structure is passed to Transport implementations for actual delivery.
138
- *
139
- * @example
140
- * ```typescript
141
- * const message: Message = {
142
- * from: { name: 'App', address: 'noreply@app.com' },
143
- * to: [{ address: 'user@example.com' }],
144
- * subject: 'Welcome to our service',
145
- * html: '<h1>Welcome!</h1><p>Thanks for joining.</p>',
146
- * text: 'Welcome! Thanks for joining.',
147
- * priority: 'normal',
148
- * headers: { 'X-Custom-Header': 'value' }
149
- * }
150
- * ```
151
- *
152
- * @see {@link Envelope} For the construction phase structure
153
- * @see {@link Transport} For implementations that send messages
154
- *
155
- * @public
156
- * @since 3.0.0
157
- */
158
- export interface Message extends Envelope {
159
- /** The mandatory sender's address. */
160
- from: Address
161
- /** At least one recipient is required. */
162
- to: Address[]
163
- /** Mandatory subject. */
164
- subject: string
165
- /** The rendered HTML body content. */
166
- html: string
167
- /** Optional rendered plain text body content. */
168
- text?: string
169
- /** Custom SMTP headers. */
170
- headers?: Record<string, string>
171
- }
172
-
173
- /**
174
- * Global configuration options for OrbitSignal and Mailable instances.
175
- *
176
- * Configures the mail service behavior, transport mechanism, development tools,
177
- * and internationalization support.
178
- *
179
- * @example
180
- * ```typescript
181
- * import { OrbitSignal, SmtpTransport } from '@gravito/signal'
182
- *
183
- * const config: MailConfig = {
184
- * from: { name: 'My App', address: 'noreply@myapp.com' },
185
- * transport: new SmtpTransport({
186
- * host: 'smtp.mailtrap.io',
187
- * port: 2525,
188
- * auth: { user: 'user', pass: 'pass' }
189
- * }),
190
- * devMode: process.env.NODE_ENV === 'development',
191
- * viewsDir: './src/emails',
192
- * devUiPrefix: '/__mail',
193
- * translator: (key, replace, locale) => i18n.t(key, { ...replace, locale })
194
- * }
195
- *
196
- * const mail = new OrbitSignal(config)
197
- * ```
198
- *
199
- * @see {@link OrbitSignal} For the mail service implementation
200
- * @see {@link Transport} For available transport options
201
- *
202
- * @public
203
- * @since 3.0.0
204
- */
205
- export interface MailConfig {
206
- /**
207
- * Default sender address used if not specified in the Mailable.
208
- *
209
- * @example
210
- * ```typescript
211
- * from: { name: 'My App', address: 'noreply@myapp.com' }
212
- * ```
213
- */
214
- from?: Address
215
-
216
- /**
217
- * The transport mechanism used to send emails (e.g., SMTP, SES, Log).
218
- *
219
- * @example
220
- * ```typescript
221
- * import { SmtpTransport } from '@gravito/signal'
222
- * transport: new SmtpTransport({ host: 'smtp.example.com', port: 587 })
223
- * ```
224
- */
225
- transport?: Transport
226
-
227
- /**
228
- * Enable development mode.
229
- * When true, emails are intercepted by the DevMailbox instead of being sent.
230
- *
231
- * @default false
232
- * @example
233
- * ```typescript
234
- * devMode: process.env.NODE_ENV === 'development'
235
- * ```
236
- */
237
- devMode?: boolean | undefined
238
-
239
- /**
240
- * Directory where email templates are located for use with OrbitPrism.
241
- *
242
- * @default "src/emails"
243
- * @example
244
- * ```typescript
245
- * viewsDir: './resources/views/emails'
246
- * ```
247
- */
248
- viewsDir?: string | undefined
249
-
250
- /**
251
- * URL prefix for the Mail Dev UI.
252
- *
253
- * @default "/__mail"
254
- * @example
255
- * ```typescript
256
- * devUiPrefix: '/dev/mailbox'
257
- * ```
258
- */
259
- devUiPrefix?: string | undefined
260
-
261
- /**
262
- * Whether to allow access to the Mail Dev UI in production environments.
263
- *
264
- * @default false
265
- * @example
266
- * ```typescript
267
- * devUiAllowInProduction: process.env.ALLOW_MAIL_UI === 'true'
268
- * ```
269
- */
270
- devUiAllowInProduction?: boolean | undefined
271
-
272
- /**
273
- * Authorization gate for the Mail Dev UI.
274
- * Should return true to allow access to the UI.
275
- *
276
- * @example
277
- * ```typescript
278
- * devUiGate: async (ctx) => {
279
- * const user = await ctx.get('auth').user()
280
- * return user?.role === 'admin'
281
- * }
282
- * ```
283
- */
284
- devUiGate?: ((ctx: GravitoContext) => boolean | Promise<boolean>) | undefined
285
-
286
- /**
287
- * Translation function for internationalization within emails.
288
- *
289
- * @example
290
- * ```typescript
291
- * translator: (key, replace, locale) => {
292
- * return i18n.t(key, { ...replace, locale: locale || 'en' })
293
- * }
294
- * ```
295
- */
296
- translator?:
297
- | ((key: string, replace?: Record<string, unknown>, locale?: string) => string)
298
- | undefined
299
-
300
- /**
301
- * URL prefix for Webhook endpoints.
302
- */
303
- webhookPrefix?: string | undefined
304
-
305
- /**
306
- * Dictionary of registered webhook drivers.
307
- */
308
- webhookDrivers?: Record<string, WebhookDriver> | undefined
309
- }
package/src/utils/html.ts DELETED
@@ -1,43 +0,0 @@
1
- /**
2
- * HTML utility functions.
3
- *
4
- * Provides helper methods for processing and transforming HTML content,
5
- * primarily for generating plain text alternatives for emails.
6
- *
7
- * @module utils/html
8
- * @since 3.1.0
9
- */
10
-
11
- /**
12
- * Convert HTML content to plain text.
13
- *
14
- * Removes all HTML tags, styles, scripts, and normalizes whitespace.
15
- * This is essential for generating the `text` part of a multipart email
16
- * to ensure compatibility with mail clients that do not support HTML.
17
- *
18
- * @param html - Source HTML string to be stripped
19
- * @returns Plain text content with tags and entities removed
20
- *
21
- * @example
22
- * ```typescript
23
- * const text = stripHtml('<h1>Hello</h1><p>World</p>')
24
- * // Returns: 'Hello World'
25
- * ```
26
- *
27
- * @public
28
- * @since 3.1.0
29
- */
30
- export function stripHtml(html: string): string {
31
- return html
32
- .replace(/<style(?:\s[^>]*)?>[\s\S]*?<\/style>/gi, '')
33
- .replace(/<script(?:\s[^>]*)?>[\s\S]*?<\/script>/gi, '')
34
- .replace(/<[^>]+>/g, '')
35
- .replace(/&nbsp;/g, ' ')
36
- .replace(/&amp;/g, '&')
37
- .replace(/&lt;/g, '<')
38
- .replace(/&gt;/g, '>')
39
- .replace(/&quot;/g, '"')
40
- .replace(/&#39;/g, "'")
41
- .replace(/\s+/g, ' ')
42
- .trim()
43
- }
@@ -1,80 +0,0 @@
1
- import type { GravitoContext } from '@gravito/core'
2
- import type { WebhookDriver } from '../types'
3
-
4
- /**
5
- * Configuration for SendGrid Webhook Driver.
6
- */
7
- export interface SendGridWebhookConfig {
8
- /**
9
- * Public key or Verification Secret for signature validation.
10
- * If provided, all requests will be validated.
11
- */
12
- publicKey?: string
13
- }
14
-
15
- /**
16
- * SendGrid Webhook Driver.
17
- *
18
- * Handles Event Webhooks from SendGrid (delivered, bounced, opened, clicked, etc.).
19
- *
20
- * @see https://docs.sendgrid.com/for-developers/tracking-events/event-webhook
21
- * @public
22
- * @since 1.1.0
23
- */
24
- export class SendGridWebhookDriver implements WebhookDriver {
25
- constructor(private config: SendGridWebhookConfig = {}) {}
26
-
27
- /**
28
- * Handles the SendGrid webhook request.
29
- */
30
- async handle(c: GravitoContext): Promise<{ event: string; payload: any }[] | null> {
31
- const body = await c.req.json()
32
-
33
- // SendGrid events are usually sent as an array
34
- const events = Array.isArray(body) ? body : [body]
35
-
36
- if (this.config.publicKey) {
37
- const signature = c.req.header('X-Twilio-Email-Event-Webhook-Signature')
38
- const timestamp = c.req.header('X-Twilio-Email-Event-Webhook-Timestamp')
39
-
40
- if (!signature || !timestamp) {
41
- throw new Error('Missing SendGrid signature headers')
42
- }
43
-
44
- if (!this.verifySignature(JSON.stringify(body), signature, timestamp)) {
45
- throw new Error('Invalid SendGrid signature')
46
- }
47
- }
48
-
49
- if (events.length === 0) {
50
- return null
51
- }
52
-
53
- return events.map((e) => ({
54
- event: e.event,
55
- payload: e,
56
- }))
57
- }
58
-
59
- /**
60
- * Verifies the SendGrid webhook signature.
61
- *
62
- * @param payload - Raw request body string.
63
- * @param signature - Signature from X-Twilio-Email-Event-Webhook-Signature header.
64
- * @param timestamp - Timestamp from X-Twilio-Email-Event-Webhook-Timestamp header.
65
- * @returns True if signature is valid.
66
- *
67
- * @remarks
68
- * Real SendGrid validation uses Elliptic Curve (ECDSA).
69
- * This is a placeholder for the logic structure.
70
- */
71
- private verifySignature(_payload: string, _signature: string, _timestamp: string): boolean {
72
- if (!this.config.publicKey) {
73
- return true
74
- }
75
-
76
- // In a real implementation, you would use crypto.verify with the SendGrid public key.
77
- // SendGrid uses ECDSA with SHA256.
78
- return true // Placeholder: Actual implementation requires 'crypto' public key verification
79
- }
80
- }
@@ -1,44 +0,0 @@
1
- import type { GravitoContext } from '@gravito/core'
2
- import type { WebhookDriver } from '../types'
3
-
4
- /**
5
- * AWS SES Webhook Driver.
6
- *
7
- * Handles SES Notifications via Amazon SNS (Complaints, Bounces, Deliveries).
8
- *
9
- * @see https://docs.aws.amazon.com/ses/latest/dg/monitor-sending-activity-using-notifications.html
10
- * @public
11
- * @since 1.1.0
12
- */
13
- export class SesWebhookDriver implements WebhookDriver {
14
- /**
15
- * Handles the AWS SES/SNS webhook request.
16
- *
17
- * @param c - The Gravito request context.
18
- * @returns Array of processed events or null if ignored.
19
- */
20
- async handle(c: GravitoContext): Promise<{ event: string; payload: any }[] | null> {
21
- const body = (await c.req.json()) as any
22
-
23
- // Handle SNS Subscription Confirmation
24
- if (body.Type === 'SubscriptionConfirmation') {
25
- // In production, you would visit body.SubscribeURL to confirm
26
- return [{ event: 'sns:subscription', payload: body }]
27
- }
28
-
29
- // Handle SNS Notifications
30
- if (body.Type === 'Notification') {
31
- const message = typeof body.Message === 'string' ? JSON.parse(body.Message) : body.Message
32
- const eventType = message.notificationType?.toLowerCase() || 'unknown'
33
-
34
- return [
35
- {
36
- event: eventType,
37
- payload: message,
38
- },
39
- ]
40
- }
41
-
42
- return null
43
- }
44
- }
@@ -1,54 +0,0 @@
1
- import { describe, expect, it } from 'bun:test'
2
- import { DevMailbox } from '../src/dev/DevMailbox'
3
- import type { Message } from '../src/types'
4
-
5
- describe('DevMailbox', () => {
6
- const mockMessage: Message = {
7
- from: { address: 'from@example.com' },
8
- to: [{ address: 'to@example.com' }],
9
- subject: 'Test Subject',
10
- html: '<h1>Hello</h1>',
11
- }
12
-
13
- it('should respect the default maximum entries (50)', async () => {
14
- const mailbox = new DevMailbox()
15
-
16
- // Add 60 messages
17
- for (let i = 0; i < 60; i++) {
18
- await mailbox.add({ ...mockMessage, subject: `Test ${i}` })
19
- }
20
-
21
- const list = await mailbox.list()
22
- expect(list.length).toBe(50)
23
- // Should keep the newest (Test 59)
24
- expect(list[0].envelope.subject).toBe('Test 59')
25
- // Should have removed Test 0-9, list[49] should be Test 10
26
- expect(list[49].envelope.subject).toBe('Test 10')
27
- })
28
-
29
- it('should respect custom maximum entries via constructor', async () => {
30
- const mailbox = new DevMailbox(10)
31
-
32
- for (let i = 0; i < 15; i++) {
33
- await mailbox.add(mockMessage)
34
- }
35
-
36
- const list = await mailbox.list()
37
- expect(list.length).toBe(10)
38
- })
39
-
40
- it('should respect setMaxEntries and trim existing entries', async () => {
41
- const mailbox = new DevMailbox(20)
42
-
43
- for (let i = 0; i < 20; i++) {
44
- await mailbox.add({ ...mockMessage, subject: `Test ${i}` })
45
- }
46
-
47
- expect((await mailbox.list()).length).toBe(20)
48
-
49
- await mailbox.setMaxEntries(5)
50
- const list = await mailbox.list()
51
- expect(list.length).toBe(5)
52
- expect(list[0].envelope.subject).toBe('Test 19')
53
- })
54
- })
@@ -1,56 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2
- import { mkdir, rm } from 'node:fs/promises'
3
- import { join } from 'node:path'
4
- import { DevMailbox } from '../src/dev/DevMailbox'
5
- import { FileMailboxStorage } from '../src/dev/storage/FileMailboxStorage'
6
- import type { Message } from '../src/types'
7
-
8
- describe('FileMailboxStorage Persistence', () => {
9
- const testDir = join(import.meta.dir, 'tmp-mailbox')
10
- const mockMessage: Message = {
11
- from: { address: 'from@example.com' },
12
- to: [{ address: 'to@example.com' }],
13
- subject: 'Persistent Subject',
14
- html: '<h1>Hello</h1>',
15
- }
16
-
17
- beforeEach(async () => {
18
- await mkdir(testDir, { recursive: true })
19
- })
20
-
21
- afterEach(async () => {
22
- await rm(testDir, { recursive: true, force: true })
23
- })
24
-
25
- it('should persist messages to disk and survive re-instantiation', async () => {
26
- const storage1 = new FileMailboxStorage(testDir)
27
- const mailbox1 = new DevMailbox(10, storage1)
28
-
29
- await mailbox1.add({ ...mockMessage, subject: 'First Message' })
30
- await mailbox1.add({ ...mockMessage, subject: 'Second Message' })
31
-
32
- const list1 = await mailbox1.list()
33
- expect(list1.length).toBe(2)
34
-
35
- // Simulate server restart by creating new instance with same dir
36
- const storage2 = new FileMailboxStorage(testDir)
37
- const mailbox2 = new DevMailbox(10, storage2)
38
-
39
- const list2 = await mailbox2.list()
40
- expect(list2.length).toBe(2)
41
- expect(list2[0].envelope.subject).toBe('Second Message')
42
- })
43
-
44
- it('should trim files according to max entries', async () => {
45
- const storage = new FileMailboxStorage(testDir)
46
- const mailbox = new DevMailbox(3, storage)
47
-
48
- for (let i = 0; i < 5; i++) {
49
- await mailbox.add({ ...mockMessage, subject: `Msg ${i}` })
50
- }
51
-
52
- const list = await mailbox.list()
53
- expect(list.length).toBe(3)
54
- expect(list[0].envelope.subject).toBe('Msg 4')
55
- })
56
- })
@@ -1,28 +0,0 @@
1
- import { describe, expect, it } from 'bun:test'
2
- import { Mailable } from '../src/Mailable'
3
-
4
- class TestMailable extends Mailable {
5
- build() {
6
- return this
7
- }
8
- }
9
-
10
- describe('Mailable MJML Layout', () => {
11
- it('should wrap content with layout if provided', async () => {
12
- const mailable = new TestMailable()
13
- mailable.mjml('<mj-text>Hello</mj-text>', {
14
- layout: '<mjml><mj-body>{{content}}</mj-body></mjml>',
15
- })
16
-
17
- // Access private renderer via cast to check final content
18
- const content = await mailable.renderContent()
19
- // The mock renderer would contain the final wrapped string
20
- // Since we are using dynamic import in renderContent,
21
- // it will actually try to load MjmlRenderer.
22
- // We can verify the finalContent logic by checking the renderer state if possible
23
- // or just let it render (mjml is installed)
24
-
25
- expect(content.html).toContain('<div') // MJML rendered output
26
- expect(content.html).toContain('Hello')
27
- })
28
- })
@@ -1,53 +0,0 @@
1
- import { describe, expect, it, mock } from 'bun:test'
2
- import { MjmlRenderer } from '../src/renderers/MjmlRenderer'
3
-
4
- describe('MjmlRenderer', () => {
5
- const mjmlContent =
6
- '<mjml><mj-body><mj-section><mj-column><mj-text>Hello</mj-text></mj-column></mj-section></mj-body></mjml>'
7
-
8
- it('should render MJML to HTML', async () => {
9
- // Mock mjml dependency
10
- const mockMjml2Html = mock((content) => ({
11
- html: `<html><body>${content}</body></html>`,
12
- errors: [],
13
- }))
14
-
15
- const renderer = new MjmlRenderer(mjmlContent, {}, { mjml2html: mockMjml2Html })
16
- const result = await renderer.render()
17
-
18
- expect(result.html).toContain('<html><body>')
19
- expect(result.html).toContain(mjmlContent)
20
- expect(mockMjml2Html).toHaveBeenCalled()
21
- })
22
-
23
- it('should throw error if MJML has errors and validationLevel is strict', async () => {
24
- const mockMjmlWithErrors = mock(() => ({
25
- html: '',
26
- errors: [{ formattedMessage: 'Line 1: Error' }],
27
- }))
28
-
29
- const renderer = new MjmlRenderer(
30
- mjmlContent,
31
- { validationLevel: 'strict' },
32
- { mjml2html: mockMjmlWithErrors }
33
- )
34
-
35
- expect(renderer.render()).rejects.toThrow('MJML rendering failed')
36
- })
37
-
38
- it('should not throw error if MJML has errors and validationLevel is soft', async () => {
39
- const mockMjmlWithErrors = mock(() => ({
40
- html: 'partial html',
41
- errors: [{ formattedMessage: 'Soft error' }],
42
- }))
43
-
44
- const renderer = new MjmlRenderer(
45
- mjmlContent,
46
- { validationLevel: 'soft' },
47
- { mjml2html: mockMjmlWithErrors }
48
- )
49
-
50
- const result = await renderer.render()
51
- expect(result.html).toBe('partial html')
52
- })
53
- })