From af6411d5f8c9fd1c3d9b4f3c2d79e8f1bd0efbf2 Mon Sep 17 00:00:00 2001 From: Jacob Bolda Date: Tue, 27 Apr 2021 09:14:24 -0500 Subject: [PATCH] feat: setup testing for CTA (#1615) * feat: setup testing for CTA * install with yarn * build before test * add yarn to npm runs for install / test start * add dev mode to link cli.js and api locally * remove fixtures * run tests serially * cli.js build-release avoids webpack error * assert on package.json contents as first check * run tauri build and split out custom asserts * add changefile * shorten workflow name * too short * exclude npm@6 on node@16 * increase timeout, tauri build takes a bit of time * only assert that the tauri script exists --- .changes/cta-testing-suite.md | 5 + .github/workflows/test-cta.yml | 97 +++++++++++ tooling/create-tauri-app/.eslintrc.js | 2 +- .../create-tauri-app/bin/create-tauri-app.js | 62 ++++--- tooling/create-tauri-app/jest.config.js | 16 ++ tooling/create-tauri-app/package.json | 11 +- tooling/create-tauri-app/test/index.spec.ts | 161 ++++++++++++++++++ tooling/create-tauri-app/tsconfig.json | 5 +- .../types/fixturez/index.d.ts | 1 + 9 files changed, 331 insertions(+), 29 deletions(-) create mode 100644 .changes/cta-testing-suite.md create mode 100644 .github/workflows/test-cta.yml create mode 100644 tooling/create-tauri-app/jest.config.js create mode 100644 tooling/create-tauri-app/test/index.spec.ts create mode 100644 tooling/create-tauri-app/types/fixturez/index.d.ts diff --git a/.changes/cta-testing-suite.md b/.changes/cta-testing-suite.md new file mode 100644 index 000000000..7f77533b3 --- /dev/null +++ b/.changes/cta-testing-suite.md @@ -0,0 +1,5 @@ +--- +"create-tauri-app": patch +--- + +We setup an e2e type test suite for CTA. It is mostly an internal change, but should help with stability moving forward. diff --git a/.github/workflows/test-cta.yml b/.github/workflows/test-cta.yml new file mode 100644 index 000000000..5a75721cb --- /dev/null +++ b/.github/workflows/test-cta.yml @@ -0,0 +1,97 @@ +# Copyright 2019-2021 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: test create-tauri-app + +on: + workflow_dispatch: + inputs: + branch: + default: "dev" + pull_request: + paths: + - "tooling/create-tauri-app/**" + +env: + RUST_BACKTRACE: 1 + +jobs: + create-recipe-with-npm: + name: "node@${{ matrix.node }} + npm@${{ matrix.manager }}: ${{ matrix.recipe }}" + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node: ["14", "16"] + manager: ["6", "7"] + recipe: ["vanillajs", "reactjs", "reactts", "vite", "vuecli"] + exclude: + - node: "16" + manager: "6" + + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref || github.event.inputs.branch }} + - name: install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - uses: volta-cli/action@v1 + with: + node-version: ${{ matrix.node }} + npm-version: ${{ matrix.manager }} + yarn-version: 1.22.5 + - name: install webkit2gtk (ubuntu only) + if: matrix.platform == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y webkit2gtk-4.0 + - run: yarn + working-directory: tooling/create-tauri-app + - run: yarn build + working-directory: tooling/create-tauri-app + - run: yarn test + working-directory: tooling/create-tauri-app + env: + TAURI_RECIPE: ${{ matrix.recipe }} + TAURI_RUN_MANAGER: "npm" + + create-recipe-with-yarn: + name: "node@${{ matrix.node }} + yarn@1: ${{ matrix.recipe }}" + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node: ["14", "16"] + recipe: ["vanillajs", "reactjs", "reactts", "vite", "vuecli"] + + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref || github.event.inputs.branch }} + - name: install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - uses: volta-cli/action@v1 + with: + node-version: ${{ matrix.node }} + yarn-version: 1.22.5 + - name: install webkit2gtk (ubuntu only) + if: matrix.platform == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y webkit2gtk-4.0 + - run: yarn + working-directory: tooling/create-tauri-app + - run: yarn build + working-directory: tooling/create-tauri-app + - run: yarn test + working-directory: tooling/create-tauri-app + env: + TAURI_RECIPE: ${{ matrix.recipe }} + TAURI_RUN_MANAGER: "yarn" diff --git a/tooling/create-tauri-app/.eslintrc.js b/tooling/create-tauri-app/.eslintrc.js index 24b1e39d2..0eb2bd097 100644 --- a/tooling/create-tauri-app/.eslintrc.js +++ b/tooling/create-tauri-app/.eslintrc.js @@ -7,7 +7,7 @@ module.exports = { }, parser: '@typescript-eslint/parser', - + ignorePatterns: ['.eslintrc.js', 'jest.config.js', 'test/**/*'], extends: [ 'standard-with-typescript', 'plugin:@typescript-eslint/recommended-requiring-type-checking', diff --git a/tooling/create-tauri-app/bin/create-tauri-app.js b/tooling/create-tauri-app/bin/create-tauri-app.js index 16fb0de9b..0def3fafa 100755 --- a/tooling/create-tauri-app/bin/create-tauri-app.js +++ b/tooling/create-tauri-app/bin/create-tauri-app.js @@ -29,6 +29,7 @@ const { * @property {boolean} log * @property {boolean} d * @property {boolean} directory + * @property {boolean} dev * @property {string} r * @property {string} recipe */ @@ -41,7 +42,7 @@ const createTauriApp = async (cliArgs) => { l: 'log', m: 'manager', d: 'directory', - b: 'binary', + dev: 'dev', t: 'tauri-path', A: 'app-name', W: 'window-title', @@ -49,7 +50,7 @@ const createTauriApp = async (cliArgs) => { P: 'dev-path', r: 'recipe' }, - boolean: ['h', 'l', 'ci'] + boolean: ['h', 'l', 'ci', 'dev'] }) if (argv.help) { @@ -62,13 +63,9 @@ const createTauriApp = async (cliArgs) => { return false // do this for node consumers and tests } - if (argv.ci) { - return runInit(argv) - } else { - return getOptionsInteractive(argv).then((responses) => - runInit(argv, responses) - ) - } + return getOptionsInteractive(argv, !argv.ci).then((responses) => + runInit(argv, responses) + ) } function printUsage() { @@ -95,8 +92,12 @@ function printUsage() { `) } -const getOptionsInteractive = (argv) => { - let defaultAppName = argv.A || 'tauri-app' +const getOptionsInteractive = (argv, ask) => { + const defaults = { + appName: argv.A || 'tauri-app', + tauri: { window: { title: 'Tauri App' } }, + recipeName: argv.r || 'vanillajs' + } return inquirer .prompt([ @@ -104,29 +105,33 @@ const getOptionsInteractive = (argv) => { type: 'input', name: 'appName', message: 'What is your app name?', - default: defaultAppName, - when: !argv.A + default: defaults.appName, + when: ask && !argv.A }, { type: 'input', name: 'tauri.window.title', message: 'What should the window title be?', - default: 'Tauri App', - when: () => !argv.W + default: defaults.tauri.window.title, + when: ask && !argv.W }, { type: 'list', name: 'recipeName', message: 'Would you like to add a UI recipe?', choices: recipeDescriptiveNames, - default: 'No recipe', - when: () => !argv.r + default: defaults.recipeName, + when: ask && !argv.r } ]) + .then((answers) => ({ + ...defaults, + ...answers + })) .catch((error) => { if (error.isTtyError) { // Prompt couldn't be rendered in the current environment - console.log( + console.warn( 'It appears your terminal does not support interactive prompts. Using default values.' ) runInit() @@ -150,10 +155,10 @@ async function runInit(argv, config = {}) { let recipe - if (recipeName !== undefined) { - recipe = recipeByDescriptiveName(recipeName) - } else if (argv.r) { + if (argv.r) { recipe = recipeByShortName(argv.r) + } else if (recipeName !== undefined) { + recipe = recipeByDescriptiveName(recipeName) } let buildConfig = { @@ -201,10 +206,21 @@ async function runInit(argv, config = {}) { // Vue CLI plugin automatically runs these if (recipe.shortName !== 'vuecli') { console.log('===== installing any additional needed deps =====') + if (argv.dev) { + await shell('yarn', ['link', '@tauri-apps/cli'], { + cwd: appDirectory + }) + await shell('yarn', ['link', '@tauri-apps/api'], { + cwd: appDirectory + }) + } + await install({ appDir: appDirectory, dependencies: recipe.extraNpmDependencies, - devDependencies: ['@tauri-apps/cli', ...recipe.extraNpmDevDependencies], + devDependencies: argv.dev + ? [...recipe.extraNpmDevDependencies] + : ['@tauri-apps/cli'].concat(recipe.extraNpmDevDependencies), packageManager }) @@ -216,7 +232,7 @@ async function runInit(argv, config = {}) { packageManager === 'npm' && !argv.b ? ['run', 'tauri', '--', 'init'] : ['tauri', 'init'] - await shell(binary, [...runTauriArgs, ...initArgs], { + await shell(binary, [...runTauriArgs, ...initArgs, '--ci'], { cwd: appDirectory }) } diff --git a/tooling/create-tauri-app/jest.config.js b/tooling/create-tauri-app/jest.config.js new file mode 100644 index 000000000..895b7e211 --- /dev/null +++ b/tooling/create-tauri-app/jest.config.js @@ -0,0 +1,16 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + modulePathIgnorePatterns: ['__fixtures__'], + testMatch: ['/test/**/*.spec.ts'], + moduleFileExtensions: ['ts', 'js', 'json'], + globals: { + 'ts-jest': { + tsconfig: 'tsconfig.json' + } + } +} diff --git a/tooling/create-tauri-app/package.json b/tooling/create-tauri-app/package.json index 78ee98601..dd2f25172 100644 --- a/tooling/create-tauri-app/package.json +++ b/tooling/create-tauri-app/package.json @@ -29,7 +29,8 @@ "lint-fix": "eslint --fix --ext ts \"./src/**/*.ts\"", "lint:lockfile": "lockfile-lint --path yarn.lock --type yarn --validate-https --allowed-hosts npm yarn", "format": "prettier --write --end-of-line=auto \"./**/*.{js,jsx,ts,tsx,html,css,json}\" --ignore-path .gitignore", - "format:check": "prettier --check --end-of-line=auto \"./**/*.{js,jsx,ts,tsx,html,css,json}\" --ignore-path .gitignore" + "format:check": "prettier --check --end-of-line=auto \"./**/*.{js,jsx,ts,tsx,html,css,json}\" --ignore-path .gitignore", + "test": "jest --runInBand" }, "dependencies": { "execa": "^5.0.0", @@ -41,11 +42,12 @@ "@rollup/plugin-commonjs": "18.0.0", "@rollup/plugin-node-resolve": "11.2.1", "@rollup/plugin-typescript": "8.2.1", - "@typescript-eslint/eslint-plugin": "4.22.0", - "@typescript-eslint/parser": "4.22.0", "@types/cross-spawn": "6.0.2", "@types/inquirer": "7.3.1", + "@types/jest": "^26.0.22", "@types/semver": "7.3.4", + "@typescript-eslint/eslint-plugin": "4.22.0", + "@typescript-eslint/parser": "4.22.0", "eslint": "7.24.0", "eslint-config-prettier": "8.2.0", "eslint-config-standard-with-typescript": "20.0.0", @@ -54,8 +56,11 @@ "eslint-plugin-node": "11.1.0", "eslint-plugin-promise": "5.1.0", "eslint-plugin-security": "1.4.0", + "fixturez": "^1.1.0", + "jest": "^26.6.3", "prettier": "2.2.1", "rollup": "2.45.1", + "ts-jest": "^26.5.5", "tslib": "2.2.0", "typescript": "4.2.4" } diff --git a/tooling/create-tauri-app/test/index.spec.ts b/tooling/create-tauri-app/test/index.spec.ts new file mode 100644 index 000000000..62b8f997e --- /dev/null +++ b/tooling/create-tauri-app/test/index.spec.ts @@ -0,0 +1,161 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import execa from 'execa' +import fixtures from 'fixturez' +const f = fixtures(__dirname) +import path from 'path' +import fs from 'fs' + +const ctaBinary = path.resolve('./bin/create-tauri-app.js') +const clijs = path.resolve('../cli.js/') +const api = path.resolve('../api/') + +const manager = process.env.TAURI_RUN_MANAGER ?? 'npm' +const recipes = process.env.TAURI_RECIPE + ? [process.env.TAURI_RECIPE] + : ['vanillajs', 'reactjs', 'reactts', 'vite', 'vuecli'] +const timeoutLong = 900000 +const timeoutLittleLonger = 930000 +const logOut = false ? 'inherit' : 'pipe' + +beforeAll(async () => { + const installCLI = await execa('yarn', [], { + stdio: logOut, + cwd: clijs, + timeout: timeoutLong + }) + + const buildCLI = await execa('yarn', ['build-release'], { + stdio: logOut, + cwd: clijs, + timeout: timeoutLong + }) + + const linkCLI = await execa('yarn', ['link'], { + stdio: logOut, + cwd: clijs, + timeout: timeoutLong + }) + + const installAPI = await execa('yarn', [], { + stdio: logOut, + cwd: api, + timeout: timeoutLong + }) + + const buildAPI = await execa('yarn', ['build'], { + stdio: logOut, + cwd: api, + timeout: timeoutLong + }) + + const linkAPI = await execa('yarn', ['link'], { + stdio: logOut, + cwd: api, + timeout: timeoutLong + }) +}, timeoutLittleLonger) + +describe('CTA', () => { + describe.each(recipes.map((recipe) => [recipe, 'tauri-app']))( + `%s recipe`, + (recipe: string, appName: string) => { + it( + 'runs', + async () => { + // creates a temp folder to run CTA within (this is our cwd) + const folder = f.temp() + const appFolder = path.join(folder, appName) + + // runs CTA with all args set to avoid any prompts + const cta = await execa( + 'node', + [ + ctaBinary, + '--manager', + manager, + '--recipe', + recipe, + '--ci', + '--dev' + ], + { + all: true, + stdio: logOut, + cwd: folder, + timeout: timeoutLong + } + ) + + // check to make certain it didn't fail anywhere + expect(cta.failed).toBe(false) + expect(cta.timedOut).toBe(false) + expect(cta.isCanceled).toBe(false) + expect(cta.killed).toBe(false) + expect(cta.signal).toBe(undefined) + + // run a tauri build to check if what we produced + // can actually create an app + // TODO long term we will want to hook this up to a real test harness + // and then run that test suite instead + let opts: string[] = [] + if (manager === 'npm') { + opts = ['run', 'tauri', '--', 'build'] + } else if (manager === 'yarn') { + opts = ['tauri', 'build'] + } + const tauriBuild = await execa(manager, opts, { + all: true, + stdio: logOut, + cwd: appFolder, + timeout: timeoutLong + }) + + expect(tauriBuild.failed).toBe(false) + expect(tauriBuild.timedOut).toBe(false) + expect(tauriBuild.isCanceled).toBe(false) + expect(tauriBuild.killed).toBe(false) + expect(tauriBuild.signal).toBe(undefined) + + const packageFileOutput: { + [k: string]: string | object + } = JSON.parse( + await fs.promises.readFile( + path.join(appFolder, 'package.json'), + 'utf-8' + ) + ) + expect(packageFileOutput['name']).toBe(appName) + + const assertCustom: { [k: string]: Function } = { + vanillajs: () => { + expect(packageFileOutput['scripts']).toMatchObject({ + tauri: 'tauri' + }) + }, + reactjs: () => { + expect(packageFileOutput['scripts']).toEqual( + expect.objectContaining({ + tauri: 'tauri' + }) + ) + }, + reactts: () => { + expect(packageFileOutput['scripts']).toEqual( + expect.objectContaining({ + tauri: 'tauri' + }) + ) + } + } + + const getCustomAsserts = assertCustom[recipe] + if (getCustomAsserts) getCustomAsserts() + }, + timeoutLittleLonger + ) + } + ) +}) diff --git a/tooling/create-tauri-app/tsconfig.json b/tooling/create-tauri-app/tsconfig.json index 9fd056544..de5e3dd92 100644 --- a/tooling/create-tauri-app/tsconfig.json +++ b/tooling/create-tauri-app/tsconfig.json @@ -7,8 +7,9 @@ "pretty": true, "esModuleInterop": true, "resolveJsonModule": true, - "moduleResolution": "node" + "moduleResolution": "node", + "typeRoots": ["./types", "node_modules/@types"] }, "include": ["src"], - "exclude": ["src/templates"] + "exclude": ["src/templates", "types", "test", "__fixtures__"] } diff --git a/tooling/create-tauri-app/types/fixturez/index.d.ts b/tooling/create-tauri-app/types/fixturez/index.d.ts new file mode 100644 index 000000000..3d7242c6a --- /dev/null +++ b/tooling/create-tauri-app/types/fixturez/index.d.ts @@ -0,0 +1 @@ +declare module 'fixturez'