浏览代码

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
Jacob Bolda 4 年之前
父节点
当前提交
af6411d5f8

+ 5 - 0
.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.

+ 97 - 0
.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"

+ 1 - 1
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',

+ 39 - 23
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
     })
   }

+ 16 - 0
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: ['<rootDir>/test/**/*.spec.ts'],
+  moduleFileExtensions: ['ts', 'js', 'json'],
+  globals: {
+    'ts-jest': {
+      tsconfig: 'tsconfig.json'
+    }
+  }
+}

+ 8 - 3
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"
   }

+ 161 - 0
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
+      )
+    }
+  )
+})

+ 3 - 2
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__"]
 }

+ 1 - 0
tooling/create-tauri-app/types/fixturez/index.d.ts

@@ -0,0 +1 @@
+declare module 'fixturez'