diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index de12ae239f90..dd040dab8d4a 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -12,6 +12,10 @@ _Released 9/25/2024_ - Fixed an issue where Firefox was incorrectly mutating the state of click events on checkboxes after Firefox version `129` and up. Addressed in [#30245](https://github.com/cypress-io/cypress/pull/30245). - Fixed a regression introduced in 13.13.0 where 'Open in IDE' would not work for filepaths containing spaces and various other characters on Windows. Addresses [#29820](https://github.com/cypress-io/cypress/issues/29820). +**Bugfixes:** + +- Fixed an issue where run Cypress internal unit test cases error on Windows. Addressed in [#30157](https://github.com/cypress-io/cypress/pull/30157) + **Misc:** - Pass along the related log to the `createSnapshot` function for protocol usage. Addressed in [#30244](https://github.com/cypress-io/cypress/pull/30244). diff --git a/packages/data-context/src/codegen/code-generator.ts b/packages/data-context/src/codegen/code-generator.ts index f9c7250797a8..3b7be1fcb3a4 100644 --- a/packages/data-context/src/codegen/code-generator.ts +++ b/packages/data-context/src/codegen/code-generator.ts @@ -5,6 +5,7 @@ import * as ejs from 'ejs' import fm from 'front-matter' import _ from 'lodash' import Debug from 'debug' +import { toOS } from '../util' const debug = Debug('cypress:data-context:codegen:code-generator') @@ -179,7 +180,7 @@ export async function hasNonExampleSpec (testTemplateDir: string, specs: string[ return templateFiles.some((templateFile) => templateFile.substring(testTemplateDir.length + 1) === spec) } - return specs.some((spec) => !specInTemplates(spec)) + return specs.map(((spec) => toOS(spec))).some((spec) => !specInTemplates(spec)) } export async function getExampleSpecPaths (testTemplateDir: string): Promise { diff --git a/packages/data-context/src/sources/FileDataSource.ts b/packages/data-context/src/sources/FileDataSource.ts index 7c2b1d52f6be..f36149d70680 100644 --- a/packages/data-context/src/sources/FileDataSource.ts +++ b/packages/data-context/src/sources/FileDataSource.ts @@ -5,7 +5,7 @@ import os from 'os' import globby, { GlobbyOptions } from 'globby' import Debug from 'debug' -import { toPosix } from '../util/file' +import { toOS, toPosix } from '../util/file' const debug = Debug('cypress:data-context:sources:FileDataSource') @@ -36,14 +36,15 @@ export class FileDataSource { async getFilesByGlob (cwd: string, glob: string | string[], globOptions: GlobbyOptions = {}): Promise { const globs = ([] as string[]).concat(glob).map((globPattern) => { - const workingDirectoryPrefix = path.join(cwd, path.sep) + const workingDirectoryPrefix = toOS(path.join(cwd, path.sep)) + const globPatternWithOS = toOS(globPattern) // If the pattern includes the working directory, we strip it from the pattern. // The working directory path may include characters that conflict with glob // syntax (brackets, parentheses, etc.) and cause our searches to inadvertently fail. // We scope our search to the working directory using the `cwd` globby option. - if (globPattern.startsWith(workingDirectoryPrefix)) { - return globPattern.replace(workingDirectoryPrefix, '') + if (globPatternWithOS.startsWith(workingDirectoryPrefix)) { + return globPatternWithOS.replace(workingDirectoryPrefix, '') } return globPattern diff --git a/packages/data-context/src/util/file.ts b/packages/data-context/src/util/file.ts index 307d4f782092..0415db4f256c 100644 --- a/packages/data-context/src/util/file.ts +++ b/packages/data-context/src/util/file.ts @@ -3,3 +3,16 @@ import path from 'path' export function toPosix (file: string, sep: string = path.sep) { return file.split(sep).join(path.posix.sep) } + +/** + * Converts a POSIX file path to the current operating system's file path format. + * + * This function takes a file path string that uses POSIX separators ('/') and + * replaces them with the current operating system's path separators. + * + * @param file The file path to convert. + * @returns The converted file path with the current OS's path separators. + */ +export function toOS (file: string) { + return file.split(path.posix.sep).join(path.sep) +} diff --git a/packages/data-context/test/unit/codegen/code-generator.spec.ts b/packages/data-context/test/unit/codegen/code-generator.spec.ts index 01964fec5002..b3df266a0490 100644 --- a/packages/data-context/test/unit/codegen/code-generator.spec.ts +++ b/packages/data-context/test/unit/codegen/code-generator.spec.ts @@ -1,6 +1,5 @@ import { parse } from '@babel/parser' import { expect } from 'chai' -import dedent from 'dedent' import fs from 'fs-extra' import path from 'path' import { DataContext } from '../../../src' @@ -14,7 +13,7 @@ import { } from '../../../src/codegen/code-generator' import { SpecOptions } from '../../../src/codegen/spec-options' import templates from '../../../src/codegen/templates' -import { createTestDataContext } from '../helper' +import { createTestDataContext, dedentWithOSLineWrap } from '../helper' import { CT_FRAMEWORKS } from '@packages/scaffold-config' import { defaultSpecPattern } from '@packages/config' @@ -128,7 +127,7 @@ describe('code-generator', () => { type: 'text', status: 'add', file: fileAbsolute, - content: `${dedent` + content: `${dedentWithOSLineWrap` describe('template spec', () => { it('passes', () => { cy.visit('https://example.cypress.io') @@ -167,7 +166,7 @@ describe('code-generator', () => { type: 'text', status: 'add', file: fileAbsolute, - content: dedent` + content: dedentWithOSLineWrap` describe('Button.tsx', () => { it('playground', () => { // cy.mount() @@ -208,7 +207,7 @@ describe('code-generator', () => { type: 'text', status: 'add', file: fileAbsolute, - content: dedent`import ${codeGenArgs.componentName} from '${codeGenArgs.componentPath}' + content: dedentWithOSLineWrap`import ${codeGenArgs.componentName} from '${codeGenArgs.componentPath}' describe('<${codeGenArgs.componentName} />', () => { it('renders', () => { @@ -253,7 +252,7 @@ describe('code-generator', () => { type: 'text', status: 'add', file: fileAbsolute, - content: dedent` + content: dedentWithOSLineWrap` import React from 'react' import { ${codeGenArgs.componentName} } from '${codeGenArgs.componentPath}' @@ -300,7 +299,7 @@ describe('code-generator', () => { type: 'text', status: 'add', file: fileAbsolute, - content: dedent` + content: dedentWithOSLineWrap` import React from 'react' import ${codeGenArgs.componentName} from '${codeGenArgs.componentPath}' diff --git a/packages/data-context/test/unit/helper.ts b/packages/data-context/test/unit/helper.ts index b2f5be39ec6d..e6df98f9b29f 100644 --- a/packages/data-context/test/unit/helper.ts +++ b/packages/data-context/test/unit/helper.ts @@ -17,6 +17,8 @@ import { remoteSchema } from '@packages/graphql/src/stitching/remoteSchema' import type { OpenModeOptions, RunModeOptions } from '@packages/types' import { MAJOR_VERSION_FOR_CONTENT } from '@packages/types' import { RelevantRunInfo } from '../../src/gen/graphcache-config.gen' +import dedent from 'dedent' +import os from 'os' type SystemTestProject = typeof fixtureDirs[number] type SystemTestProjectPath = `${string}/system-tests/projects/${T}` @@ -122,3 +124,7 @@ export function createRelevantRun (runNumber: number): RelevantRunInfo { totalFailed: 0, } } + +export function dedentWithOSLineWrap (literals: string|TemplateStringsArray, ...placeholders: any[]): string { + return dedent(literals, ...placeholders).replace(/\n/g, os.EOL) +} diff --git a/packages/data-context/test/unit/sources/FileDataSource.spec.ts b/packages/data-context/test/unit/sources/FileDataSource.spec.ts index 33503f096b6d..ccb62a9561de 100644 --- a/packages/data-context/test/unit/sources/FileDataSource.spec.ts +++ b/packages/data-context/test/unit/sources/FileDataSource.spec.ts @@ -42,8 +42,8 @@ describe('FileDataSource', () => { ) expect(files).to.have.length(2) - expect(files[0]).to.eq(path.join(projectPath, 'root-script-1.js')) - expect(files[1]).to.eq(path.join(projectPath, 'root-script-2.js')) + expect(files[0]).to.eq(fileUtil.toPosix(path.join(projectPath, 'root-script-1.js'))) + expect(files[1]).to.eq(fileUtil.toPosix(path.join(projectPath, 'root-script-2.js'))) }) it('finds files matching relative patterns in working dir', async () => { @@ -67,7 +67,9 @@ describe('FileDataSource', () => { it('does not replace working directory in glob pattern if it is not leading', async () => { // Create a redundant structure within the project dir matching its absolute path // and write a new script in that location - const nestedScriptPath = path.join(projectPath, 'cypress', projectPath) + + // project path on windows contains drive letter, cannot be joined directly + const nestedScriptPath = path.join(projectPath, 'cypress', path.parse(projectPath).base) await fs.mkdirs(nestedScriptPath) await fs.writeFile(path.join(nestedScriptPath, 'nested-script.js'), '') @@ -76,7 +78,7 @@ describe('FileDataSource', () => { // to the working directory let files = await fileDataSource.getFilesByGlob( projectPath, - `./cypress${projectPath}/nested-script.js`, + `./cypress/${path.parse(projectPath).base}/nested-script.js`, ) expect(files).to.have.length(1) diff --git a/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts b/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts index 110d450953ce..4f8c258ec1f5 100644 --- a/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts +++ b/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts @@ -14,6 +14,7 @@ import type { FindSpecs } from '../../../src/actions' import { createTestDataContext } from '../helper' import { defaultExcludeSpecPattern, defaultSpecPattern } from '@packages/config' import FixturesHelper from '@tooling/system-tests' +import { toOS } from '../../../src/util' chai.use(sinonChai) const { expect } = chai @@ -42,7 +43,7 @@ describe('matchedSpecs', () => { fileExtension: '.js', fileName: 'screenshot_element_capture_spec', name: 'cypress/integration/screenshot_element_capture_spec.js', - relative: 'cypress/integration/screenshot_element_capture_spec.js', + relative: toOS('cypress/integration/screenshot_element_capture_spec.js'), relativeToCommonRoot: 'screenshot_element_capture_spec.js', specFileExtension: '.js', specType: 'integration', @@ -129,7 +130,7 @@ describe('transformSpec', () => { specType: 'integration', baseName: 'spec.cy.js', fileName: 'spec', - relative: 'src/spec.cy.js', + relative: toOS('src/spec.cy.js'), name: 'src/spec.cy.js', relativeToCommonRoot: 'C:/Windows/Project/src/spec.cy.js', } @@ -265,7 +266,7 @@ describe('getLongestCommonPrefixFromPaths', () => { 'cypress/component/bar/meta-component-test.cy.ts', ]) - expect(lcp).to.equal('cypress/component') + expect(lcp).to.equal(toOS('cypress/component')) }) it('with src and cypress', () => { @@ -292,7 +293,7 @@ describe('getLongestCommonPrefixFromPaths', () => { 'src/frontend/MyComponent.cy.ts', ]) - expect(lcp).to.equal('src/frontend') + expect(lcp).to.equal(toOS('src/frontend')) }) }) @@ -776,14 +777,14 @@ describe('ProjectDataSource', () => { const defaultSpecFileName = await ctx.project.defaultSpecFileName() - expect(defaultSpecFileName).to.equal('cypress/e2e/spec.cy.js') + expect(defaultSpecFileName).to.equal(toOS('cypress/e2e/spec.cy.js')) }) it('yields default if the spec pattern is default', async () => { sinon.stub(ctx.project, 'specPatterns').resolves({ specPattern: [defaultSpecPattern.e2e] }) const defaultSpecFileName = await ctx.project.defaultSpecFileName() - expect(defaultSpecFileName).to.equal('cypress/e2e/spec.cy.js') + expect(defaultSpecFileName).to.equal(toOS('cypress/e2e/spec.cy.js')) }) it('yields common prefix if there are existing specs', async () => { @@ -795,7 +796,7 @@ describe('ProjectDataSource', () => { const defaultSpecFileName = await ctx.project.defaultSpecFileName() - expect(defaultSpecFileName).to.equal('cypress/e2e/foo/spec.cy.js') + expect(defaultSpecFileName).to.equal(toOS('cypress/e2e/foo/spec.cy.js')) }) it('yields spec pattern guess if there are no existing specs', async () => { @@ -816,7 +817,7 @@ describe('ProjectDataSource', () => { const defaultSpecFileName = await ctx.project.defaultSpecFileName() - expect(defaultSpecFileName).to.equal('cypress/component-tests/foo/ComponentName.spec.js') + expect(defaultSpecFileName).to.equal(toOS('cypress/component-tests/foo/ComponentName.spec.js')) }) describe('jsx/tsx handling', () => { @@ -831,7 +832,7 @@ describe('ProjectDataSource', () => { const defaultSpecFileName = await ctx.project.defaultSpecFileName() - expect(defaultSpecFileName).to.equal('cypress/component/ComponentName.cy.jsx', defaultSpecFileName) + expect(defaultSpecFileName).to.equal(toOS('cypress/component/ComponentName.cy.jsx'), defaultSpecFileName) }) it('yields non-jsx extension if there are jsx files but specPattern disallows', async () => { diff --git a/packages/data-context/test/unit/util/file.spec.ts b/packages/data-context/test/unit/util/file.spec.ts new file mode 100644 index 000000000000..f5ad70735165 --- /dev/null +++ b/packages/data-context/test/unit/util/file.spec.ts @@ -0,0 +1,26 @@ +import { toOS } from '../../../src/util' +import { expect } from 'chai' +import sinon from 'sinon' +import path from 'path' + +describe('file', () => { + describe('#toOS', () => { + const posixPath = '/foo/bar/baz' + + it('should convert posix path to Windows path', () => { + sinon.stub(path, 'sep').value('\\') + + expect(toOS(posixPath)).to.equal('\\foo\\bar\\baz') + }) + + it('should return the same path if OS path separator is posix', () => { + sinon.stub(path, 'sep').value('/') + + expect(toOS(posixPath)).to.equal(posixPath) + }) + + afterEach(() => { + sinon.restore() + }) + }) +})