diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 09b5e373b..de23b7c5e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -59,6 +59,11 @@ jobs:
with:
node-version: lts/*
cache: "pnpm"
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
+ with:
+ bun-version: latest
- name: Install dependencies
run: pnpm install
diff --git a/build.config.ts b/build.config.ts
index e14761090..107722ecf 100644
--- a/build.config.ts
+++ b/build.config.ts
@@ -16,6 +16,7 @@ export default defineBuildConfig({
],
externals: [
'#dirs',
+ 'bun:test',
'#app/entry',
'#build/root-component.mjs',
'#imports',
diff --git a/examples/app-bun/app.vue b/examples/app-bun/app.vue
new file mode 100644
index 000000000..a495b7573
--- /dev/null
+++ b/examples/app-bun/app.vue
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/examples/app-bun/nuxt.config.ts b/examples/app-bun/nuxt.config.ts
new file mode 100644
index 000000000..fad722b66
--- /dev/null
+++ b/examples/app-bun/nuxt.config.ts
@@ -0,0 +1,7 @@
+import { defineNuxtConfig } from 'nuxt/config'
+
+// https://nuxt.com/docs/api/configuration/nuxt-config
+export default defineNuxtConfig({
+ devtools: { enabled: true },
+ compatibilityDate: '2024-04-03',
+})
diff --git a/examples/app-bun/package.json b/examples/app-bun/package.json
new file mode 100644
index 000000000..a1a5696e6
--- /dev/null
+++ b/examples/app-bun/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "example-app-jest",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "nuxt build",
+ "dev": "nuxt dev",
+ "generate": "nuxt generate",
+ "preview": "nuxt preview",
+ "postinstall": "nuxt prepare",
+ "test": "bun test"
+ },
+ "dependencies": {
+ "nuxt": "^3.17.0"
+ },
+ "devDependencies": {
+ "@nuxt/test-utils": "latest",
+ "@types/bun": "1.2.10",
+ "playwright-core": "1.52.0",
+ "typescript": "5.8.3"
+ }
+}
diff --git a/examples/app-bun/test/browser.e2e.spec.ts b/examples/app-bun/test/browser.e2e.spec.ts
new file mode 100644
index 000000000..f2569a201
--- /dev/null
+++ b/examples/app-bun/test/browser.e2e.spec.ts
@@ -0,0 +1,18 @@
+import { fileURLToPath } from 'node:url'
+import { describe, it, expect } from 'bun:test'
+import { createPage, setup } from '@nuxt/test-utils/e2e'
+import { isWindows } from 'std-env'
+
+await setup({
+ rootDir: fileURLToPath(new URL('../', import.meta.url)),
+ browser: true,
+})
+
+describe('browser', () => {
+ it('runs a test', async () => {
+ const page = await createPage('/')
+ const text = await page.getByRole('heading', { name: 'Welcome to Nuxt!' }).textContent()
+ expect(text).toContain('Welcome to Nuxt!')
+ await page.close()
+ }, isWindows ? 120000 : 20000)
+})
diff --git a/examples/app-bun/test/dev.e2e.spec.ts b/examples/app-bun/test/dev.e2e.spec.ts
new file mode 100644
index 000000000..056396ca8
--- /dev/null
+++ b/examples/app-bun/test/dev.e2e.spec.ts
@@ -0,0 +1,15 @@
+import { fileURLToPath } from 'node:url'
+import { describe, it, expect } from 'bun:test'
+import { $fetch, setup } from '@nuxt/test-utils/e2e'
+
+await setup({
+ rootDir: fileURLToPath(new URL('../', import.meta.url)),
+ dev: true,
+})
+
+describe('server (dev)', () => {
+ it('runs a test', async () => {
+ const html = await $fetch('/')
+ expect(html.slice(0, 15)).toMatchInlineSnapshot(`""`)
+ })
+})
diff --git a/examples/app-bun/test/server.e2e.spec.ts b/examples/app-bun/test/server.e2e.spec.ts
new file mode 100644
index 000000000..ef088add9
--- /dev/null
+++ b/examples/app-bun/test/server.e2e.spec.ts
@@ -0,0 +1,14 @@
+import { fileURLToPath } from 'node:url'
+import { describe, it, expect } from 'bun:test'
+import { $fetch, setup } from '@nuxt/test-utils/e2e'
+
+await setup({
+ rootDir: fileURLToPath(new URL('../', import.meta.url)),
+})
+
+describe('app', () => {
+ it('runs a test', async () => {
+ const html = await $fetch('/')
+ expect(html.slice(0, 15)).toMatchInlineSnapshot(`""`)
+ })
+})
diff --git a/examples/app-bun/tsconfig.json b/examples/app-bun/tsconfig.json
new file mode 100644
index 000000000..2f88cdf67
--- /dev/null
+++ b/examples/app-bun/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./.nuxt/tsconfig.json",
+ "compilerOptions": {
+ "module": "ESNext",
+ "esModuleInterop": true,
+ "moduleResolution": "Bundler",
+ "verbatimModuleSyntax": false,
+ "target": "ESNext",
+ "types": [
+ "bun"
+ ],
+ "resolveJsonModule": true
+ }
+}
diff --git a/package.json b/package.json
index ab5d29042..d045bd461 100644
--- a/package.json
+++ b/package.json
@@ -97,6 +97,7 @@
"@nuxt/eslint-config": "1.3.0",
"@playwright/test": "1.52.0",
"@testing-library/vue": "8.1.0",
+ "@types/bun": "1.2.10",
"@types/estree": "1.0.7",
"@types/jsdom": "21.1.7",
"@types/node": "22.15.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a4f9ccc03..3864877ce 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -123,6 +123,9 @@ importers:
'@testing-library/vue':
specifier: 8.1.0
version: 8.1.0(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.3))
+ '@types/bun':
+ specifier: 1.2.10
+ version: 1.2.10
'@types/estree':
specifier: 1.0.7
version: 1.0.7
@@ -190,6 +193,25 @@ importers:
specifier: 2.2.10
version: 2.2.10(typescript@5.8.3)
+ examples/app-bun:
+ dependencies:
+ nuxt:
+ specifier: ^3.17.0
+ version: 3.17.0(@netlify/blobs@8.2.0)(@parcel/watcher@2.4.1)(@types/node@22.15.3)(better-sqlite3@11.9.1)(db0@0.3.2(better-sqlite3@11.9.1))(encoding@0.1.13)(eslint@9.25.1(jiti@2.4.2))(idb-keyval@6.2.1)(ioredis@5.6.1)(magicast@0.3.5)(rollup@4.40.1)(terser@5.24.0)(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.3)(jiti@2.4.2)(terser@5.24.0)(yaml@2.7.1))(vue-tsc@2.2.10(typescript@5.8.3))(yaml@2.7.1)
+ devDependencies:
+ '@nuxt/test-utils':
+ specifier: workspace:*
+ version: link:../..
+ '@types/bun':
+ specifier: 1.2.10
+ version: 1.2.10
+ playwright-core:
+ specifier: 1.52.0
+ version: 1.52.0
+ typescript:
+ specifier: 5.8.3
+ version: 5.8.3
+
examples/app-cucumber:
dependencies:
nuxt:
@@ -2453,6 +2475,9 @@ packages:
'@types/babel__traverse@7.20.3':
resolution: {integrity: sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==}
+ '@types/bun@1.2.10':
+ resolution: {integrity: sha512-eilv6WFM3M0c9ztJt7/g80BDusK98z/FrFwseZgT4bXCq2vPhXD4z8R3oddmAn+R/Nmz9vBn4kweJKmGTZj+lg==}
+
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -3122,6 +3147,9 @@ packages:
resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==}
engines: {node: '>=18.20'}
+ bun-types@1.2.10:
+ resolution: {integrity: sha512-b5ITZMnVdf3m1gMvJHG+gIfeJHiQPJak0f7925Hxu6ZN5VKA8AGy4GZ4lM+Xkn6jtWxg5S3ldWvfmXdvnkp3GQ==}
+
bundle-name@4.1.0:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'}
@@ -10391,6 +10419,10 @@ snapshots:
dependencies:
'@babel/types': 7.27.0
+ '@types/bun@1.2.10':
+ dependencies:
+ bun-types: 1.2.10
+
'@types/debug@4.1.12':
dependencies:
'@types/ms': 0.7.34
@@ -11183,6 +11215,10 @@ snapshots:
builtin-modules@5.0.0: {}
+ bun-types@1.2.10:
+ dependencies:
+ '@types/node': 22.15.3
+
bundle-name@4.1.0:
dependencies:
run-applescript: 7.0.0
diff --git a/src/core/context.ts b/src/core/context.ts
index 5616f314b..213be0c38 100644
--- a/src/core/context.ts
+++ b/src/core/context.ts
@@ -2,7 +2,7 @@ import { resolve } from 'node:path'
import { defu } from 'defu'
import { withTrailingSlash } from 'ufo'
import type { DateString } from 'compatx'
-import { isWindows } from 'std-env'
+import { isBun, isWindows } from 'std-env'
import type { TestContext, TestOptions } from './types'
let currentContext: TestContext | undefined
@@ -44,6 +44,9 @@ export function createTestContext(options: Partial): TestContext {
else if (process.env.JEST_WORKER_ID) {
_options.runner ||= 'jest'
}
+ else if (isBun) {
+ _options.runner ||= 'bun'
+ }
return setTestContext({
options: _options as TestOptions,
diff --git a/src/core/setup/bun.ts b/src/core/setup/bun.ts
new file mode 100644
index 000000000..c9e7e14be
--- /dev/null
+++ b/src/core/setup/bun.ts
@@ -0,0 +1,14 @@
+import type { TestHooks } from '../types'
+
+export default async function setupBun(hooks: TestHooks) {
+ // @ts-expect-error we do not want bun types present in global context
+ const bunTest = await import('bun:test')
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ hooks.ctx.mockFn = bunTest.mock as any
+
+ bunTest.beforeAll(hooks.beforeAll)
+ bunTest.beforeEach(hooks.beforeEach)
+ bunTest.afterEach(hooks.afterEach)
+ bunTest.afterAll(hooks.afterAll)
+}
diff --git a/src/core/setup/index.ts b/src/core/setup/index.ts
index ef8ee683b..b7fd93552 100644
--- a/src/core/setup/index.ts
+++ b/src/core/setup/index.ts
@@ -3,11 +3,13 @@ import { buildFixture, loadFixture } from '../nuxt'
import { startServer, stopServer } from '../server'
import { createBrowser } from '../browser'
import type { TestHooks, TestOptions } from '../types'
+import setupBun from './bun'
import setupCucumber from './cucumber'
import setupJest from './jest'
import setupVitest from './vitest'
export const setupMaps = {
+ bun: setupBun,
cucumber: setupCucumber,
jest: setupJest,
vitest: setupVitest,
diff --git a/src/core/types.ts b/src/core/types.ts
index feb8f2cac..b6dcd816e 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -3,7 +3,7 @@ import type { Browser, LaunchOptions } from 'playwright-core'
import type { exec } from 'tinyexec'
import type { StartServerOptions } from './server'
-export type TestRunner = 'vitest' | 'jest' | 'cucumber'
+export type TestRunner = 'vitest' | 'jest' | 'cucumber' | 'bun'
export interface TestOptions {
testDir: string
@@ -43,7 +43,7 @@ export interface TestOptions {
*/
browser: boolean
/**
- * Specify the runner for the test suite. One of `'vitest' | 'jest' | 'cucumber'`.
+ * Specify the runner for the test suite. One of `'vitest' | 'jest' | 'cucumber' | 'bun'`.
* @default `vitest`
*/
runner: TestRunner
diff --git a/tsconfig.json b/tsconfig.json
index ca6f416b4..ba33c278d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,7 @@
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
- "moduleResolution": "Bundler",
+ "moduleResolution": "Bundler"
},
"exclude": [
"config.d.ts",