🏡 index : quine-global/webdriverio.git

import path from 'node:path'
import fs from 'node:fs/promises'
import inquirer from 'inquirer'
import { configHelperSuccessMessage, CONFIG_HELPER_SERENITY_BANNER, SUPPORTED_CONFIG_FILE_EXTENSION, CONFIG_HELPER_INTRO, isNuxtProject, SUPPORTED_PACKAGES } from '../constants.js'
import type { ParsedAnswers } from '../types.js'
import { createPackageJSON, setupTypeScript, npmInstall, createWDIOConfig, createWDIOScript, runAppiumInstaller, convertPackageHashToObject, getAnswers, getPathForFileGeneration, getProjectProps, getProjectRoot, getSerenityPackages } from '../utils.js'

export async function runConfigCommand(parsedAnswers: ParsedAnswers, npmTag: string) {
    console.log('\n')

    await createPackageJSON(parsedAnswers)
    await setupTypeScript(parsedAnswers)
    await npmInstall(parsedAnswers, npmTag)
    await createWDIOConfig(parsedAnswers)
    await createWDIOScript(parsedAnswers)

    /**
     * print success message
     */
    console.log(
        configHelperSuccessMessage({
            projectRootDir: parsedAnswers.projectRootDir,
            runScript: parsedAnswers.serenityAdapter ? 'serenity' : 'wdio',
            extraInfo: parsedAnswers.serenityAdapter ? CONFIG_HELPER_SERENITY_BANNER : ''
        }),
    )

    await runAppiumInstaller(parsedAnswers)
}

/**
 * Helper utility used in `run` and `install` command to create config if none exist
 * @param {string} command to be executed by user
 * @param {string} configPath the path to a wdio.conf.[js/ts] file
 * @param {Function} runConfigCmd runConfig method to be replaceable for unit testing
 */
export async function missingConfigurationPrompt(command: string, configPath: string, runConfigCmd: Function = runConfigCommand) {
    const message = (
        `Could not execute "${command}" due to missing configuration, file ` +
        `"${path.parse(configPath).name}[.js/.ts]" not found! ` +
        'Would you like to create one?'
    )

    const { config } = await inquirer.prompt({
        type: 'confirm',
        name: 'config',
        message,
        default: false
    })

    /**
     * don't exit if running unit tests
     */
    if (!config) {
        /* istanbul ignore next */
        console.log(`No WebdriverIO configuration found in "${process.cwd()}"`)

        /* istanbul ignore next */
        if (!process.env.WDIO_UNIT_TESTS) {
            process.exit(0)
        }
        return configPath
    }

    const parsedAnswers = await parseAnswers(false)
    await runConfigCmd(parsedAnswers, 'latest')
    return configPath
}

export const parseAnswers = async function (yes: boolean): Promise<ParsedAnswers> {
    console.log(CONFIG_HELPER_INTRO)
    const answers = await getAnswers(yes)
    const frameworkPackage = convertPackageHashToObject(answers.framework)
    const runnerPackage = convertPackageHashToObject(answers.runner || SUPPORTED_PACKAGES.runner[0].value)
    const servicePackages = answers.services.map((service) => convertPackageHashToObject(service))
    const pluginPackages = answers.plugins.map((plugin) => convertPackageHashToObject(plugin))
    const serenityPackages = getSerenityPackages(answers)
    const reporterPackages = answers.reporters.map((reporter) => convertPackageHashToObject(reporter))
    const presetPackage = convertPackageHashToObject(answers.preset || '')
    const projectProps = await getProjectProps(process.cwd())
    const projectRootDir = await getProjectRoot(answers)

    const packagesToInstall: string[] = [
        runnerPackage.package,
        frameworkPackage.package,
        presetPackage.package,
        ...reporterPackages.map(reporter => reporter.package),
        ...pluginPackages.map(plugin => plugin.package),
        ...servicePackages.map(service => service.package),
        ...serenityPackages,
    ].filter(Boolean)

    /**
     * find relative paths between tests and pages
     */
    const hasRootTSConfig = await fs.access(path.resolve(projectRootDir, 'tsconfig.json')).then(() => true, () => false)
    const tsConfigFilePath = !hasRootTSConfig
        /**
         * if no tsconfig.json exists in project, create one
         */
        ? path.resolve(projectRootDir, 'tsconfig.json')
        /**
         * otherwise make it dependent on whether the user wants to autogenerate files
         */
        : answers.specs
            /**
             * if we have autogenerated spec files, put the tsconfig one above the spec file dir
             */
            ? path.resolve(
                path.dirname(answers.specs.split(path.sep).filter((s) => !s.includes('*')).join(path.sep)),
                'tsconfig.json'
            )
            /**
             * if no spec files are auto generated, create a wdio tsconfig and let the user deal with it
             */
            : path.resolve(projectRootDir, `tsconfig.${runnerPackage.short === 'local' ? 'e2e' : 'wdio'}.json`)
    const parsedPaths = getPathForFileGeneration(answers, projectRootDir)
    const isUsingTypeScript = answers.isUsingTypeScript
    const wdioConfigFilename = `wdio.conf.${isUsingTypeScript ? 'ts' : 'js'}`
    const wdioConfigPath = path.resolve(projectRootDir, wdioConfigFilename)

    return {
        projectName: projectProps?.packageJson.name || 'Test Suite',
        // default values required in templates
        ...({
            usePageObjects: false,
            installTestingLibrary: false
        }),
        ...answers,
        useSauceConnect: isNuxtProject || answers.useSauceConnect,
        rawAnswers: answers,
        runner: runnerPackage.short as 'local' | 'browser',
        preset: presetPackage.short,
        framework: frameworkPackage.short,
        purpose: runnerPackage.purpose,
        serenityAdapter: frameworkPackage.package === '@serenity-js/webdriverio' && frameworkPackage.purpose,
        reporters: reporterPackages.map(({ short }) => short),
        plugins: pluginPackages.map(({ short }) => short),
        services: servicePackages.map(({ short }) => short),
        specs: answers.specs && `./${path.relative(projectRootDir, answers.specs).replaceAll(path.sep, '/')}`,
        stepDefinitions: answers.stepDefinitions && `./${path.relative(projectRootDir, answers.stepDefinitions).replaceAll(path.sep, '/')}`,
        packagesToInstall,
        isUsingTypeScript,
        esmSupport: projectProps && !(projectProps.esmSupported) ? false : true,
        isSync: false,
        _async: 'async ',
        _await: 'await ',
        projectRootDir,
        destSpecRootPath: parsedPaths.destSpecRootPath,
        destStepRootPath: parsedPaths.destStepRootPath,
        destPageObjectRootPath: parsedPaths.destPageObjectRootPath,
        destSerenityLibRootPath: parsedPaths.destSerenityLibRootPath,
        relativePath: parsedPaths.relativePath,
        hasRootTSConfig,
        tsConfigFilePath,
        tsProject: `./${path.relative(projectRootDir, tsConfigFilePath).replaceAll(path.sep, '/')}`,
        wdioConfigPath
    }
}

/**
 * Helper utility used in `run` and `install` command to check whether a config file currently exists
 * @param configPathNoExtension the file path to the WDIO config file without extension
 * @param configPath the file path to the WDIO config file that is checked first if set
 * @returns {string} the path to the config file that exists, otherwise undefined
 */
export async function canAccessConfigPath(configPathNoExtension:string, configPath?:string): Promise<string|undefined> {
    return new Promise<string | undefined>((resolve, reject)=>{
        if (configPath) {
            fs.access(configPath).then(()=>resolve(configPath), reject)
        } else {
            reject()
        }
    }).catch(()=>Promise.all(SUPPORTED_CONFIG_FILE_EXTENSION.map(async (supportedExtension) => {
        const configPathWithExtension = `${configPathNoExtension}.${supportedExtension}`
        return fs.access(configPathWithExtension).then(() => configPathWithExtension, () => undefined)
    })).then(
        (configFilePaths) => configFilePaths.find(Boolean),
        () => undefined
    ))
}