From 356b41da98eda0cf00a0858531ad8afcc64c19c3 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 8 Aug 2022 20:19:56 +1000 Subject: [PATCH 01/32] Refactor `runCommand` out And add cd option and support for non-scripts --- scripts/once-per-template.ts | 92 +++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 27 deletions(-) diff --git a/scripts/once-per-template.ts b/scripts/once-per-template.ts index bbc359aedb94..b87914d4acda 100644 --- a/scripts/once-per-template.ts +++ b/scripts/once-per-template.ts @@ -1,6 +1,7 @@ /* eslint-disable no-restricted-syntax, no-await-in-loop */ import { Command } from 'commander'; import execa from 'execa'; +import { resolve, join } from 'path'; import { getOptions, getCommand, getOptionsOrPrompt, createOptions } from './utils/options'; import type { OptionSpecifier } from './utils/options'; @@ -8,6 +9,8 @@ import { filterDataForCurrentCircleCINode } from './utils/concurrency'; import TEMPLATES from '../code/lib/cli/src/repro-templates'; +const sandboxDir = resolve(__dirname, '../sandbox'); + export type Cadence = 'ci' | 'daily' | 'weekly'; export type Template = { name: string; @@ -22,20 +25,26 @@ export type Templates = Record; export async function parseCommand(commandline: string) { const argv = commandline.split(' '); - const [yarn, scriptName] = argv; - if (yarn !== 'yarn') throw new Error('only works with scripts at this point'); - - const { options } = await import(`./${scriptName}`); - - const command = new Command(scriptName); - const values = getOptions(command, options as OptionSpecifier, ['yarn', ...argv]); - - return { - scriptName, - command: `yarn ${scriptName}`, - options, - values, - }; + const [yarnOrNpx, scriptName] = argv; + + try { + const { options } = await import(`./${scriptName}`); + + const command = new Command(scriptName); + const values = getOptions(command, options as OptionSpecifier, [yarnOrNpx, ...argv]); + + return { + scriptName, + command: `yarn ${scriptName}`, + options, + values, + }; + } catch (err) { + return { + scriptName, + command: commandline, + }; + } } export const options = createOptions({ @@ -53,6 +62,15 @@ export const options = createOptions({ type: 'boolean', description: 'Run commands in parallel?', }, + cd: { + type: 'boolean', + description: 'Change directory into sandbox?', + inverse: true, + }, + junit: { + type: 'string', + description: 'Report results to junit XML file at path', + }, }); export function filterTemplates(templates: Templates, cadence: Cadence, scriptName: string) { @@ -64,11 +82,30 @@ export function filterTemplates(templates: Templates, cadence: Cadence, scriptNa return Object.fromEntries(filterDataForCurrentCircleCINode(jobTemplates)); } +const logger = console; +async function runCommand( + command: string, + execaOptions: execa.Options, + { template }: { template: string } +) { + try { + logger.log(`${template}: Running ${command}`); + + await execa.command(command, execaOptions); + + console.log(`${template}: Done.`); + } catch (err) { + console.log(`${template}: Failed.`); + } +} + async function run() { const { cadence, script: commandline, parallel, + cd, + junit: junitPath, } = await getOptionsOrPrompt('yarn multiplex-templates', options); const command = await parseCommand(commandline); @@ -76,23 +113,24 @@ async function run() { const toAwait = []; for (const template of Object.keys(templates)) { - const toRun = getCommand(command.command, command.options, { - ...command.values, - template, - }); + let toRun = command.command; + + if (command.options) { + toRun = getCommand(command.command, command.options, { + ...command.values, + template, + }); + } - console.log(`Running ${toRun}`); + // Do some simple variable substitution + toRun = toRun.replace('TEMPLATE_ENV', template.toUpperCase().replace(/\/-/, '_')); + toRun = toRun.replace('TEMPLATE', template); + const execaOptions = cd ? { cwd: join(sandboxDir, template) } : {}; if (parallel) { - // Don't pipe stdio as it'll get interleaved - toAwait.push( - (async () => { - await execa.command(toRun); - console.log(`Done with ${toRun}`); - })() - ); + toAwait.push(runCommand(toRun, execaOptions, { template })); } else { - await execa.command(toRun, { stdio: 'inherit' }); + await runCommand(toRun, { stdio: 'inherit', ...execaOptions }, { template }); } } From 7ae77dc9f1eefee2e23a2866b4b64aee4c33c796 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 8 Aug 2022 20:20:02 +1000 Subject: [PATCH 02/32] Add smoketest ci job --- .circleci/config.yml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 73966b8d0383..20fb381fd6b8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -484,13 +484,28 @@ jobs: command: | mkdir sandbox cd code - yarn once-per-template --cadence ci --parallel \ + yarn once-per-template --no-cd --cadence ci --parallel \ --script "yarn sandbox --no-link --no-start --no-publish" - persist_to_workspace: root: . paths: - sandbox - + smoke-test-sandboxes: + executor: + class: medium+ + name: sb_node_14_browsers + parallelism: 2 + steps: + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' + - attach_workspace: + at: . + - run: + name: Smoke Testing Sandboxes + command: | + cd code + yarn multiplex-templates --cadence ci --junit test-results/smoketest-TEMPLATE.xml \ + --script "yarn storybook --smoke-test --quiet" workflows: test: @@ -540,4 +555,7 @@ workflows: - create-sandboxes: requires: - publish + - smoke-test-sandboxes: + requires: + - create-sandboxes \ No newline at end of file From 61b3bf22ec7163c52fcfe75e0301c202f0ce6f3c Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 8 Aug 2022 21:47:29 +1000 Subject: [PATCH 03/32] Hooked up junit in `once-per-template` --- .circleci/config.yml | 6 +-- scripts/once-per-template.ts | 73 +++++++++++++++++++++++++++++++----- scripts/package.json | 1 + scripts/yarn.lock | 10 +++++ 4 files changed, 77 insertions(+), 13 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 20fb381fd6b8..d2197b962c06 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -484,7 +484,7 @@ jobs: command: | mkdir sandbox cd code - yarn once-per-template --no-cd --cadence ci --parallel \ + yarn once-per-template --step Building --no-cd --cadence ci --parallel \ --script "yarn sandbox --no-link --no-start --no-publish" - persist_to_workspace: root: . @@ -504,8 +504,8 @@ jobs: name: Smoke Testing Sandboxes command: | cd code - yarn multiplex-templates --cadence ci --junit test-results/smoketest-TEMPLATE.xml \ - --script "yarn storybook --smoke-test --quiet" + yarn once-per-template --step Smoke Testing --cadence ci \ + --junit test-results/smoketest.xml --script "yarn storybook --smoke-test --quiet" workflows: test: diff --git a/scripts/once-per-template.ts b/scripts/once-per-template.ts index b87914d4acda..5dac1c8b3bee 100644 --- a/scripts/once-per-template.ts +++ b/scripts/once-per-template.ts @@ -2,12 +2,14 @@ import { Command } from 'commander'; import execa from 'execa'; import { resolve, join } from 'path'; +import { getJunitXml } from 'junit-xml'; import { getOptions, getCommand, getOptionsOrPrompt, createOptions } from './utils/options'; import type { OptionSpecifier } from './utils/options'; import { filterDataForCurrentCircleCINode } from './utils/concurrency'; import TEMPLATES from '../code/lib/cli/src/repro-templates'; +import { outputFile } from 'fs-extra'; const sandboxDir = resolve(__dirname, '../sandbox'); @@ -48,6 +50,10 @@ export async function parseCommand(commandline: string) { } export const options = createOptions({ + step: { + type: 'string', + description: 'What type of step are you taking per template (for logging and test results)?', + }, cadence: { type: 'string', description: 'What cadence are we running on (i.e. which templates should we use)?', @@ -82,25 +88,64 @@ export function filterTemplates(templates: Templates, cadence: Cadence, scriptNa return Object.fromEntries(filterDataForCurrentCircleCINode(jobTemplates)); } +type RunResult = { + template: TemplateKey; + timestamp: Date; + time: number; + ok: boolean; + output?: string; + err?: Error; +}; + const logger = console; async function runCommand( command: string, execaOptions: execa.Options, - { template }: { template: string } -) { + { step, template }: { step: string; template: string } +): Promise { + const timestamp = new Date(); try { - logger.log(`${template}: Running ${command}`); + logger.log(`${step} ${template}: Running ${command}`); + + const { all } = await execa.command(command, execaOptions); - await execa.command(command, execaOptions); + console.log(`${step} ${template}: Done.`); - console.log(`${template}: Done.`); + return { template, timestamp, time: (Date.now() - +timestamp) / 1000, ok: true, output: all }; } catch (err) { - console.log(`${template}: Failed.`); + console.log(`${step} ${template}: Failed.`); + return { template, timestamp, time: (Date.now() - +timestamp) / 1000, ok: false, err }; } } +async function writeJunitXml(step: string, start: Date, results: RunResult[], path: string) { + const junitXml = getJunitXml({ + time: (Date.now() - +start) / 1000, + name: `${step} Templates`, + suites: results.map(({ template, timestamp, time, ok, err, output }) => ({ + name: template, + timestamp, + time, + testCases: [ + { + name: `${step} ${template}`, + assertions: 1, + time, + systemOut: output?.split('\n'), + ...(!ok && { + errors: [err], + }), + }, + ], + })), + }); + await outputFile(path, junitXml); + console.log(`Test results written to ${resolve(path)}`); +} + async function run() { const { + step = 'Testing', cadence, script: commandline, parallel, @@ -111,6 +156,7 @@ async function run() { const command = await parseCommand(commandline); const templates = filterTemplates(TEMPLATES, cadence, command.scriptName); + const start = new Date(); const toAwait = []; for (const template of Object.keys(templates)) { let toRun = command.command; @@ -126,15 +172,22 @@ async function run() { toRun = toRun.replace('TEMPLATE_ENV', template.toUpperCase().replace(/\/-/, '_')); toRun = toRun.replace('TEMPLATE', template); - const execaOptions = cd ? { cwd: join(sandboxDir, template) } : {}; + const execaOptions = cd ? { cwd: join(sandboxDir, template.replace('/', '-')) } : {}; if (parallel) { - toAwait.push(runCommand(toRun, execaOptions, { template })); + toAwait.push(runCommand(toRun, execaOptions, { step, template })); } else { - await runCommand(toRun, { stdio: 'inherit', ...execaOptions }, { template }); + toAwait.push( + Promise.resolve( + await runCommand(toRun, { stdio: 'inherit', ...execaOptions }, { step, template }) + ) + ); } } - await Promise.all(toAwait); + const results = await Promise.all(toAwait); + if (junitPath) { + await writeJunitXml(step, start, results, junitPath); + } } if (require.main === module) { diff --git a/scripts/package.json b/scripts/package.json index 2edc8c1563ed..3e48a5128fc6 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -143,6 +143,7 @@ "jest-serializer-html": "^7.0.0", "jest-watch-typeahead": "^0.6.1", "js-yaml": "^3.14.1", + "junit-xml": "^1.2.0", "lint-staged": "^10.5.4", "lodash": "^4.17.21", "mocha-list-tests": "^1.0.5", diff --git a/scripts/yarn.lock b/scripts/yarn.lock index c7d72426e272..e6ead6e2d6b3 100644 --- a/scripts/yarn.lock +++ b/scripts/yarn.lock @@ -3330,6 +3330,7 @@ __metadata: jest-serializer-html: ^7.0.0 jest-watch-typeahead: ^0.6.1 js-yaml: ^3.14.1 + junit-xml: ^1.2.0 lint-staged: ^10.5.4 lodash: ^4.17.21 mocha-list-tests: ^1.0.5 @@ -12901,6 +12902,15 @@ __metadata: languageName: node linkType: hard +"junit-xml@npm:^1.2.0": + version: 1.2.0 + resolution: "junit-xml@npm:1.2.0" + dependencies: + xml: ^1.0.1 + checksum: 4e72ffac0f2e77784ab81380f385e9b0757bebaf2d841598423d889fbaf397cc38cd524d5cd9efe7bfeb6b96937b5c497582df6de33f67e2c5ad7f174d3f800f + languageName: node + linkType: hard + "jwa@npm:^1.4.1": version: 1.4.1 resolution: "jwa@npm:1.4.1" From 04d8f1a76d896c35d1d38b7ee977b5110d6397e9 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 8 Aug 2022 21:48:00 +1000 Subject: [PATCH 04/32] Small fix --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d2197b962c06..c71502fb0628 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -504,7 +504,7 @@ jobs: name: Smoke Testing Sandboxes command: | cd code - yarn once-per-template --step Smoke Testing --cadence ci \ + yarn once-per-template --step "Smoke Testing" --cadence ci \ --junit test-results/smoketest.xml --script "yarn storybook --smoke-test --quiet" workflows: From e99482156b772be2ce746d636f37fd5a701ed20d Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 8 Aug 2022 21:53:03 +1000 Subject: [PATCH 05/32] Add build-sandboxes job --- .circleci/config.yml | 27 ++++++++++++++++++++++++++- scripts/once-per-template.ts | 7 +++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c71502fb0628..6936176eba09 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -506,6 +506,29 @@ jobs: cd code yarn once-per-template --step "Smoke Testing" --cadence ci \ --junit test-results/smoketest.xml --script "yarn storybook --smoke-test --quiet" + - store_test_results: + path: code/test-results + build-sandboxes: + executor: + class: medium+ + name: sb_node_14_browsers + parallelism: 2 + steps: + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' + - attach_workspace: + at: . + - run: + name: Building Sandboxes + command: | + BUILD_DIR="$PWD/../built-sandboxes" + mkdir $BUILD_DIR + cd code + yarn once-per-template --step "Building" --cadence ci --junit test-results/build.xml \ + --script "yarn build-storybook --quiet --output-dir='$BUILD_DIR/$TEMPLATE_DIR'" + - store_test_results: + path: code/test-results + workflows: test: @@ -558,4 +581,6 @@ workflows: - smoke-test-sandboxes: requires: - create-sandboxes - \ No newline at end of file + - build-sandboxes: + requires: + - create-sandboxes \ No newline at end of file diff --git a/scripts/once-per-template.ts b/scripts/once-per-template.ts index 5dac1c8b3bee..99f0464f8b80 100644 --- a/scripts/once-per-template.ts +++ b/scripts/once-per-template.ts @@ -169,10 +169,13 @@ async function run() { } // Do some simple variable substitution - toRun = toRun.replace('TEMPLATE_ENV', template.toUpperCase().replace(/\/-/, '_')); + const templateEnv = template.toUpperCase().replace(/\/-/, '_'); + toRun = toRun.replace('TEMPLATE_ENV', templateEnv); + const templateDir = template.replace('/', '-'); + toRun = toRun.replace('TEMPLATE_DIR', templateDir); toRun = toRun.replace('TEMPLATE', template); - const execaOptions = cd ? { cwd: join(sandboxDir, template.replace('/', '-')) } : {}; + const execaOptions = cd ? { cwd: join(sandboxDir, templateDir) } : {}; if (parallel) { toAwait.push(runCommand(toRun, execaOptions, { step, template })); } else { From aea2bd1710c747b6f68c82630b25340fc3136928 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 8 Aug 2022 22:08:01 +1000 Subject: [PATCH 06/32] Add chromatic step --- .circleci/config.yml | 43 ++++++++++++++++++++++++++++++++---- scripts/once-per-template.ts | 6 ++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6936176eba09..e066920625b0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -504,8 +504,11 @@ jobs: name: Smoke Testing Sandboxes command: | cd code - yarn once-per-template --step "Smoke Testing" --cadence ci \ - --junit test-results/smoketest.xml --script "yarn storybook --smoke-test --quiet" + yarn once-per-template \ + --step "Smoke Testing" \ + --cadence ci \ + --junit test-results/smoketest.xml \ + --script "yarn storybook --smoke-test --quiet" - store_test_results: path: code/test-results build-sandboxes: @@ -521,13 +524,45 @@ jobs: - run: name: Building Sandboxes command: | - BUILD_DIR="$PWD/../built-sandboxes" + BUILD_DIR="$PWD/built-sandboxes" mkdir $BUILD_DIR cd code - yarn once-per-template --step "Building" --cadence ci --junit test-results/build.xml \ + yarn once-per-template \ + --step "Building" \ + --cadence ci \ + --junit test-results/build.xml \ --script "yarn build-storybook --quiet --output-dir='$BUILD_DIR/$TEMPLATE_DIR'" - store_test_results: path: code/test-results + - persist_to_workspace: + root: . + paths: + - built-sandboxes + chromatic-sandboxes: + executor: + class: medium+ + name: sb_node_14_browsers + parallelism: 2 + steps: + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' + - attach_workspace: + at: . + - run: + name: Chromatic (Sandboxes) + command: | + BUILD_DIR="$PWD/built-sandboxes" + mkdir $BUILD_DIR + cd code + yarn once-per-template \ + --step "Running Chromatic" \ + --cadence ci \ + --script "npx chromatic \ + --project-token='CHROMATIC_TOKEN_$TEMPLATE_ENV' \ + --storybook-build-dir='$BUILD_DIR/$TEMPLATE_DIR' \ + --junit-report='test-results/chromatic.xml'" + - store_test_results: + path: code/test-results workflows: diff --git a/scripts/once-per-template.ts b/scripts/once-per-template.ts index 99f0464f8b80..85c601ce4b8b 100644 --- a/scripts/once-per-template.ts +++ b/scripts/once-per-template.ts @@ -170,10 +170,10 @@ async function run() { // Do some simple variable substitution const templateEnv = template.toUpperCase().replace(/\/-/, '_'); - toRun = toRun.replace('TEMPLATE_ENV', templateEnv); + toRun = toRun.replace('$TEMPLATE_ENV', templateEnv); const templateDir = template.replace('/', '-'); - toRun = toRun.replace('TEMPLATE_DIR', templateDir); - toRun = toRun.replace('TEMPLATE', template); + toRun = toRun.replace('$TEMPLATE_DIR', templateDir); + toRun = toRun.replace('$TEMPLATE', template); const execaOptions = cd ? { cwd: join(sandboxDir, templateDir) } : {}; if (parallel) { From a0085f8c1ee56ee450da8593bc9b5ddd3bb373fa Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 8 Aug 2022 22:09:02 +1000 Subject: [PATCH 07/32] Add chromatic step --- .circleci/config.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e066920625b0..867b71f9c28a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -618,4 +618,7 @@ workflows: - create-sandboxes - build-sandboxes: requires: - - create-sandboxes \ No newline at end of file + - create-sandboxes + - chromatic-sandboxes: + requires: + - build-sandboxes \ No newline at end of file From baf5af3091d4df587f56c6cc1ca58b26a17b3084 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 9 Aug 2022 06:36:21 +1000 Subject: [PATCH 08/32] Don't make build dir --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 867b71f9c28a..07234b7fa0c5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -552,7 +552,6 @@ jobs: name: Chromatic (Sandboxes) command: | BUILD_DIR="$PWD/built-sandboxes" - mkdir $BUILD_DIR cd code yarn once-per-template \ --step "Running Chromatic" \ From de2d8057d1141b72da2043f60939826c07988bd2 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 9 Aug 2022 08:53:38 +1000 Subject: [PATCH 09/32] Use hash rather than dollar --- .circleci/config.yml | 6 +++--- scripts/once-per-template.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 07234b7fa0c5..5e309ba492a7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -531,7 +531,7 @@ jobs: --step "Building" \ --cadence ci \ --junit test-results/build.xml \ - --script "yarn build-storybook --quiet --output-dir='$BUILD_DIR/$TEMPLATE_DIR'" + --script "yarn build-storybook --quiet --output-dir='$BUILD_DIR/#TEMPLATE_DIR'" - store_test_results: path: code/test-results - persist_to_workspace: @@ -557,8 +557,8 @@ jobs: --step "Running Chromatic" \ --cadence ci \ --script "npx chromatic \ - --project-token='CHROMATIC_TOKEN_$TEMPLATE_ENV' \ - --storybook-build-dir='$BUILD_DIR/$TEMPLATE_DIR' \ + --project-token='\$CHROMATIC_TOKEN_#TEMPLATE_ENV' \ + --storybook-build-dir='$BUILD_DIR/#TEMPLATE_DIR' \ --junit-report='test-results/chromatic.xml'" - store_test_results: path: code/test-results diff --git a/scripts/once-per-template.ts b/scripts/once-per-template.ts index 85c601ce4b8b..52544b702893 100644 --- a/scripts/once-per-template.ts +++ b/scripts/once-per-template.ts @@ -170,10 +170,10 @@ async function run() { // Do some simple variable substitution const templateEnv = template.toUpperCase().replace(/\/-/, '_'); - toRun = toRun.replace('$TEMPLATE_ENV', templateEnv); + toRun = toRun.replace('#TEMPLATE_ENV', templateEnv); const templateDir = template.replace('/', '-'); - toRun = toRun.replace('$TEMPLATE_DIR', templateDir); - toRun = toRun.replace('$TEMPLATE', template); + toRun = toRun.replace('#TEMPLATE_DIR', templateDir); + toRun = toRun.replace('#TEMPLATE', template); const execaOptions = cd ? { cwd: join(sandboxDir, templateDir) } : {}; if (parallel) { From 423da0028453d9b17f20bd0f8524dd1b89951b32 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 9 Aug 2022 09:13:29 +1000 Subject: [PATCH 10/32] Fix up env vars replacement --- scripts/once-per-template.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/once-per-template.ts b/scripts/once-per-template.ts index 52544b702893..799f217aadd3 100644 --- a/scripts/once-per-template.ts +++ b/scripts/once-per-template.ts @@ -168,13 +168,16 @@ async function run() { }); } - // Do some simple variable substitution - const templateEnv = template.toUpperCase().replace(/\/-/, '_'); + // Do some simple variable substitution using a #TEMPLATE_X syntax + const templateEnv = template.toUpperCase().replace(/\/|-/g, '_'); toRun = toRun.replace('#TEMPLATE_ENV', templateEnv); const templateDir = template.replace('/', '-'); toRun = toRun.replace('#TEMPLATE_DIR', templateDir); toRun = toRun.replace('#TEMPLATE', template); + // Also substitute environment variables into command + toRun = toRun.replace(/\$([A-Z_]+)/, (_, name) => process.env[name]); + const execaOptions = cd ? { cwd: join(sandboxDir, templateDir) } : {}; if (parallel) { toAwait.push(runCommand(toRun, execaOptions, { step, template })); From bb2ed676422c8b355a02b39c57a9247b9ba317ca Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 9 Aug 2022 16:10:48 +1000 Subject: [PATCH 11/32] Ensure if jobs fail we exit 1 --- scripts/once-per-template.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/once-per-template.ts b/scripts/once-per-template.ts index 799f217aadd3..b2e1812c0601 100644 --- a/scripts/once-per-template.ts +++ b/scripts/once-per-template.ts @@ -194,6 +194,8 @@ async function run() { if (junitPath) { await writeJunitXml(step, start, results, junitPath); } + + if (results.find((result) => !result.ok)) process.exit(1); } if (require.main === module) { From 9b7778c60db1bb63423d337ca71ef122326da0fa Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 9 Aug 2022 16:14:31 +1000 Subject: [PATCH 12/32] Try different quotes --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5e309ba492a7..4b72d5f56fb9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -531,7 +531,7 @@ jobs: --step "Building" \ --cadence ci \ --junit test-results/build.xml \ - --script "yarn build-storybook --quiet --output-dir='$BUILD_DIR/#TEMPLATE_DIR'" + --script "yarn build-storybook --quiet --output-dir=\"$BUILD_DIR/#TEMPLATE_DIR\"" - store_test_results: path: code/test-results - persist_to_workspace: From dd300a6f61bd88ad239c207b3f2a95ddfecfb89d Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 9 Aug 2022 16:31:47 +1000 Subject: [PATCH 13/32] Drop the quotes entirely (?) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4b72d5f56fb9..fc2e7450fd20 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -531,7 +531,7 @@ jobs: --step "Building" \ --cadence ci \ --junit test-results/build.xml \ - --script "yarn build-storybook --quiet --output-dir=\"$BUILD_DIR/#TEMPLATE_DIR\"" + --script "yarn build-storybook --quiet --output-dir=$BUILD_DIR/#TEMPLATE_DIR" - store_test_results: path: code/test-results - persist_to_workspace: From 7e1169c7e2149d68804b8649300e4104f33c0895 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 9 Aug 2022 16:44:31 +1000 Subject: [PATCH 14/32] No need to wait for changes on Chromatic --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fc2e7450fd20..4b3b17220cf9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -559,7 +559,8 @@ jobs: --script "npx chromatic \ --project-token='\$CHROMATIC_TOKEN_#TEMPLATE_ENV' \ --storybook-build-dir='$BUILD_DIR/#TEMPLATE_DIR' \ - --junit-report='test-results/chromatic.xml'" + --junit-report='test-results/chromatic.xml' + --exit-once-uploaded" - store_test_results: path: code/test-results From 3dd83b1be1faee0143cff3dac068cea713e5559a Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 1 Aug 2022 11:54:32 +1000 Subject: [PATCH 15/32] Add a simple playwright setup --- code/.eslintrc.js | 6 ++ code/.gitignore | 3 + code/e2e-tests/example.spec.ts | 17 ++++++ code/package.json | 2 + code/playwright.config.ts | 107 +++++++++++++++++++++++++++++++++ code/yarn.lock | 34 +++++++++++ 6 files changed, 169 insertions(+) create mode 100644 code/e2e-tests/example.spec.ts create mode 100644 code/playwright.config.ts diff --git a/code/.eslintrc.js b/code/.eslintrc.js index 80316f0a5e7d..1129908e7eb0 100644 --- a/code/.eslintrc.js +++ b/code/.eslintrc.js @@ -95,5 +95,11 @@ module.exports = { 'react/no-unknown-property': 'off', // Need to deactivate otherwise eslint replaces some unknown properties with React ones }, }, + { + files: ['**/e2e-tests/**/*'], + rules: { + 'jest/no-test-callback': 'off', // These aren't jest tests + }, + }, ], }; diff --git a/code/.gitignore b/code/.gitignore index 193ada9a92e3..ef192156c384 100644 --- a/code/.gitignore +++ b/code/.gitignore @@ -43,3 +43,6 @@ junit.xml !/**/.yarn/sdks !/**/.yarn/versions /**/.pnp.* +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/code/e2e-tests/example.spec.ts b/code/e2e-tests/example.spec.ts new file mode 100644 index 000000000000..39c877a93199 --- /dev/null +++ b/code/e2e-tests/example.spec.ts @@ -0,0 +1,17 @@ +import { test, expect } from '@playwright/test'; +import process from 'process'; + +const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:4000'; + +test('Basic story test', async ({ page }) => { + await page.goto(storybookUrl); + + const preview = page.frameLocator('#storybook-preview-iframe'); + const root = preview.locator('#root:visible, #docs-root:visible'); + + // General check for any selected entry + await expect(root).not.toBeEmpty(); + + // Specific check for introduction story + await expect(root).toContainText('Welcome to Storybook'); +}); diff --git a/code/package.json b/code/package.json index 2f03200814c8..f6c1921ee55e 100644 --- a/code/package.json +++ b/code/package.json @@ -152,6 +152,7 @@ "@nrwl/nx-cloud": "12.1.1", "@nrwl/tao": "12.3.4", "@nrwl/workspace": "12.3.4", + "@playwright/test": "^1.24.2", "@rollup/plugin-babel": "^5.3.1", "@rollup/plugin-commonjs": "^21.0.1", "@rollup/plugin-json": "^4.1.0", @@ -315,6 +316,7 @@ "node-gyp": "^8.4.0", "npmlog": "^5.0.1", "p-limit": "^3.1.0", + "playwright": "^1.24.2", "postcss-loader": "^6.2.1", "prettier": ">=2.2.1 <=2.3.0", "prompts": "^2.4.0", diff --git a/code/playwright.config.ts b/code/playwright.config.ts new file mode 100644 index 000000000000..139ce346beaf --- /dev/null +++ b/code/playwright.config.ts @@ -0,0 +1,107 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './e2e-tests', + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + }, + }, + + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}; + +export default config; diff --git a/code/yarn.lock b/code/yarn.lock index 2272bd8a4116..543ffc2773e6 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6406,6 +6406,18 @@ __metadata: languageName: node linkType: hard +"@playwright/test@npm:^1.24.2": + version: 1.24.2 + resolution: "@playwright/test@npm:1.24.2" + dependencies: + "@types/node": "*" + playwright-core: 1.24.2 + bin: + playwright: cli.js + checksum: 87e943149510fd7ad1369b7e1c07b67a86f31f09c0a2ddf61c48b0e7f56f8f9542b8541d2aed9e8e08b7c3aa93459329f5af01337e00f8136202ecf93fb3ffd9 + languageName: node + linkType: hard + "@pmmmwh/react-refresh-webpack-plugin@npm:^0.5.1, @pmmmwh/react-refresh-webpack-plugin@npm:^0.5.3, @pmmmwh/react-refresh-webpack-plugin@npm:^0.5.5": version: 0.5.7 resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.7" @@ -8881,6 +8893,7 @@ __metadata: "@nrwl/nx-cloud": 12.1.1 "@nrwl/tao": 12.3.4 "@nrwl/workspace": 12.3.4 + "@playwright/test": ^1.24.2 "@rollup/plugin-babel": ^5.3.1 "@rollup/plugin-commonjs": ^21.0.1 "@rollup/plugin-json": ^4.1.0 @@ -9045,6 +9058,7 @@ __metadata: node-gyp: ^8.4.0 npmlog: ^5.0.1 p-limit: ^3.1.0 + playwright: ^1.24.2 postcss-loader: ^6.2.1 prettier: ">=2.2.1 <=2.3.0" prompts: ^2.4.0 @@ -34446,6 +34460,26 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.24.2": + version: 1.24.2 + resolution: "playwright-core@npm:1.24.2" + bin: + playwright: cli.js + checksum: ecd60ece818f1d57bf34685dd812c56f3b8cf7d8ca8e14074e1743fac56387f16791121901b34cf519d3ef5f9360968656ab661c7b589e55500144eb2cf69448 + languageName: node + linkType: hard + +"playwright@npm:^1.24.2": + version: 1.24.2 + resolution: "playwright@npm:1.24.2" + dependencies: + playwright-core: 1.24.2 + bin: + playwright: cli.js + checksum: c5e4cfd57f8030f15e019c101c867a5ce3f1d12c3a6d8a32a9fb34897746470faced7935d5cc920c85082c0aec0bcf5bb23174ba55f6eabf942d38707a40d1f0 + languageName: node + linkType: hard + "please-upgrade-node@npm:^3.2.0": version: 3.2.0 resolution: "please-upgrade-node@npm:3.2.0" From 210df609a57880afa13213dbb78037e719e5f2e5 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 1 Aug 2022 15:47:00 +1000 Subject: [PATCH 16/32] Add playwright to CircleCI --- .circleci/config.yml | 34 ++++++++++++++++++++++++++++++++++ code/.gitignore | 9 ++++++--- code/package.json | 1 + code/playwright.config.ts | 2 +- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4b3b17220cf9..0e68a79fa6a1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -377,6 +377,37 @@ jobs: - store_artifacts: path: /tmp/cypress-record destination: cypress + e2e-tests-examples-playwright: + docker: + - image: mcr.microsoft.com/playwright:v1.24.0-focal + steps: + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1 --verbose' + - attach_workspace: + at: . + - run: + name: running example + command: | + cd code + yarn serve-storybooks + background: true + - run: + name: await running examples + command: | + cd code + yarn await-serve-storybooks + - run: + name: playwright test + command: | + cd code + yarn test:e2e-examples-playwright --reporter=junit + environment: + PLAYWRIGHT_JUNIT_OUTPUT_NAME: test-results/playwright.xml + - store_test_results: + path: test-results + - store_artifacts: # this is where playwright puts more complex stuff + path: /playwright-results/ + destination: playwright smoke-tests: executor: class: medium+ @@ -578,6 +609,9 @@ workflows: - e2e-tests-examples: requires: - examples + - e2e-tests-examples-playwright: + requires: + - examples - smoke-tests: requires: - build diff --git a/code/.gitignore b/code/.gitignore index ef192156c384..8e7366b01633 100644 --- a/code/.gitignore +++ b/code/.gitignore @@ -36,6 +36,12 @@ examples/angular-cli/addon-jest.testresults.json junit.xml .next +/test-results/ +/playwright-results/ +/playwright-report/ +/playwright/.cache/ + + # Yarn stuff /**/.yarn/* !/**/.yarn/releases @@ -43,6 +49,3 @@ junit.xml !/**/.yarn/sdks !/**/.yarn/versions /**/.pnp.* -/test-results/ -/playwright-report/ -/playwright/.cache/ diff --git a/code/package.json b/code/package.json index f6c1921ee55e..f16fdd59d910 100644 --- a/code/package.json +++ b/code/package.json @@ -89,6 +89,7 @@ "test:cli": "npm --prefix lib/cli run test", "test:e2e-examples": "cypress run", "test:e2e-examples-gui": "concurrently --success first --kill-others \"cypress open\" \"yarn serve-storybooks\"", + "test:e2e-examples-playwright": "playwright test", "test:e2e-framework": "ts-node --project=../scripts/tsconfig.json ../scripts/run-e2e.ts" }, "husky": { diff --git a/code/playwright.config.ts b/code/playwright.config.ts index 139ce346beaf..65bd48e564e7 100644 --- a/code/playwright.config.ts +++ b/code/playwright.config.ts @@ -95,7 +95,7 @@ const config: PlaywrightTestConfig = { ], /* Folder for test artifacts such as screenshots, videos, traces, etc. */ - // outputDir: 'test-results/', + outputDir: 'playwright-results/', /* Run your local dev server before starting the tests */ // webServer: { From ef3dc80294e0da9f2a11a84e9531faac32326435 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 1 Aug 2022 16:43:07 +1000 Subject: [PATCH 17/32] Use correct port --- code/e2e-tests/example.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/e2e-tests/example.spec.ts b/code/e2e-tests/example.spec.ts index 39c877a93199..806717cc56a8 100644 --- a/code/e2e-tests/example.spec.ts +++ b/code/e2e-tests/example.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from '@playwright/test'; import process from 'process'; -const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:4000'; +const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:8001'; test('Basic story test', async ({ page }) => { await page.goto(storybookUrl); From 1d6729dba42cdc9334d4924df8f5add67e5e1c3d Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 2 Aug 2022 08:55:58 +1000 Subject: [PATCH 18/32] Fix paths --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0e68a79fa6a1..e0e3d6ec2c90 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -404,9 +404,9 @@ jobs: environment: PLAYWRIGHT_JUNIT_OUTPUT_NAME: test-results/playwright.xml - store_test_results: - path: test-results + path: code/test-results - store_artifacts: # this is where playwright puts more complex stuff - path: /playwright-results/ + path: code/playwright-results/ destination: playwright smoke-tests: executor: From e95f64c1dfa6b96e438452080925e1d385129f10 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 2 Aug 2022 16:06:50 +1000 Subject: [PATCH 19/32] Tweak spec to work on angular example --- code/e2e-tests/example.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/e2e-tests/example.spec.ts b/code/e2e-tests/example.spec.ts index 806717cc56a8..f13c7910f96c 100644 --- a/code/e2e-tests/example.spec.ts +++ b/code/e2e-tests/example.spec.ts @@ -13,5 +13,5 @@ test('Basic story test', async ({ page }) => { await expect(root).not.toBeEmpty(); // Specific check for introduction story - await expect(root).toContainText('Welcome to Storybook'); + await expect(root).toContainText('Welcome'); }); From 2dc6698a20db1de928708ff3e25dcb51d41e9c2a Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 2 Aug 2022 09:33:12 +1000 Subject: [PATCH 20/32] SB is at angular-cli --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index e0e3d6ec2c90..d5f449c53f48 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -403,6 +403,7 @@ jobs: yarn test:e2e-examples-playwright --reporter=junit environment: PLAYWRIGHT_JUNIT_OUTPUT_NAME: test-results/playwright.xml + STORYBOOK_URL: 'http://localhost:8001/angular-cli' - store_test_results: path: code/test-results - store_artifacts: # this is where playwright puts more complex stuff From c5e4df91cd7c29867d2f5bfae140bd11fc252d99 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 2 Aug 2022 16:05:59 +1000 Subject: [PATCH 21/32] Possibly `.not.toBeEmpty()` isn't reliable --- code/e2e-tests/example.spec.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/code/e2e-tests/example.spec.ts b/code/e2e-tests/example.spec.ts index f13c7910f96c..6caeddd4077b 100644 --- a/code/e2e-tests/example.spec.ts +++ b/code/e2e-tests/example.spec.ts @@ -9,9 +9,6 @@ test('Basic story test', async ({ page }) => { const preview = page.frameLocator('#storybook-preview-iframe'); const root = preview.locator('#root:visible, #docs-root:visible'); - // General check for any selected entry - await expect(root).not.toBeEmpty(); - // Specific check for introduction story await expect(root).toContainText('Welcome'); }); From 5daa14bcf747554f107cb4be6f48d0ddd258140e Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 10 Aug 2022 09:32:31 +1000 Subject: [PATCH 22/32] Add a `create-built-sandboxes-index` script --- code/package.json | 1 + scripts/create-built-sandboxes-index.ts | 119 ++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 scripts/create-built-sandboxes-index.ts diff --git a/code/package.json b/code/package.json index f16fdd59d910..c28f190c0982 100644 --- a/code/package.json +++ b/code/package.json @@ -61,6 +61,7 @@ "check": "NODE_ENV=production node ../scripts/check-package.js", "clean:dist": "del **/dist", "coverage": "codecov", + "create-built-sandboxes-index": "ts-node ../scripts/create-built-sandboxes-index", "danger": "danger", "generate-repros": "zx ../scripts/repros-generator/index.mjs", "github-release": "github-release-from-changelog", diff --git a/scripts/create-built-sandboxes-index.ts b/scripts/create-built-sandboxes-index.ts new file mode 100644 index 000000000000..b52b9bb2d1fb --- /dev/null +++ b/scripts/create-built-sandboxes-index.ts @@ -0,0 +1,119 @@ +import { createOptions, getOptionsOrPrompt } from './utils/options'; +import { filterTemplates } from './once-per-template'; +import type { Templates } from './once-per-template'; +import TEMPLATES from '../code/lib/cli/src/repro-templates'; +import { outputFile } from 'fs-extra'; + +export const options = createOptions({ + output: { + type: 'string', + description: 'Where to put the index.html file?', + required: true, + }, + cadence: { + type: 'string', + description: 'What cadence are we running on (i.e. which templates should we use)?', + values: ['ci', 'daily', 'weekly'] as const, + required: true, + }, +}); + +function toPath(key: keyof Templates) { + return key.replace('/', '-'); +} + +const createContent = (templates: Templates) => { + return ` + + + + + + + + +