diff --git a/npm/vite-dev-server/src/resolveConfig.ts b/npm/vite-dev-server/src/resolveConfig.ts index ecc56c8a4c12..b9e48f7931ea 100644 --- a/npm/vite-dev-server/src/resolveConfig.ts +++ b/npm/vite-dev-server/src/resolveConfig.ts @@ -16,7 +16,7 @@ import type { Vite } from './getVite' const debug = debugFn('cypress:vite-dev-server:resolve-config') -export const createViteDevServerConfig = async (config: ViteDevServerConfig, vite: Vite) => { +export const createViteDevServerConfig = async (config: ViteDevServerConfig, vite: Vite): Promise => { const { specs, cypressConfig, viteConfig: viteOverrides } = config const root = cypressConfig.projectRoot const { default: findUp } = await importModule('find-up') @@ -90,6 +90,17 @@ export const createViteDevServerConfig = async (config: ViteDevServerConfig, vit ].filter((p) => p != null), } + if (config.cypressConfig.isTextTerminal) { + viteBaseConfig.server = { + ...(viteBaseConfig.server || {}), + // Disable file watching and HMR when executing tests in `run` mode + watch: { + ignored: '**/*', + }, + hmr: false, + } + } + let resolvedOverrides: UserConfig = {} if (typeof viteOverrides === 'function') { diff --git a/npm/vite-dev-server/test/resolveConfig.spec.ts b/npm/vite-dev-server/test/resolveConfig.spec.ts index c4182607e569..a601398f2178 100644 --- a/npm/vite-dev-server/test/resolveConfig.spec.ts +++ b/npm/vite-dev-server/test/resolveConfig.spec.ts @@ -89,4 +89,30 @@ describe('resolveConfig', function () { }) }) }) + + describe('file watching', () => { + let viteDevServerConfig: ViteDevServerConfig + + beforeEach(async () => { + const projectRoot = await scaffoldSystemTestProject('vite-inspect') + + viteDevServerConfig = getViteDevServerConfig(projectRoot) + }) + + it('should be disabled in run mode', async () => { + viteDevServerConfig.cypressConfig.isTextTerminal = true + const viteConfig = await createViteDevServerConfig(viteDevServerConfig, vite) + + expect(viteConfig.server?.watch?.ignored).to.eql('**/*') + expect(viteConfig.server?.hmr).to.be.false + }) + + it('uses defaults in open mode', async () => { + viteDevServerConfig.cypressConfig.isTextTerminal = false + const viteConfig = await createViteDevServerConfig(viteDevServerConfig, vite) + + expect(viteConfig.server?.watch?.ignored).to.be.undefined + expect(viteConfig.server?.hmr).to.be.undefined + }) + }) }) diff --git a/npm/webpack-dev-server/src/createWebpackDevServer.ts b/npm/webpack-dev-server/src/createWebpackDevServer.ts index e410b4611125..7739f9eed9f2 100644 --- a/npm/webpack-dev-server/src/createWebpackDevServer.ts +++ b/npm/webpack-dev-server/src/createWebpackDevServer.ts @@ -1,5 +1,6 @@ import debugLib from 'debug' -import type { Configuration } from 'webpack-dev-server-3' +import type { Configuration as WebpackDevServer3Configuration } from 'webpack-dev-server-3' +import type { Configuration as WebpackDevServer4Configuration } from 'webpack-dev-server' import type { WebpackDevServerConfig } from './devServer' import type { SourceRelativeWebpackResult } from './helpers/sourceRelativeWebpackModules' @@ -66,8 +67,9 @@ function webpackDevServer4 ( finalWebpackConfig: Record, ) { const { devServerConfig: { cypressConfig: { devServerPublicPathRoute } } } = config + const isOpenMode = !config.devServerConfig.cypressConfig.isTextTerminal const WebpackDevServer = config.sourceWebpackModulesResult.webpackDevServer.module - const webpackDevServerConfig = { + const webpackDevServerConfig: WebpackDevServer4Configuration = { host: '127.0.0.1', port: 'auto', // @ts-ignore @@ -77,6 +79,8 @@ function webpackDevServer4 ( stats: finalWebpackConfig.stats ?? 'minimal', }, hot: false, + // Only enable file watching & reload when executing tests in `open` mode + liveReload: isOpenMode, } const server = new WebpackDevServer(webpackDevServerConfig, compiler) @@ -93,8 +97,9 @@ function webpackDevServer3 ( finalWebpackConfig: Record, ) { const { devServerConfig: { cypressConfig: { devServerPublicPathRoute } } } = config + const isOpenMode = !config.devServerConfig.cypressConfig.isTextTerminal const WebpackDevServer = config.sourceWebpackModulesResult.webpackDevServer.module - const webpackDevServerConfig: Configuration = { + const webpackDevServerConfig: WebpackDevServer3Configuration = { // @ts-ignore ...finalWebpackConfig.devServer ?? {}, hot: false, @@ -103,6 +108,8 @@ function webpackDevServer3 ( publicPath: devServerPublicPathRoute, noInfo: false, stats: finalWebpackConfig.stats ?? 'minimal', + // Only enable file watching & reload when executing tests in `open` mode + liveReload: isOpenMode, } const server = new WebpackDevServer(compiler, webpackDevServerConfig) diff --git a/npm/webpack-dev-server/src/makeDefaultWebpackConfig.ts b/npm/webpack-dev-server/src/makeDefaultWebpackConfig.ts index 03275548f97d..9401a9a06ff6 100644 --- a/npm/webpack-dev-server/src/makeDefaultWebpackConfig.ts +++ b/npm/webpack-dev-server/src/makeDefaultWebpackConfig.ts @@ -20,6 +20,7 @@ export function makeDefaultWebpackConfig ( importPath, } = config.sourceWebpackModulesResult.htmlWebpackPlugin const indexHtmlFile = config.devServerConfig.cypressConfig.indexHtmlFile + const isRunMode = config.devServerConfig.cypressConfig.isTextTerminal const HtmlWebpackPlugin = _HtmlWebpackPlugin as typeof import('html-webpack-plugin-5') debug(`Using HtmlWebpackPlugin version ${version} from ${importPath}`) @@ -55,6 +56,13 @@ export function makeDefaultWebpackConfig ( devtool: 'inline-source-map', } as any + if (isRunMode) { + // Disable file watching when executing tests in `run` mode + finalConfig.watchOptions = { + ignored: '**/*', + } + } + if (config.sourceWebpackModulesResult.webpackDevServer.majorVersion === 4) { return { ...finalConfig, diff --git a/npm/webpack-dev-server/test/devServer-e2e.spec.ts b/npm/webpack-dev-server/test/devServer-e2e.spec.ts index 0fa39d3a32dd..42f3a621374b 100644 --- a/npm/webpack-dev-server/test/devServer-e2e.spec.ts +++ b/npm/webpack-dev-server/test/devServer-e2e.spec.ts @@ -2,11 +2,12 @@ import path from 'path' import { expect } from 'chai' import { once, EventEmitter } from 'events' import http from 'http' -import fs from 'fs' +import fs from 'fs-extra' import { devServer } from '..' import { restoreLoadHook } from '../src/helpers/sourceRelativeWebpackModules' import './support' +import type { ConfigHandler } from '../src/devServer' const requestSpecFile = (file: string, port: number) => { return new Promise((res) => { @@ -34,7 +35,7 @@ const requestSpecFile = (file: string, port: number) => { const root = path.join(__dirname, '..') -const webpackConfig = { +const webpackConfig: ConfigHandler = { devServer: { static: { directory: root } }, } @@ -48,6 +49,20 @@ const createSpecs = (name: string): Cypress.Cypress['spec'][] => { ] } +type DevServerCloseFn = Awaited>['close'] + +const closeServer = async (closeFn: DevServerCloseFn) => { + await new Promise((resolve, reject) => { + closeFn((err?: Error) => { + if (err) { + return reject(err) + } + + resolve() + }) + }) +} + const cypressConfig = { projectRoot: root, supportFile: '', @@ -78,15 +93,7 @@ describe('#devServer', () => { expect(response).to.eq('const foo = () => {}\n') - await new Promise((resolve, reject) => { - close((err) => { - if (err) { - return reject(err) - } - - resolve() - }) - }) + await closeServer(close) }) it('serves specs in directory with [] chars via a webpack dev server', async () => { @@ -101,9 +108,7 @@ describe('#devServer', () => { expect(response).to.eq(`it('this is a spec with a path containing []', () => {})\n`) - return new Promise((res) => { - close(() => res()) - }) + return closeServer(close) }) it('serves specs in directory with non English chars via a webpack dev server', async () => { @@ -118,9 +123,7 @@ describe('#devServer', () => { expect(response).to.eq(`it('サイプレス', () => {})\n`) - return new Promise((res) => { - close(() => res()) - }) + return closeServer(close) }) it('serves specs in directory with ... in the file name via a webpack dev server', async () => { @@ -135,9 +138,7 @@ describe('#devServer', () => { expect(response).to.eq(`it('...bar', () => {})\n`) - return new Promise((res) => { - close(() => res()) - }) + return closeServer(close) }) it('serves a file with spaces via a webpack dev server', async () => { @@ -152,9 +153,7 @@ describe('#devServer', () => { expect(response).to.eq(`it('this is a spec with a path containing a space', () => {})\n`) - return new Promise((res) => { - close(() => res()) - }) + return closeServer(close) }) it('emits dev-server:compile:success event on successful compilation', async () => { @@ -167,18 +166,13 @@ describe('#devServer', () => { }) await once(devServerEvents, 'dev-server:compile:success') - await new Promise((resolve, reject) => { - close((err) => { - if (err) { - return reject(err) - } - resolve() - }) - }) + await closeServer(close) }) it('touches browser.js when a spec file is added and recompile', async function () { + // File watching only enabled when running in `open` mode + cypressConfig.isTextTerminal = false const devServerEvents = new EventEmitter() const { close } = await devServer({ webpackConfig, @@ -203,14 +197,58 @@ describe('#devServer', () => { expect(oldmtime).to.not.equal(updatedmtime) - await new Promise((resolve, reject) => { - close((err) => { - if (err) { - return reject(err) + await closeServer(close) + }) + + ;[{ + title: 'does not watch/recompile files in `run` mode', + isRunMode: true, + updateExpected: false, + message: 'Files should not be watched in `run` mode', + }, { + title: 'watches and recompiles files on change in `open` mode', + isRunMode: false, + updateExpected: true, + message: 'Files should be watched and automatically rebuild on update in `open` mode', + }].forEach(({ title, isRunMode, updateExpected, message }) => { + it(title, async () => { + const originalContent = await fs.readFile(`./test/fixtures/dependency.js`) + + try { + cypressConfig.devServerPublicPathRoute = '/__cypress/src' + cypressConfig.isTextTerminal = isRunMode + const devServerEvents = new EventEmitter() + const { close, port } = await devServer({ + webpackConfig: {}, + cypressConfig, + specs: createSpecs('bar.spec.js'), + devServerEvents, + }) + + // Wait for initial "ready" from server + await once(devServerEvents, 'dev-server:compile:success') + + // Get the initial version of the bundled spec + const original = await requestSpecFile('/__cypress/src/spec-0.js', port) + + // Update a dependency of the spec + await fs.writeFile('./test/fixtures/dependency.js', `window.TEST = true;${originalContent}`) + // Brief wait to give server time to detect changes + await new Promise((resolve) => setTimeout(resolve, 500)) + + // Re-fetch the spec + const updated = await requestSpecFile('/__cypress/src/spec-0.js', port) + + if (updateExpected) { + expect(original, message).not.to.equal(updated) + } else { + expect(original, message).to.equal(updated) } - resolve() - }) + await closeServer(close) + } finally { + fs.writeFile('./test/fixtures/dependency.js', originalContent) + } }) }) @@ -229,15 +267,7 @@ describe('#devServer', () => { expect(response).to.eq('const foo = () => {}\n') - await new Promise((resolve, reject) => { - close((err) => { - if (err) { - return reject(err) - } - - resolve() - }) - }) + await closeServer(close) }) }) .timeout(5000) diff --git a/npm/webpack-dev-server/test/fixtures/bar.spec.js b/npm/webpack-dev-server/test/fixtures/bar.spec.js index 2615ea3fc22c..8464f0be6318 100644 --- a/npm/webpack-dev-server/test/fixtures/bar.spec.js +++ b/npm/webpack-dev-server/test/fixtures/bar.spec.js @@ -1 +1,3 @@ +import './dependency' + const bar = () => {} diff --git a/npm/webpack-dev-server/test/fixtures/dependency.js b/npm/webpack-dev-server/test/fixtures/dependency.js new file mode 100644 index 000000000000..693da49fc40b --- /dev/null +++ b/npm/webpack-dev-server/test/fixtures/dependency.js @@ -0,0 +1 @@ +export {} \ No newline at end of file diff --git a/npm/webpack-dev-server/test/makeWebpackConfig.spec.ts b/npm/webpack-dev-server/test/makeWebpackConfig.spec.ts index 253065c61c60..0688b9d9039e 100644 --- a/npm/webpack-dev-server/test/makeWebpackConfig.spec.ts +++ b/npm/webpack-dev-server/test/makeWebpackConfig.spec.ts @@ -7,6 +7,7 @@ import { CYPRESS_WEBPACK_ENTRYPOINT, makeWebpackConfig } from '../src/makeWebpac import { createModuleMatrixResult } from './test-helpers/createModuleMatrixResult' import sinon from 'sinon' import SinonChai from 'sinon-chai' +import type { SourceRelativeWebpackResult } from '../src/helpers/sourceRelativeWebpackModules' Chai.use(SinonChai) @@ -192,4 +193,85 @@ describe('makeWebpackConfig', () => { expect(actual.plugins[1].constructor.name).to.eq('HtmlWebpackPlugin') expect(actual.plugins[2].constructor.name).to.eq('CypressCTWebpackPlugin') }) + + describe('file watching', () => { + let sourceWebpackModulesResult: SourceRelativeWebpackResult + let devServerConfig: WebpackDevServerConfig + + beforeEach(() => { + devServerConfig = { + specs: [], + cypressConfig: { + projectRoot: '.', + devServerPublicPathRoute: '/test-public-path', + } as Cypress.PluginConfigOptions, + webpackConfig: { + entry: { main: 'src/index.js' }, + }, + devServerEvents: new EventEmitter(), + } + }) + + describe('webpack-dev-server v3', () => { + beforeEach(() => { + sourceWebpackModulesResult = createModuleMatrixResult({ + webpack: 4, + webpackDevServer: 4, + }) + }) + + it('is disabled in run mode', async () => { + devServerConfig.cypressConfig.isTextTerminal = true + + const actual = await makeWebpackConfig({ + devServerConfig, + sourceWebpackModulesResult, + }) + + expect(actual.watchOptions.ignored).to.eql('**/*') + }) + + it('uses defaults in open mode', async () => { + devServerConfig.cypressConfig.isTextTerminal = false + + const actual = await makeWebpackConfig({ + devServerConfig, + sourceWebpackModulesResult, + }) + + expect(actual.watchOptions?.ignored).to.be.undefined + }) + }) + + describe('webpack-dev-server v4', () => { + beforeEach(() => { + sourceWebpackModulesResult = createModuleMatrixResult({ + webpack: 5, + webpackDevServer: 4, + }) + }) + + it('is disabled in run mode', async () => { + devServerConfig.cypressConfig.isTextTerminal = true + + const actual = await makeWebpackConfig({ + devServerConfig, + sourceWebpackModulesResult, + }) + + expect(actual.watchOptions.ignored).to.eql('**/*') + }) + + it('uses defaults in open mode', async () => { + devServerConfig.cypressConfig.isTextTerminal = false + + const actual = await makeWebpackConfig({ + devServerConfig, + sourceWebpackModulesResult, + }) + + expect(actual.watchOptions?.ignored).to.be.undefined + }) + }) + }) })