diff --git a/browser-versions.json b/browser-versions.json index 02d247ccea58..7cda3cd22949 100644 --- a/browser-versions.json +++ b/browser-versions.json @@ -1,4 +1,4 @@ { - "chrome:beta": "101.0.4951.26", - "chrome:stable": "100.0.4896.88" + "chrome:beta": "101.0.4951.34", + "chrome:stable": "100.0.4896.127" } diff --git a/circle.yml b/circle.yml index 40daecc2e589..9ea657a9e057 100644 --- a/circle.yml +++ b/circle.yml @@ -29,7 +29,7 @@ mainBuildFilters: &mainBuildFilters only: - develop - 10.0-release - - unify-1449-check-path-length-in-build + - zachw/add-dev-server-deps # uncomment & add to the branch conditions below to disable the main linux # flow if we don't want to test it for a certain branch @@ -47,7 +47,7 @@ macWorkflowFilters: &mac-workflow-filters or: - equal: [ develop, << pipeline.git.branch >> ] - equal: [ '10.0-release', << pipeline.git.branch >> ] - - equal: [ unify-1449-check-path-length-in-build, << pipeline.git.branch >> ] + - equal: [ zachw/add-dev-server-deps, << pipeline.git.branch >> ] - matches: pattern: "-release$" value: << pipeline.git.branch >> @@ -57,7 +57,7 @@ windowsWorkflowFilters: &windows-workflow-filters or: - equal: [ develop, << pipeline.git.branch >> ] - equal: [ '10.0-release', << pipeline.git.branch >> ] - - equal: [ 'tgriesser/chore/fix-windows-build', << pipeline.git.branch >> ] + - equal: [ zachw/add-dev-server-deps, << pipeline.git.branch >> ] - matches: pattern: "-release$" value: << pipeline.git.branch >> @@ -452,6 +452,20 @@ commands: path: /tmp/artifacts - store-npm-logs + windows-install-chrome: + parameters: + browser: + description: browser shortname to target + type: string + steps: + - run: + # TODO: How can we have preinstalled browsers on CircleCI? + name: 'Install Chrome on Windows' + command: | + # install with `--ignore-checksums` to avoid checksum error + # https://www.gep13.co.uk/blog/chocolatey-error-hashes-do-not-match + [[ $PLATFORM == 'windows' && '<>' == 'chrome' ]] && choco install googlechrome --ignore-checksums || [[ $PLATFORM != 'windows' ]] + run-new-ui-tests: parameters: package: @@ -475,13 +489,8 @@ commands: default: '' steps: - restore_cached_workspace - - run: - # TODO: How can we have preinstalled browsers on CircleCI? - name: 'Install Chrome on Windows' - command: | - # install with `--ignore-checksums` to avoid checksum error - # https://www.gep13.co.uk/blog/chocolatey-error-hashes-do-not-match - [[ $PLATFORM == 'windows' && '<>' == 'chrome' ]] && choco install googlechrome --ignore-checksums || [[ $PLATFORM != 'windows' ]] + - windows-install-chrome: + browser: <> - run: command: | cmd=$([[ <> == 'true' ]] && echo 'yarn percy exec --parallel -- --') || true @@ -589,24 +598,26 @@ commands: - restore_cached_binary - run: name: "Cloning test project and checking out release branch: <>" - working_directory: ~/ + working_directory: /tmp/<> command: | - git clone --depth 1 --no-single-branch https://github.com/cypress-io/<>.git /tmp/<> + git clone --depth 1 --no-single-branch https://github.com/cypress-io/<>.git . + cd ~/cypress/.. # install some deps for get-next-version npm i semver@7.3.2 conventional-recommended-bump@6.1.0 conventional-changelog-angular@5.0.12 NEXT_VERSION=$(node ./cypress/scripts/get-next-version.js) + cd - - cd /tmp/<> && (git checkout $NEXT_VERSION || true) + git checkout $NEXT_VERSION || true - when: - condition: <> - steps: - - run: - name: Check out PR <> - working_directory: /tmp/<> - command: | - git fetch origin pull/<>/head:pr-<> - git checkout pr-<> + condition: <> + steps: + - run: + name: Check out PR <> + working_directory: /tmp/<> + command: | + git fetch origin pull/<>/head:pr-<> + git checkout pr-<> test-binary-against-rwa: description: | @@ -821,6 +832,8 @@ commands: condition: <> name: "Waiting on server to boot: <>" command: "npx wait-on <> --timeout 120000" + - windows-install-chrome: + browser: <> - when: condition: <> steps: @@ -1714,7 +1727,7 @@ jobs: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "unify-1449-check-path-length-in-build" && "$CIRCLE_BRANCH" != "10.0-release" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "zachw/add-dev-server-deps" && "$CIRCLE_BRANCH" != "10.0-release" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi @@ -2466,8 +2479,6 @@ mac-workflow: &mac-workflow requires: - darwin-build - # maybe run all unit tests? - - create-build-artifacts: name: darwin-create-build-artifacts context: @@ -2539,6 +2550,11 @@ windows-workflow: &windows-workflow - test-runner:commit-status-checks requires: - windows-build + - test-binary-against-kitchensink-chrome: + name: windows-test-binary-against-kitchensink-chrome + executor: windows + requires: + - windows-create-build-artifacts workflows: linux: diff --git a/cli/package.json b/cli/package.json index d4f69fd5b899..410f97c70cb7 100644 --- a/cli/package.json +++ b/cli/package.json @@ -8,7 +8,7 @@ "prebuild": "yarn postinstall && node ./scripts/start-build.js", "build": "node ./scripts/build.js", "dtslint": "dtslint types", - "postinstall": "node ./scripts/post-install.js", + "postinstall": "patch-package && node ./scripts/post-install.js", "size": "t=\"cypress-v0.0.0.tgz\"; yarn pack --filename \"${t}\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", "test": "yarn test-unit", "test-debug": "node --inspect-brk $(yarn bin mocha)", @@ -140,5 +140,10 @@ "./mount-utils": { "require": "./mount-utils/dist/index.js" } + }, + "workspaces": { + "nohoist": [ + "@types/*" + ] } } diff --git a/patches/@types+chai+4.2.15.dev.patch b/cli/patches/@types+chai+4.2.15.dev.patch similarity index 100% rename from patches/@types+chai+4.2.15.dev.patch rename to cli/patches/@types+chai+4.2.15.dev.patch diff --git a/patches/@types+jquery+3.3.31.dev.patch b/cli/patches/@types+jquery+3.3.31.dev.patch similarity index 100% rename from patches/@types+jquery+3.3.31.dev.patch rename to cli/patches/@types+jquery+3.3.31.dev.patch diff --git a/patches/@types+mocha+8.0.3.dev.patch b/cli/patches/@types+mocha+8.0.3.dev.patch similarity index 100% rename from patches/@types+mocha+8.0.3.dev.patch rename to cli/patches/@types+mocha+8.0.3.dev.patch diff --git a/cli/scripts/post-install.js b/cli/scripts/post-install.js index cd680de089df..16c5b75a348a 100644 --- a/cli/scripts/post-install.js +++ b/cli/scripts/post-install.js @@ -25,8 +25,6 @@ fs.ensureDirSync(join(__dirname, '..', 'types')) includeTypes.forEach((folder) => { const source = resolvePkg(`@types/${folder}`, { cwd: __dirname }) - console.log(`copying ${folder} from ${source}`) - fs.copySync(source, join(__dirname, '..', 'types', folder)) }) diff --git a/guides/README.md b/guides/README.md index 3ba116a5a096..93e321f56c14 100644 --- a/guides/README.md +++ b/guides/README.md @@ -13,6 +13,7 @@ For general contributor information, check out [`CONTRIBUTING.md`](../CONTRIBUTI * [Building release artifacts](./building-release-artifacts.md) * [Code signing](./code-signing.md) * [Determining the next version of Cypress to be released](./next-version.md) +* [Remaining Platform Agnostic](./remaining-platform-agnostic.md) * [Error handling](./error-handling.md) * [Patching packages](./patch-package.md) * [Release process](./release-process.md) diff --git a/guides/writing-cross-platform-javascript.md b/guides/writing-cross-platform-javascript.md new file mode 100644 index 000000000000..a10580903444 --- /dev/null +++ b/guides/writing-cross-platform-javascript.md @@ -0,0 +1,85 @@ +# Writing Cross-Platform JavaScript + +Cypress works on Linux, macOS and Windows. This includes both installing from npm, as well as for local development. Code should be written in a platform agnostic style. + +## Handling File Paths + +Throughout the code base, we access the file system in various ways, and need to be conscious of how we do so to ensure Cypress can be used and developed seamlessly on multiple platforms. One thing to keep in mind is file paths and file separators. macOS and Linux systems use `/`, and Windows uses `\`. + + +As a general rule, we want to use **native paths** where possible. There are a few reasons for this. Whereever we display a file path, we want to use the native file separator, since that is what the user will expect on their platform. In general, we can use the Node.js `path` module to handle this: + +```js +// on linux-like systems +path.join('cypress', 'e2e') //=> `cypress/e2e` + +// on Windows +path.join('cypress', 'e2e') //=> `cypress\e2e` +``` + +There are some exceptions to this, namely the [`globby`](https://www.npmjs.com/package/globby) module, which only supports `/` (see [here](https://github.com/sindresorhus/globby#api)) when writing glob patterns. In these cases, where an API is posix only, you can use `path.posix.join`, which will always use `/`, even on a Windows system: + +```js +// don't do +const files = await globby('my-project\cypress\e2e\**\*') + +// do +const files = await globby('my-project/cypress/e2e/**/*') + +// or you can convert it by splitting by path.sep +// and joining with `path.posix.join` +const glob = path.posix.join('my-project/cypress/e2e/**/*'.split(path.sep)) +``` + +The general rule of using `path` where possible applies to moving around the file system, too: + +```js +path.resolve('../', '/../', '../') +// '/home' on Linux +// '/Users' on OSX +// 'C:\\Users' on Windows +``` + +In general, you want to avoid writing file system code using `/` and `\`, and use Node.js APIs where possible - those are cross platform and guarenteed to work. + +## Use Node.js Scripts + +For many developers, it's tempting to write a quick bash script to automate tasks. Maybe you'd like to delete all `.js` files, so you add a script to `package.json`: + +```json +{ + "scripts": { + "clean": "rm -rf **/*.js" + } +} +``` + +This will stop developers on Windows from running `yarn clean` unless they are specifically using a POSIX shell (like Git Bash). Instead, opt for a Node.js script where possible, or use a cross-platform Node.js module. In this case, we could use the [`rimraf`](https://www.npmjs.com/package/rimraf) module: + +```json +{ + "devDependencies": { + "rimraf": "3.0.2", + }, + "scripts": { + "clean": "rimraf '**/*.js'" + } +} +``` + +Now your script is cross-platform. + +## Use the os Module + +You can use the `os` module to handle platform differences. One such example is line endings; `\n` on linux systems, and `\r\n` on Windows. Instead. use `os.EOL`. To check the current platform, use `os.arch()`: + +```ts +import os from 'os' + +os.EOL // \n on linux, \r\n on windows + +os.platform() +// 'linux' on Linux +// 'win32' on Windows (32-bit / 64-bit) +// 'darwin' on OSX +``` diff --git a/npm/vite-dev-server-fresh/src/devServer.ts b/npm/vite-dev-server-fresh/src/devServer.ts index b93afb1ac121..526b569ee036 100644 --- a/npm/vite-dev-server-fresh/src/devServer.ts +++ b/npm/vite-dev-server-fresh/src/devServer.ts @@ -1,6 +1,6 @@ import debugFn from 'debug' import getPort from 'get-port' -import { createServer as viteCreateServer } from 'vite' +import { getVite, Vite } from './getVite' import { createViteDevServerConfig } from './resolveConfig' const debug = debugFn('cypress:vite-dev-server:devServer') @@ -17,8 +17,11 @@ export type ViteDevServerConfig = { } export async function devServer (config: ViteDevServerConfig): Promise { + // This has to be the first thing we do as we need to source vite from their project's dependencies + const vite = getVite(config) + debug('Creating Vite Server') - const server = await devServer.create(config) as import('vite').ViteDevServer + const server = await devServer.create(config, vite) debug('Vite server created') const port = await getPort({ port: 3000 }) @@ -36,11 +39,11 @@ export async function devServer (config: ViteDevServerConfig): Promise { let base = '/' @@ -82,7 +83,7 @@ export const Cypress = ( debug('handleHotUpdate - file', file) // If the user provided IndexHtml is changed, do a full-reload - if (normalizePath(file) === resolve(projectRoot, indexHtmlFile)) { + if (vite.normalizePath(file) === resolve(projectRoot, indexHtmlFile)) { server.ws.send({ type: 'full-reload', }) diff --git a/npm/vite-dev-server-fresh/src/plugins/inspect.ts b/npm/vite-dev-server-fresh/src/plugins/inspect.ts index 3f15524910f7..aed4cd134313 100644 --- a/npm/vite-dev-server-fresh/src/plugins/inspect.ts +++ b/npm/vite-dev-server-fresh/src/plugins/inspect.ts @@ -1,9 +1,10 @@ import debugFn from 'debug' import type { PluginOption } from 'vite' +import type { ViteDevServerConfig } from '../devServer' const debug = debugFn('cypress:vite-dev-server:plugins:inspect') -export const CypressInspect = async (): Promise => { +export const CypressInspect = (config: ViteDevServerConfig): PluginOption | null => { if (!process.env.CYPRESS_INTERNAL_VITE_INSPECT) { debug('skipping vite inspect because CYPRESS_INTERNAL_VITE_INSPECT is not set') @@ -13,7 +14,9 @@ export const CypressInspect = async (): Promise => { let Inspect try { - Inspect = (await import('vite-plugin-inspect')).default + const inspectPluginPath = require.resolve('vite-plugin-inspect', { paths: [config.cypressConfig.projectRoot] }) + + Inspect = require(inspectPluginPath).default debug('inspect was found', Inspect) } catch (err) { debug(`Tried to import the inspect plugin 'vite-plugin-inspect'. It's an optional peerDependency so install it if you'd like.`) diff --git a/npm/vite-dev-server-fresh/src/resolveConfig.ts b/npm/vite-dev-server-fresh/src/resolveConfig.ts index 9af9423ab424..83cc0b01a0bc 100644 --- a/npm/vite-dev-server-fresh/src/resolveConfig.ts +++ b/npm/vite-dev-server-fresh/src/resolveConfig.ts @@ -6,16 +6,17 @@ import debugFn from 'debug' import { importModule } from 'local-pkg' import { relative, resolve } from 'pathe' -import { InlineConfig, mergeConfig } from 'vite' +import type { InlineConfig } from 'vite' import path from 'path' import { configFiles } from './constants' import type { ViteDevServerConfig } from './devServer' import { Cypress, CypressInspect } from './plugins/index' +import type { Vite } from './getVite' const debug = debugFn('cypress:vite-dev-server:resolve-config') -export const createViteDevServerConfig = async (config: ViteDevServerConfig) => { +export const createViteDevServerConfig = async (config: ViteDevServerConfig, vite: Vite) => { const { specs, cypressConfig, viteConfig: viteOverrides = {} } = config const root = cypressConfig.projectRoot const { default: findUp } = await importModule('find-up') @@ -60,12 +61,12 @@ export const createViteDevServerConfig = async (config: ViteDevServerConfig) => }, }, plugins: [ - Cypress(config), - await CypressInspect(), + Cypress(config, vite), + CypressInspect(config), ], } - const finalConfig = mergeConfig(viteBaseConfig, viteOverrides as Record) + const finalConfig = vite.mergeConfig(viteBaseConfig, viteOverrides as Record) debug('The resolved server config is', JSON.stringify(finalConfig, null, 2)) diff --git a/npm/webpack-dev-server-fresh/cypress/e2e/next.cy.ts b/npm/webpack-dev-server-fresh/cypress/e2e/next.cy.ts new file mode 100644 index 000000000000..025e816220ef --- /dev/null +++ b/npm/webpack-dev-server-fresh/cypress/e2e/next.cy.ts @@ -0,0 +1,74 @@ +// +/// +import type { ProjectFixtureDir } from '@tooling/system-tests/lib/fixtureDirs' + +const WEBPACK_REACT: ProjectFixtureDir[] = ['next-11', 'next-12', 'next-11-webpack-4'] + +// Add to this list to focus on a particular permutation +const ONLY_PROJECTS: ProjectFixtureDir[] = [] + +for (const project of WEBPACK_REACT) { + if (ONLY_PROJECTS.length && !ONLY_PROJECTS.includes(project)) { + continue + } + + describe(`Working with ${project}`, () => { + beforeEach(() => { + cy.scaffoldProject(project) + cy.openProject(project) + cy.startAppServer('component') + }) + + it('should mount a passing test', () => { + cy.visitApp() + cy.contains('index.cy.js').click() + cy.get('.passed > .num').should('contain', 1) + }) + + it('should live-reload on src changes', () => { + cy.visitApp() + + cy.contains('index.cy.js').click() + cy.get('.passed > .num').should('contain', 1) + + cy.withCtx(async (ctx) => { + const indexPath = ctx.path.join('pages', 'index.js') + + await ctx.actions.file.writeFileInProject( + indexPath, + (await ctx.file.readFileInProject(indexPath)).replace('Welcome to', 'Hello from'), + ) + }) + + cy.get('.failed > .num', { timeout: 10000 }).should('contain', 1) + + cy.withCtx(async (ctx) => { + const indexTestPath = ctx.path.join('pages', 'index.cy.js') + + await ctx.actions.file.writeFileInProject( + indexTestPath, + (await ctx.file.readFileInProject(indexTestPath)).replace('Welcome to', 'Hello from'), + ) + }) + + cy.get('.passed > .num').should('contain', 1) + }) + + it('should detect new spec', () => { + cy.visitApp() + + cy.withCtx(async (ctx) => { + const newTestPath = ctx.path.join('pages', 'New.cy.js') + const indexTestPath = ctx.path.join('pages', 'index.cy.js') + + await ctx.actions.file.writeFileInProject( + newTestPath, + await ctx.file.readFileInProject(indexTestPath), + ) + }) + + cy.contains('New.cy.js').click() + cy.get('.passed > .num').should('contain', 1) + }) + }) +} diff --git a/npm/webpack-dev-server-fresh/package.json b/npm/webpack-dev-server-fresh/package.json index b71318c9cbb5..8886d2bc4ab3 100644 --- a/npm/webpack-dev-server-fresh/package.json +++ b/npm/webpack-dev-server-fresh/package.json @@ -5,12 +5,12 @@ "private": true, "main": "dist/index.js", "scripts": { + "prebuild": "rimraf dist", "build": "tsc || echo 'built, with type errors'", - "build-prod": "tsc || echo 'built, with type errors'", + "build-prod": "yarn build", "check-ts": "tsc --noEmit", "dev": "tsc --watch", "clean": "rimraf dist", - "postinstall": "yarn clean && yarn build", "cypress:run": "yarn cypress:run-cypress-in-cypress node ../../scripts/cypress run --project . --browser chrome", "cypress:run-cypress-in-cypress": "cross-env HTTP_PROXY_TARGET_FOR_ORIGIN_REQUESTS=http://localhost:4455 CYPRESS_REMOTE_DEBUGGING_PORT=6666 TZ=America/New_York", "cypress:open": "yarn cypress:run-cypress-in-cypress gulp open --project .", @@ -38,5 +38,8 @@ "webpack": "npm:webpack@^5", "webpack-4": "npm:webpack@^4", "webpack-dev-server-3": "npm:webpack-dev-server@^3" - } + }, + "files": [ + "dist" + ] } diff --git a/npm/webpack-dev-server-fresh/src/devServer.ts b/npm/webpack-dev-server-fresh/src/devServer.ts index 5247ad280e97..f842b6f9236c 100644 --- a/npm/webpack-dev-server-fresh/src/devServer.ts +++ b/npm/webpack-dev-server-fresh/src/devServer.ts @@ -4,13 +4,14 @@ import type WebpackDevServer from 'webpack-dev-server' import type { Compiler, Configuration } from 'webpack' import { createWebpackDevServer } from './createWebpackDevServer' -import { sourceRelativeWebpackModules } from './helpers/sourceRelativeWebpackModules' import type { AddressInfo } from 'net' import debugLib from 'debug' import type { Server } from 'http' import { vueCliHandler } from './helpers/vueCliHandler' import { nuxtHandler } from './helpers/nuxtHandler' import { createReactAppHandler } from './helpers/createReactAppHandler' +import { nextHandler } from './helpers/nextHandler' +import { sourceDefaultWebpackDependencies, SourceRelativeWebpackResult } from './helpers/sourceRelativeWebpackModules' const debug = debugLib('cypress:webpack-dev-server-fresh:devServer') @@ -23,7 +24,7 @@ export type WebpackDevServerConfig = { webpackConfig?: unknown // Derived from the user's webpack } -const ALL_FRAMEWORKS = ['create-react-app', 'nuxt', 'react', 'vue-cli'] as const +const ALL_FRAMEWORKS = ['create-react-app', 'nuxt', 'react', 'vue-cli', 'next', 'vue'] as const /** * @internal @@ -111,6 +112,31 @@ export function devServer (devServerConfig: WebpackDevServerConfig): Promise { + switch (devServerConfig.framework) { + case 'create-react-app': + return createReactAppHandler(devServerConfig) + case 'nuxt': + return await nuxtHandler(devServerConfig) + + case 'vue-cli': + return vueCliHandler(devServerConfig) + + case 'next': + return await nextHandler(devServerConfig) + + case 'react': + case 'vue': + case undefined: + return { sourceWebpackModulesResult: sourceDefaultWebpackDependencies(devServerConfig) } + + default: + throw new Error(`Unexpected framework ${devServerConfig.framework}, expected one of ${ALL_FRAMEWORKS.join(', ')}`) + } +} + /** * Synchronously create the webpack server instance, without starting. * Useful for testing @@ -118,29 +144,7 @@ export function devServer (devServerConfig: WebpackDevServerConfig): Promise +import type { PresetHandlerResult, WebpackDevServerConfig } from '../devServer' +import { sourceDefaultWebpackDependencies } from './sourceRelativeWebpackModules' const debug = debugLib('cypress:webpack-dev-server-fresh:create-react-app') /** * Sourcing the config for create-react-app */ -export function createReactAppHandler (presetHandler: PresetHandlerOptions) { - const { devServerConfig, sourceWebpackModulesResult } = presetHandler +export function createReactAppHandler (devServerConfig: WebpackDevServerConfig): PresetHandlerResult { + const sourceWebpackModulesResult = sourceDefaultWebpackDependencies(devServerConfig) // this is required because // 1) we use our own HMR and we don't need react-refresh transpiling overhead // 2) it doesn't work with process.env=test @see https://github.com/cypress-io/cypress-realworld-app/pull/832 process.env.FAST_REFRESH = 'false' - const webpackConfig = loadWebpackConfig(presetHandler) + const webpackConfig = loadWebpackConfig(devServerConfig) addCypressToWebpackEslintRulesInPlace(webpackConfig) @@ -32,10 +31,13 @@ export function createReactAppHandler (presetHandler: PresetHandlerOptions) { reactScriptsFiveModifications(webpackConfig) } - return webpackConfig + return { + frameworkConfig: webpackConfig, + sourceWebpackModulesResult, + } } -function loadWebpackConfig ({ devServerConfig }: PresetHandlerOptions): Configuration { +function loadWebpackConfig (devServerConfig: WebpackDevServerConfig): Configuration { let webpackConfigPath: string const envName = 'test' diff --git a/npm/webpack-dev-server-fresh/src/helpers/nextHandler.ts b/npm/webpack-dev-server-fresh/src/helpers/nextHandler.ts new file mode 100644 index 000000000000..dfed465ed552 --- /dev/null +++ b/npm/webpack-dev-server-fresh/src/helpers/nextHandler.ts @@ -0,0 +1,250 @@ +import debugLib from 'debug' +import Module from 'module' +import type { Configuration } from 'webpack' +import * as fs from 'fs' +import * as path from 'path' +import type { PresetHandlerResult, WebpackDevServerConfig } from '../devServer' +import { cypressWebpackPath, getMajorVersion, ModuleClass, SourcedDependency, SourcedWebpack, sourceFramework, sourceHtmlWebpackPlugin, sourceWebpackDevServer } from './sourceRelativeWebpackModules' + +const debug = debugLib('cypress:webpack-dev-server-fresh:nextHandler') + +export async function nextHandler (devServerConfig: WebpackDevServerConfig): Promise { + const webpackConfig = await loadWebpackConfig(devServerConfig) + + debug('resolved next.js webpack config %o', webpackConfig) + + checkSWC(webpackConfig, devServerConfig.cypressConfig) + + // Next webpack compiler ignored watching any node_modules changes, but we need to watch + // for changes to 'dist/browser.js' in order to detect new specs that have been added + if (webpackConfig.watchOptions && Array.isArray(webpackConfig.watchOptions.ignored)) { + webpackConfig.watchOptions = { + ...webpackConfig.watchOptions, + ignored: [...webpackConfig.watchOptions.ignored.filter((pattern: string) => !/node_modules/.test(pattern)), '**/node_modules/!(@cypress/webpack-dev-server/dist/browser.js)**'], + } + + debug('found options next.js watchOptions.ignored %O', webpackConfig.watchOptions.ignored) + } + + return { frameworkConfig: webpackConfig, sourceWebpackModulesResult: sourceNextWebpackDeps(devServerConfig) } +} + +/** + * Acquire the modules needed to load the Next webpack config. We are using Next's APIs to grab the webpackConfig + * but since this is in the binary, we have to `require.resolve` them from the projectRoot + * `loadConfig` acquires the next.config.js + * `getNextJsBaseWebpackConfig` acquires the webpackConfig dependent on the next.config.js + */ +function getNextJsPackages (devServerConfig: WebpackDevServerConfig) { + const resolvePaths = { paths: [devServerConfig.cypressConfig.projectRoot] } + const packages = {} as { loadConfig: Function, getNextJsBaseWebpackConfig: Function } + + try { + const loadConfigPath = require.resolve('next/dist/server/config', resolvePaths) + + packages.loadConfig = require(loadConfigPath).default + } catch (e: any) { + throw new Error(`Failed to load "next/dist/server/config" with error: ${e.message ?? e}`) + } + + try { + const getNextJsBaseWebpackConfigPath = require.resolve('next/dist/build/webpack-config', resolvePaths) + + packages.getNextJsBaseWebpackConfig = require(getNextJsBaseWebpackConfigPath).default + } catch (e: any) { + throw new Error(`Failed to load "next/dist/build/webpack-config" with error: ${ e.message ?? e}`) + } + + return packages +} + +async function loadWebpackConfig (devServerConfig: WebpackDevServerConfig): Promise { + const { loadConfig, getNextJsBaseWebpackConfig } = getNextJsPackages(devServerConfig) + + const nextConfig = await loadConfig('development', devServerConfig.cypressConfig.projectRoot) + const runWebpackSpan = await getRunWebpackSpan() + const webpackConfig = await getNextJsBaseWebpackConfig( + devServerConfig.cypressConfig.projectRoot, + { + buildId: `@cypress/react-${Math.random().toString()}`, + config: nextConfig, + dev: true, + isServer: false, + pagesDir: findPagesDir(devServerConfig.cypressConfig.projectRoot), + entrypoints: {}, + rewrites: { fallback: [], afterFiles: [], beforeFiles: [] }, + ...runWebpackSpan, + }, + ) + + return webpackConfig +} + +/** + * Check if Next is using the SWC compiler. Compilation will fail if user has `nodeVersion: "bundled"` set + * due to SWC certificate issues. + */ +function checkSWC ( + webpackConfig: Configuration, + cypressConfig: Cypress.PluginConfigOptions, +) { + const hasSWCLoader = webpackConfig.module?.rules?.some((rule) => { + return typeof rule !== 'string' && rule.oneOf?.some( + (oneOf) => (oneOf.use as any)?.loader === 'next-swc-loader', + ) + }) + + // "resolvedNodePath" is only set when using the user's Node.js, which is required to compile Next.js with SWC optimizations + // If it is not set, they have either explicitly set "nodeVersion" to "bundled" or are are using Cypress < 9.0.0 where it was set to "bundled" by default + if (hasSWCLoader && cypressConfig.nodeVersion === 'bundled') { + throw new Error(`Cypress cannot compile your Next.js application when "nodeVersion" is set to "bundled". Please remove this option from your Cypress configuration file.`) + } + + return false +} + +const existsSync = (file: string) => { + try { + fs.accessSync(file, fs.constants.F_OK) + + return true + } catch (_) { + return false + } +} + +/** + * Next allows the `pages` directory to be located at either + * `${projectRoot}/pages` or `${projectRoot}/src/pages`. + * If neither is found, return projectRoot + */ +function findPagesDir (projectRoot: string) { + // prioritize ./pages over ./src/pages + let pagesDir = path.join(projectRoot, 'pages') + + if (existsSync(pagesDir)) { + return pagesDir + } + + pagesDir = path.join(projectRoot, 'src', 'pages') + if (existsSync(pagesDir)) { + return pagesDir + } + + return projectRoot +} + +// Starting with v11.1.1, a trace is required. +// 'next/dist/telemetry/trace/trace' only exists since v10.0.9 +// and our peerDeps support back to v8 so try-catch this import +// Starting from 12.0 trace is now located in 'next/dist/trace/trace' +async function getRunWebpackSpan (): Promise<{ runWebpackSpan?: any }> { + let trace: (name: string) => any + + try { + try { + trace = await import('next/dist/telemetry/trace/trace').then((m) => m.trace) + + return { runWebpackSpan: trace('cypress') } + } catch (_) { + // @ts-ignore + trace = await import('next/dist/trace/trace').then((m) => m.trace) + + return { runWebpackSpan: trace('cypress') } + } + } catch (_) { + return {} + } +} + +const originalModuleLoad = (Module as ModuleClass)._load + +function sourceNextWebpackDeps (devServerConfig: WebpackDevServerConfig) { + const framework = sourceFramework(devServerConfig)! + const webpack = sourceNextWebpack(devServerConfig, framework) + const webpackDevServer = sourceWebpackDevServer(devServerConfig, framework) + const htmlWebpackPlugin = sourceHtmlWebpackPlugin(devServerConfig, framework, webpack) + + return { + framework, + webpack, + webpackDevServer, + htmlWebpackPlugin, + } +} + +function sourceNextWebpack (devServerConfig: WebpackDevServerConfig, framework: SourcedDependency) { + const searchRoot = framework.importPath + + debug('NextWebpack: Attempting to load NextWebpack from %s', searchRoot) + + let webpackJsonPath: string + const webpack = {} as SourcedWebpack + + try { + webpackJsonPath = require.resolve('next/dist/compiled/webpack/package.json', { + paths: [searchRoot], + }) + } catch (e) { + debug('NextWebpack: Failed to load NextWebpack - %s', e) + throw e + } + + // Next 11 allows the choice of webpack@4 or webpack@5, depending on the "webpack5" property in their next.config.js + // The webpackModule.init" for Next 11 returns a webpack@4 or webpack@4 compiler instance based on this boolean + let webpack5 = true + const importPath = path.join(path.dirname(webpackJsonPath), 'webpack.js') + const webpackModule = require(importPath) + + try { + const nextConfig = require(path.resolve(devServerConfig.cypressConfig.projectRoot, 'next.config.js')) + + debug('NextWebpack: next.config.js found - %o', nextConfig) + + if (nextConfig.webpack5 === false) { + webpack5 = false + } + } catch (e) { + // No next.config.js, assume webpack 5 + } + + debug('NextWebpack: webpack5 - %s', webpack5) + webpackModule.init(webpack5) + + const packageJson = require(webpackJsonPath) + + webpack.importPath = importPath + // The package.json of "next/dist/compiled/webpack/package.json" has no version so we supply the version for later use + webpack.packageJson = { ...packageJson, version: webpack5 ? '5' : '4' } + webpack.module = webpackModule.webpack + webpack.majorVersion = getMajorVersion(webpack.packageJson, [4, 5]) + + debug('NextWebpack: Successfully loaded NextWebpack - %o', webpack) + + ;(Module as ModuleClass)._load = function (request, parent, isMain) { + // Next with webpack@4 doesn't ship certain dependencies that HtmlWebpackPlugin requires, so we patch the resolution through to our bundled version + if ((request === 'webpack' || request.startsWith('webpack/')) && webpack.majorVersion === 4) { + const resolvePath = require.resolve(request, { + paths: [cypressWebpackPath], + }) + + debug('NextWebpack: Module._load for webpack@4 - %s', resolvePath) + + return originalModuleLoad(resolvePath, parent, isMain) + } + + if (request === 'webpack' || request.startsWith('webpack/')) { + const resolvePath = require.resolve(request, { + paths: [framework.importPath], + }) + + debug('NextWebpack: Module._load - %s', resolvePath) + + return originalModuleLoad(resolvePath, parent, isMain) + } + + return originalModuleLoad(request, parent, isMain) + } + + return webpack +} diff --git a/npm/webpack-dev-server-fresh/src/helpers/nuxtHandler.ts b/npm/webpack-dev-server-fresh/src/helpers/nuxtHandler.ts index d486e529ba6f..eac9251e813c 100644 --- a/npm/webpack-dev-server-fresh/src/helpers/nuxtHandler.ts +++ b/npm/webpack-dev-server-fresh/src/helpers/nuxtHandler.ts @@ -1,12 +1,12 @@ -import type { CreateFinalWebpackConfig } from '../createWebpackDevServer' import debugLib from 'debug' -import type { Configuration } from 'webpack' - -type PresetHandler = Omit +import type { PresetHandlerResult, WebpackDevServerConfig } from '../devServer' +import { sourceDefaultWebpackDependencies } from './sourceRelativeWebpackModules' const debug = debugLib('cypress:webpack-dev-server-fresh:nuxtHandler') -export async function nuxtHandler ({ devServerConfig }: PresetHandler): Promise { +export async function nuxtHandler (devServerConfig: WebpackDevServerConfig): Promise { + const sourceWebpackModulesResult = sourceDefaultWebpackDependencies(devServerConfig) + try { const nuxt = require.resolve('nuxt', { paths: [devServerConfig.cypressConfig.projectRoot], @@ -22,7 +22,7 @@ export async function nuxtHandler ({ devServerConfig }: PresetHandler): Promise< debug('webpack config %o', webpackConfig) - return webpackConfig + return { frameworkConfig: webpackConfig, sourceWebpackModulesResult } } catch (e) { console.error(e) // eslint-disable-line no-console throw Error(`Error loading nuxt. Looked in ${require.resolve.paths(devServerConfig.cypressConfig.projectRoot)}`) diff --git a/npm/webpack-dev-server-fresh/src/helpers/sourceRelativeWebpackModules.ts b/npm/webpack-dev-server-fresh/src/helpers/sourceRelativeWebpackModules.ts index 17ee430ce6bf..30896ed38f24 100644 --- a/npm/webpack-dev-server-fresh/src/helpers/sourceRelativeWebpackModules.ts +++ b/npm/webpack-dev-server-fresh/src/helpers/sourceRelativeWebpackModules.ts @@ -5,7 +5,7 @@ import debugFn from 'debug' const debug = debugFn('cypress:webpack-dev-server-fresh:sourceRelativeWebpackModules') -type ModuleClass = typeof Module & { +export type ModuleClass = typeof Module & { _load(id: string, parent: Module, isMain: boolean): any _resolveFilename(request: string, parent: Module, isMain: boolean, options?: { paths: string[] }): string _cache: Record @@ -16,97 +16,89 @@ export interface PackageJson { version: string } -export interface SourceRelativeWebpackResult { - framework?: { - importPath: string - packageJson: PackageJson - } - /** - * The webpack module instance - */ - webpack: { - importPath: string - module: Function - packageJson: PackageJson - majorVersion: 4 | 5 - } - /** - * The webpack dev-server instance - */ - webpackDevServer: { - importPath: string - module: { - new (...args: unknown[]): unknown - } - packageJson: PackageJson - majorVersion: 3 | 4 - } - /** - * html-webpack-plugin - */ - htmlWebpackPlugin: { - importPath: string - module: unknown - packageJson: PackageJson - majorVersion: 4 | 5 +export interface SourcedDependency { + importPath: string + packageJson: PackageJson +} + +export interface SourcedWebpack extends SourcedDependency { + module: Function + majorVersion: 4 | 5 +} + +export interface SourcedWebpackDevServer extends SourcedDependency { + module: { + new (...args: unknown[]): unknown } + majorVersion: 3 | 4 +} + +export interface SourcedHtmlWebpackPlugin extends SourcedDependency { + module: unknown + majorVersion: 4 | 5 +} + +export interface SourceRelativeWebpackResult { + framework: SourcedDependency | null + webpack: SourcedWebpack + webpackDevServer: SourcedWebpackDevServer + htmlWebpackPlugin: SourcedHtmlWebpackPlugin } const originalModuleLoad = (Module as ModuleClass)._load const originalModuleResolveFilename = (Module as ModuleClass)._resolveFilename -/** - * Based on the current project config, we look for the closest webpack, - * webpack-dev-server, and html-webpack-plugin for a user's project - * - * @internal - */ -export function sourceRelativeWebpackModules (config: WebpackDevServerConfig) { - let searchRoot = config.cypressConfig.projectRoot - const result = { - webpackDevServer: {}, - webpack: {}, - htmlWebpackPlugin: {}, - } as SourceRelativeWebpackResult - - // First, we source the framework, ensuring it's sourced from the user's project and not the - // Cypress binary. This is the path we use to relative-resolve the - // This is generally used for Create React App and Vue CLI and other packages - // that ship webpack as a dependency. e.g. your-project/node_modules/react-scripts/node_modules/webpack - // So what we do, is we grab the framework's path, and try and find webpack relative to that framework. - if (config.framework) { - try { - const frameworkJsonPath = require.resolve(`${config.framework}/package.json`, { - paths: [searchRoot], - }) +// We ship webpack@4 as part of '@cypress/webpack-batteries-included-preprocessor'. The path to this module +// serves as our fallback. +export const cypressWebpackPath = require.resolve('@cypress/webpack-batteries-included-preprocessor', { + paths: [__dirname], +}) - debug('Framework JSON path is %s', frameworkJsonPath) - const frameworkPathRoot = path.dirname(frameworkJsonPath) +// Source the users framework from the provided projectRoot. The framework, if available, will server +// as the resolve base for webpack dependency resolution. +export function sourceFramework (config: WebpackDevServerConfig): SourcedDependency | null { + debug('Framework: Attempting to source framework for %s', config.cypressConfig.projectRoot) + if (!config.framework) { + debug('Framework: No framework provided') - debug('Framework JSON path root is %s', frameworkPathRoot) + return null + } - // Want to make sure we're sourcing this from the user's code. Otherwise we can - // warn and tell them they don't have their dependencies installed - if (!frameworkPathRoot.includes(config.cypressConfig.cypressBinaryRoot)) { - result.framework = { - importPath: frameworkPathRoot, - packageJson: require(frameworkJsonPath), - } + const framework = { } as SourcedDependency - searchRoot = frameworkPathRoot - } - } catch (e) { - debug('Error %o', e) - // TODO - } + try { + const frameworkJsonPath = require.resolve(`${config.framework}/package.json`, { + paths: [config.cypressConfig.projectRoot], + }) + const frameworkPathRoot = path.dirname(frameworkJsonPath) + + // Want to make sure we're sourcing this from the user's code. Otherwise we can + // warn and tell them they don't have their dependencies installed + framework.importPath = frameworkPathRoot + framework.packageJson = require(frameworkJsonPath) + + debug('Framework: Successfully sourced framework - %o', framework) + + return framework + } catch (e) { + debug('Framework: Failed to source framework - %s', e) + + // TODO + return null } +} - // Webpack: - // At this point, we know where we're looking for webpack! - // We've made accommodations for certain frameworks that bundle it in (e.g. react-scripts) - let webpackJsonPath: string +// Source the webpack module from the provided framework or projectRoot. We override the module resolution +// so that other packages that import webpack resolve to the version we found. +// If none is found, we fallback to the bundled version in '@cypress/webpack-batteries-included-preprocessor'. +export function sourceWebpack (config: WebpackDevServerConfig, framework: SourcedDependency | null): SourcedWebpack { + const searchRoot = framework?.importPath ?? config.cypressConfig.projectRoot - debug('search root is %s', searchRoot) + debug('Webpack: Attempting to source webpack from %s', searchRoot) + + const webpack = { } as SourcedWebpack + + let webpackJsonPath: string try { webpackJsonPath = require.resolve('webpack/package.json', { @@ -114,53 +106,63 @@ export function sourceRelativeWebpackModules (config: WebpackDevServerConfig) { }) } catch (e) { if ((e as {code?: string}).code !== 'MODULE_NOT_FOUND') { + debug('Webpack: Failed to source webpack - %s', e) throw e } + debug('Webpack: Falling back to bundled version') + webpackJsonPath = require.resolve('webpack/package.json', { - paths: [ - require.resolve('@cypress/webpack-batteries-included-preprocessor', { - paths: [__dirname], - }), - ], + paths: [cypressWebpackPath], }) - - debug('using webpack-batteries-included %s', webpackJsonPath) } - result.webpack.importPath = path.dirname(webpackJsonPath) - result.webpack.packageJson = require(webpackJsonPath) - result.webpack.module = require(result.webpack.importPath) - result.webpack.majorVersion = getMajorVersion(result.webpack.packageJson, [4, 5]) + webpack.importPath = path.dirname(webpackJsonPath) + webpack.packageJson = require(webpackJsonPath) + webpack.module = require(webpack.importPath) + webpack.majorVersion = getMajorVersion(webpack.packageJson, [4, 5]) - const webpackImportPath = result.webpack.importPath + debug('Webpack: Successfully sourced webpack - %o', webpack) ;(Module as ModuleClass)._load = function (request, parent, isMain) { if (request === 'webpack' || request.startsWith('webpack/')) { const resolvePath = require.resolve(request, { - paths: [webpackImportPath], + paths: [webpack.importPath], }) - debug('Resolve path %s', resolvePath) + debug('Webpack: Module._load resolvePath - %s', resolvePath) return originalModuleLoad(resolvePath, parent, isMain) } return originalModuleLoad(request, parent, isMain) - }; + } - (Module as ModuleClass)._resolveFilename = function (request, parent, isMain, options) { + ;(Module as ModuleClass)._resolveFilename = function (request, parent, isMain, options) { if (request === 'webpack' || request.startsWith('webpack/') && !options?.paths) { - return originalModuleResolveFilename(request, parent, isMain, { - paths: [webpackImportPath], + const resolveFilename = originalModuleResolveFilename(request, parent, isMain, { + paths: [webpack.importPath], }) + + debug('Webpack: Module._resolveFilename resolveFilename - %s', resolveFilename) + + return resolveFilename } return originalModuleResolveFilename(request, parent, isMain, options) } - // Webpack dev server: + return webpack +} + +// Source the webpack-dev-server module from the provided framework or projectRoot. +// If none is found, we fallback to the version bundled with this package. +export function sourceWebpackDevServer (config: WebpackDevServerConfig, framework?: SourcedDependency | null): SourcedWebpackDevServer { + const searchRoot = framework?.importPath ?? config.cypressConfig.projectRoot + + debug('WebpackDevServer: Attempting to source webpack-dev-server from %s', searchRoot) + const webpackDevServer = { } as SourcedWebpackDevServer let webpackDevServerJsonPath: string try { @@ -169,23 +171,36 @@ export function sourceRelativeWebpackModules (config: WebpackDevServerConfig) { }) } catch (e) { if ((e as {code?: string}).code !== 'MODULE_NOT_FOUND') { + debug('WebpackDevServer: Failed to source webpack-dev-server - %s', e) throw e } + debug('WebpackDevServer: Falling back to bundled version') + webpackDevServerJsonPath = require.resolve('webpack-dev-server/package.json', { - paths: [ - __dirname, - ], + paths: [cypressWebpackPath], }) } - result.webpackDevServer.importPath = path.dirname(webpackDevServerJsonPath) - result.webpackDevServer.packageJson = require(webpackDevServerJsonPath) - result.webpackDevServer.module = require(result.webpackDevServer.importPath) - result.webpackDevServer.majorVersion = getMajorVersion(result.webpackDevServer.packageJson, [3, 4]) + webpackDevServer.importPath = path.dirname(webpackDevServerJsonPath) + webpackDevServer.packageJson = require(webpackDevServerJsonPath) + webpackDevServer.module = require(webpackDevServer.importPath) + webpackDevServer.majorVersion = getMajorVersion(webpackDevServer.packageJson, [3, 4]) + + debug('WebpackDevServer: Successfully sourced webpack-dev-server - %o', webpackDevServer) + + return webpackDevServer +} - // Webpack HTML Plugin: +// Source the html-webpack-plugin module from the provided framework or projectRoot. +// If none is found, we fallback to the version bundled with this package dependent on the major version of webpack. +// We ship both v4 and v5 of 'html-webpack-plugin' by aliasing the package with the major version (check package.json). +export function sourceHtmlWebpackPlugin (config: WebpackDevServerConfig, framework: SourcedDependency | null, webpack: SourcedWebpack): SourcedHtmlWebpackPlugin { + const searchRoot = framework?.importPath ?? config.cypressConfig.projectRoot + debug('HtmlWebpackPlugin: Attempting to source html-webpack-plugin from %s', searchRoot) + + const htmlWebpackPlugin = { } as SourcedHtmlWebpackPlugin let htmlWebpackPluginJsonPath: string try { @@ -193,18 +208,21 @@ export function sourceRelativeWebpackModules (config: WebpackDevServerConfig) { paths: [searchRoot], }) - result.htmlWebpackPlugin.packageJson = require(htmlWebpackPluginJsonPath) + htmlWebpackPlugin.packageJson = require(htmlWebpackPluginJsonPath) // Check that they're not using v3 of html-webpack-plugin. Since we should be the only consumer of it, // we shouldn't be concerned with using our own copy if they've shipped w/ an earlier version - result.htmlWebpackPlugin.majorVersion = getMajorVersion(result.htmlWebpackPlugin.packageJson, [4, 5]) + htmlWebpackPlugin.majorVersion = getMajorVersion(htmlWebpackPlugin.packageJson, [4, 5]) } catch (e) { const err = e as Error & {code?: string} if (err.code !== 'MODULE_NOT_FOUND' && !err.message.includes('Unexpected major version')) { + debug('HtmlWebpackPlugin: Failed to source html-webpack-plugin - %s', e) throw e } - const htmlWebpack = `html-webpack-plugin-${result.webpack.majorVersion}` + const htmlWebpack = `html-webpack-plugin-${webpack.majorVersion}` + + debug('HtmlWebpackPlugin: Falling back to bundled version %s', htmlWebpack) htmlWebpackPluginJsonPath = require.resolve(`${htmlWebpack}/package.json`, { paths: [ @@ -213,15 +231,32 @@ export function sourceRelativeWebpackModules (config: WebpackDevServerConfig) { }) } - result.htmlWebpackPlugin.importPath = path.dirname(htmlWebpackPluginJsonPath) - result.htmlWebpackPlugin.packageJson = require(htmlWebpackPluginJsonPath) - result.htmlWebpackPlugin.module = require(result.htmlWebpackPlugin.importPath) - result.htmlWebpackPlugin.majorVersion = getMajorVersion(result.htmlWebpackPlugin.packageJson, [4, 5]) + htmlWebpackPlugin.importPath = path.dirname(htmlWebpackPluginJsonPath), + htmlWebpackPlugin.packageJson = require(htmlWebpackPluginJsonPath), + htmlWebpackPlugin.module = require(htmlWebpackPlugin.importPath), + htmlWebpackPlugin.majorVersion = getMajorVersion(htmlWebpackPlugin.packageJson, [4, 5]) + + debug('HtmlWebpackPlugin: Successfully sourced html-webpack-plugin - %o', htmlWebpackPlugin) - return result + return htmlWebpackPlugin +} + +// Most frameworks follow a similar path for sourcing webpack dependencies so this is a utility to handle all the sourcing. +export function sourceDefaultWebpackDependencies (config: WebpackDevServerConfig): SourceRelativeWebpackResult { + const framework = sourceFramework(config) + const webpack = sourceWebpack(config, framework) + const webpackDevServer = sourceWebpackDevServer(config, framework) + const htmlWebpackPlugin = sourceHtmlWebpackPlugin(config, framework, webpack) + + return { + framework, + webpack, + webpackDevServer, + htmlWebpackPlugin, + } } -function getMajorVersion (json: PackageJson, acceptedVersions: T[]): T { +export function getMajorVersion (json: PackageJson, acceptedVersions: T[]): T { const major = Number(json.version.split('.')[0]) if (!acceptedVersions.includes(major as T)) { diff --git a/npm/webpack-dev-server-fresh/src/helpers/vueCliHandler.ts b/npm/webpack-dev-server-fresh/src/helpers/vueCliHandler.ts index 5850e7d4b79c..e12272a1429e 100644 --- a/npm/webpack-dev-server-fresh/src/helpers/vueCliHandler.ts +++ b/npm/webpack-dev-server-fresh/src/helpers/vueCliHandler.ts @@ -1,22 +1,23 @@ -import type { CreateFinalWebpackConfig } from '../createWebpackDevServer' import debugLib from 'debug' import type { Configuration } from 'webpack' - -type PresetHandler = Omit +import type { PresetHandlerResult, WebpackDevServerConfig } from '../devServer' +import { sourceDefaultWebpackDependencies } from './sourceRelativeWebpackModules' const debug = debugLib('cypress:webpack-dev-server-fresh:vueCliHandler') -export function vueCliHandler ({ devServerConfig }: PresetHandler): Configuration { +export function vueCliHandler (devServerConfig: WebpackDevServerConfig): PresetHandlerResult { + const sourceWebpackModulesResult = sourceDefaultWebpackDependencies(devServerConfig) + try { const config = require.resolve('@vue/cli-service/webpack.config', { paths: [devServerConfig.cypressConfig.projectRoot], }) - const webpackConfig = require(config) + const webpackConfig = require(config) as Configuration debug('webpack config %o', webpackConfig) - return webpackConfig + return { frameworkConfig: webpackConfig, sourceWebpackModulesResult } } catch (e) { console.error(e) // eslint-disable-line no-console throw Error(`Error loading @vue/cli-service/webpack.config.js. Looked in ${require.resolve.paths(devServerConfig.cypressConfig.projectRoot)}`) diff --git a/npm/webpack-dev-server-fresh/test/devServer-e2e.spec.ts b/npm/webpack-dev-server-fresh/test/devServer-e2e.spec.ts index bc38538b73d0..cce81792fe4e 100644 --- a/npm/webpack-dev-server-fresh/test/devServer-e2e.spec.ts +++ b/npm/webpack-dev-server-fresh/test/devServer-e2e.spec.ts @@ -7,6 +7,7 @@ import fs from 'fs' import { devServer } from '..' import { restoreLoadHook } from '../src/helpers/sourceRelativeWebpackModules' +import './support' const requestSpecFile = (file: string, port: number) => { return new Promise((res) => { diff --git a/npm/webpack-dev-server-fresh/test/devServer-unit.spec.ts b/npm/webpack-dev-server-fresh/test/devServer-unit.spec.ts index d0b46ab6140e..6298c2c2a300 100644 --- a/npm/webpack-dev-server-fresh/test/devServer-unit.spec.ts +++ b/npm/webpack-dev-server-fresh/test/devServer-unit.spec.ts @@ -17,7 +17,7 @@ describe('devServer', function () { it('creates a new devServer webpack4, webpackDevServer3', async () => { const { devServer } = proxyquire('../src/devServer', { './helpers/sourceRelativeWebpackModules': { - sourceRelativeWebpackModules: () => { + sourceDefaultWebpackDependencies: () => { return createModuleMatrixResult({ webpack: 4, webpackDevServer: 3, @@ -38,7 +38,7 @@ describe('devServer', function () { it('creates a new devServer webpack4, webpackDevServer4', async () => { const { devServer } = proxyquire('../src/devServer', { './helpers/sourceRelativeWebpackModules': { - sourceRelativeWebpackModules: () => { + sourceDefaultWebpackDependencies: () => { return createModuleMatrixResult({ webpack: 4, webpackDevServer: 4, @@ -59,7 +59,7 @@ describe('devServer', function () { it('creates a new devServer webpack5, webpackDevServer4', async () => { const { devServer } = proxyquire('../src/devServer', { './helpers/sourceRelativeWebpackModules': { - sourceRelativeWebpackModules: () => { + sourceDefaultWebpackDependencies: () => { return createModuleMatrixResult({ webpack: 5, webpackDevServer: 4, diff --git a/npm/webpack-dev-server-fresh/test/handlers/createReactAppHandler.spec.ts b/npm/webpack-dev-server-fresh/test/handlers/createReactAppHandler.spec.ts index b3d206602e84..bcef18318d4d 100644 --- a/npm/webpack-dev-server-fresh/test/handlers/createReactAppHandler.spec.ts +++ b/npm/webpack-dev-server-fresh/test/handlers/createReactAppHandler.spec.ts @@ -1,10 +1,10 @@ import { scaffoldMigrationProject } from '../test-helpers/scaffoldProject' import { expect } from 'chai' import { createReactAppHandler, cypressGlobals } from '../../src/helpers/createReactAppHandler' -import { SourceRelativeWebpackResult } from '../../src/helpers/sourceRelativeWebpackModules' import { WebpackDevServerConfig } from '../../src/devServer' import { Configuration } from 'webpack' import * as path from 'path' +import '../support' const expectEslintModifications = (webpackConfig: Configuration) => { const eslintPlugin: any = webpackConfig.plugins?.find((plugin) => plugin.constructor.name === 'ESLintWebpackPlugin') @@ -55,12 +55,9 @@ describe('createReactAppHandler', function () { process.chdir(projectRoot) - const webpackConfig = createReactAppHandler({ - devServerConfig: { - cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, - } as WebpackDevServerConfig, - sourceWebpackModulesResult: { webpack: { majorVersion: 4 } } as SourceRelativeWebpackResult, - }) + const { frameworkConfig: webpackConfig } = createReactAppHandler({ + cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, + } as WebpackDevServerConfig) expect(webpackConfig.mode).eq('development') expectEslintModifications(webpackConfig) @@ -73,12 +70,9 @@ describe('createReactAppHandler', function () { process.chdir(projectRoot) - const webpackConfig = createReactAppHandler({ - devServerConfig: { - cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, - } as WebpackDevServerConfig, - sourceWebpackModulesResult: { webpack: { majorVersion: 5 } } as SourceRelativeWebpackResult, - }) + const { frameworkConfig: webpackConfig } = createReactAppHandler({ + cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, + } as WebpackDevServerConfig) expect(webpackConfig.mode).eq('development') expectEslintModifications(webpackConfig) @@ -88,16 +82,13 @@ describe('createReactAppHandler', function () { }) it('sources the config from ejected cra', async () => { - const projectRoot = await scaffoldMigrationProject('cra-5') + const projectRoot = await scaffoldMigrationProject('cra-ejected') process.chdir(projectRoot) - const webpackConfig = createReactAppHandler({ - devServerConfig: { - cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, - } as WebpackDevServerConfig, - sourceWebpackModulesResult: { webpack: { majorVersion: 5 } } as SourceRelativeWebpackResult, - }) + const { frameworkConfig: webpackConfig } = createReactAppHandler({ + cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, + } as WebpackDevServerConfig) expect(webpackConfig.mode).eq('development') expectEslintModifications(webpackConfig) diff --git a/npm/webpack-dev-server-fresh/test/handlers/nextHandler.spec.ts b/npm/webpack-dev-server-fresh/test/handlers/nextHandler.spec.ts new file mode 100644 index 000000000000..f5b1c7b9ec9c --- /dev/null +++ b/npm/webpack-dev-server-fresh/test/handlers/nextHandler.spec.ts @@ -0,0 +1,77 @@ +import { scaffoldMigrationProject } from '../test-helpers/scaffoldProject' +import { expect } from 'chai' +import { nextHandler } from '../../src/helpers/nextHandler' +import type { Configuration } from 'webpack' +import * as path from 'path' +import { WebpackDevServerConfig } from '../../src/devServer' +import '../support' + +const expectWatchOverrides = (webpackConfig: Configuration) => { + expect(webpackConfig.watchOptions.ignored).to.contain('**/node_modules/!(@cypress/webpack-dev-server/dist/browser.js)**') +} + +const expectPagesDir = (webpackConfig: Configuration, projectRoot: string) => { + const ReactLoadablePlugin: any = webpackConfig.plugins.find((plugin) => plugin.constructor.name === 'ReactLoadablePlugin') + + expect(ReactLoadablePlugin.pagesDir).eq(path.join(projectRoot, 'pages')) +} + +const expectWebpackSpan = (webpackConfig: Configuration) => { + const ProfilingPlugin: any = webpackConfig.plugins.find((plugin) => plugin.constructor.name === 'ProfilingPlugin') + + expect(ProfilingPlugin.runWebpackSpan).to.exist +} + +describe('nextHandler', function () { + // can take a while since we install node_modules + this.timeout(1000 * 60) + + it('sources from a next-12 project', async () => { + const projectRoot = await scaffoldMigrationProject('next-12') + + process.chdir(projectRoot) + + const { frameworkConfig: webpackConfig } = await nextHandler({ + framework: 'next', + cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, + } as WebpackDevServerConfig) + + expectWatchOverrides(webpackConfig) + expectPagesDir(webpackConfig, projectRoot) + expectWebpackSpan(webpackConfig) + }) + + it('sources from a next-11 project', async () => { + const projectRoot = await scaffoldMigrationProject('next-11') + + process.chdir(projectRoot) + + const { frameworkConfig: webpackConfig } = await nextHandler({ + framework: 'next', + cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, + } as WebpackDevServerConfig) + + expectWatchOverrides(webpackConfig) + expectPagesDir(webpackConfig, projectRoot) + expectWebpackSpan(webpackConfig) + }) + + it('throws if nodeVersion is set to bundled', async () => { + const projectRoot = await scaffoldMigrationProject('next-12') + + process.chdir(projectRoot) + + let err + + try { + await nextHandler({ + + framework: 'next', cypressConfig: { projectRoot, nodeVersion: 'bundled' } as Cypress.PluginConfigOptions, + } as WebpackDevServerConfig) + } catch (e) { + err = e + } + + expect(err.message).to.contain('Cypress cannot compile your Next.js application when "nodeVersion" is set to "bundled".') + }) +}) diff --git a/npm/webpack-dev-server-fresh/test/handlers/nuxtHandler.spec.ts b/npm/webpack-dev-server-fresh/test/handlers/nuxtHandler.spec.ts index 36eb916bc054..edba8b9b5a0c 100644 --- a/npm/webpack-dev-server-fresh/test/handlers/nuxtHandler.spec.ts +++ b/npm/webpack-dev-server-fresh/test/handlers/nuxtHandler.spec.ts @@ -1,6 +1,8 @@ import { scaffoldMigrationProject } from '../test-helpers/scaffoldProject' import { expect } from 'chai' import { nuxtHandler } from '../../src/helpers/nuxtHandler' +import { WebpackDevServerConfig } from '../../src/devServer' +import '../support' describe('nuxtHandler', function () { // can take a while since we install node_modules @@ -11,14 +13,12 @@ describe('nuxtHandler', function () { process.chdir(projectRoot) - const config = await nuxtHandler({ - devServerConfig: { - cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, - }, - } as any) + const { frameworkConfig: webpackConfig } = await nuxtHandler({ + cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, + } as WebpackDevServerConfig) // Verify it's a Vue-specific webpack config by seeing if VueLoader is present. - expect(config.plugins.find((plug) => plug.constructor.name === 'VueLoader')) - expect(config.performance).to.be.undefined + expect(webpackConfig.plugins.find((plug) => plug.constructor.name === 'VueLoader')) + expect(webpackConfig.performance).to.be.undefined }) }) diff --git a/npm/webpack-dev-server-fresh/test/handlers/vueCliHandler.spec.ts b/npm/webpack-dev-server-fresh/test/handlers/vueCliHandler.spec.ts index a400ebb39048..11ebd37c3a96 100644 --- a/npm/webpack-dev-server-fresh/test/handlers/vueCliHandler.spec.ts +++ b/npm/webpack-dev-server-fresh/test/handlers/vueCliHandler.spec.ts @@ -1,6 +1,8 @@ import { scaffoldMigrationProject } from '../test-helpers/scaffoldProject' import { expect } from 'chai' import { vueCliHandler } from '../../src/helpers/vueCliHandler' +import { WebpackDevServerConfig } from '../../src/devServer' +import '../support' describe('vueCliHandler', function () { // can take a while since we install node_modules @@ -11,14 +13,12 @@ describe('vueCliHandler', function () { process.chdir(projectRoot) - const config = vueCliHandler({ - devServerConfig: { - cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, - }, - } as any) + const { frameworkConfig: webpackConfig } = vueCliHandler({ + cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, + } as WebpackDevServerConfig) // Verify it's a Vue-specific webpack config by seeing if VueLoader is present. - expect(config.plugins.find((plug) => plug.constructor.name === 'VueLoader')) + expect(webpackConfig.plugins.find((plug) => plug.constructor.name === 'VueLoader')) }) it('sources from a @vue/cli-service@4.x project with Vue 2', async () => { @@ -26,13 +26,11 @@ describe('vueCliHandler', function () { process.chdir(projectRoot) - const config = vueCliHandler({ - devServerConfig: { - cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, - }, - } as any) + const { frameworkConfig: webpackConfig } = vueCliHandler({ + cypressConfig: { projectRoot } as Cypress.PluginConfigOptions, + } as WebpackDevServerConfig) // Verify it's a Vue-specific webpack config by seeing if VueLoader is present. - expect(config.plugins.find((plug) => plug.constructor.name === 'VueLoader')) + expect(webpackConfig.plugins.find((plug) => plug.constructor.name === 'VueLoader')) }) }) diff --git a/npm/webpack-dev-server-fresh/test/makeWebpackConfig.spec.ts b/npm/webpack-dev-server-fresh/test/makeWebpackConfig.spec.ts index e2d458b119c5..ad8fe5c43b8f 100644 --- a/npm/webpack-dev-server-fresh/test/makeWebpackConfig.spec.ts +++ b/npm/webpack-dev-server-fresh/test/makeWebpackConfig.spec.ts @@ -2,7 +2,7 @@ import { expect } from 'chai' import EventEmitter from 'events' import snapshot from 'snap-shot-it' import { WebpackDevServerConfig } from '../src/devServer' -import { sourceRelativeWebpackModules } from '../src/helpers/sourceRelativeWebpackModules' +import { sourceDefaultWebpackDependencies } from '../src/helpers/sourceRelativeWebpackModules' import { makeWebpackConfig } from '../src/makeWebpackConfig' describe('makeWebpackConfig', () => { @@ -24,7 +24,7 @@ describe('makeWebpackConfig', () => { } const actual = await makeWebpackConfig({ devServerConfig, - sourceWebpackModulesResult: sourceRelativeWebpackModules(devServerConfig), + sourceWebpackModulesResult: sourceDefaultWebpackDependencies(devServerConfig), }) // plugins contain circular deps which cannot be serialized in a snapshot. diff --git a/npm/webpack-dev-server-fresh/test/sourceRelativeWebpackModules.spec.ts b/npm/webpack-dev-server-fresh/test/sourceRelativeWebpackModules.spec.ts index 8925d72f6986..27ce4e0a3d24 100644 --- a/npm/webpack-dev-server-fresh/test/sourceRelativeWebpackModules.spec.ts +++ b/npm/webpack-dev-server-fresh/test/sourceRelativeWebpackModules.spec.ts @@ -5,8 +5,9 @@ import { expect } from 'chai' import path from 'path' import fs from 'fs' -import { restoreLoadHook, sourceRelativeWebpackModules } from '../src/helpers/sourceRelativeWebpackModules' +import { sourceDefaultWebpackDependencies } from '../src/helpers/sourceRelativeWebpackModules' import { WebpackDevServerConfig } from '../src/devServer' +import './support' type ProjectDirs = typeof fixtureDirs @@ -45,7 +46,7 @@ async function sourceModulesForProject (fixture: ProjectDirs[number]) { await FixturesScaffold.scaffoldProjectNodeModules(fixture) - const result = sourceRelativeWebpackModules({ + const result = sourceDefaultWebpackDependencies({ cypressConfig: { projectRoot, }, @@ -56,16 +57,7 @@ async function sourceModulesForProject (fixture: ProjectDirs[number]) { // Ensures that we are properly sourcing the webpacks from the node_modules in the given project, // rather than from the node_modules in the project root -describe('sourceRelativeWebpackModules', () => { - beforeEach(() => { - delete require.cache - restoreLoadHook() - }) - - after(() => { - restoreLoadHook() - }) - +describe('sourceDefaultWebpackDependencies', () => { for (const [fixture, versionsToMatch] of Object.entries(WEBPACK_REACT)) { describe(fixture, () => { it(`sources the correct webpack versions for ${fixture}`, async () => { diff --git a/npm/webpack-dev-server-fresh/test/support.ts b/npm/webpack-dev-server-fresh/test/support.ts new file mode 100644 index 000000000000..e04c5259d13f --- /dev/null +++ b/npm/webpack-dev-server-fresh/test/support.ts @@ -0,0 +1,10 @@ +import { restoreLoadHook } from '../src/helpers/sourceRelativeWebpackModules' + +beforeEach(() => { + delete require.cache + restoreLoadHook() +}) + +after(() => { + restoreLoadHook() +}) diff --git a/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts index 9e2758b64661..fbb1a2504d20 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts @@ -174,8 +174,20 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout: cy.contains('switch spec') cy.contains('withWait.spec').click() - cy.wait(5000) - cy.get('.passed > .num').should('contain', 4) + cy.get('.passed > .num', { timeout: 10000 }).should('contain', 4) cy.get('.failed > .num').should('not.contain', 1) }) + + it('executes a test, navigates back to the spec list, creates a new spec, and runs the new spec', () => { + cy.visitApp() + cy.contains('dom-content.spec').click() + cy.get('[data-model-state="passed"]').should('contain', 'renders the test content') + cy.contains('a', 'Specs').click() + cy.withCtx(async (ctx, o) => { + await ctx.actions.file.writeFileInProject(o.path, `describe('Simple Test', () => { it('true is true', () => { expect(true).to.be.true }) })`) + }, { path: getPathForPlatform('cypress/e2e/new-file.spec.js') }) + + cy.contains('new-file.spec').click() + cy.get('[data-model-state="passed"]').should('contain', 'expected true to be true') + }) }) diff --git a/packages/app/cypress/e2e/cypress-in-cypress.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress.cy.ts index 4548780f9626..736ae6c01818 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress.cy.ts @@ -1,14 +1,26 @@ +import type { DraggablePanel } from '../../src/runner/useRunnerStyle' + +const testingTypes = ['component', 'e2e'] as const + +const dragHandleToClientX = (panel: DraggablePanel, x: number) => { + return cy.get(`[data-cy="${panel}ResizeHandle"]`).trigger('mousedown', { eventConstructor: 'MouseEvent' }) + .trigger('mousemove', { clientX: x }) + .trigger('mouseup', { eventConstructor: 'MouseEvent' }) +} + +function startAtSpecsPage (testingType: typeof testingTypes[number]) { + cy.scaffoldProject('cypress-in-cypress') + cy.findBrowsers() + cy.openProject('cypress-in-cypress') + cy.startAppServer(testingType) + cy.visitApp() +} + // For Cypress-in-Cypress tests that do not vary based on testing type describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 10000 }, () => { - const testingTypes = ['component', 'e2e'] as const - testingTypes.forEach((testingType) => { it(`handles automation disconnects in ${testingType}`, () => { - cy.scaffoldProject('cypress-in-cypress') - cy.findBrowsers() - cy.openProject('cypress-in-cypress') - cy.startAppServer(testingType) - cy.visitApp() + startAtSpecsPage(testingType) cy.get('[data-cy="spec-item"]').first().click() // Let runner stabilize @@ -36,11 +48,7 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100 it(`handles automation missing in ${testingType}`, () => { let connectedCallback: any - cy.scaffoldProject('cypress-in-cypress') - cy.findBrowsers() - cy.openProject('cypress-in-cypress') - cy.startAppServer(testingType) - cy.visitApp() + startAtSpecsPage(testingType) cy.window().then((win) => { if (!win.ws) { @@ -84,5 +92,78 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100 expect(ctx.actions.project.launchProject).to.have.been.called }) }) + + it(`scales the AUT correctly in ${testingType}`, () => { + const assertNoScaleShown = () => { + // check that no message about scale % is shown, + // meaning the AUT is at 100% scale + cy.contains('%)').should('not.exist') + } + + cy.scaffoldProject('cypress-in-cypress') + cy.findBrowsers() + cy.openProject('cypress-in-cypress') + cy.withCtx((ctx) => { + ctx.coreData.localSettings.preferences.reporterWidth = 800 + }) + + cy.startAppServer(testingType) + cy.visitApp() + + cy.get('[data-cy="spec-item"]').first().click() + // Let runner stabilize + cy.get('#unified-reporter').should('be.visible') + + // validate that the width we set in `withCtx` above is the starting point + cy.get(`[data-cy="reporter-panel"]`).invoke('outerWidth').should('eq', 800) + cy.percySnapshot('initial state') + + // we will move the right-hand handle of the Reporter + // to these positions from the left of the screen + const dragPositions = [1000, 1090, 900, 600] + + // based on viewport sizes for CT and e2e tests in the `cypress-in-cypress` + // projects, we expect certain scale % values to be shown + const testingTypeExpectedScales = { + component: ['93%', '75%'], + e2e: ['46%', '37%', '56%', '85%'], + componentShortViewport: '61%', + e2eShortViewport: '46%', + } + + // resize the reporter using each of the dragPositions and take Percy snapshots + dragPositions.forEach((position, index) => { + dragHandleToClientX('panel2', position).then(() => { + const expectedScale = testingTypeExpectedScales[testingType][index] + + // CT hits 100% scale "earlier" than E2E, so sometimes there is no expected scale + if (expectedScale) { + cy.contains(expectedScale).should('be.visible') + } else { + assertNoScaleShown() + } + + cy.percySnapshot(`panel 2 at ${ position } px`) + }) + }) + + // now check vertical scaling with viewport resize, and take some snapshots too + + // this viewport should be tall enough to not scale even the e2e test + cy.viewport(1500, 1300) + + // make sure the reporter is narrow enough (should be, but don't want to depend on leftover state from above) + dragHandleToClientX('panel2', 400).then(() => { + // but we have to also collapse the Specs List to remove any reason to scale horizontally + cy.contains('[aria-controls=reporter-inline-specs-list]', 'Specs').click() + + assertNoScaleShown() + cy.percySnapshot('tall viewport') + + cy.viewport(1500, 400) + cy.contains(testingTypeExpectedScales[`${ testingType }ShortViewport`]).should('exist') + cy.percySnapshot('short viewport') + }) + }) }) }) diff --git a/packages/app/cypress/e2e/runner/retries.mochaEvents.cy.ts b/packages/app/cypress/e2e/runner/retries.mochaEvents.cy.ts index 8ad4077cd967..31c2f331c777 100644 --- a/packages/app/cypress/e2e/runner/retries.mochaEvents.cy.ts +++ b/packages/app/cypress/e2e/runner/retries.mochaEvents.cy.ts @@ -2,7 +2,13 @@ import { runSpec } from './support/spec-loader' import { runCypressInCypressMochaEventsTest } from './support/mochaEventsUtils' import { snapshots } from './retries.mochaEvents.snapshots' -describe('src/cypress/runner retries mochaEvents', { retries: 0 }, () => { +/** + * These tests are slow, particular slow on windows, thus the + * 7500m timeout. + * TODO: Find out if they are objectively slower on windows than on linux, + * and if it's a 10.x specific performance regression. + */ +describe('src/cypress/runner retries mochaEvents', { retries: 0, defaultCommandTimeout: 7500 }, () => { // NOTE: for test-retries it('simple retry', (done) => { diff --git a/packages/app/cypress/e2e/runner/runner.mochaEvents.cy.ts b/packages/app/cypress/e2e/runner/runner.mochaEvents.cy.ts index 6f3c276cdfa8..f2443de13c4c 100644 --- a/packages/app/cypress/e2e/runner/runner.mochaEvents.cy.ts +++ b/packages/app/cypress/e2e/runner/runner.mochaEvents.cy.ts @@ -2,7 +2,7 @@ import { runSpec } from './support/spec-loader' import { runCypressInCypressMochaEventsTest } from './support/mochaEventsUtils' import { snapshots } from './runner.mochaEvents.snapshots' -describe('src/cypress/runner', { retries: 0 }, () => { +describe('src/cypress/runner', { retries: 0, defaultCommandTimeout: 7500 }, () => { describe('tests finish with correct state', () => { describe('hook failures', () => { it('fail in [before]', (done) => { diff --git a/packages/app/cypress/e2e/runner/runner.ui.cy.ts b/packages/app/cypress/e2e/runner/runner.ui.cy.ts index eb328524c8fa..f30959a79889 100644 --- a/packages/app/cypress/e2e/runner/runner.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/runner.ui.cy.ts @@ -1,4 +1,5 @@ import { loadSpec } from './support/spec-loader' +import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json' describe('src/cypress/runner', () => { describe('tests finish with correct state', () => { @@ -110,8 +111,10 @@ describe('src/cypress/runner', () => { .click() .should('have.class', 'command-is-pinned') + const { highlightsLabel } = defaultMessages.runner.snapshot + // disable highlights - cy.get('[id="toggle-highlights"]') + cy.findByLabelText(highlightsLabel) .should('have.attr', 'aria-checked', 'true') .click() .should('have.attr', 'aria-checked', 'false') @@ -123,7 +126,8 @@ describe('src/cypress/runner', () => { .click() .should('have.class', 'command-is-pinned') - cy.get('[id="toggle-highlights"]').should('have.attr', 'aria-checked', 'true') + cy.findByLabelText(highlightsLabel) + .should('have.attr', 'aria-checked', 'true') }) it('correctly resets named snapshot toggle state when pinning new command', () => { diff --git a/packages/app/cypress/e2e/runs.cy.ts b/packages/app/cypress/e2e/runs.cy.ts index 2aac845978c0..fb450748f13a 100644 --- a/packages/app/cypress/e2e/runs.cy.ts +++ b/packages/app/cypress/e2e/runs.cy.ts @@ -255,7 +255,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { cy.get('[data-cy="selectProject"] button').click() cy.findByText('Mock Project').click() cy.findByText(defaultMessages.runs.connect.modal.selectProject.connectProject).click() - cy.get('[data-cy="runs"]') + cy.get('[data-cy="runs"]', { timeout: 7500 }) }) }) diff --git a/packages/app/cypress/e2e/sidebar_navigation.cy.ts b/packages/app/cypress/e2e/sidebar_navigation.cy.ts index 77c9cab23e21..af97ffe3e451 100644 --- a/packages/app/cypress/e2e/sidebar_navigation.cy.ts +++ b/packages/app/cypress/e2e/sidebar_navigation.cy.ts @@ -25,7 +25,6 @@ describe('Sidebar Navigation', () => { context('as e2e testing type', () => { beforeEach(() => { cy.scaffoldProject('todos') - cy.scaffoldProject('pristine-with-e2e-testing') cy.openProject('todos') cy.startAppServer() cy.visitApp() @@ -51,6 +50,7 @@ describe('Sidebar Navigation', () => { it('closes the left nav bar when clicking the expand button (if expanded)', () => { cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'true') + cy.contains('todos') cy.findAllByText('todos').eq(1).as('title') cy.get('@title').should('be.visible') @@ -114,20 +114,20 @@ describe('Sidebar Navigation', () => { cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'false') cy.get('[data-cy="sidebar-header"').trigger('mouseenter') - cy.contains('#tooltip-target > div', 'todos') + cy.contains('.v-popper--some-open--tooltip', 'todos') cy.percySnapshot() cy.get('[data-cy="sidebar-header"]').trigger('mouseout') cy.get('[data-e2e-href="/runs"]').trigger('mouseenter') - cy.contains('#tooltip-target > div', 'Runs') + cy.contains('.v-popper--some-open--tooltip', 'Runs') cy.get('[data-e2e-href="/runs"]').trigger('mouseout') cy.get('[data-e2e-href="/specs"]').trigger('mouseenter') - cy.contains('#tooltip-target > div', 'Specs') + cy.contains('.v-popper--some-open--tooltip', 'Specs') cy.get('[data-e2e-href="/specs"]').trigger('mouseout') cy.get('[data-e2e-href="/settings"]').trigger('mouseenter') - cy.contains('#tooltip-target > div', 'Settings') + cy.contains('.v-popper--some-open--tooltip', 'Settings') cy.get('[data-e2e-href="/settings"]').trigger('mouseout') }) @@ -342,40 +342,5 @@ describe('Sidebar Navigation', () => { expect(ctx.actions.project.reconfigureProject).to.have.been.called }) }) - - it('shows dropdown to reconfigure project when clicking switch testing type', () => { - cy.scaffoldProject('pristine-with-ct-testing') - cy.openProject('pristine-with-ct-testing') - cy.startAppServer('component') - cy.visitApp() - - cy.get('[data-cy="sidebar-header"]').as('switchTestingType').click() - cy.findByRole('dialog', { - name: 'Choose a testing type', - }).should('be.visible') - - cy.get('[data-cy-testingtype=component]').within(() => { - cy.contains('Running') - }).click() - - cy.findByRole('dialog', { - name: 'Choose a testing type', - }).should('not.exist') - - cy.get('@switchTestingType').click() - cy.findByRole('dialog', { - name: 'Choose a testing type', - }).should('be.visible') - - cy.get('[data-cy-testingtype="e2e"]').within(() => { - cy.contains('Not Configured') - }) - - cy.get('[data-cy-testingtype="component"]').within(() => { - cy.get('[data-cy=status-badge-menu]').click() - cy.get('[data-cy="Choose a Browser"]').should('not.exist') - cy.get('[data-cy="Reconfigure"]').should('exist') - }) - }) }) }) diff --git a/packages/app/cypress/e2e/specs_list_actual_git_repo.cy.ts b/packages/app/cypress/e2e/specs_list_actual_git_repo.cy.ts index de703b9d993d..f545e1c0d58d 100644 --- a/packages/app/cypress/e2e/specs_list_actual_git_repo.cy.ts +++ b/packages/app/cypress/e2e/specs_list_actual_git_repo.cy.ts @@ -13,21 +13,33 @@ describe('Spec List - Git Status', () => { it('shows correct git status for files using real git repo', () => { // newly created, not yet committed // this is performed by the task `initGitRepoForTestProject` - cy.get('[data-cy-row="cypress/e2e/foo.spec.js"]') + cy.get('[data-cy-row="foo.spec.js"]') .contains('Created') .get('[data-cy="git-status-created"]') // modified by not yet committed // this is performed by the task `initGitRepoForTestProject` - cy.get('[data-cy-row="cypress/e2e/blank-contents.spec.js"]') + cy.get('[data-cy-row="blank-contents.spec.js"]') .contains('Modified') .get('[data-cy="git-status-modified"]') // unmodified by current user // we still show "modified" but a different style, indicating the last // person to touch the file. - cy.get('[data-cy-row="cypress/e2e/dom-container.spec.js"]') + cy.get('[data-cy-row="dom-container.spec.js"]') .contains('Modified') .get('[data-cy="git-status-unmodified"]') + + cy.withCtx((ctx) => { + ctx.fs.writeFileSync( + ctx.path.join(ctx.currentProject!, 'cypress', 'e2e', 'dom-container.spec.js'), + '// modifying the spec.', + ) + }) + + // should update via GraphQL subscription, now the status is modified. + cy.get('[data-cy-row="dom-container.spec.js"]') + .contains('Modified') + .get('[data-cy="git-status-modified"]') }) }) diff --git a/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts b/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts index 27d95a913c15..035aa5e58852 100644 --- a/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts +++ b/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts @@ -135,7 +135,7 @@ module.exports = { }`) }) - cy.get('[data-cy="spec-item-link"]') + cy.get('[data-cy="spec-item-link"]', { timeout: 7500 }) .should('have.length', 2) .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -250,7 +250,7 @@ module.exports = { }`) }) - cy.get('[data-testid="spec-file-item"]') + cy.get('[data-testid="spec-file-item"]', { timeout: 7500 }) .should('have.length', 2) .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -352,7 +352,7 @@ module.exports = { }`) }) - cy.get('[data-cy="file-match-indicator"]') + cy.get('[data-cy="file-match-indicator"]', { timeout: 7500 }) .should('contain', '2 Matches') }) }) diff --git a/packages/app/src/layouts/default.vue b/packages/app/src/layouts/default.vue index cf83d10a902c..f25b7ad725a9 100644 --- a/packages/app/src/layouts/default.vue +++ b/packages/app/src/layouts/default.vue @@ -33,7 +33,6 @@ -
- - diff --git a/packages/app/src/runner/SnapshotControls.cy.tsx b/packages/app/src/runner/SnapshotControls.cy.tsx index 54047cb66719..67d5e6d30ac7 100644 --- a/packages/app/src/runner/SnapshotControls.cy.tsx +++ b/packages/app/src/runner/SnapshotControls.cy.tsx @@ -2,6 +2,7 @@ import SnapshotControls from './SnapshotControls.vue' import { autSnapshot } from '../../cypress/support/fixtures' import { useSnapshotStore } from './snapshot-store' import { createEventManager, createTestAutIframe } from '../../cypress/component/support/ctSupport' +import { defaultMessages } from '@cy/i18n' const snapshotWithSnapshots = { ...autSnapshot } const snapshotPinned = { ...autSnapshot, snapshots: [] } @@ -91,9 +92,7 @@ describe('SnapshotControls', { viewportHeight: 200, viewportWidth: 500 }, () => mountSnapshotControls(eventManager, autIframe) cy.get('body') - .findByText('Highlights') - .should('be.visible') - .findByLabelText('Toggle highlights') + .findByLabelText(defaultMessages.runner.snapshot.highlightsLabel) .click({ force: true }) }) diff --git a/packages/app/src/runner/SnapshotHighlightControls.vue b/packages/app/src/runner/SnapshotHighlightControls.vue index 9aa3e2688be4..a7183924a2bf 100644 --- a/packages/app/src/runner/SnapshotHighlightControls.vue +++ b/packages/app/src/runner/SnapshotHighlightControls.vue @@ -1,16 +1,16 @@ diff --git a/packages/app/src/runner/SpecRunnerContainerOpenMode.vue b/packages/app/src/runner/SpecRunnerContainerOpenMode.vue index 76e42a1867a3..47d7853a6f35 100644 --- a/packages/app/src/runner/SpecRunnerContainerOpenMode.vue +++ b/packages/app/src/runner/SpecRunnerContainerOpenMode.vue @@ -22,24 +22,23 @@ const specStore = useSpecStore() const router = useRouter() const route = useRoute() -const { initialized, watchSpec } = useUnifiedRunner() - const specs = computed(() => { return props.gql.currentProject?.specs ?? [] }) -watchSpec(specs) +const { initialized, watchSpecs } = useUnifiedRunner() -specStore.$subscribe((mutation, state) => { - const file = getPathForPlatform(route.query.file as string) +watchSpecs(specs) - const shouldRedirect = route.name === 'SpecRunner' && file && state.activeSpec === null +specStore.$subscribe((mutation, state) => { + const queryFile = getPathForPlatform(route.query.file as string) + const shouldRedirect = route.name === 'SpecRunner' && queryFile && state.activeSpec === null if (shouldRedirect) { router.push({ name: 'Specs', params: { - unrunnable: file, + unrunnable: queryFile, }, }) } diff --git a/packages/app/src/runner/SpecRunnerContainerRunMode.vue b/packages/app/src/runner/SpecRunnerContainerRunMode.vue index 7ded77c467ea..322ea079751e 100644 --- a/packages/app/src/runner/SpecRunnerContainerRunMode.vue +++ b/packages/app/src/runner/SpecRunnerContainerRunMode.vue @@ -22,7 +22,7 @@ const props = defineProps<{ const specStore = useSpecStore() -const { initialized, watchSpec } = useUnifiedRunner() +const { initialized, watchSpecs } = useUnifiedRunner() -watchSpec(ref(props.runModeSpecs)) +watchSpecs(ref(props.runModeSpecs)) diff --git a/packages/app/src/runner/SpecRunnerHeaderOpenMode.vue b/packages/app/src/runner/SpecRunnerHeaderOpenMode.vue index f7714a4f5d16..0c363ac7bc2e 100644 --- a/packages/app/src/runner/SpecRunnerHeaderOpenMode.vue +++ b/packages/app/src/runner/SpecRunnerHeaderOpenMode.vue @@ -1,6 +1,7 @@