import os from 'node:os' import fs from 'node:fs/promises' import { vi, test, expect, describe, beforeEach, it } from 'vitest' import { canAccessConfigPath, missingConfigurationPrompt, parseAnswers, runConfigCommand } from '../../src/cli/utils.js' import { createPackageJSON, npmInstall, createWDIOConfig, createWDIOScript, runAppiumInstaller, getAnswers } from '../../src/utils.js' import { BackendChoice } from '../../src/constants.js' import inquirer from 'inquirer' import path from 'node:path' import type { Questionnair } from '../../src/types.js' vi.mock('node:fs/promises', async (orig) => ({ ...(await orig()) as any, default: { access: vi.fn().mockRejectedValue('Yay') } })) vi.mock('inquirer') vi.mock('../../src/utils.js', async () => { const actual = await vi.importActual('../../src/utils.js') as Promise return { ...actual, getAnswers: vi.fn(), getProjectProps: vi.fn().mockResolvedValue({ path: '/foo/bar', esmSupported: true, packageJson: { name: 'my-module' } }), getProjectRoot: vi.fn().mockReturnValue('/foo/bar'), createPackageJSON: vi.fn(), npmInstall: vi.fn(), createWDIOConfig: vi.fn(), createWDIOScript: vi.fn(), runAppiumInstaller: vi.fn() } }) vi.mock('../src/install', () => ({ installPackages: vi.fn(), getInstallCommand: vi.fn().mockReturnValue('npm install foo bar --save-dev') })) global.console.log = vi.fn() const isUsingWindows = os.platform() === 'win32' test('runConfigCommand', async () => { await runConfigCommand({ projectRootDir: '/foo/bar' } as any, 'next') expect(createPackageJSON).toBeCalledTimes(1) expect(npmInstall).toBeCalledTimes(1) expect(createWDIOConfig).toBeCalledTimes(1) expect(createWDIOScript).toBeCalledTimes(1) expect(runAppiumInstaller).toBeCalledTimes(1) expect(vi.mocked(console.log).mock.calls).toMatchSnapshot() }) describe('canAccessConfigPath', () => { beforeEach(()=>{ vi.mocked(fs.access).mockClear() }) test('no extension only', async () => { vi.mocked(fs.access) .mockRejectedValueOnce(new Error('not found')) .mockRejectedValueOnce(new Error('not found')) .mockRejectedValueOnce(new Error('not found')) .mockResolvedValue('Yay' as any) expect(await canAccessConfigPath('/foo/bar')).toBe('/foo/bar.mts') expect(fs.access).not.toBeCalledWith('/foo/bar') expect(fs.access).toBeCalledWith('/foo/bar.js') expect(fs.access).toBeCalledWith('/foo/bar.ts') expect(fs.access).toBeCalledWith('/foo/bar.mjs') expect(fs.access).toBeCalledWith('/foo/bar.mts') }) test('with full path of the existed file', async () => { vi.mocked(fs.access) .mockResolvedValue('Yay' as any) expect(await canAccessConfigPath('/foo/bar', '/foo/bar.ts')).toBe('/foo/bar.ts') expect(fs.access).toHaveBeenCalledTimes(1) expect(fs.access).toBeCalledWith('/foo/bar.ts') }) test('with full path of the not existed file', async () => { vi.mocked(fs.access) .mockRejectedValueOnce(new Error('not found')) .mockRejectedValueOnce(new Error('not found')) .mockRejectedValueOnce(new Error('not found')) .mockRejectedValueOnce(new Error('not found')) .mockResolvedValue('Yay' as any) expect(await canAccessConfigPath('/foo/bar', '/foo/bar.ts')).toBe('/foo/bar.mts') expect(fs.access).toBeCalledWith('/foo/bar.js') expect(fs.access).toBeCalledWith('/foo/bar.ts') expect(fs.access).toBeCalledWith('/foo/bar.mjs') expect(fs.access).toBeCalledWith('/foo/bar.mts') }) }) test('missingConfigurationPrompt does not init wizard if user does not want to', async () => { vi.mocked(getAnswers).mockResolvedValue({ backend: BackendChoice.Local, generateTestFiles: false, baseUrl: 'http://localhost', runner: '@wdio/local-runner$--$local', framework: '@wdio/mocha-framework$--$mocha', preset: '@sveltejs/vite-plugin-svelte$--$svelte', reporters: [], plugins: [], services: [], npmInstall: true } as any) const runConfigCmd = vi.fn() vi.mocked(inquirer.prompt).mockResolvedValue({}) await missingConfigurationPrompt('config', 'foobar', runConfigCmd) expect(runConfigCmd).toBeCalledTimes(0) }) test.skipIf(isUsingWindows)('parseAnswers', async () => { vi.mocked(getAnswers).mockResolvedValue({ backend: BackendChoice.Local, specs: '/tmp/foobar/specs', pages: '/tmp/foobar/pageobjects', stepDefinitions: '/tmp/foobar/steps', generateTestFiles: true, usePageObjects: true, baseUrl: 'http://localhost', runner: '@wdio/local-runner$--$local', framework: '@wdio/mocha-framework$--$mocha', preset: '@sveltejs/vite-plugin-svelte$--$svelte', isUsingTypeScript: false, reporters: [ '@wdio/spec-reporter$--$spec' ], plugins: [ 'wdio-wait-for$--$wait-for' ], services: [ '@wdio/sauce-service$--$sauce' ], npmInstall: true } as any) const parsedAnswers = await parseAnswers(true) expect(parsedAnswers).toMatchSnapshot() }) describe('Serenity/JS project generation', () => { const defaultAnswers: Questionnair = { runner: '@wdio/local-runner$--$local$--$e2e', framework: undefined, // overridden in the tests backend: BackendChoice.Local, e2eEnvironment: 'web', browserEnvironment: ['chrome'], specs: '/foo/bar/specs', pages: '/foo/bar/pageobjects', generateTestFiles: true, usePageObjects: true, baseUrl: 'http://localhost', reporters: [], plugins: [], services: [], npmInstall: true, isUsingTypeScript: false, } as any it('marks serenityAdapter as false and destSerenityLibRootPath as blank if not using Serenity/JS', async () => { vi.mocked(getAnswers).mockResolvedValue({ ...defaultAnswers, framework: '@wdio/mocha-framework$--$mocha', }) const parsedAnswers = await parseAnswers(true) expect(parsedAnswers.framework).toEqual('mocha') expect(parsedAnswers.serenityAdapter).toEqual(false) expect(parsedAnswers.destSerenityLibRootPath).toEqual('') }) it('adds projectName so that it can be used in templates', async () => { vi.mocked(getAnswers).mockResolvedValue({ ...defaultAnswers, framework: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$mocha', serenityLibPath: './serenity', }) const parsedAnswers = await parseAnswers(true) expect(parsedAnswers.projectName).toEqual('my-module') }) describe('serenityLibPath', () => { it('supports a relative path', async () => { vi.mocked(getAnswers).mockResolvedValue({ ...defaultAnswers, framework: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$mocha', serenityLibPath: './serenity', }) const parsedAnswers = await parseAnswers(true) expect(parsedAnswers.destSerenityLibRootPath.split(path.sep)) .toEqual(expect.arrayContaining(['foo', 'bar', 'serenity'])) }) it('supports an absolute path', async () => { vi.mocked(getAnswers).mockResolvedValue({ ...defaultAnswers, framework: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$mocha', serenityLibPath: '/foo/bar/src/serenity', }) const parsedAnswers = await parseAnswers(true) expect(parsedAnswers.destSerenityLibRootPath.split(path.sep)) .toEqual(expect.arrayContaining(['foo', 'bar', 'src', 'serenity'])) }) }) describe('with Cucumber', () => { it('adds necessary packages', async () => { vi.mocked(getAnswers).mockResolvedValue({ ...defaultAnswers, framework: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$cucumber', specs: 'features/**/*.feature', stepDefinitions: 'features/step-definitions/steps' }) const parsedAnswers = await parseAnswers(true) expect(parsedAnswers.framework).toEqual('@serenity-js/webdriverio') expect(parsedAnswers.serenityAdapter).toEqual('cucumber') expect(parsedAnswers.destSpecRootPath.split(path.sep)).toEqual(expect.arrayContaining(['foo', 'bar', 'features'])) expect(parsedAnswers.packagesToInstall).toEqual([ '@wdio/local-runner', '@serenity-js/webdriverio', '@cucumber/cucumber', '@serenity-js/assertions', '@serenity-js/console-reporter', '@serenity-js/core', '@serenity-js/cucumber', '@serenity-js/rest', '@serenity-js/serenity-bdd', '@serenity-js/web', 'npm-failsafe', 'rimraf', ]) }) it('supports TypeScript projects', async () => { vi.mocked(getAnswers).mockResolvedValue({ ...defaultAnswers, framework: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$cucumber', specs: 'features/**/*.feature', stepDefinitions: 'features/step-definitions/steps', isUsingTypeScript: true, }) const parsedAnswers = await parseAnswers(true) expect(parsedAnswers.framework).toEqual('@serenity-js/webdriverio') expect(parsedAnswers.serenityAdapter).toEqual('cucumber') expect(parsedAnswers.packagesToInstall).toEqual([ '@wdio/local-runner', '@serenity-js/webdriverio', '@cucumber/cucumber', '@serenity-js/assertions', '@serenity-js/console-reporter', '@serenity-js/core', '@serenity-js/cucumber', '@serenity-js/rest', '@serenity-js/serenity-bdd', '@serenity-js/web', '@types/node', 'npm-failsafe', 'rimraf', ]) }) }) describe('with Jasmine', () => { it('adds necessary packages', async () => { vi.mocked(getAnswers).mockResolvedValue({ ...defaultAnswers, framework: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$jasmine' }) const parsedAnswers = await parseAnswers(true) expect(parsedAnswers.framework).toEqual('@serenity-js/webdriverio') expect(parsedAnswers.serenityAdapter).toEqual('jasmine') expect(parsedAnswers.packagesToInstall).toEqual([ '@wdio/local-runner', '@serenity-js/webdriverio', '@serenity-js/assertions', '@serenity-js/console-reporter', '@serenity-js/core', '@serenity-js/jasmine', '@serenity-js/rest', '@serenity-js/serenity-bdd', '@serenity-js/web', 'jasmine', 'npm-failsafe', 'rimraf', ]) }) it('supports TypeScript projects', async () => { vi.mocked(getAnswers).mockResolvedValue({ ...defaultAnswers, framework: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$jasmine', isUsingTypeScript: true, }) const parsedAnswers = await parseAnswers(true) expect(parsedAnswers.framework).toEqual('@serenity-js/webdriverio') expect(parsedAnswers.serenityAdapter).toEqual('jasmine') expect(parsedAnswers.packagesToInstall).toEqual([ '@wdio/local-runner', '@serenity-js/webdriverio', '@serenity-js/assertions', '@serenity-js/console-reporter', '@serenity-js/core', '@serenity-js/jasmine', '@serenity-js/rest', '@serenity-js/serenity-bdd', '@serenity-js/web', '@types/jasmine', '@types/node', 'jasmine', 'npm-failsafe', 'rimraf', ]) }) }) describe('with Mocha', () => { it('adds necessary packages', async () => { vi.mocked(getAnswers).mockResolvedValue({ ...defaultAnswers, framework: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$mocha', serenityLibPath: './serenity', }) const parsedAnswers = await parseAnswers(true) expect(parsedAnswers.framework).toEqual('@serenity-js/webdriverio') expect(parsedAnswers.serenityAdapter).toEqual('mocha') expect(parsedAnswers.packagesToInstall).toEqual([ '@wdio/local-runner', '@serenity-js/webdriverio', '@serenity-js/assertions', '@serenity-js/console-reporter', '@serenity-js/core', '@serenity-js/mocha', '@serenity-js/rest', '@serenity-js/serenity-bdd', '@serenity-js/web', 'mocha', 'npm-failsafe', 'rimraf', ]) }) it('supports TypeScript projects', async () => { vi.mocked(getAnswers).mockResolvedValue({ ...defaultAnswers, framework: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$mocha', serenityLibPath: './serenity', isUsingTypeScript: true, }) const parsedAnswers = await parseAnswers(true) expect(parsedAnswers.framework).toEqual('@serenity-js/webdriverio') expect(parsedAnswers.serenityAdapter).toEqual('mocha') expect(parsedAnswers.packagesToInstall).toEqual([ '@wdio/local-runner', '@serenity-js/webdriverio', '@serenity-js/assertions', '@serenity-js/console-reporter', '@serenity-js/core', '@serenity-js/mocha', '@serenity-js/rest', '@serenity-js/serenity-bdd', '@serenity-js/web', '@types/mocha', '@types/node', 'mocha', 'npm-failsafe', 'rimraf', ]) }) }) }) test('missingConfigurationPrompt does run config if user agrees', async () => { vi.mocked(getAnswers).mockResolvedValue({ backend: BackendChoice.Local, generateTestFiles: false, baseUrl: 'http://localhost', runner: '@wdio/local-runner$--$local', framework: '@wdio/mocha-framework$--$mocha', preset: '@sveltejs/vite-plugin-svelte$--$svelte', reporters: [], plugins: [], services: [], npmInstall: true } as any) const runConfigCmd = vi.fn() vi.mocked(inquirer.prompt).mockResolvedValue({ config: true }) await missingConfigurationPrompt('config', 'foobar', runConfigCmd) expect(runConfigCmd).toBeCalledTimes(1) })