import fs from 'node:fs/promises'
import * as cp from 'node:child_process'
import { vi, test, expect, beforeEach, afterEach, describe, it } from 'vitest'
import readDir from 'recursive-readdir'
import ejs from 'ejs'
import { $ } from 'execa'
import { runProgram, getPackageVersion,
convertPackageHashToObject,
generateTestFiles,
getPathForFileGeneration,
getDefaultFiles,
getProjectRoot,
getAnswers,
getProjectProps,
createPackageJSON,
npmInstall,
setupTypeScript,
createWDIOConfig,
createWDIOScript,
runAppiumInstaller,
detectCompiler,
findInConfig,
replaceConfig,
formatConfigFilePaths,
} from '../src/utils.js'
import { parseAnswers } from '../src/cli/utils.js'
import path from 'node:path'
import { readPackageUp } from 'read-pkg-up'
import type { Questionnair } from '../src/types.js'
import inquirer from 'inquirer'
import { installPackages } from '../src/install.js'
const consoleLog = console.log.bind(console)
const processExit = process.exit.bind(process)
vi.mock('node:fs/promises', () => ({
default: {
access: vi.fn().mockRejectedValue(new Error('ENOENT')),
mkdir: vi.fn(),
writeFile: vi.fn().mockReturnValue(Promise.resolve())
}
}))
vi.mock('child_process', () => {
const m = {
execSyncRes: 'APPIUM_MISSING',
execSync: () => m.execSyncRes,
exec: vi.fn(),
spawn: vi.fn().mockReturnValue({ on: vi.fn().mockImplementation((ev, fn) => fn(0)) })
}
return m
})
vi.mock('inquirer')
vi.mock('read-pkg-up')
vi.mock('ejs')
vi.mock('execa', () => ({
execa: vi.fn(()=>({ stdout:'', stderr:'', exitCode:0 })),
$: vi.fn().mockReturnValue(async (sh: string) => sh)
}))
vi.mock('recursive-readdir', () => ({
default: vi.fn().mockResolvedValue([
'/foo/bar/loo/page.js.ejs',
'/foo/bar/example.e2e.js'
] as any)
}))
vi.mock('../src/install.js', () => ({
installPackages: vi.fn(),
getInstallCommand: vi.fn().mockReturnValue('npm install foo bar --save-dev')
}))
beforeEach(() => {
process.exit = vi.fn() as any
console.log = vi.fn()
vi.mocked(readPackageUp).mockResolvedValue({
path: '/foo/package.json',
packageJson: {
name: 'cool-test-module',
type: 'module'
}
})
})
afterEach(() => {
process.exit = processExit
console.log = consoleLog
})
test('runProgram', async () => {
expect(await runProgram('echo', ['123'], {})).toBe(undefined)
await runProgram('node', ['-e', 'throw new Error(\'ups\')'], {}).catch((e) => e)
expect(vi.mocked(console.log).mock.calls[0][0]).toMatch(/Error calling: node -e throw new Error/)
expect(process.exit).toBeCalledTimes(1)
await runProgram('foobarloo', [], {}).catch((e) => e)
expect(vi.mocked(console.log).mock.calls[1][0]).toMatch(/spawn foobarloo ENOENT/)
expect(process.exit).toBeCalledTimes(2)
})
test('getPackageVersion', async () => {
expect(await getPackageVersion()).toEqual(expect.any(String))
})
describe('convertPackageHashToObject', () => {
it('works with default `$--$` hash', () => {
expect(convertPackageHashToObject('test/package-name$--$package-name')).toMatchObject({
package: 'test/package-name',
short: 'package-name'
})
})
it('works with custom hash', () => {
expect(convertPackageHashToObject('test/package-name##-##package-name', '##-##')).toMatchObject({
package: 'test/package-name',
short: 'package-name'
})
})
})
describe('generateTestFiles', () => {
it('Mocha with page objects', async () => {
vi.mocked(readDir).mockResolvedValue([
'/foo/bar/loo/page.js.ejs',
'/foo/bar/example.e2e.js'
] as any)
const answers = {
runner: 'local',
framework: 'mocha',
usePageObjects: true,
generateTestFiles: true,
destPageObjectRootPath: '/tests/page/objects/model',
destSpecRootPath: '/tests/specs'
}
await generateTestFiles(answers as any)
expect(readDir).toBeCalledTimes(2)
expect(vi.mocked(readDir).mock.calls[0][0]).toContain('mocha')
expect(vi.mocked(readDir).mock.calls[1][0]).toContain('pageobjects')
* test readDir callback
*/
const readDirCb = vi.mocked(readDir).mock.calls[0][1][0] as Function
const stats = { isDirectory: vi.fn().mockReturnValue(false) }
expect(readDirCb('/foo/bar.lala', stats)).toBe(true)
expect(readDirCb('/foo/bar.js.ejs', stats)).toBe(false)
expect(readDirCb('/foo/bar.feature', stats)).toBe(false)
stats.isDirectory.mockReturnValue(true)
expect(readDirCb('/foo/bar.lala', stats)).toBe(false)
expect(readDirCb('/foo/bar.js.ejs', stats)).toBe(false)
expect(readDirCb('/foo/bar.feature', stats)).toBe(false)
expect(ejs.renderFile).toBeCalledTimes(4)
expect(ejs.renderFile).toBeCalledWith(
'/foo/bar/loo/page.js.ejs',
{ answers },
expect.any(Function)
)
expect(ejs.renderFile).toBeCalledWith(
'/foo/bar/example.e2e.js',
{ answers },
expect.any(Function)
)
expect(fs.mkdir).toBeCalledTimes(4)
expect((vi.mocked(fs.writeFile).mock.calls[0][0] as string)
.endsWith(`${path.sep}page${path.sep}objects${path.sep}model${path.sep}page.js`))
.toBe(true)
expect((vi.mocked(fs.writeFile).mock.calls[1][0] as string)
.endsWith(`${path.sep}example.e2e.js`))
.toBe(true)
})
it('jasmine with page objects', async () => {
const answers = {
runner: 'local',
framework: 'jasmine',
usePageObjects: true,
generateTestFiles: true,
destPageObjectRootPath: '/tests/page/objects/model',
destSpecRootPath: '/tests/specs'
}
await generateTestFiles(answers as any)
expect(readDir).toBeCalledTimes(2)
expect(vi.mocked(readDir).mock.calls[0][0]).toContain('mochaJasmine')
expect(vi.mocked(readDir).mock.calls[1][0]).toContain('pageobjects')
* test readDir callback
*/
const readDirCb = vi.mocked(readDir).mock.calls[0][1][0] as Function
const stats = { isDirectory: vi.fn().mockReturnValue(false) }
expect(readDirCb('/foo/bar.lala', stats)).toBe(true)
expect(readDirCb('/foo/bar.js.ejs', stats)).toBe(false)
expect(readDirCb('/foo/bar.feature', stats)).toBe(false)
stats.isDirectory.mockReturnValue(true)
expect(readDirCb('/foo/bar.lala', stats)).toBe(false)
expect(readDirCb('/foo/bar.js.ejs', stats)).toBe(false)
expect(readDirCb('/foo/bar.feature', stats)).toBe(false)
expect(ejs.renderFile).toBeCalledTimes(4)
expect(ejs.renderFile).toBeCalledWith(
'/foo/bar/loo/page.js.ejs',
{ answers },
expect.any(Function)
)
expect(ejs.renderFile).toBeCalledWith(
'/foo/bar/example.e2e.js',
{ answers },
expect.any(Function)
)
expect(fs.mkdir).toBeCalledTimes(4)
expect((vi.mocked(fs.writeFile).mock.calls[0][0] as string)
.endsWith(`${path.sep}page${path.sep}objects${path.sep}model${path.sep}page.js`))
.toBe(true)
expect((vi.mocked(fs.writeFile).mock.calls[1][0] as string)
.endsWith(`${path.sep}example.e2e.js`))
.toBe(true)
})
it('Jasmine with page generation and no pageObjects', async () => {
vi.mocked(readDir).mockResolvedValue([] as any)
const answers = {
runner: 'local',
specs: './tests/e2e/**/*.js',
framework: 'jasmine',
generateTestFiles: false,
usePageObjects: false
}
await generateTestFiles(answers as any)
expect(readDir).toBeCalledTimes(1)
expect(ejs.renderFile).toBeCalledTimes(0)
})
it('Cucumber with page generation and no pageObjects', async () => {
vi.mocked(readDir).mockResolvedValue([] as any)
const answers = {
runner: 'local',
specs: './tests/e2e/**/*.js',
framework: 'cucumber',
generateTestFiles: false,
usePageObjects: false,
}
await generateTestFiles(answers as any)
expect(readDir).toBeCalledTimes(1)
expect(ejs.renderFile).toBeCalledTimes(0)
})
it('Cucumber without page objects', async () => {
vi.mocked(readDir).mockResolvedValue([
'/foo/bar/loo/step_definition/example.step.js',
'/foo/bar/example.feature'
] as any)
const answers = {
runner: 'local',
specs: './tests/e2e/*.js',
framework: 'cucumber',
stepDefinitions: '/some/step/defs',
usePageObjects: false,
generateTestFiles: true,
destSpecRootPath: '/tests/specs',
destStepRootPath: '/tests/stepDefinitions'
}
await generateTestFiles(answers as any)
expect(readDir).toBeCalledTimes(1)
expect(vi.mocked(readDir).mock.calls[0][0]).toContain('cucumber')
expect(ejs.renderFile).toBeCalledTimes(2)
expect(ejs.renderFile).toBeCalledWith(
'/foo/bar/loo/step_definition/example.step.js',
{ answers },
expect.any(Function)
)
expect(ejs.renderFile).toBeCalledWith(
'/foo/bar/example.feature',
{ answers },
expect.any(Function)
)
expect(fs.mkdir).toBeCalledTimes(2)
})
it('Cucumber with page objects and TypeScript', async () => {
vi.mocked(readDir).mockResolvedValue([
'/foo/bar/loo/page.js.ejs',
'/foo/bar/loo/step_definition/example.step.js',
'/foo/bar/example.feature'
] as any)
const answers = {
runner: 'local',
framework: 'cucumber',
usePageObjects: true,
isUsingTypeScript: true,
destStepRootPath: '/tests/stepDefinitions',
destSpecRootPath: '/tests/specs',
destPageObjectRootPath: '/some/page/objects',
relativePath: '../page/object'
}
await generateTestFiles(answers as any)
expect(readDir).toBeCalledTimes(2)
expect(vi.mocked(readDir).mock.calls[0][0]).toContain('cucumber')
expect(ejs.renderFile).toBeCalledTimes(6)
expect(ejs.renderFile).toBeCalledWith(
'/foo/bar/loo/step_definition/example.step.js',
{ answers },
expect.any(Function)
)
expect(ejs.renderFile).toBeCalledWith(
'/foo/bar/example.feature',
{ answers },
expect.any(Function)
)
expect(fs.mkdir).toBeCalledTimes(6)
expect(
(vi.mocked(fs.writeFile).mock.calls[0][0] as string)
.endsWith(`${path.sep}some${path.sep}page${path.sep}objects${path.sep}page.ts`)
).toBe(true)
expect(
(vi.mocked(fs.writeFile).mock.calls[2][0] as string)
.endsWith(`${path.sep}example.feature`)
).toBe(true)
})
})
describe('findInConfig', () => {
it('finds text for services', () => {
const str = "services: ['foo', 'bar'],"
expect(findInConfig(str, 'service')).toMatchObject([
'services: [\'foo\', \'bar\']'
])
})
it('finds text for frameworks', () => {
const str = "framework: 'mocha'"
expect(findInConfig(str, 'framework')).toMatchObject([
"framework: 'mocha'"
])
})
})
describe('formatConfigFilePaths', () => {
it('should format properly', async () => {
expect(await formatConfigFilePaths('/path/to/foo.js')).toMatchObject({
fullPath:'/path/to/foo.js',
fullPathNoExtension:'/path/to/foo'
})
})
})
describe('replaceConfig', () => {
it('correctly changes framework', () => {
const fakeConfig = `exports.config = {
runner: 'local',
specs: [
'./test/specs/**/*.js'
],
framework: 'mocha',
}`
expect(replaceConfig(fakeConfig, 'framework', 'jasmine')).toBe(
`exports.config = {
runner: 'local',
specs: [
'./test/specs/**/*.js'
],
framework: 'jasmine',
}`
)
})
it('correctly changes service', () => {
const fakeConfig = `exports.config = {
runner: 'local',
specs: [
'./test/specs/**/*.js'
],
services: [],
framework: 'mocha',
}`
expect(replaceConfig(fakeConfig, 'service', 'sauce')).toBe(
`exports.config = {
runner: 'local',
specs: [
'./test/specs/**/*.js'
],
services: ['sauce'],
framework: 'mocha',
}`
)
})
})
describe('getPathForFileGeneration', () => {
it('Cucumber with pageobjects default values', () => {
const generatedPaths = getPathForFileGeneration({
runner: 'local',
stepDefinitions: './features/step-definitions/steps.js',
pages: './features/pageobjects/**/*.js',
generateTestFiles: true,
usePageObjects: true,
framework: '@wdio/cucumber-service$--$cucumber'
} as any, '/foo/bar')
expect(generatedPaths.relativePath).toEqual('../pageobjects')
})
it('Cucumber with pageobjects default different path', () => {
const generatedPaths = getPathForFileGeneration({
runner: 'local',
stepDefinitions: './features/step-definitions/steps.js',
pages: './features/page/objects/**/*.js',
generateTestFiles: true,
usePageObjects: true,
framework: '@wdio/cucumber-service$--$cucumber'
} as any, '/foo/bar')
expect(generatedPaths.relativePath).toEqual('../page/objects')
})
it('Cucumber with pageobjects and steps different path', () => {
const generatedPaths = getPathForFileGeneration({
runner: 'local',
stepDefinitions: 'cucumber/features/steps',
pages: 'cucumber/features/pages',
generateTestFiles: true,
usePageObjects: true,
framework: '@wdio/cucumber-service$--$cucumber'
} as any, '/foo/bar')
expect(generatedPaths.relativePath).toEqual('')
})
it('Cucumber with answer that is not a path', () => {
const generatedPaths = getPathForFileGeneration({
runner: 'local',
stepDefinitions: 'y',
pages: 'h',
generateTestFiles: true,
usePageObjects: true,
framework: '@wdio/cucumber-service$--$cucumber'
} as any, '/foo/bar')
expect(generatedPaths.relativePath).toEqual('../h')
})
it('Mocha with pageobjects default values', () => {
const generatedPaths = getPathForFileGeneration({
runner: 'local',
specs: './test/specs/**/*.js',
pages: './test/pageobjects/**/*.js',
generateTestFiles: true,
usePageObjects: true,
framework: '@wdio/cucumber-service$--$mocha'
} as any, '/foo/bar')
expect(generatedPaths.relativePath).toEqual('../pageobjects')
})
it('Mocha with pageobjects different path', () => {
const generatedPaths = getPathForFileGeneration({
runner: 'local',
specs: './test/specs/files/**/*.js',
pages: './test/pageobjects/**/*.js',
generateTestFiles: true,
usePageObjects: true,
framework: '@wdio/cucumber-service$--$mocha'
} as any, '/foo/bar')
expect(generatedPaths.relativePath).toEqual('../../pageobjects')
})
it('Do not auto generate file', () => {
const generatedPaths = getPathForFileGeneration({
runner: 'local',
specs: './test/specs/files/**/*.js',
pages: './test/pageobjects/**/*.js',
generateTestFiles: false,
usePageObjects: true,
framework: '@wdio/cucumber-service$--$mocha'
} as any, '/foo/bar')
expect(generatedPaths.relativePath).toEqual('')
})
it('Do not use PageObjects', () => {
const generatedPaths = getPathForFileGeneration({
runner: 'local',
specs: './test/specs/files/**/*.js',
pages: './test/pageobjects/**/*.js',
generateTestFiles: true,
usePageObjects: false,
framework: '@wdio/cucumber-service$--$mocha'
} as any, '/foo/bar')
expect(generatedPaths.relativePath).toEqual('')
})
})
test('getDefaultFiles', async () => {
const files = '/foo/bar'
expect(await getDefaultFiles({ projectRootCorrect: false, projectRoot: '/bar', isUsingTypeScript: true } as any, files))
.toBe(path.join('/bar', 'foo', 'bar.ts'))
expect(await getDefaultFiles({ projectRootCorrect: false, projectRoot: '/bar', isUsingTypeScript: true, preset: 'vite-plugin-solid$--$solid' } as any, files))
.toBe(path.join('/bar', 'foo', 'bar.tsx'))
expect(await getDefaultFiles({ projectRootCorrect: false, projectRoot: '/bar', isUsingTypeScript: false } as any, files))
.toBe(path.join('/bar', 'foo', 'bar.js'))
})
test('original implementation of getDefaultFiles handles projectRoot with no package.json', async () => {
const files = '/foo/bar'
vi.mocked(readPackageUp).mockRestore()
expect(await getDefaultFiles({ createPackageJSON: true, projectRoot: '/project-root-with-no-package.json', isUsingTypeScript: false } as any, files))
.toBe(path.join('/project-root-with-no-package.json', 'foo', 'bar.js'))
})
test('getProjectRoot', async () => {
expect(await getProjectRoot()).toBe('/foo')
expect(await getProjectRoot({
projectRootCorrect: true
} as any)).toBe('/foo')
expect(await getProjectRoot({
projectRootCorrect: false,
projectRoot: '/bar/foo'
} as any)).toBe('/bar/foo')
})
test('detectCompiler', async () => {
vi.mocked(readPackageUp).mockRestore()
const answers = { createPackageJSON: true } as Questionnair
expect(await detectCompiler(answers)).toBe(false)
})
test('getAnswers', async () => {
let answers = await getAnswers(true)
delete answers.pages
delete answers.specs
expect(answers).toMatchSnapshot()
vi.mocked(inquirer.prompt).mockReturnValue('some value' as any)
answers = await getAnswers(false)
delete answers.pages
delete answers.specs
expect(answers).toBe('some value')
expect(inquirer.prompt).toBeCalledTimes(2)
expect(vi.mocked(inquirer.prompt).mock.calls[0][0][0].when).toBe(false)
vi.mocked(readPackageUp).mockResolvedValue(undefined)
vi.mocked(inquirer.prompt).mockClear()
expect(await getAnswers(false)).toBe('some value')
expect(inquirer.prompt).toBeCalledTimes(2)
expect(vi.mocked(inquirer.prompt).mock.calls[0][0][0].when).toBe(true)
})
test('getProjectProps', async () => {
vi.mocked(readPackageUp).mockResolvedValue(undefined)
expect(await getProjectProps('/foo/bar')).toBe(undefined)
expect(readPackageUp).toBeCalledWith({ cwd: '/foo/bar' })
vi.mocked(readPackageUp).mockResolvedValue({
path: '/foo/bar',
packageJson: {
name: 'cool-test-module2',
}
})
expect(await getProjectProps('/foo/bar')).toEqual({
esmSupported: false,
packageJson: { name: 'cool-test-module2' },
path: '/foo'
})
})
test('createPackageJSON', async () => {
await createPackageJSON({} as any)
expect(fs.writeFile).toBeCalledTimes(0)
await createPackageJSON({
createPackageJSON: true
} as any)
expect(console.log).toBeCalledTimes(2)
})
test('npmInstall', async () => {
const parsedAnswers = {
rawAnswers: {
services: ['foo$--$bar'],
preset: 'barfoo$--$vue'
},
projectRootCorrect: false,
projectRoot: '/foo/bar',
isUsingTypeScript: true,
framework: 'jasmine',
installTestingLibrary: true,
packagesToInstall: ['foo$--$bar', 'bar$--$foo'],
npmInstall: true,
includeVisualTesting: true
} as any
await npmInstall(parsedAnswers, 'next')
expect(installPackages).toBeCalledTimes(1)
expect(vi.mocked(installPackages).mock.calls).toMatchSnapshot()
})
test('not npmInstall', async () => {
const parsedAnswers = {
rawAnswers: {
services: ['foo$--$bar'],
preset: 'barfoo$--$vue'
},
installTestingLibrary: true,
packagesToInstall: ['foo$--$bar', 'bar$--$foo'],
npmInstall: false
} as any
await npmInstall(parsedAnswers, 'next')
expect(installPackages).toBeCalledTimes(0)
expect(vi.mocked(console.log).mock.calls[0][0]).toContain('To install dependencies, execute')
})
test('setupTypeScript', async () => {
await setupTypeScript({} as any)
expect(fs.writeFile).toBeCalledTimes(0)
const parsedAnswers = {
isUsingTypeScript: true,
esmSupport: true,
rawAnswers: {
framework: 'foo',
services: [
'wdio-foobar-service$--$foobar',
'wdio-electron-service$--$electron'
]
},
packagesToInstall: [],
tsConfigFilePath: '/foobar/tsconfig.json'
} as any
await setupTypeScript(parsedAnswers)
expect(vi.mocked(fs.writeFile).mock.calls[0][1]).toMatchSnapshot()
})
test('setupTypeScript does not create tsconfig.json if TypeScript was not selected', async () => {
const parsedAnswers = {
isUsingTypeScript: false,
esmSupport: true,
rawAnswers: {
framework: 'foo',
services: [
'wdio-foobar-service$--$foobar',
'wdio-electron-service$--$electron'
]
},
packagesToInstall: [],
tsConfigFilePath: '/foobar/tsconfig.json'
} as any
await setupTypeScript(parsedAnswers)
expect(fs.writeFile).not.toBeCalled()
expect(parsedAnswers.packagesToInstall).toEqual([])
})
test('setupTypeScript does not create tsconfig.json if there is already one', async () => {
const parsedAnswers = {
isUsingTypeScript: true,
esmSupport: true,
rawAnswers: {
framework: 'foo',
services: [
'wdio-foobar-service$--$foobar',
'wdio-electron-service$--$electron'
]
},
packagesToInstall: [],
tsConfigFilePath: '/foobar/tsconfig.json',
hasRootTSConfig: true
} as any
await setupTypeScript(parsedAnswers)
expect(fs.writeFile).not.toBeCalled()
})
describe.skip('createWDIOScript', () => {
it('can run with success', async () => {
const promise = createWDIOScript({ wdioConfigPath: '/foo/bar/wdio.conf.js' } as any)
expect(await promise)
.toBe(true)
expect(cp.spawn).toBeCalledTimes(1)
})
it('does not fail the process if spawn errors out', async () => {
vi.mocked(cp.spawn).mockReturnValue({ on: vi.fn().mockImplementation((ev, fn) => fn(1)) } as any)
expect(await createWDIOScript({ wdioConfigPath: '/foo/bar/wdio.conf.js' } as any))
.toBe(false)
expect(cp.spawn).toBeCalledTimes(1)
})
})
test('createWDIOConfig', async () => {
const answers = await parseAnswers(true)
answers.destSpecRootPath = '/tests/specs'
answers.destPageObjectRootPath = '/tests/specs'
answers.stepDefinitions = './foo/bar'
answers.specs = '/foo/bar/**'
await createWDIOConfig(answers as any)
expect(fs.writeFile).toBeCalledTimes(7)
expect(
vi.mocked(fs.writeFile).mock.calls[0][0]
.toString()
.endsWith(path.resolve('wdio.conf.js'))
).toBe(true)
})
test('runAppiumInstaller', async () => {
expect(await runAppiumInstaller({ e2eEnvironment: 'web' } as any))
.toBe(undefined)
expect(console.log).toBeCalledTimes(0)
expect($).toBeCalledTimes(0)
vi.mocked(inquirer.prompt).mockResolvedValue({
continueWithAppiumSetup: false
})
expect(await runAppiumInstaller({ e2eEnvironment: 'mobile' } as any))
.toBe(undefined)
expect(console.log).toBeCalledTimes(1)
expect($).toBeCalledTimes(0)
vi.mocked(inquirer.prompt).mockResolvedValue({
continueWithAppiumSetup: true
})
expect(await runAppiumInstaller({ e2eEnvironment: 'mobile' } as any))
.toEqual(['npx appium-installer'])
expect($).toBeCalledTimes(1)
})
afterEach(()=>{
vi.mocked(inquirer.prompt).mockClear()
vi.mocked(readDir).mockClear()
vi.mocked(ejs.renderFile).mockClear()
vi.mocked(fs.writeFile).mockClear()
vi.mocked(fs.mkdir).mockClear()
vi.mocked(cp.spawn).mockClear()
vi.mocked(installPackages).mockClear()
})