diff --git a/.github/workflows/test-vue3.yml b/.github/workflows/test-vue3.yml index 5813a3b2..b977ca62 100644 --- a/.github/workflows/test-vue3.yml +++ b/.github/workflows/test-vue3.yml @@ -13,13 +13,13 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + dir: ./examples/vue3 + jobs: - build-and-test: + build: runs-on: ubuntu-latest - name: Build and test - - env: - dir: ./examples/vue3 + name: Build steps: - uses: actions/checkout@v2 @@ -57,9 +57,67 @@ jobs: working-directory: ${{env.dir}} run: pnpm run story:build + - name: Save build + uses: actions/upload-artifact@v3 + with: + name: histoire-build + if-no-files-found: error + path: ${{env.dir}}/.histoire/dist + retention-days: 1 + + + test: + runs-on: ubuntu-latest + name: Test + needs: build + + strategy: + fail-fast: false + matrix: + containers: [0, 1, 2, 3, 4] + + steps: + - uses: actions/checkout@v2 + + - name: Install node + uses: actions/setup-node@v2 + with: + node-version: 16 + + - name: Install pnpm + uses: pnpm/action-setup@v2.2.4 + + - name: Get pnpm store directory + id: pnpm-cache + run: | + echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" + + - name: Cache pnpm modules + uses: actions/cache@v2 + with: + path: | + ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} + ~/.cache/Cypress + key: pnpm-v1-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + pnpm-v1-${{ runner.os }}- + + - name: Install dependencies + run: pnpm install + + - name: Download the build + uses: actions/download-artifact@v3 + with: + name: histoire-build + path: ${{env.dir}}/.histoire/dist + - name: Run tests working-directory: ${{env.dir}} run: pnpm run ci + env: + # the number of containers in the job matrix + TOTAL_RUNNERS: 5 + THIS_RUNNER: ${{ matrix.containers }} - uses: actions/upload-artifact@v2 if: failure() diff --git a/examples/vue3/cypress-parallel.mjs b/examples/vue3/cypress-parallel.mjs new file mode 100644 index 00000000..25440800 --- /dev/null +++ b/examples/vue3/cypress-parallel.mjs @@ -0,0 +1,116 @@ +// @TODO move to playwright + +import fs from 'fs/promises' +import { globby } from 'globby' +import { minimatch } from 'minimatch' +import { exec } from 'child_process' + +const totalRunners = parseInt(process.env.TOTAL_RUNNERS) +const thisRunner = parseInt(process.env.THIS_RUNNER) + +// These are the same properties that are set in cypress.config. +// In practice, it's better to export these from another file, and +// import them here and in cypress.config, so that both files use +// the same values. +const specPatterns = { + specPattern: './cypress/integration/*.{ts,tsx,js,jsx}', + excludeSpecPattern: ['tsconfig.json'], +} + +// used to roughly determine how many tests are in a file +const testPattern = /(^|\s)(it|test)\(/g + +async function getTestCount (filePath) { + const content = await fs.readFile(filePath, 'utf8') + return content.match(testPattern)?.length || 0 +} + +// adapated from: +// https://github.com/bahmutov/find-cypress-specs/blob/main/src/index.js +async function getSpecFilePaths () { + const options = specPatterns + + const files = await globby(options.specPattern, { + ignore: options.excludeSpecPattern, + }) + + // go through the files again and eliminate files that match + // the ignore patterns + const ignorePatterns = [...(options.excludeSpecPattern || [])] + + // a function which returns true if the file does NOT match + // all of our ignored patterns + const doesNotMatchAllIgnoredPatterns = (file) => { + // using {dot: true} here so that folders with a '.' in them are matched + // as regular characters without needing an '.' in the + // using {matchBase: true} here so that patterns without a globstar ** + // match against the basename of the file + const MINIMATCH_OPTIONS = { dot: true, matchBase: true } + return ignorePatterns.every((pattern) => { + return !minimatch(file, pattern, MINIMATCH_OPTIONS) + }) + } + + const filtered = files.filter(doesNotMatchAllIgnoredPatterns) + + return filtered +} + +async function sortSpecFilesByTestCount (specPathsOriginal) { + const specPaths = [...specPathsOriginal] + + const testPerSpec = {} + + for (const specPath of specPaths) { + testPerSpec[specPath] = await getTestCount(specPath) + } + + return ( + Object.entries(testPerSpec) + // Sort by the number of tests per spec file, so that we get a bit closer to + // splitting up the files evenly between the runners. It won't be perfect, + // but better than just splitting them randomly. And this will create a + // consistent file list/ordering so that file division is deterministic. + .sort((a, b) => b[1] - a[1]) + .map((x) => x[0]) + ) +} + +export function splitSpecs (specs, totalRunners, thisRunner) { + return specs.filter((_, index) => index % totalRunners === thisRunner) +} + +(async () => { + try { + const specFilePaths = await sortSpecFilesByTestCount(await getSpecFilePaths()) + + if (!specFilePaths.length) { + throw Error('No spec files found.') + } + + const specsToRun = splitSpecs(specFilePaths, totalRunners, thisRunner) + + const command = `yarn cypress run --spec "${specsToRun.join(',')}"` + + console.log(`Running: ${command}`) + + const commandProcess = exec(command) + + // pipe output because we want to see the results of the run + + if (commandProcess.stdout) { + commandProcess.stdout.pipe(process.stdout) + } + + if (commandProcess.stderr) { + commandProcess.stderr.pipe(process.stderr) + } + + commandProcess.on('exit', (code) => { + process.exit(code || 0) + }) + } catch (err) { + console.error(err) + process.exit(1) + } +})() diff --git a/examples/vue3/package.json b/examples/vue3/package.json index 01f8fcc5..602f2d60 100644 --- a/examples/vue3/package.json +++ b/examples/vue3/package.json @@ -8,7 +8,7 @@ "story:build": "histoire build", "story:preview": "histoire preview --port 4567", "ci": "start-server-and-test story:preview http://localhost:4567/ test", - "test": "cypress run", + "test": "node ./cypress-parallel.mjs", "test:dev": "cypress open --config baseUrl=http://localhost:6006", "test:examples": "pnpm run story:build && pnpm run ci" }, @@ -23,7 +23,9 @@ "@histoire/vendors": "workspace:*", "@vitejs/plugin-vue": "^4.0.0", "cypress": "^9.5.3", + "globby": "^13.2.2", "histoire": "workspace:*", + "minimatch": "^9.0.3", "nodemon": "^2.0.20", "sass": "^1.50.0", "start-server-and-test": "^1.14.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d534955..e53a21b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -228,9 +228,15 @@ importers: cypress: specifier: ^9.5.3 version: 9.7.0 + globby: + specifier: ^13.2.2 + version: 13.2.2 histoire: specifier: workspace:* version: link:../../packages/histoire + minimatch: + specifier: ^9.0.3 + version: 9.0.3 nodemon: specifier: ^2.0.20 version: 2.0.20 @@ -2762,7 +2768,7 @@ packages: c12: 1.1.0 consola: 2.15.3 defu: 6.1.2 - globby: 13.1.3 + globby: 13.2.2 hash-sum: 2.0.0 ignore: 5.2.4 jiti: 1.18.2 @@ -2788,7 +2794,7 @@ packages: c12: 1.1.0 consola: 2.15.3 defu: 6.1.2 - globby: 13.1.3 + globby: 13.2.2 hash-sum: 2.0.0 ignore: 5.2.4 jiti: 1.18.2 @@ -2815,7 +2821,7 @@ packages: c12: 1.2.0 consola: 2.15.3 defu: 6.1.2 - globby: 13.1.3 + globby: 13.2.2 hash-sum: 2.0.0 ignore: 5.2.4 jiti: 1.18.2 @@ -7880,6 +7886,16 @@ packages: merge2: 1.4.1 micromatch: 4.0.5 + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -8370,6 +8386,16 @@ packages: merge2: 1.4.1 slash: 4.0.0 + /globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.1 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 4.0.0 + /globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} dev: true @@ -9857,6 +9883,13 @@ packages: dependencies: brace-expansion: 2.0.1 + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist@1.2.7: resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} dev: true @@ -10029,7 +10062,7 @@ packages: escape-string-regexp: 5.0.0 etag: 1.8.1 fs-extra: 11.1.1 - globby: 13.1.3 + globby: 13.2.2 gzip-size: 7.0.0 h3: 1.4.0 hookable: 5.4.2 @@ -10100,7 +10133,7 @@ packages: escape-string-regexp: 5.0.0 etag: 1.8.1 fs-extra: 11.1.1 - globby: 13.1.3 + globby: 13.2.2 gzip-size: 7.0.0 h3: 1.6.4 hookable: 5.5.3 @@ -10324,7 +10357,7 @@ packages: escape-string-regexp: 5.0.0 estree-walker: 3.0.3 fs-extra: 11.1.0 - globby: 13.1.3 + globby: 13.2.2 h3: 1.4.0 hash-sum: 2.0.0 hookable: 5.4.2