From 9d3cd494106ea09f0d80acde487f250a0480cfd3 Mon Sep 17 00:00:00 2001 From: Drew Powers <1369770+drwpow@users.noreply.github.com> Date: Fri, 11 Jun 2021 15:07:30 -0600 Subject: [PATCH] Improve templates test (#377) --- .github/workflows/nodejs.yml | 19 +- examples/blog/package.json | 3 +- package.json | 3 +- packages/create-astro/package.json | 2 +- .../create-astro/test/create-astro.test.js | 200 ++++++++++-------- 5 files changed, 131 insertions(+), 96 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 8b362e7a1..db7ef212f 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -70,4 +70,21 @@ jobs: - run: yarn test env: CI: true - GITHUB_SHA: $GITHUB_SHA + # test-templates: + # if: ${{ github.head_ref }} == "changeset-release/main" # only run on version PRs (this test is too slow to run every PR) + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v2 + # - uses: actions/setup-node@v2 + # with: + # node-version: 14.x + # - run: yarn --frozen-lockfile --ignore-engines + # env: + # CI: true + # - run: yarn build + # env: + # CI: true + # - run: yarn test:templates + # env: + # GITHUB_SHA: $GITHUB_SHA + # CI: true diff --git a/examples/blog/package.json b/examples/blog/package.json index 2ff9a89c4..bef8b9d08 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -7,8 +7,7 @@ "build": "astro build" }, "devDependencies": { - "astro": "^0.13.0", - "nodemon": "^2.0.7" + "astro": "^0.13.0" }, "snowpack": { "workspaceRoot": "../.." diff --git a/package.json b/package.json index 873561591..507503b30 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "format": "prettier -w \"**/*.{js,jsx,ts,tsx,md,json}\"", "lint": "eslint \"packages/**/*.ts\"", "test": "lerna run test --scope astro --stream", - "test:core": "yarn workspace astro run test" + "test:core": "yarn workspace astro run test", + "test:templates": "lerna run test --scope create-astro --stream" }, "workspaces": [ "packages/renderers/*", diff --git a/packages/create-astro/package.json b/packages/create-astro/package.json index b4cc2a7e9..23dbde044 100644 --- a/packages/create-astro/package.json +++ b/packages/create-astro/package.json @@ -11,7 +11,7 @@ "scripts": { "build": "astro-scripts build \"src/*.ts\"", "prepare": "yarn build", - "test": "uvu -i test/fixtures" + "test": "rm -rf test/fixtures && mkdir test/fixtures && node --unhandled-rejections=strict test/create-astro.test.js" }, "files": [ "dist", diff --git a/packages/create-astro/test/create-astro.test.js b/packages/create-astro/test/create-astro.test.js index 4335f77e1..30f466d30 100644 --- a/packages/create-astro/test/create-astro.test.js +++ b/packages/create-astro/test/create-astro.test.js @@ -2,24 +2,27 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import http from 'http'; -import { suite } from 'uvu'; +import { green, red } from 'kleur/colors'; import execa from 'execa'; -import del from 'del'; import glob from 'tiny-glob'; -import * as assert from 'uvu/assert'; import { TEMPLATES } from '../dist/templates.js'; // config const GITHUB_SHA = process.env.GITHUB_SHA || execa.sync('git', ['rev-parse', 'HEAD']).stdout; // process.env.GITHUB_SHA will be set in CI; if testing locally execa() will gather this -const MAX_TEST_TIME = 60000; // maximum time a test may take (60s) -const TIMER = {}; // keep track of every test’s run time (uvu requires manual setup for this) const FIXTURES_DIR = path.join(fileURLToPath(path.dirname(import.meta.url)), 'fixtures'); -// helper +// helpers async function fetch(url) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { http .get(url, (res) => { + // not OK + if (res.statusCode !== 200) { + reject(res.statusCode); + return; + } + + // OK let body = ''; res.on('data', (chunk) => { body += chunk; @@ -27,94 +30,109 @@ async function fetch(url) { res.on('end', () => resolve({ statusCode: res.statusCode, body })); }) .on('error', (err) => { + // other error reject(err); }); }); } -// test -const CreateAstro = suite('npm init astro'); - -CreateAstro.before(async () => { - // clean install dir - await del(FIXTURES_DIR); - await fs.promises.mkdir(FIXTURES_DIR); - - // install all templates & deps before running tests - await Promise.all( - TEMPLATES.map(async ({ value: template }) => { - const templateDir = path.join(FIXTURES_DIR, template); - await execa('../../create-astro.mjs', [templateDir, '--template', template, '--commit', GITHUB_SHA, '--force-overwrite'], { - cwd: FIXTURES_DIR, - }); - await execa('yarn', ['--frozen-lockfile', '--silent'], { cwd: templateDir }); - }) - ); -}); - -// enforce MAX_TEST_TIME -CreateAstro.before.each(({ __test__ }) => { - if (TIMER[__test__]) throw new Error(`Test "${__test__}" already declared`); - TIMER[__test__] = setTimeout(() => { - throw new Error(`"${__test__}" did not finish within allowed time`); - }, MAX_TEST_TIME); -}); -CreateAstro.after.each(({ __test__ }) => { - clearTimeout(TIMER[__test__]); -}); - -for (let n = 0; n < TEMPLATES.length; n++) { - const template = TEMPLATES[n].value; - const templateDir = path.join(FIXTURES_DIR, template); - - CreateAstro(`${template} (install)`, async () => { - const DOES_HAVE = ['.gitignore', 'package.json', 'public', 'src']; - const DOES_NOT_HAVE = ['.git', 'meta.json']; - - // test: template contains essential files & folders - for (const file of DOES_HAVE) { - assert.ok(fs.existsSync(path.join(templateDir, file)), `missing ${file}`); - } - - // test: template DOES NOT contain files supposed to be stripped away - for (const file of DOES_NOT_HAVE) { - assert.not.ok(fs.existsSync(path.join(templateDir, file)), `failed to clean up ${file}`); - } - }); - - CreateAstro(`${template} (dev)`, async () => { - // start dev server - const port = 3000 + n; // start new port per test - const devServer = execa('yarn', ['start', '--port', port], { cwd: templateDir }); - await new Promise((resolve) => { - setTimeout(() => resolve(), 15000); - }); // give dev server flat 15s to set up - // TODO: try to ping dev server ASAP rather than waiting flat 15s - - // ping dev server - const { statusCode, body } = await fetch(`http://localhost:${port}`); - - // expect 200 to be returned with some response - assert.equal(statusCode, 200, 'didn’t respond with 200'); - assert.ok(body, 'returned empty response'); - - // clean up - devServer.kill(); - }); - - CreateAstro(`${template} (build)`, async () => { - const MUST_HAVE_FILES = ['index.html', '_astro']; - - // build template - await execa('yarn', ['build'], { cwd: templateDir }); - - // scan build dir - const builtFiles = await glob('**/*', { cwd: path.join(templateDir, 'dist') }); - for (const file of MUST_HAVE_FILES) { - assert.ok(builtFiles.includes(file), `didn’t build ${file}`); - } - }); +function assert(a, b, message) { + if (a !== b) throw new Error(red(`✘ ${message}`)); + // console.log(green(`✔ ${message}`)); // don’t show successes } -// run tests -CreateAstro.run(); +async function testTemplate(template) { + const templateDir = path.join(FIXTURES_DIR, template); + + // test 1: install + const DOES_HAVE = ['.gitignore', 'package.json', 'public', 'src']; + const DOES_NOT_HAVE = ['.git', 'meta.json']; + + // test 1a: expect template contains essential files & folders + for (const file of DOES_HAVE) { + assert(fs.existsSync(path.join(templateDir, file)), true, `[${template}] has ${file}`); + } + // test 1b: expect template DOES NOT contain files supposed to be stripped away + for (const file of DOES_NOT_HAVE) { + assert(fs.existsSync(path.join(templateDir, file)), false, `[${template}] cleaned up ${file}`); + } + + // test 2: build + const MUST_HAVE_FILES = ['index.html', '_astro']; + await execa('npm', ['run', 'build'], { cwd: templateDir }); + const builtFiles = await glob('**/*', { cwd: path.join(templateDir, 'dist') }); + // test 2a: expect all files built successfully + for (const file of MUST_HAVE_FILES) { + assert(builtFiles.includes(file), true, `[${template}] built ${file}`); + } + + // test 3: dev server (should happen after build so dependency install can be reused) + + // TODO: fix dev server test in CI + if (process.env.CI === true) { + return; + } + + // start dev server in background & wait until ready + const templateIndex = TEMPLATES.findIndex(({ value }) => value === template); + const port = 3000 + templateIndex; // use different port per-template + const devServer = execa('npm', ['run', 'start', '--', '--port', port], { cwd: templateDir }); + let sigkill = setTimeout(() => { + throw new Error(`Dev server failed to start`); // if 10s has gone by with no update, kill process + }, 10000); + + // read stdout until "Server started" appears + await new Promise((resolve, reject) => { + devServer.stdout.on('data', (data) => { + clearTimeout(sigkill); + sigkill = setTimeout(() => { + reject(`Dev server failed to start`); + }, 10000); + if (data.toString('utf8').includes('Server started')) resolve(); + }); + devServer.stderr.on('data', (data) => { + reject(data.toString('utf8')); + }); + }); + clearTimeout(sigkill); // done! + + // send request to dev server that should be ready + const { statusCode, body } = (await fetch(`http://localhost:${port}`)) || {}; + + // test 3a: expect 200 status code + assert(statusCode, 200, `[${template}] 200 response`); + // test 3b: expect non-empty response + assert(body.length > 0, true, `[${template}] non-empty response`); + + // clean up + devServer.kill(); +} + +async function testAll() { + // setup + await Promise.all( + TEMPLATES.map(async ({ value: template }) => { + // setup: `npm init astro` + await execa('../../create-astro.mjs', [template, '--template', template, '--commit', GITHUB_SHA, '--force-overwrite'], { + cwd: FIXTURES_DIR, + }); + // setup: `npm install` (note: running multiple `yarn`s in parallel in CI will conflict) + await execa('npm', ['install', '--no-package-lock', '--silent'], { cwd: path.join(FIXTURES_DIR, template) }); + }) + ); + + // test (note: not parallelized because Snowpack HMR reuses same port in dev) + for (let n = 0; n < TEMPLATES.length; n += 1) { + const template = TEMPLATES[n].value; + + try { + await testTemplate(template); + } catch (err) { + console.error(red(`✘ [${template}]`)); + throw err; + } + + console.info(green(`✔ [${template}] All tests passed (${n + 1}/${TEMPLATES.length})`)); + } +} +testAll();