diff --git a/.eslintrc.js b/.eslintrc.js index 852411053239..7abb027d019b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,6 +5,7 @@ const { specifiedRules } = require('graphql') const graphqlOpts = { env: 'literal', tagName: 'gql', + // eslint-disable-next-line no-restricted-syntax schemaString: fs.readFileSync( path.join(__dirname, 'packages/graphql/schemas/schema.graphql'), 'utf8', @@ -33,6 +34,24 @@ module.exports = { 'plugin:@cypress/dev/tests', ], parser: '@typescript-eslint/parser', + overrides: [ + { + files: [ + // ignore in tests and scripts + '**/scripts/**', + '**/test/**', + '**/system-tests/**', + 'packages/{app,driver,frontend-shared,launchpad}/cypress/**', + '*.test.ts', + // ignore in packages that don't run in the Cypress process + 'npm/create-cypress-tests/**', + ], + rules: { + 'no-restricted-properties': 'off', + 'no-restricted-syntax': 'off', + }, + }, + ], rules: { 'no-duplicate-imports': 'off', 'import/no-duplicates': 'off', @@ -55,6 +74,16 @@ module.exports = { message: 'os.userInfo() will throw when there is not an `/etc/passwd` entry for the current user (like when running with --user 12345 in Docker). Do not use it unless you catch any potential errors.', }, ], + 'no-restricted-syntax': [ + // esquery tool: https://estools.github.io/esquery/ + 'error', + { + // match sync FS methods except for `existsSync` + // examples: fse.readFileSync, fs.readFileSync, this.ctx.fs.readFileSync... + selector: `MemberExpression[object.name='fs'][property.name=/^[A-z]+Sync$/]:not(MemberExpression[property.name='existsSync']), MemberExpression[property.name=/^[A-z]+Sync$/]:not(MemberExpression[property.name='existsSync']):has(MemberExpression[property.name='fs'])`, + message: 'Synchronous fs calls should not be used in Cypress. Use an async API instead.', + }, + ], 'graphql/capitalized-type-name': ['warn', graphqlOpts], 'graphql/no-deprecated-fields': ['error', graphqlOpts], 'graphql/template-strings': ['error', { ...graphqlOpts, validators }], diff --git a/circle.yml b/circle.yml index 0241aa54efaf..ede5ccf5b0fc 100644 --- a/circle.yml +++ b/circle.yml @@ -27,8 +27,7 @@ mainBuildFilters: &mainBuildFilters branches: only: - develop - - 10.0-release - - linux-arm64 + - issue-22147-nohoist # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -129,7 +128,7 @@ commands: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "linux-arm64" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "issue-22147-nohoist" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi diff --git a/cli/scripts/build.js b/cli/scripts/build.js index 7ce6b3d9a8d5..b5fe4fb8c24d 100644 --- a/cli/scripts/build.js +++ b/cli/scripts/build.js @@ -31,6 +31,7 @@ function preparePackageForNpmRelease (json) { delete json['private'] // no need to include "nyc" code coverage settings delete json.nyc + delete json.workspaces _.extend(json, { version, diff --git a/npm/eslint-plugin-dev/lib/custom-rules/index.js b/npm/eslint-plugin-dev/lib/custom-rules/index.js index 4acf20ebcac0..8dbc761bc096 100644 --- a/npm/eslint-plugin-dev/lib/custom-rules/index.js +++ b/npm/eslint-plugin-dev/lib/custom-rules/index.js @@ -2,6 +2,7 @@ const fs = require('fs') const path = require('path') module.exports = + // eslint-disable-next-line no-restricted-syntax Object.assign({}, ...fs.readdirSync(__dirname) .filter((filename) => filename.endsWith('.js') && filename !== 'index.js') .map((filename) => ({ [filename.replace(/\.js$/u, '')]: require(path.resolve(__dirname, filename)) }))) diff --git a/npm/vite-dev-server/cypress/e2e/vite-dev-server.cy.ts b/npm/vite-dev-server/cypress/e2e/vite-dev-server.cy.ts index 12d59f6da467..f0dac35ea855 100644 --- a/npm/vite-dev-server/cypress/e2e/vite-dev-server.cy.ts +++ b/npm/vite-dev-server/cypress/e2e/vite-dev-server.cy.ts @@ -38,7 +38,7 @@ describe('Config options', () => { cy.startAppServer('component') cy.withCtx(async (ctx, { specWithWhitespace }) => { - ctx.actions.file.writeFileInProject( + await ctx.actions.file.writeFileInProject( ctx.path.join('src', specWithWhitespace), await ctx.file.readFileInProject(ctx.path.join('src', 'App.cy.jsx')), ) diff --git a/npm/vite-dev-server/src/plugins/cypress.ts b/npm/vite-dev-server/src/plugins/cypress.ts index 1e6fdd5a46b1..4ba23ad3aebc 100644 --- a/npm/vite-dev-server/src/plugins/cypress.ts +++ b/npm/vite-dev-server/src/plugins/cypress.ts @@ -38,6 +38,8 @@ export const Cypress = ( const indexHtmlFile = options.cypressConfig.indexHtmlFile let specsPathsSet = getSpecsPathsSet(specs) + // TODO: use async fs methods here + // eslint-disable-next-line no-restricted-syntax let loader = fs.readFileSync(INIT_FILEPATH, 'utf8') devServerEvents.on('dev-server:specs:changed', (specs: Spec[]) => { diff --git a/npm/webpack-dev-server/src/CypressCTWebpackPlugin.ts b/npm/webpack-dev-server/src/CypressCTWebpackPlugin.ts index f625d6d2c98a..8cb128b2bbca 100644 --- a/npm/webpack-dev-server/src/CypressCTWebpackPlugin.ts +++ b/npm/webpack-dev-server/src/CypressCTWebpackPlugin.ts @@ -99,13 +99,15 @@ export class CypressCTWebpackPlugin { * has been "updated on disk", causing a recompliation (and pulling the new specs in as * dependencies). */ - private onSpecsChange = (specs: Cypress.Cypress['spec'][]) => { + private onSpecsChange = async (specs: Cypress.Cypress['spec'][]) => { if (!this.compilation || _.isEqual(specs, this.files)) { return } this.files = specs const inputFileSystem = this.compilation.inputFileSystem + // TODO: don't use a sync fs method here + // eslint-disable-next-line no-restricted-syntax const utimesSync: UtimesSync = inputFileSystem.fileSystem.utimesSync ?? fs.utimesSync utimesSync(path.resolve(__dirname, 'browser.js'), new Date(), new Date()) diff --git a/npm/webpack-dev-server/src/helpers/nextHandler.ts b/npm/webpack-dev-server/src/helpers/nextHandler.ts index 5f73a7ae1b61..ea6fa62bd9da 100644 --- a/npm/webpack-dev-server/src/helpers/nextHandler.ts +++ b/npm/webpack-dev-server/src/helpers/nextHandler.ts @@ -69,7 +69,7 @@ async function loadWebpackConfig (devServerConfig: WebpackDevServerConfig): Prom buildId: `@cypress/react-${Math.random().toString()}`, config: nextConfig, dev: true, - pagesDir: findPagesDir(devServerConfig.cypressConfig.projectRoot), + pagesDir: await findPagesDir(devServerConfig.cypressConfig.projectRoot), entrypoints: {}, rewrites: { fallback: [], afterFiles: [], beforeFiles: [] }, ...runWebpackSpan, @@ -106,9 +106,9 @@ function checkSWC ( return false } -const existsSync = (file: string) => { +const exists = async (file: string) => { try { - fs.accessSync(file, fs.constants.F_OK) + await fs.promises.access(file, fs.constants.F_OK) return true } catch (_) { @@ -121,16 +121,16 @@ const existsSync = (file: string) => { * `${projectRoot}/pages` or `${projectRoot}/src/pages`. * If neither is found, return projectRoot */ -function findPagesDir (projectRoot: string) { +async function findPagesDir (projectRoot: string) { // prioritize ./pages over ./src/pages let pagesDir = path.join(projectRoot, 'pages') - if (existsSync(pagesDir)) { + if (await exists(pagesDir)) { return pagesDir } pagesDir = path.join(projectRoot, 'src', 'pages') - if (existsSync(pagesDir)) { + if (await exists(pagesDir)) { return pagesDir } diff --git a/npm/webpack-dev-server/src/measureWebpackPerformance.ts b/npm/webpack-dev-server/src/measureWebpackPerformance.ts index e3630c8104c6..39f871fb2641 100644 --- a/npm/webpack-dev-server/src/measureWebpackPerformance.ts +++ b/npm/webpack-dev-server/src/measureWebpackPerformance.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-restricted-syntax */ /* eslint-disable no-console */ import path from 'path' import fs from 'fs' diff --git a/npm/webpack-preprocessor/package.json b/npm/webpack-preprocessor/package.json index 166861106648..465ad7db54a5 100644 --- a/npm/webpack-preprocessor/package.json +++ b/npm/webpack-preprocessor/package.json @@ -11,7 +11,7 @@ "secure": "nsp check", "semantic-release": "semantic-release", "size": "npm pack --dry", - "test": "node ./test-webpack-5.js", + "test": "node ./scripts/test-webpack-5.js", "test-debug": "node --inspect --debug-brk ./node_modules/.bin/_mocha", "test-e2e": "mocha test/e2e/*.spec.*", "test-unit": "mocha test/unit/*.spec.*", diff --git a/npm/webpack-preprocessor/test-webpack-5.js b/npm/webpack-preprocessor/scripts/test-webpack-5.js similarity index 87% rename from npm/webpack-preprocessor/test-webpack-5.js rename to npm/webpack-preprocessor/scripts/test-webpack-5.js index 4e85c6489303..bb0945ee56a2 100644 --- a/npm/webpack-preprocessor/test-webpack-5.js +++ b/npm/webpack-preprocessor/scripts/test-webpack-5.js @@ -1,7 +1,10 @@ const execa = require('execa') -const pkg = require('./package.json') +const path = require('path') +const pkg = require('../package.json') const fs = require('fs') +const pkgJsonPath = path.join(__dirname, '..', 'package.json') + /** * This file installs Webpack 5 and runs the unit and e2e tests for the preprocessor. * We read package.json, update the webpack version, then re-run yarn install. @@ -17,7 +20,7 @@ const main = async () => { const install = () => execa('yarn', ['install', '--ignore-scripts'], { stdio: 'inherit' }) const resetPkg = async () => { - fs.writeFileSync('package.json', originalPkg, 'utf8') + fs.writeFileSync(pkgJsonPath, originalPkg, 'utf8') await install() } @@ -38,7 +41,7 @@ const main = async () => { delete pkg.devDependencies['webpack'] // eslint-disable-next-line no-console console.log('[@cypress/webpack-preprocessor]: updating package.json...') - fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)) + fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2)) // eslint-disable-next-line no-console console.log('[@cypress/webpack-preprocessor]: install dependencies...') diff --git a/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts index ee3110f508d9..d1b480379a04 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts @@ -208,8 +208,8 @@ describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout: }).invoke('connected').should('be.true') }) - cy.withCtx((ctx, o) => { - ctx.actions.file.writeFileInProject(o.path, ` + cy.withCtx(async (ctx, o) => { + await ctx.actions.file.writeFileInProject(o.path, ` import React from 'react' import { mount } from '@cypress/react' 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 4cf9c55fffec..7da4e7badfcd 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts @@ -259,8 +259,8 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout: }).invoke('connected').should('be.true') }) - cy.withCtx((ctx, o) => { - ctx.actions.file.writeFileInProject(o.path, ` + cy.withCtx(async (ctx, o) => { + await ctx.actions.file.writeFileInProject(o.path, ` describe('Dom Content', () => { it('renders the new test content', () => { cy.visit('cypress/e2e/dom-content.html') diff --git a/packages/app/cypress/e2e/cypress-in-cypress.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress.cy.ts index b9af2b6e1db9..b50001331544 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress.cy.ts @@ -228,13 +228,13 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100 startAtSpecsPage('e2e') cy.get('[data-cy="spec-item"]') - cy.withCtx((ctx, o) => { + cy.withCtx(async (ctx, o) => { ctx.coreData.app.browserStatus = 'open' - let config = ctx.actions.file.readFileInProject('cypress.config.js') + let config = await ctx.actions.file.readFileInProject('cypress.config.js') config = config.replace(`e2e: {`, `e2e: {\n chromeWebSecurity: false,\n`) - ctx.actions.file.writeFileInProject('cypress.config.js', config) + await ctx.actions.file.writeFileInProject('cypress.config.js', config) o.sinon.stub(ctx.actions.browser, 'closeBrowser') o.sinon.stub(ctx.actions.browser, 'relaunchBrowser') @@ -252,24 +252,24 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100 it('restarts browser if there is a before:browser:launch task and there is a change on the config', () => { startAtSpecsPage('e2e') - cy.withCtx((ctx, o) => { + cy.withCtx(async (ctx, o) => { ctx.coreData.app.browserStatus = 'open' - let config = ctx.actions.file.readFileInProject('cypress.config.js') + let config = await ctx.actions.file.readFileInProject('cypress.config.js') config = config.replace(`e2e: {`, `e2e: {\n setupNodeEvents(on) {\n on('before:browser:launch', () => {})\n},\n`) - ctx.actions.file.writeFileInProject('cypress.config.js', config) + await ctx.actions.file.writeFileInProject('cypress.config.js', config) }) cy.get('[data-cy="spec-item"]') - cy.withCtx((ctx, o) => { + cy.withCtx(async (ctx, o) => { ctx.coreData.app.browserStatus = 'open' - let config = ctx.actions.file.readFileInProject('cypress.config.js') + let config = await ctx.actions.file.readFileInProject('cypress.config.js') config = config.replace(`e2e: {`, `e2e: {\n viewportHeight: 600,\n`) - ctx.actions.file.writeFileInProject('cypress.config.js', config) + await ctx.actions.file.writeFileInProject('cypress.config.js', config) o.sinon.stub(ctx.actions.browser, 'closeBrowser') o.sinon.stub(ctx.actions.browser, 'relaunchBrowser') @@ -288,14 +288,14 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100 startAtSpecsPage('e2e') cy.get('[data-cy="spec-item"]') - cy.withCtx((ctx, o) => { + cy.withCtx(async (ctx, o) => { ctx.coreData.app.browserStatus = 'open' o.sinon.stub(ctx.actions.project, 'initializeActiveProject') - let config = ctx.actions.file.readFileInProject('cypress.config.js') + let config = await ctx.actions.file.readFileInProject('cypress.config.js') config = config.replace(`{`, `{\n watchForFileChanges: false,\n`) - ctx.actions.file.writeFileInProject('cypress.config.js', config) + await ctx.actions.file.writeFileInProject('cypress.config.js', config) }) cy.get('[data-cy="loading-spinner"]').should('be.visible') @@ -310,14 +310,14 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100 startAtSpecsPage('e2e') cy.get('[data-cy="spec-item"]') - cy.withCtx((ctx, o) => { + cy.withCtx(async (ctx, o) => { ctx.coreData.app.browserStatus = 'open' o.sinon.stub(ctx.actions.project, 'initializeActiveProject') - let config = ctx.actions.file.readFileInProject('cypress.config.js') + let config = await ctx.actions.file.readFileInProject('cypress.config.js') config = config.replace(` e2e: {`, ` e2e: {\n baseUrl: 'https://example.cypress.io',\n`) - ctx.actions.file.writeFileInProject('cypress.config.js', config) + await ctx.actions.file.writeFileInProject('cypress.config.js', config) }) cy.get('[data-cy="loading-spinner"]').should('be.visible') diff --git a/packages/app/cypress/e2e/specs.cy.ts b/packages/app/cypress/e2e/specs.cy.ts index 062eda47086e..1716658ce2e4 100644 --- a/packages/app/cypress/e2e/specs.cy.ts +++ b/packages/app/cypress/e2e/specs.cy.ts @@ -423,7 +423,7 @@ describe('App: Specs', () => { it('generates spec with file name that does not contain a known spec extension', () => { cy.withCtx(async (ctx) => { - let config = ctx.actions.file.readFileInProject('cypress.config.js') + let config = await ctx.actions.file.readFileInProject('cypress.config.js') config = config.replace( `specPattern: 'src/**/*.{cy,spec}.{js,jsx}'`, @@ -700,7 +700,7 @@ describe('App: Specs', () => { it('generates spec with file name that does not contain a known spec extension', () => { cy.withCtx(async (ctx) => { - let config = ctx.actions.file.readFileInProject('cypress.config.js') + let config = await ctx.actions.file.readFileInProject('cypress.config.js') config = config.replace( `specPattern: 'src/specs-folder/*.cy.{js,jsx}'`, diff --git a/packages/app/cypress/e2e/subscriptions/configChange-subscription.cy.ts b/packages/app/cypress/e2e/subscriptions/configChange-subscription.cy.ts index 0e91b3a7f9f9..c2215175c2cc 100644 --- a/packages/app/cypress/e2e/subscriptions/configChange-subscription.cy.ts +++ b/packages/app/cypress/e2e/subscriptions/configChange-subscription.cy.ts @@ -1,18 +1,18 @@ function updateProjectIdInCypressConfig (value: string) { - return cy.withCtx((ctx, o) => { - let config = ctx.actions.file.readFileInProject('cypress.config.js') + return cy.withCtx(async (ctx, o) => { + let config = await ctx.actions.file.readFileInProject('cypress.config.js') config = config.replace(`projectId: 'abc123'`, `projectId: '${o.value}'`) - ctx.actions.file.writeFileInProject('cypress.config.js', config) + await ctx.actions.file.writeFileInProject('cypress.config.js', config) }, { value }) } function updateViewportHeightInCypressConfig (value: number) { - return cy.withCtx((ctx, o) => { - let config = ctx.actions.file.readFileInProject('cypress.config.js') + return cy.withCtx(async (ctx, o) => { + let config = await ctx.actions.file.readFileInProject('cypress.config.js') config = config.replace(`e2e: {`, `e2e: {\n viewportHeight: ${o.value},\n`) - ctx.actions.file.writeFileInProject('cypress.config.js', config) + await ctx.actions.file.writeFileInProject('cypress.config.js', config) }, { value }) } diff --git a/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts b/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts index 9f5f9b968687..95e5a37f3b61 100644 --- a/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts +++ b/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts @@ -18,8 +18,8 @@ describe('specChange subscription', () => { .should('contain', 'dom-content.spec.js') .should('contain', 'dom-list.spec.js') - cy.withCtx((ctx, o) => { - ctx.actions.file.writeFileInProject(o.path, '') + cy.withCtx(async (ctx, o) => { + await ctx.actions.file.writeFileInProject(o.path, '') }, { path: getPathForPlatform('cypress/e2e/new-file.spec.js') }) cy.get('[data-cy="spec-item-link"]') diff --git a/packages/app/src/specs/CreateSpecModal.cy.tsx b/packages/app/src/specs/CreateSpecModal.cy.tsx index ea0b567c83ae..2b79f7108c3f 100644 --- a/packages/app/src/specs/CreateSpecModal.cy.tsx +++ b/packages/app/src/specs/CreateSpecModal.cy.tsx @@ -64,6 +64,94 @@ describe('', () => { }) }) +describe('Modal Text Input', () => { + it('focuses text input and selects file name by default', () => { + const show = ref(true) + + cy.mount(() => (
+ show.value = false} + /> +
)) + + cy.focused().as('specNameInput') + + // focused should yield the input element since it should be auto-focused + cy.get('@specNameInput').invoke('val').should('equal', 'cypress/e2e/ComponentName.cy.js') + + // only the file name should be focused, so backspacing should erase the whole file name + cy.get('@specNameInput').type('{backspace}') + + cy.get('@specNameInput').invoke('val').should('equal', 'cypress/e2e/.cy.js') + }) + + it('focuses text input but does not select if default file name does not match regex', () => { + const show = ref(true) + + cy.mount(() => (
+ show.value = false} + /> +
)) + + cy.focused().as('specNameInput') + + // focused should yield the input element since it should be auto-focused + cy.get('@specNameInput').invoke('val').should('equal', 'this/path/does/not/produce/regex/match-') + + // nothing should be selected, so backspacing should only delete the last character in the file path + cy.get('@specNameInput').type('{backspace}') + + cy.get('@specNameInput').invoke('val').should('equal', 'this/path/does/not/produce/regex/match') + }) +}) + describe('playground', () => { it('can be opened and closed via the show prop', () => { const show = ref(false) diff --git a/packages/app/src/specs/DirectoryItem.vue b/packages/app/src/specs/DirectoryItem.vue index 6651c5666e4c..dee4c999e102 100644 --- a/packages/app/src/specs/DirectoryItem.vue +++ b/packages/app/src/specs/DirectoryItem.vue @@ -1,5 +1,6 @@ diff --git a/packages/app/src/specs/SpecFileItem.cy.tsx b/packages/app/src/specs/SpecFileItem.cy.tsx index dce2abe40ab6..e02d40d06168 100644 --- a/packages/app/src/specs/SpecFileItem.cy.tsx +++ b/packages/app/src/specs/SpecFileItem.cy.tsx @@ -22,4 +22,23 @@ describe('SpecFileItem', () => { cy.percySnapshot() }) + + it('truncates spec name if it exceeds container width and provides title for full spec name', () => { + const specFileName = `${'Long'.repeat(20)}Name` + + // Shrink viewport width so spec name is truncated + cy.viewport(400, 850) + + cy.mount(() => ( +
+ +
)) + + // We should be able to see at least the first 20 characters of the spec name + // It should have a title attribute that is equal to the full file name + cy.contains(specFileName.substring(0, 20)).should('be.visible').parent().should('have.attr', 'title', `${specFileName}.cy.tsx`) + + // The file extension shouldn't be visible because it is past the truncation point + cy.contains('.cy.tsx').should('not.be.visible') + }) }) diff --git a/packages/app/src/specs/SpecFileItem.vue b/packages/app/src/specs/SpecFileItem.vue index 83aa76010638..5b84938cfd96 100644 --- a/packages/app/src/specs/SpecFileItem.vue +++ b/packages/app/src/specs/SpecFileItem.vue @@ -9,17 +9,22 @@ group-hocus:icon-light-indigo-600" :class="selected ? 'icon-dark-indigo-300 icon-light-indigo-600' : 'icon-dark-gray-800 icon-light-gray-1000'" /> - - +
+ + +