import fs from 'fs'; import path from 'path'; import http from 'http'; import { green, red } from 'kleur/colors'; import { execa } from 'execa'; import glob from 'tiny-glob'; import { TEMPLATES } from '../dist/templates.js'; import { GITHUB_SHA, FIXTURES_DIR } from './helpers.js'; // helpers async function fetch(url) { 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; }); res.on('end', () => resolve({ statusCode: res.statusCode, body })); }) .on('error', (err) => { // other error reject(err); }); }); } function assert(a, b, message) { if (a !== b) throw new Error(red(`✘ ${message}`)); } 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: `pnpm install` (note: running multiple `pnpm`s in parallel in CI will conflict) await execa('pnpm', ['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();