Skip to main content

Testing your emails

Render templates and assert on their output in unit tests with the @lupinum/nuxt-email/testing utilities.

Nuxt Email exposes a standalone renderer from @lupinum/nuxt-email/testing. It renders templates using the eighteen built-in components without booting a Nuxt app.

ts
import { renderEmailComponent } from '@lupinum/nuxt-email/testing'

renderEmailComponent

Renders a Vue email component to { html, text, subject? } — the same RenderedEmail shape renderEmail returns, produced by the same renderer.

test/welcome.test.ts
import { renderEmailComponent } from '@lupinum/nuxt-email/testing'
import Welcome from '../app/emails/welcome.vue'

test('welcome renders the recipient name', async () => {
  const { html, text } = await renderEmailComponent(Welcome, {
    firstName: 'Ada',
    activationUrl: 'https://example.com/activate',
  })

  expect(html).toContain('Welcome, Ada')
  expect(text).toContain('Welcome, Ada')
})

Because this test imports a Vue single-file component (.vue file) directly, enable Vue's Vite transform in the test project:

vitest.config.ts
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vitest/config'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'node',
  },
})

Install vitest and @vitejs/plugin-vue as direct development dependencies. The helper is Node-only; do not import it from application components, shared browser code, or client plugins.

renderEmailComponent infers the second argument from the imported Vue component. Required props make it required, optional-only props make it optional, and prop-free components reject invented props. It returns Promise<RenderedEmail>. Because rendering is deterministic, the same component and props always produce the same output.

Testing configured components

When a template uses an application-configured component such as ECodeBlock, resolve the aliases generated by nuxt prepare and import the generated renderer binding:

vitest.config.ts
import vue from '@vitejs/plugin-vue'
import { readFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'

const tsconfigPath = fileURLToPath(new URL('./.nuxt/tsconfig.json', import.meta.url))
const tsconfig = JSON.parse(readFileSync(tsconfigPath, 'utf8'))
const rendererPath = tsconfig.compilerOptions.paths['#nuxt-email/testing']?.[0]
if (!rendererPath) throw new Error('Run `nuxt prepare` before configured email tests')

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '#nuxt-email/testing': resolve(dirname(tsconfigPath), rendererPath),
    },
  },
  test: {
    environment: 'node',
  },
})

Keep these configured-renderer tests under test/nuxt/, which Nuxt includes in that generated TypeScript project.

ts
import { renderEmailComponent } from '#nuxt-email/testing'

Before running these tests:

  1. Install vitest and @vitejs/plugin-vue.
  2. Run nuxt prepare to generate the alias path.
  3. Keep configured-renderer tests under test/nuxt/.
  4. Import #nuxt-email/testing only from Node tests. Do not import it from application or shared client code.

The alias reads Nuxt's generated path as its source of truth. The Vue plugin compiles the .vue file without starting a browser application. Nuxt unit tests disable server-side rendering (SSR), but email components are server-only. The generated binding and production renderEmail use the same complete component registry.

The standalone package helper rejects unresolved E* components. It does not return fake custom-element markup in development or production.

Prefer focused assertions on recipient-visible text, links, required attributes, and client-specific markup. Whole-document snapshots can approve accidental output. The React and Vue comparison normalizer remains internal because its rules belong to the conformance suite, not the public HTML contract.

Asserting on the subject

If a template declares a subject with defineEmail, renderEmailComponent computes it from the props you pass:

ts
const { subject } = await renderEmailComponent(Welcome, { firstName: 'Ada', activationUrl: '…' })
expect(subject).toBe('Welcome aboard, Ada')

What testing does not replace

Unit tests prove your template's structure and content are stable and deterministic. They do not prove how a real client renders the message. Pair them with the manual client compatibility checklist before a release.