Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/email/emails/all-policy-notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export const AllPolicyNotificationEmail = ({
return (
<Html>
<Tailwind>
<Preview>{subjectText}</Preview>
<head />
<Preview>{subjectText}</Preview>

<Body className="mx-auto my-auto bg-[#fff] font-sans">
<Container
Expand Down
3 changes: 2 additions & 1 deletion packages/email/emails/invite-portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export const InvitePortalEmail = ({ email, inviteLink, organizationName }: Props
return (
<Html>
<Tailwind>
<Preview>You've been invited to the Comp AI Portal</Preview>
<head />
<Preview>You've been invited to the Comp AI Portal</Preview>

<Body className="mx-auto my-auto bg-[#fff] font-sans">
<Container
Expand Down
3 changes: 2 additions & 1 deletion packages/email/emails/invite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export const InviteEmail = ({ email, organizationName, inviteLink }: Props) => {
return (
<Html>
<Tailwind>
<Preview>You've been invited to join Comp AI</Preview>
<head />
<Preview>You've been invited to join Comp AI</Preview>

<Body className="mx-auto my-auto bg-[#fff] font-sans">
<Container
Expand Down
3 changes: 2 additions & 1 deletion packages/email/emails/magic-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export const MagicLinkEmail = ({ email, url, inviteCode }: Props) => {
return (
<Html>
<Tailwind>
<Preview>Login Link for Comp AI</Preview>
<head />
<Preview>Login Link for Comp AI</Preview>

<Body className="mx-auto my-auto bg-[#fff] font-sans">
<Container
Expand Down
3 changes: 2 additions & 1 deletion packages/email/emails/marketing/welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export const WelcomeEmail = ({ name }: Props) => {
return (
<Html>
<Tailwind>
<Preview>Get started with Comp AI</Preview>
<head />
<Preview>Get started with Comp AI</Preview>

<Body className="mx-auto my-auto bg-[#fff] font-sans">
<Container
Expand Down
3 changes: 2 additions & 1 deletion packages/email/emails/otp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export const OTPVerificationEmail = ({ email, otp }: Props) => {
return (
<Html>
<Tailwind>
<Preview>One-Time Password for Comp AI</Preview>
<head />
<Preview>One-Time Password for Comp AI</Preview>

<Body className="mx-auto my-auto bg-[#fff] font-sans">
<Container
Expand Down
3 changes: 2 additions & 1 deletion packages/email/emails/policy-notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export const PolicyNotificationEmail = ({
return (
<Html>
<Tailwind>
<Preview>{subjectText}</Preview>
<head />
<Preview>{subjectText}</Preview>

<Body className="mx-auto my-auto bg-[#fff] font-sans">
<Container
Expand Down
3 changes: 2 additions & 1 deletion packages/email/emails/reminders/task-reminder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export const TaskReminderEmail = ({ email, name, dueDate, recordId }: Props) =>
return (
<Html>
<Tailwind>
<Preview>Comp AI - Task Reminder</Preview>
<head />
<Preview>Comp AI - Task Reminder</Preview>

<Body className="mx-auto my-auto bg-[#fff] font-sans">
<Container
Expand Down
3 changes: 2 additions & 1 deletion packages/email/emails/reminders/task-status-notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export const TaskStatusNotificationEmail = ({
return (
<Html>
<Tailwind>
<Preview>
<head />
<Preview>
Task &quot;{taskName}&quot; {statusLabel} - {organizationName}
</Preview>

Expand Down
3 changes: 2 additions & 1 deletion packages/email/emails/reminders/weekly-task-digest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export const WeeklyTaskDigestEmail = ({
return (
<Html>
<Tailwind>
<Preview>{taskCountMessage}</Preview>
<head />
<Preview>{taskCountMessage}</Preview>

<Body className="mx-auto my-auto bg-[#fff] font-sans">
<Container
Expand Down
157 changes: 157 additions & 0 deletions packages/email/emails/render.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { render } from '@react-email/render';
import { describe, expect, it } from 'vitest';
import { AllPolicyNotificationEmail } from './all-policy-notification';
import { InviteEmail } from './invite';
import { InvitePortalEmail } from './invite-portal';
import { MagicLinkEmail } from './magic-link';
import { WelcomeEmail } from './marketing/welcome';
import { OTPVerificationEmail } from './otp';
import { PolicyNotificationEmail } from './policy-notification';
import { TaskReminderEmail } from './reminders/task-reminder';
import { TaskStatusNotificationEmail } from './reminders/task-status-notification';
import { WeeklyTaskDigestEmail } from './reminders/weekly-task-digest';
import { TrainingCompletedEmail } from './training-completed';
import { UnassignedItemsNotificationEmail } from './unassigned-items-notification';

// Regression: PR #2501 removed <head> from every template, which broke
// @react-email/tailwind's media-query injection (md:* classes) and caused
// every email to render to an empty Suspense fallback in production.
// These tests fail if any template renders to that fallback or otherwise
// produces broken output, so the same mistake can't ship again.

const SUSPENSE_ERROR_MARKER = '<!--$!-->';

const cases = [
{
name: 'weekly-task-digest',
el: (
<WeeklyTaskDigestEmail
email="user@example.com"
userName="User"
organizationName="Acme"
organizationId="org_123"
tasks={[{ id: 't1', title: 'Task one' }]}
/>
),
},
{
name: 'task-reminder',
el: (
<TaskReminderEmail
email="user@example.com"
name="User"
dueDate="2026-04-20"
recordId="r1"
/>
),
},
{
name: 'task-status-notification',
el: (
<TaskStatusNotificationEmail
email="user@example.com"
userName="User"
taskName="Task"
oldStatus="todo"
newStatus="done"
organizationName="Acme"
organizationId="org_123"
taskId="t1"
changedByName="Admin"
/>
),
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test passes wrong props to TaskStatusNotificationEmail

Medium Severity

The regression test for TaskStatusNotificationEmail passes props (oldStatus, newStatus, organizationId, taskId, changedByName) that don't exist in the component's Props interface. The actually required props taskStatus and taskUrl are never provided. Since vitest uses esbuild which strips types, this test silently runs with undefined for both, giving false confidence that the component renders correctly while not actually testing it with valid data.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3376196. Configure here.

{
name: 'invite-portal',
el: (
<InvitePortalEmail
email="user@example.com"
inviteLink="https://app.trycomp.ai/invite"
organizationName="Acme"
/>
),
},
{
name: 'invite',
el: (
<InviteEmail
email="user@example.com"
organizationName="Acme"
inviteLink="https://app.trycomp.ai/invite"
/>
),
},
{ name: 'welcome', el: <WelcomeEmail name="User" /> },
{
name: 'policy-notification',
el: (
<PolicyNotificationEmail
email="user@example.com"
userName="User"
organizationName="Acme"
organizationId="org_123"
policyName="Acceptable Use"
policyId="p1"
isUpdate={false}
/>
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test passes wrong props to PolicyNotificationEmail

Medium Severity

The regression test for PolicyNotificationEmail passes policyId and isUpdate which don't exist in the component's Props interface, and omits the required notificationType prop (typed as 'new' | 'updated' | 're-acceptance'). At runtime, notificationType is undefined, so the switch always hits the default case. The test silently passes but doesn't validate the component with valid inputs.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3376196. Configure here.

},
{
name: 'magic-link',
el: (
<MagicLinkEmail
email="user@example.com"
url="https://app.trycomp.ai/magic"
inviteCode="abc"
/>
),
},
{
name: 'all-policy-notification',
el: (
<AllPolicyNotificationEmail
email="user@example.com"
userName="User"
organizationName="Acme"
organizationId="org_123"
isUpdate={false}
/>
),
},
{ name: 'otp', el: <OTPVerificationEmail email="user@example.com" otp="123456" /> },
{
name: 'training-completed',
el: (
<TrainingCompletedEmail
email="user@example.com"
userName="User"
organizationName="Acme"
completedAt={new Date('2026-04-13')}
/>
),
},
{
name: 'unassigned-items-notification',
el: (
<UnassignedItemsNotificationEmail
email="user@example.com"
userName="User"
organizationName="Acme"
organizationId="org_123"
removedMemberName="Former"
unassignedItems={[{ type: 'task', id: 't1', name: 'Task' }]}
/>
),
},
];

describe('email templates render to non-empty HTML', () => {
for (const { name, el } of cases) {
it(name, async () => {
const html = await render(el);
expect(html).not.toContain(SUSPENSE_ERROR_MARKER);
expect(html).toContain('<body');
expect(html.length).toBeGreaterThan(2000);
});
}
});
3 changes: 2 additions & 1 deletion packages/email/emails/training-completed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export const TrainingCompletedEmail = ({
return (
<Html>
<Tailwind>
<Preview>Congratulations! You've completed your Security Awareness Training</Preview>
<head />
<Preview>Congratulations! You've completed your Security Awareness Training</Preview>

<Body className="mx-auto my-auto bg-[#fff] font-sans">
<Container
Expand Down
3 changes: 2 additions & 1 deletion packages/email/emails/unassigned-items-notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ export const UnassignedItemsNotificationEmail = ({
return (
<Html>
<Tailwind>
<Preview>Member removed - items require reassignment</Preview>
<head />
<Preview>Member removed - items require reassignment</Preview>

<Body className="mx-auto my-auto bg-[#fff] font-sans">
<Container
Expand Down
4 changes: 3 additions & 1 deletion packages/email/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"react": "^19.1.1",
"react-dom": "^19.1.0",
"tsup": "^8.5.0",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"vitest": "^3.2.4"
},
"exports": {
".": {
Expand Down Expand Up @@ -57,6 +58,7 @@
"dev": "tsup index.ts --format cjs,esm --watch --dts",
"format": "prettier --write .",
"lint": "prettier --check .",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"types": "./dist/index.d.ts"
Expand Down
10 changes: 10 additions & 0 deletions packages/email/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
environment: 'node',
globals: true,
include: ['emails/**/*.test.{ts,tsx}', 'lib/**/*.test.{ts,tsx}'],
exclude: ['node_modules', 'dist'],
},
});
Loading