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.
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.
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 SFC directly, enable Vue's Vite transform in the test project:
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:
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.
import { renderEmailComponent } from '#nuxt-email/testing'Install vitest and @vitejs/plugin-vue, then run nuxt prepare before the test command. The alias above reads Nuxt's generated path as its single source of truth; the Vue plugin compiles the SFC without booting a browser-oriented Nuxt app. This is intentional: Nuxt's unit-test environment runs the app with SSR disabled, while email components are server-only. The generated binding and production renderEmail use the same complete component registry. It is a Node-test-only alias even though Nuxt preparation makes it discoverable to the project type system; never import it from app or shared client code. The standalone package helper rejects unresolved E* components rather than returning fake custom-element markup in both development and production.
Prefer focused assertions on recipient-visible text, links, required attributes, and client-specific markup. Whole-document snapshots tend to approve accidental output. The React/Vue comparison normalizer remains internal because its rules are specific to the conformance oracle, not a general HTML contract.
Asserting on the subject
If a template declares a subject with defineEmail, renderEmailComponent computes it from the props you pass:
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.