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",