Packages
volt
0.15.0
0.23.0
0.22.1
0.22.0
0.21.0
0.20.0
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.11
0.17.10
0.17.9
0.17.8
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.12
0.14.11
0.14.10
0.14.9
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
0.13.0
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.9
0.10.8
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.1
0.7.0
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
Elixir-native frontend build tool — dev server, HMR, and production builds powered by OXC and Vize.
Current section
Files
Jump to
Current section
Files
guides/features/testing.md
# JavaScript and TypeScript Testing
Volt can register JavaScript and TypeScript test files as ordinary ExUnit tests. You still run one command:
```bash
mix test
```
The JS/TS runner is Volt-native: test files are bundled with OXC, executed in QuickBEAM, and reported through ExUnit. There is no separate `mix volt.test` command and no Vitest/Node process to manage.
## Setup
Add the Volt test installer to `test/test_helper.exs` after `ExUnit.start/1`:
```elixir
ExUnit.start(exclude: [:integration])
Volt.Test.ExUnit.install()
```
By default, Volt looks under `assets/` for:
```text
**/*.{test,spec}.{js,ts,jsx,tsx}
```
Configure discovery with `config :volt, :test`:
```elixir
config :volt, :test,
root: "assets",
include: ["**/*.{test,spec}.{js,ts,jsx,tsx}"],
exclude: ["vendor/**", "node_modules/**"],
timeout: 30_000
```
You can also pass options directly from `test/test_helper.exs`, which is useful for package dogfood tests or custom fixture roots:
```elixir
Volt.Test.ExUnit.install(
root: "test/volt/test/fixtures",
include: ["core_api.test.ts"]
)
```
Profile-specific configuration follows the rest of Volt:
```elixir
config :volt, :my_app_web,
test: [root: "apps/my_app_web/assets", include: ["**/*.test.ts"]]
```
Then install that profile:
```elixir
Volt.Test.ExUnit.install(profile: :my_app_web)
```
## Writing tests
Import the Vitest-like API from `volt:test`:
```ts
import { describe, test, expect, beforeEach, afterEach } from 'volt:test'
import { add } from './math'
describe('add', () => {
beforeEach(() => {
// setup
})
test('adds numbers', () => {
expect(add(1, 2)).toBe(3)
})
})
```
Async tests can return promises or use `async` functions:
```ts
test('loads data', async () => {
const value = await Promise.resolve(42)
expect(value).toBe(42)
})
```
Relative imports are bundled before execution:
```ts
import { frameworkName } from './support'
test('uses helper module', () => {
expect(frameworkName).toContain('volt')
})
```
## Test and suite modifiers
Skip or mark tests as TODO:
```ts
test.skip('not ready yet', () => {
throw new Error('will not run')
})
test.todo('add coverage for browser behavior')
```
Suites support the same modifiers:
```ts
describe.skip('external service', () => {
test('calls service', () => {
// skipped
})
})
describe.todo('future behavior', () => {
test('documents planned coverage')
})
```
You can also skip dynamically from the test context:
```ts
test('platform-specific behavior', ({ skip }) => {
skip(!navigator.userAgent.includes('Firefox'), 'Firefox only')
expect(true).toBe(true)
})
```
## Parameterized tests
Use `test.each` for table-driven tests:
```ts
test.each([
[1, 2, 3],
[2, 3, 5]
])('adds %d + %d = %d', (left, right, total) => {
expect(Number(left) + Number(right)).toBe(total)
})
```
Use `describe.each` to repeat a suite for multiple values:
```ts
describe.each(['en', 'fr'])('locale %s', (locale) => {
test('has a locale code', () => {
expect(String(locale)).toHaveLength(2)
})
})
```
Supported placeholders in each-test names are `%s`, `%d`, `%i`, `%f`, `%j`, and `%o`.
## Matchers
Volt includes a small core matcher set:
```ts
expect(value).toBe(expected)
expect(value).toEqual(expected)
expect(value).toContain(expected)
expect(value).toMatch(/pattern/)
expect(value).toHaveLength(3)
expect(value).toHaveProperty('nested.value', 42)
expect(value).toBeDefined()
expect(value).toBeUndefined()
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()
expect(value).toBeNaN()
expect(value).toBeCloseTo(0.3, 5)
expect(value).toBeGreaterThan(1)
expect(value).toBeGreaterThanOrEqual(1)
expect(value).toBeLessThan(10)
expect(value).toBeLessThanOrEqual(10)
expect(fn).toThrow('message')
```
All matchers support `.not`:
```ts
expect('volt').not.toContain('vite')
expect(1 + 1).not.toBe(3)
```
## Browser tests
Use `browser: true` when tests need real browser globals such as `window`, `document`, layout APIs, or DOM events:
```elixir
Volt.Test.ExUnit.install(
root: "test/browser",
include: ["**/*.browser.test.ts"],
browser: true
)
```
Browser tests still use the same `volt:test` API and still register one ExUnit test per JS `test(...)`. They are best suited for browser-owned runtime behavior such as DOM helpers, client preload logic, and other code that needs real browser globals. Keep Elixir-owned behavior such as manifests, Plug responses, cache state, and build output in ordinary ExUnit tests.
```ts
import { test, expect } from 'volt:test'
test('updates the DOM', () => {
document.body.innerHTML = '<button id="save">Save</button>'
expect(document.querySelector('#save')?.textContent).toBe('Save')
})
```
Volt runs these tests through PlaywrightEx. Install Playwright for local browser execution:
```bash
npm install --save-dev playwright
npx playwright install chromium
```
By default Volt uses `node_modules/.bin/playwright` when present, otherwise `playwright` from `PATH`. Pass PlaywrightEx supervisor options with `playwright: [...]` if your executable lives elsewhere.
## ExUnit integration
Each collected JS/TS `test(...)` becomes an ExUnit test. That means normal ExUnit filtering, formatters, failures, and CI behavior continue to work.
Volt adds useful tags:
- `:js` — JS/TS test generated by Volt
- `:volt_file` — source test file path
- `:volt_test_id` — collected test id inside the file
- `:volt_tags` — tags passed from JS test options
For example:
```ts
test('slow calculation', { tags: ['slow'] }, () => {
expect(1 + 1).toBe(2)
})
```
Skipped and TODO tests are registered as skipped ExUnit tests, so they are visible in normal ExUnit output.
## ExUnit fixture sigils
When an Elixir test needs readable JS/TS input, import `Volt.Test.Sigils`:
```elixir
import Volt.Test.Sigils
source = ~TS"""
export const answer: number = 42
"""
```
Use the `v` modifier to parse-check a snippet with OXC:
```elixir
source = ~TS"export const value: string = 'ok'"v
```
Available sigils: `~JS`, `~TS`, `~JSX`, `~TSX`, `~CSS`, and `~HTML`.
## Current scope
The QuickBEAM runner is intended for fast JS/TS unit tests. The browser runner is intended for tests that genuinely need a browser environment. Higher-level Phoenix end-to-end flows can still use PlaywrightEx or Phoenix-oriented browser-test helpers alongside Volt's JS test runner.