diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a7a88586..0097fe35e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: ${{ runner.os }}-playwright-bin-v1- - name: Install Playwright - run: pnpm playwright-core install chromium + run: pnpm playwright install chromium - name: Prepare build environment run: pnpm dev:prepare diff --git a/build.config.ts b/build.config.ts index 73e15ac4f..edf28ea53 100644 --- a/build.config.ts +++ b/build.config.ts @@ -6,6 +6,7 @@ export default defineBuildConfig({ declaration: true, entries: [ 'src/e2e', + 'src/playwright', 'src/experimental', 'src/config', 'src/module.ts', diff --git a/examples/app-playwright/.gitignore b/examples/app-playwright/.gitignore new file mode 100644 index 000000000..aaa9103e4 --- /dev/null +++ b/examples/app-playwright/.gitignore @@ -0,0 +1,2 @@ +test-results/ +playwright-report/ diff --git a/examples/app-playwright/app.vue b/examples/app-playwright/app.vue new file mode 100644 index 000000000..8edd0c5cd --- /dev/null +++ b/examples/app-playwright/app.vue @@ -0,0 +1,5 @@ + diff --git a/examples/app-playwright/nuxt.config.ts b/examples/app-playwright/nuxt.config.ts new file mode 100644 index 000000000..8851e7746 --- /dev/null +++ b/examples/app-playwright/nuxt.config.ts @@ -0,0 +1,4 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + devtools: { enabled: true } +}) diff --git a/examples/app-playwright/package.json b/examples/app-playwright/package.json new file mode 100644 index 000000000..23b28a76a --- /dev/null +++ b/examples/app-playwright/package.json @@ -0,0 +1,22 @@ +{ + "name": "nuxt-app-playwright", + "private": true, + "type": "module", + "scripts": { + "build": "nuxt build", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "postinstall": "nuxt prepare", + "test": "playwright test" + }, + "dependencies": { + "nuxt": "^3.9.1", + "vue": "^3.4.7", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@nuxt/test-utils": "latest", + "@playwright/test": "^1.42.1" + } +} diff --git a/examples/app-playwright/playwright.config.ts b/examples/app-playwright/playwright.config.ts new file mode 100644 index 000000000..ad0b569db --- /dev/null +++ b/examples/app-playwright/playwright.config.ts @@ -0,0 +1,41 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig, devices } from '@playwright/test' +import type { ConfigOptions } from '@nuxt/test-utils/playwright' + +const devicesToTest = [ + 'Desktop Chrome', + // Test against other common browser engines. + // 'Desktop Firefox', + // 'Desktop Safari', + // Test against mobile viewports. + // 'Pixel 5', + // 'iPhone 12', + // Test against branded browsers. + // { ...devices['Desktop Edge'], channel: 'msedge' }, + // { ...devices['Desktop Chrome'], channel: 'chrome' }, +] satisfies Array + +/* See https://playwright.dev/docs/test-configuration.*/ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + /* Nuxt configuration options */ + nuxt: { + rootDir: fileURLToPath(new URL('.', import.meta.url)) + } + }, + projects: devicesToTest.map(p => typeof p === 'string' ? ({ name: p, use: devices[p] }) : p), +}) diff --git a/examples/app-playwright/tests/basic.test.ts b/examples/app-playwright/tests/basic.test.ts new file mode 100644 index 000000000..b935edd6a --- /dev/null +++ b/examples/app-playwright/tests/basic.test.ts @@ -0,0 +1,6 @@ +import { expect, test } from '@nuxt/test-utils/playwright' + +test('test', async ({ page, goto }) => { + await goto('/', { waitUntil: 'hydration' }) + await expect(page.getByRole('heading')).toHaveText('Welcome to Playwright!') +}) diff --git a/examples/app-playwright/tsconfig.json b/examples/app-playwright/tsconfig.json new file mode 100644 index 000000000..4b34df157 --- /dev/null +++ b/examples/app-playwright/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nuxt/tsconfig.json" +} diff --git a/package.json b/package.json index d3e014446..1c43997f8 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ ".": "./dist/e2e.mjs", "./config": "./dist/config.mjs", "./e2e": "./dist/e2e.mjs", + "./playwright": "./dist/playwright.mjs", "./experimental": "./dist/experimental.mjs", "./module": "./dist/module.mjs", "./runtime": "./dist/runtime-utils/index.mjs", @@ -68,6 +69,7 @@ "@nuxt/devtools": "1.0.8", "@nuxt/eslint-config": "0.2.0", "@nuxt/module-builder": "0.5.5", + "@playwright/test": "1.42.1", "@testing-library/vue": "8.0.2", "@types/estree": "1.0.5", "@types/jsdom": "21.1.6", @@ -96,6 +98,7 @@ "peerDependencies": { "@cucumber/cucumber": "^10.3.1", "@jest/globals": "^29.5.0", + "@playwright/test": "^1.42.1", "@testing-library/vue": "^7.0.0 || ^8.0.1", "@vitest/ui": "^0.34.6 || ^1.0.0", "@vue/test-utils": "^2.4.2", @@ -112,6 +115,9 @@ "@cucumber/cucumber": { "optional": true }, + "@playwright/test": { + "optional": true + }, "@testing-library/vue": { "optional": true }, @@ -149,4 +155,4 @@ "node": "^14.18.0 || >=16.10.0" }, "packageManager": "pnpm@8.15.4" -} \ No newline at end of file +} diff --git a/playwright.d.ts b/playwright.d.ts new file mode 100644 index 000000000..1b71747d5 --- /dev/null +++ b/playwright.d.ts @@ -0,0 +1 @@ +export * from './dist/playwright' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e2c25a20..393b2a8b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: '@nuxt/module-builder': specifier: 0.5.5 version: 0.5.5(@nuxt/kit@3.10.3)(nuxi@3.10.1)(typescript@5.3.3) + '@playwright/test': + specifier: 1.42.1 + version: 1.42.1 '@testing-library/vue': specifier: 8.0.2 version: 8.0.2(vue@3.4.21) @@ -239,6 +242,25 @@ importers: specifier: ^5.3.3 version: 5.3.3 + examples/app-playwright: + dependencies: + nuxt: + specifier: ^3.9.1 + version: 3.10.3(eslint@8.57.0)(rollup@4.13.0)(typescript@5.3.3)(vite@5.1.6)(vue-tsc@2.0.6) + vue: + specifier: ^3.4.21 + version: 3.4.21(typescript@5.3.3) + vue-router: + specifier: ^4.2.5 + version: 4.3.0(vue@3.4.21) + devDependencies: + '@nuxt/test-utils': + specifier: workspace:* + version: link:../.. + '@playwright/test': + specifier: ^1.42.1 + version: 1.42.1 + examples/app-vitest: dependencies: nuxt: @@ -262,7 +284,7 @@ importers: version: 12.10.3 playwright-core: specifier: ^1.40.1 - version: 1.41.0 + version: 1.42.1 typescript: specifier: ^5.3.3 version: 5.3.3 @@ -998,7 +1020,6 @@ packages: cpu: [ppc64] os: [aix] requiresBuild: true - dev: true optional: true /@esbuild/android-arm64@0.18.20: @@ -1024,7 +1045,6 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: true optional: true /@esbuild/android-arm@0.18.20: @@ -1050,7 +1070,6 @@ packages: cpu: [arm] os: [android] requiresBuild: true - dev: true optional: true /@esbuild/android-x64@0.18.20: @@ -1076,7 +1095,6 @@ packages: cpu: [x64] os: [android] requiresBuild: true - dev: true optional: true /@esbuild/darwin-arm64@0.18.20: @@ -1102,7 +1120,6 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: true optional: true /@esbuild/darwin-x64@0.18.20: @@ -1128,7 +1145,6 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: true optional: true /@esbuild/freebsd-arm64@0.18.20: @@ -1154,7 +1170,6 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true - dev: true optional: true /@esbuild/freebsd-x64@0.18.20: @@ -1180,7 +1195,6 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true - dev: true optional: true /@esbuild/linux-arm64@0.18.20: @@ -1206,7 +1220,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-arm@0.18.20: @@ -1232,7 +1245,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-ia32@0.18.20: @@ -1258,7 +1270,6 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-loong64@0.18.20: @@ -1284,7 +1295,6 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-mips64el@0.18.20: @@ -1310,7 +1320,6 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-ppc64@0.18.20: @@ -1336,7 +1345,6 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-riscv64@0.18.20: @@ -1362,7 +1370,6 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-s390x@0.18.20: @@ -1388,7 +1395,6 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-x64@0.18.20: @@ -1414,7 +1420,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/netbsd-x64@0.18.20: @@ -1440,7 +1445,6 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true - dev: true optional: true /@esbuild/openbsd-x64@0.18.20: @@ -1466,7 +1470,6 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true - dev: true optional: true /@esbuild/sunos-x64@0.18.20: @@ -1492,7 +1495,6 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true - dev: true optional: true /@esbuild/win32-arm64@0.18.20: @@ -1518,7 +1520,6 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: true optional: true /@esbuild/win32-ia32@0.18.20: @@ -1544,7 +1545,6 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true - dev: true optional: true /@esbuild/win32-x64@0.18.20: @@ -1570,7 +1570,6 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: true optional: true /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): @@ -2271,7 +2270,6 @@ packages: transitivePeerDependencies: - rollup - supports-color - dev: true /@nuxt/devtools-kit@1.0.8(nuxt@3.9.1)(rollup@4.13.0)(vite@5.1.6): resolution: {integrity: sha512-j7bNZmoAXQ1a8qv6j6zk4c/aekrxYqYVQM21o/Hy4XHCUq4fajSgpoc8mjyWJSTfpkOmuLyEzMexpDWiIVSr6A==} @@ -2453,7 +2451,6 @@ packages: - rollup - supports-color - utf-8-validate - dev: true /@nuxt/devtools@1.0.8(nuxt@3.9.1)(rollup@4.13.0)(vite@5.1.6): resolution: {integrity: sha512-o6aBFEBxc8OgVHV4OPe2g0q9tFIe9HiTxRiJnlTJ+jHvOQsBLS651ArdVtwLChf9UdMouFlpLLJ1HteZqTbtsQ==} @@ -2688,7 +2685,7 @@ packages: h3: 1.11.1 knitwork: 1.0.0 magic-string: 0.30.8 - mlly: 1.6.0 + mlly: 1.6.1 ohash: 1.1.3 pathe: 1.1.2 perfect-debounce: 1.0.0 @@ -2701,7 +2698,7 @@ packages: unenv: 1.9.0 unplugin: 1.10.0 vite: 5.1.6(@types/node@20.10.5) - vite-node: 1.3.1(@types/node@20.10.5) + vite-node: 1.4.0 vite-plugin-checker: 0.6.4(eslint@8.57.0)(typescript@5.3.3)(vite@5.1.6)(vue-tsc@2.0.6) vue: 3.4.21(typescript@5.3.3) vue-bundle-renderer: 2.0.0 @@ -2724,7 +2721,6 @@ packages: - vls - vti - vue-tsc - dev: true /@nuxt/vite-builder@3.9.1(eslint@8.57.0)(rollup@4.13.0)(typescript@5.3.3)(vue-tsc@1.8.26)(vue@3.4.21): resolution: {integrity: sha512-V0GxTYuajNlf+kX3ak7klaFnkQ43MkXY2mAYqdAuUytvjx/S0O4hESy6gU1r/3no7IZAdoaEN27KLB4OOJ7vag==} @@ -3058,6 +3054,14 @@ packages: requiresBuild: true optional: true + /@playwright/test@1.42.1: + resolution: {integrity: sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright: 1.42.1 + dev: true + /@polka/url@1.0.0-next.24: resolution: {integrity: sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==} @@ -5846,7 +5850,6 @@ packages: '@esbuild/win32-arm64': 0.20.1 '@esbuild/win32-ia32': 0.20.1 '@esbuild/win32-x64': 0.20.1 - dev: true /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -6355,6 +6358,14 @@ packages: /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -9085,7 +9096,6 @@ packages: hasBin: true optionalDependencies: fsevents: 2.3.3 - dev: true /nuxt@3.10.3(eslint@8.57.0)(rollup@4.13.0)(typescript@5.3.3)(vite@5.1.6)(vue-tsc@2.0.6): resolution: {integrity: sha512-NchGNiiz9g/ErJAb462W/lpX2NqcXYb9hugySKWvLXNdrjeAPiJ2/7mhgwUSiZA9MpjuQg3saiEajr1zlRIOCg==} @@ -9110,7 +9120,7 @@ packages: '@unhead/dom': 1.8.10 '@unhead/ssr': 1.8.10 '@unhead/vue': 1.8.10(vue@3.4.21) - '@vue/shared': 3.4.19 + '@vue/shared': 3.4.21 acorn: 8.11.3 c12: 1.10.0 chokidar: 3.6.0 @@ -9129,7 +9139,7 @@ packages: klona: 2.0.6 knitwork: 1.0.0 magic-string: 0.30.8 - mlly: 1.6.0 + mlly: 1.6.1 nitropack: 2.8.1(idb-keyval@6.2.1) nuxi: 3.10.1 nypm: 0.3.6 @@ -9191,7 +9201,6 @@ packages: - vti - vue-tsc - xml2js - dev: true /nuxt@3.9.1(eslint@8.57.0)(idb-keyval@6.2.1)(rollup@4.13.0)(typescript@5.3.3)(vite@5.1.6)(vue-tsc@1.8.26): resolution: {integrity: sha512-jyD9E74bx8cdDc3nmYMNsJR0dIOrpcDH/mBlo1FmpB2zeXXMwupOD2tm033MJpHIEBbhQGHYFQlRyd7wHg2DyA==} @@ -9255,12 +9264,12 @@ packages: unenv: 1.9.0 unimport: 3.7.1(rollup@4.13.0) unplugin: 1.6.0 - unplugin-vue-router: 0.7.0(rollup@4.13.0)(vue-router@4.2.5)(vue@3.4.21) + unplugin-vue-router: 0.7.0(rollup@4.13.0)(vue-router@4.3.0)(vue@3.4.21) untyped: 1.4.0 vue: 3.4.21(typescript@5.3.3) vue-bundle-renderer: 2.0.0 vue-devtools-stub: 0.1.0 - vue-router: 4.2.5(vue@3.4.21) + vue-router: 4.3.0(vue@3.4.21) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -9468,12 +9477,12 @@ packages: unenv: 1.9.0 unimport: 3.7.1(rollup@4.13.0) unplugin: 1.6.0 - unplugin-vue-router: 0.7.0(rollup@4.13.0)(vue-router@4.2.5)(vue@3.4.21) + unplugin-vue-router: 0.7.0(rollup@4.13.0)(vue-router@4.3.0)(vue@3.4.21) untyped: 1.4.0 vue: 3.4.21(typescript@5.3.3) vue-bundle-renderer: 2.0.0 vue-devtools-stub: 0.1.0 - vue-router: 4.2.5(vue@3.4.21) + vue-router: 4.3.0(vue@3.4.21) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -9877,16 +9886,20 @@ packages: hasBin: true dev: true - /playwright-core@1.41.0: - resolution: {integrity: sha512-UGKASUhXmvqm2Lxa1fNr8sFwAtqjpgBRr9jQ7XBI8Rn5uFiEowGUGwrruUQsVPIom4bk7Lt+oLGpXobnXzrBIw==} + /playwright-core@1.42.1: + resolution: {integrity: sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==} engines: {node: '>=16'} hasBin: true dev: true - /playwright-core@1.42.1: - resolution: {integrity: sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==} + /playwright@1.42.1: + resolution: {integrity: sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==} engines: {node: '>=16'} hasBin: true + dependencies: + playwright-core: 1.42.1 + optionalDependencies: + fsevents: 2.3.2 dev: true /pluralize@8.0.0: @@ -11598,7 +11611,6 @@ packages: /ultrahtml@1.5.3: resolution: {integrity: sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==} - dev: true /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -11809,6 +11821,7 @@ packages: transitivePeerDependencies: - rollup - vue + dev: false /unplugin-vue-router@0.7.0(rollup@4.13.0)(vue-router@4.3.0)(vue@3.4.21): resolution: {integrity: sha512-ddRreGq0t5vlSB7OMy4e4cfU1w2AwBQCwmvW3oP/0IHQiokzbx4hd3TpwBu3eIAFVuhX2cwNQwp1U32UybTVCw==} @@ -11835,7 +11848,6 @@ packages: transitivePeerDependencies: - rollup - vue - dev: true /unplugin@1.10.0: resolution: {integrity: sha512-CuZtvvO8ua2Wl+9q2jEaqH6m3DoQ38N7pvBYQbbaeNlWGvK2l6GHiKi29aIHDPoSxdUzQ7Unevf1/ugil5X6Pg==} @@ -12108,7 +12120,6 @@ packages: - sugarss - supports-color - terser - dev: true /vite-plugin-checker@0.6.4(eslint@8.57.0)(typescript@5.3.3)(vite@5.1.6)(vue-tsc@1.8.26): resolution: {integrity: sha512-2zKHH5oxr+ye43nReRbC2fny1nyARwhxdm0uNYp/ERy4YvU9iZpNOsueoi/luXw5gnpqRSvjcEPxXbS153O2wA==} @@ -12547,7 +12558,6 @@ packages: dependencies: '@vue/devtools-api': 6.5.1 vue: 3.4.21(typescript@5.3.3) - dev: true /vue-template-compiler@2.7.15: resolution: {integrity: sha512-yQxjxMptBL7UAog00O8sANud99C6wJF+7kgbcwqkvA38vCGF7HWE66w0ZFnS/kX5gSoJr/PQ4/oS3Ne2pW37Og==} diff --git a/src/core/browser.ts b/src/core/browser.ts index 45030be0c..e7957a027 100644 --- a/src/core/browser.ts +++ b/src/core/browser.ts @@ -1,4 +1,4 @@ -import type { Browser, BrowserContextOptions, Page } from 'playwright-core' +import type { Browser, BrowserContextOptions, Page, Response } from 'playwright-core' import { useTestContext } from './context' import { url } from './server' @@ -33,12 +33,12 @@ export async function getBrowser (): Promise { } type _GotoOptions = NonNullable[1]> -interface GotoOptions extends Omit<_GotoOptions, 'waitUntil'> { +export interface GotoOptions extends Omit<_GotoOptions, 'waitUntil'> { waitUntil?: 'hydration' | 'route' | _GotoOptions['waitUntil'] } interface NuxtPage extends Omit { - goto: (url: string, options?: GotoOptions) => Promise + goto: (url: string, options?: GotoOptions) => Promise } export async function createPage (path?: string, options?: BrowserContextOptions): Promise { @@ -46,17 +46,13 @@ export async function createPage (path?: string, options?: BrowserContextOptions const page = await browser.newPage(options) as unknown as NuxtPage const _goto = page.goto.bind(page) - page.goto = async (url, options) => { + page.goto = async (url, options): Promise => { const waitUntil = options?.waitUntil if (waitUntil && ['hydration', 'route'].includes(waitUntil)) { delete options.waitUntil } - const res = await _goto(url, options) - if (waitUntil === 'hydration') { - await page.waitForFunction(() => window.useNuxtApp?.().isHydrating === false) - } else if (waitUntil === 'route') { - await page.waitForFunction((route) => window.useNuxtApp?.()._route.fullPath === route, path) - } + const res = await _goto(url, options as Parameters[1]) + await waitForHydration(page, url, waitUntil) return res } @@ -66,3 +62,11 @@ export async function createPage (path?: string, options?: BrowserContextOptions return page } + +export async function waitForHydration (page: Page, url: string, waitUntil?: GotoOptions['waitUntil']): Promise { + if (waitUntil === 'hydration') { + await page.waitForFunction(() => window.useNuxtApp?.().isHydrating === false) + } else if (waitUntil === 'route') { + await page.waitForFunction((route) => window.useNuxtApp?.()._route.fullPath === route, url) + } +} diff --git a/src/core/context.ts b/src/core/context.ts index 3e270e4db..67758492a 100644 --- a/src/core/context.ts +++ b/src/core/context.ts @@ -15,12 +15,17 @@ export function createTestContext (options: Partial): TestContext { server: true, build: (options.browser !== false) || (options.server !== false), nuxtConfig: {}, - runner: process.env.VITEST === 'true' ? 'vitest' : 'jest', browserOptions: { type: 'chromium' as const } } satisfies Partial) + if (process.env.VITEST === 'true') { + _options.runner ||= 'vitest' + } else if (process.env.JEST_WORKER_ID) { + _options.runner ||= 'jest' + } + return setTestContext({ options: _options as TestOptions }) diff --git a/src/playwright.ts b/src/playwright.ts new file mode 100644 index 000000000..b6866db92 --- /dev/null +++ b/src/playwright.ts @@ -0,0 +1,85 @@ +import { test as base } from '@playwright/test' +import type { Page, Response } from 'playwright-core' +import type { GotoOptions, TestOptions as SetupOptions, TestHooks } from './e2e' +import { createTest, url, waitForHydration } from './e2e' + +export type ConfigOptions = { + nuxt: Partial | undefined +} + +{ + // Can be removed after Playwright v1.43 is released. + // Waiting for https://github.com/microsoft/playwright/pull/29865 + if (process.env.TEST_WORKER_INDEX) { + for (const stream of [process.stdout, process.stderr]) { + // Stubs for the rest of the methods to avoid exceptions in user code. + if (!(stream as any).clearLine) { + stream.clearLine = (dir: any, callback?: () => void) => { + callback?.() + return true + } + } + if (!(stream as any).cursorTo) { + (stream as any).cursorTo = (x: number, y?: number | (() => void), callback?: () => void) => { + if (callback) + callback() + else if (y instanceof Function) + y() + return true + } + } + } + } +} + +type WorkerOptions = { + _nuxtHooks: TestHooks +} + +type TestOptions = { + goto: (url: string, options?: GotoOptions) => Promise +} + +/** + * Use a preconfigured Nuxt fixture. + * + * You can pass a `nuxt: {}` object in your device configuration, in the `use` key of your config file, + * or use the following syntax within your test file to configure your Nuxt fixture: + * + ```ts + test.use({ + nuxt: { + rootDir: fileURLToPath(new URL('.', import.meta.url)), + } + }) + ``` + */ +export const test = base.extend({ + nuxt: [undefined, { option: true, scope: 'worker' }], + _nuxtHooks: [ + async ({ nuxt }, use) => { + const hooks = createTest(nuxt || {}) + await hooks.setup() + await use(hooks) + await hooks.afterAll() + }, { scope: 'worker' } + ], + baseURL: async ({ _nuxtHooks }, use) => { + _nuxtHooks.beforeEach() + await use(url('/')) + _nuxtHooks.afterEach() + }, + goto: async ({ page }, use) => { + await use(async (url, options) => { + const waitUntil = options?.waitUntil + if (waitUntil && ['hydration', 'route'].includes(waitUntil)) { + delete options.waitUntil + } + const response = await page.goto(url, options as Parameters[1]) + await waitForHydration(page, url, waitUntil) + return response + }) + }, +}) + +export { expect } from '@playwright/test' diff --git a/tsconfig.json b/tsconfig.json index 7683b60a9..ca6f416b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "exclude": [ "config.d.ts", "e2e.d.ts", + "playwright.d.ts", "experimental.d.ts", "module.d.ts", "runtime.d.ts",