cypress
cypress copied to clipboard
Nuxt 3 CT Support
What would you like?
Support for Nuxt 3 Cypress Component Testing
Why is this needed?
We support Nuxt 2, and with Nuxt 3 on the horizon we should add support for Nuxt 3.
Other
There is a tech-brief with initial findings of Nuxt 3 support. There is also a Nuxt 3 issue with some interesting info on Nuxt 3 selective rendering which we might incorporate.
Would make sense to convert this issue into an epic once work beings.
Hey @ZachJW34! Was wondering if we could make the tech-brief
you mentioned public? Would be nice to see your findings in the meantime
It is a private document but contains no sensitive information so I'll copy and paste the contents below
Our current implementation of Nuxt 2 relies on accessing Nuxt's webpack config via an exposed getWebpackConfig function. Not only does this function no longer exist, but the Nuxt team has added Vite as it's primary bundler. Webpack can be used, but it is opt in via specifying vite: false in the nuxt.config.ts and installing @nuxt/webpack-builder.
Nuxt 3 internals
The majority of Nuxt 3 internals rely on building a global nuxt context that is delegated to each builder. This is facilitated via the loadNuxt function which can then be passed to a webpack or vite builder.
Vite
The source code related to sourcing and building a Nuxt 3 application using their Vite builder can be found here. This file resides in the @nuxt/vite-builder package.
There is no getViteConfig function that we can utilize since the sourcing and building occur in one function. There is a vite client config that extends from a shared viteConfig, but we might need the configuration from the base config (or we can provide it ourselves).
Webpack
The source code related to sourcing and building a Nuxt 3 application using their Webpack builder can be found here. This file resides in the @nuxt/webpack-builder. Webpack support is opt-in by specifying vite: false in the nuxt.config.ts and installing @nuxt/webpack-builder.
There is no getWebpackConfig function, which is what we used in our Nuxt 2 support. There is a webpack client config preset, but it relies on Nuxt internals and isn't a raw webpack config.
Global Components
A new Nuxt 3 feature is the automatic import of components/dependencies. For example, when generating a Nuxt 3 application with npx nuxi init nuxt-app, the app.vue file contains:
We should support components that utilize this global dependency injection. Rendering the app.vue should work out of the box.
Trade-offs
The trade-off for pursing a solution that matches our Nuxt 2 implementation is that we are investing time in supporting a framework that we don't and haven't had full support for (SSR and server-side APIs).
Alternatives considered
Nuxt 3 offers a Module API that allows plugin authors to extend and tap into the build/serve lifecycle of a Nuxt 3 application. For example, there is a vite:extendConfig lifecycle that provides the client vite config for users (webpack:config for webpack users). Working within the Module API could provide us with the necessary extension points to add our Cypress logic.
For meta-frameworks like Next and Nuxt, we are grabbing their internals and plugging it up to our dev-server. There could be a solution where we plug our internals into their dev-server. This would be a drastic departure from our current solution.
Spike
I was able to get vue components rendering with our vite-dev-server by utilizing patch-package to make some slight modifications to the nuxt source code to allow us to grab a working vite configuration. I was not able to get an auto-imported component rendering, nor was I able to get the webpack version working. Sourcing the config (given the modifications to enable this) isn't too difficult, but modifying a complex Vite/Webpack configuration can be.
Nuxt 3 app with patches: https://github.com/ZachJW34/nuxt-3-cypress-ct Cypress branch with webpack/vite handlers: https://github.com/cypress-io/cypress/tree/zachw/nuxt-3-spike
References
Open issue for exposing Webpack/Vite Internals: https://github.com/nuxt/nuxt.js/issues/14534
Also maybe useful: https://github.com/danielroe/nuxt-vitest
#25719
Let's track all Nuxt related activity here. Update:
I am working on https://github.com/cypress-io/cypress/issues/25637 right now. This will let third parties slot their own integrations into Cypress.
Then, we can write a community plugin that makes Nuxt work. I'll try to do this in parallel with https://github.com/cypress-io/cypress/issues/25637.
Related: https://github.com/cypress-io/cypress/issues/24141
Let's track all Nuxt related activity here. Update:
I am working on #25637 right now. This will let third parties slot their own integrations into Cypress.
Then, we can write a community plugin that makes Nuxt work. I'll try to do this in parallel with #25637.
Related: #24141
Thanks, @lmiller1990 for taking on this task and moving forward with the Cypress integration with Nuxt.
Please let me know if I can be of any help with the plugin (feel free to assign me/tag me in the issue once the public API for 3rd parties is implemented)
Will do. I suspect we just need to figure out how to integrate Nuxt's Vite config - I know about as much as anyone else (not much) on this topic.
You should be able do it without the public API, it'll just be a bit messier. Something like
// cypress.config.ts
const { defineConfig } = require('cypress')
// https://github.com/nuxt/nuxt/issues/14534#issuecomment-1419025107
const { loadNuxt, buildNuxt } = require('@nuxt/kit');
// https://github.com/nuxt/framework/issues/6496
async function getViteConfig() {
const nuxt = await loadNuxt({ cwd: process.cwd(), dev: false, ssr: false });
return new Promise((resolve, reject) => {
nuxt.hook('vite:extendConfig', (config) => {
resolve(config);
throw new Error('_stop_');
});
buildNuxt(nuxt).catch((err) => {
if (!err.toString().includes('_stop_')) {
reject(err);
}
});
}).finally(() => nuxt.close());
}
module.exports = defineConfig({
component: {
devServer: {
framework: 'vue',
bundler: 'vite',
viteConfig: async () => {
const config = await getViteConfig();
return config
}
},
},
})
Note the ssr: false
- this will NOT work work with SSR or server side hooks. This may even work (untested).
The Nuxt team does not plan to expose the functionality to kick off the server ref, so we will need to explore some strategies around this.
This should hopefully support things like Nuxt auto imports, etc.
@lmiller1990 Yes, if you take a look at https://github.com/cypress-io/cypress/issues/25719#event-8452461319 I had already tried that integration (you can find a minimal reproduction here).
I get the following error when starting Cypress:
Your configFile is invalid: /Users/floroz/Repository/base-nuxt3/cypress.config.ts
Stack Trace:
Error: Cannot find module 'file:///Users/floroz/Repository/base-nuxt3/cypress.config.ts'
Require stack:
- /Users/floroz/Library/Caches/Cypress/12.5.1/Cypress.app/Contents/Resources/app/packages/server/lib/plugins/child/run_require_async_child.js
- /Users/floroz/Library/Caches/Cypress/12.5.1/Cypress.app/Contents/Resources/app/packages/server/lib/plugins/child/require_async_child.js
at Function.Module._resolveFilename (node:internal/modules/cjs/loader:995:15)
at Function.Module._resolveFilename (/Users/floroz/Library/Caches/Cypress/12.5.1/Cypress.app/Contents/Resources/app/node_modules/tsconfig-paths/lib/register.js:75:40)
at Function.Module._resolveFilename.sharedData.moduleResolveFilenameHook.installedValue [as _resolveFilename] (/Users/floroz/Library/Caches/Cypress/12.5.1/Cypress.app/Contents/Resources/app/node_modules/@cspotcode/source-map-support/source-map-support.js:811:30)
at Function.Module._load (node:internal/modules/cjs/loader:841:27)
at Module.require (node:internal/modules/cjs/loader:1061:19)
at require (node:internal/modules/cjs/helpers:103:18)
at /Users/floroz/Library/Caches/Cypress/12.5.1/Cypress.app/Contents/Resources/app/packages/server/lib/plugins/child/run_require_async_child.js:106:34
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at async loadFile (/Users/floroz/Library/Caches/Cypress/12.5.1/Cypress.app/Contents/Resources/app/packages/server/lib/plugins/child/run_require_async_child.js:106:14)
at async EventEmitter. (/Users/floroz/Library/Caches/Cypress/12.5.1/Cypress.app/Contents/Resources/app/packages/server/lib/plugins/child/run_require_async_child.js:116:32)
If I isolate the code, I could identify this line as the one causing the error
import { loadNuxt, buildNuxt } from "@nuxt/kit";
Here's the debugging gets a little hard... but somehow during the module resolution of @nuxt/kit
something goes wrong.
I have also patched the @nuxt/kit/dist/index.mjs
to contain noting but two empty functions loadNuxt
and buildNuxt
, but the same error gets thrown.
If I place some logs at the beginning of the file, I can see that that file is never executed.
Also running the debug flag
➜ nuxt3-starting-template git:(main) ✗ DEBUG=cypress:scaffold-config:detect yarn cypress open
yarn run v1.22.19
$ /Users/xxx/Repository/nuxt3-starting-template/node_modules/.bin/cypress open
cypress:scaffold-config:detect Checking for default Cypress config file +0ms
cypress:scaffold-config:detect Detected cypress.config.ts - using TS +1ms
Okay, a few updates after investigating the issue a bit more in depth:
1. adding "type": "module"
to the project package.json
solved the config resolution issue.
@lmiller1990 you might know better whether this can be resolved within the cypress config, rather than adding the module to the package json.
2. The following setup lets you start the component testing
viteConfig: async () => {
console.log("log:entering viteconfig");
await getViteConfig();
return {};
},
This validates that the nuxt
object is loaded correctly and the config is resolved.
However, since no vite
config is provided, the necessary vue plugin that is required fails the tests from running.
3. Loading the vite config in the cypress config is now yielding the following error:
Error: Cannot call server.listen in middleware mode.
Found some info here: https://github.com/vitejs/vite/issues/2094
4. Disable Middleware Mode ?
If I return the following config, I can load the Cypress tests.
return {
...config,
server: {
middlewareMode: false,
},
};
However, the mounting doesn't happen correctly.
hi i following https://github.com/cypress-io/cypress/issues/23619#issuecomment-1420459541 and try to setup components test for nuxt3. (tnx @floroz)
if we remove replace plugin from vite config, components can mount!
async viteConfig() {
const config = await getNuxtConfig();
config.plugins = config.plugins.filter(
item => !['replace', 'vite-plugin-eslint'].includes(item.name)
);
config.server.middlewareMode = false;
return config;
}
All good! but one problem in here. nuxt not install vue plugins... and we can't use top level async component...
Hello again I could do it (tnx @floroz). https://github.com/nuxt/nuxt/discussions/19304#discussion-4901358
^ Comment is very useful - we can use this if/when we decide to add Nuxt 3 support.
Amazing job @xtoolkit cracking the solution :)
Could you help me understand this line below? How did you identify those two plugins being responsible for failing the .mount(Component)
?
config.plugins = config.plugins.filter(
item => !['replace', 'vite-plugin-eslint'].includes(item.name)
);
Problem is replace
plugin. Remove vite-plugin-eslint
is optional for better test develop DX.
I don't know why but problem at this line: https://github.com/nuxt/nuxt/blob/503b7acd554fd90949d7c58ca35a6d796b42542f/packages/vite/src/vite.ts#L79
Related: https://github.com/vuejs/test-utils/issues/2119#issuecomment-1627946966
Is anyone using Nuxt and Cypress (or Vitest)? How is your experience? It looks like with the right wiring it should mostly work, but I haven't seen any comprehensive examples yet. You won't get SSR, but you should get the Nuxt auto imports like $fetch
, etc.
Hello, I am using Nuxt and Cypress to do component testing. I had to do some boilerplate manual work to get it working. Here's my cypress/support/component.ts
if it is any help. There are probably some unnecessary bits already, but at least it is working.
import "./commands"
import { mount } from "cypress/vue"
import { getContext } from "unctx"
import { Suspense, defineComponent, h } from "vue"
import { createRouter, createMemoryHistory, RouteRecordName } from "vue-router"
// import VueRouter from "vue-router"
import vuetify from "../plugins/vuetify"
import MarkdownPlugin from "../../plugins/markdownit"
export const nghBaseUrl = "http://ngh-mock.test/ngh/"
export const searchBaseUrl = "http://ngh-mock.test/search/"
// Context mocking for cypress component tests
const nuxtAppCtx = getContext("nuxt-app")
const generateNuxtCTX = (): Record<string, any> => ({
static: { data: {} },
payload: { data: {}, _errors: {} },
hook: () => () => ({}),
hooks: {
callHook: () => Promise.resolve(),
},
_asyncData: {},
_asyncDataPromises: {},
_useHead: () => ({}),
_route: {},
_router: {},
$router: {},
router: {},
$config: { public: { nghBaseUrl, searchBaseUrl } },
})
const nuxtCTX = generateNuxtCTX()
nuxtAppCtx.set(nuxtCTX)
nuxtCTX.$md = MarkdownPlugin().provide.md
// Context mocking ends
const scheduler = typeof setImmediate === "function" ? setImmediate : setTimeout
export function flushPromises(): Promise<void> {
return new Promise((resolve) => {
scheduler(resolve, 0)
})
}
export function wrapInSuspense(
component: ReturnType<typeof defineComponent>,
{ props, slots }: { props: object; slots: object }
): ReturnType<typeof defineComponent> {
return defineComponent({
render() {
return h(
"div",
{ id: "root" },
h(Suspense, null, {
default() {
return h(component, props, slots)
},
fallback: h("div", "Component test fallback"),
})
)
},
})
}
// Cypress.Commands.add('mount', mount)
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
Cypress.Commands.add("mount", (MountedComponent, options) => {
options = options || {}
options.global = options.global || {}
// Add vuetify to make V-components work
options.global.plugins = [vuetify]
// Do not stub transitions
options.global.stubs = { transition: false }
options.global.mocks = options.global.mocks || {}
options.global.provide = options.global.provide || {}
// mocking the $fetch method response
// eslint-disable-next-line require-await
const fetchMethod = async (_url) => ({
"hello": "world"
})
const fetchPlugin = {
install(_app, _options) {
globalThis.$fetch = fetchMethod
},
}
const router = createRouter({
routes: [
{
name:
(options.global.mocks.route?.name as RouteRecordName) || "testRoute",
path: "/x/:gameId/:guideId*",
redirect: "",
},
],
history: createMemoryHistory(),
})
options.global.plugins.push(router)
options.global.plugins.push(fetchPlugin)
nuxtCTX.$router = router
if (!options.global.mocks.$route) {
options.global.mocks.$route = { name: "testRoute" }
}
if (options.global.mocks?.$route) {
nuxtCTX._route = options.global.mocks.$route
/*
const $route = options.global.mocks?.route
options.global.mocks.$route = $route
nuxtCTX.router = router
nuxtCTX.$router = router
*/
// delete options.global.mocks.route
}
options.global.provide.nghBaseUrl =
options.global.provide.nghBaseUrl || nghBaseUrl
return mount(
wrapInSuspense(MountedComponent, {
props: options.props,
slots: options.slots,
}),
options
)
})
I would be interested in this topic. Was there some branch to test or make pull requests to?
That is a heck of a lot of boilerplate. Lots of stubbing of Nuxt. I'd like to try this out and see how the experience is.
No branch right now - I think the best step is playing around in a separate, personal repo and smoothing things out. If we get to a point we are happy with, we can consider releasing something under the Cypress org.
Are there any indication of progress on this topic for Nuxt3 component testing? So far I'm in combo with cypress (e2e) and vitest (component).