Skip to content

Commit

Permalink
Refactor helpers to a shared location so that we don't have too big o…
Browse files Browse the repository at this point in the history
…f a file (this happened mostly because v2 addon watch infra is large, and we don't want its responsibilities mingling with the other helpers -- no code changes have been made -- only function moves

Update relative directory reference since fixtures helpers are now in a directory
  • Loading branch information
NullVoxPopuli committed Jun 19, 2023
1 parent a4d18ab commit f19c012
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 66 deletions.
67 changes: 1 addition & 66 deletions tests/scenarios/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1 @@
import { PreparedApp } from 'scenario-tester';
import { join } from 'path';
import { readFileSync } from 'fs';
import globby from 'globby';
import { set } from 'lodash';
import type { JSDOM } from 'jsdom';

export interface FastbootTestHelpers {
visit(url: string): Promise<JSDOM>;
fetchAsset(url: string): Promise<string>;
}

export async function setupFastboot(
app: PreparedApp,
environment = 'development',
envVars?: Record<string, string>
): Promise<FastbootTestHelpers> {
let result = await app.execute(`node node_modules/ember-cli/bin/ember build --environment=${environment}`, {
env: envVars,
});

if (result.exitCode !== 0) {
throw new Error(`failed to build app for fastboot: ${result.output}`);
}

const FastBoot = require('fastboot');

let fastboot = new FastBoot({
distPath: join(app.dir, 'dist'),
resilient: false,
});

async function visit(url: string) {
const jsdom = require('jsdom');
const { JSDOM } = jsdom;
let visitOpts = {
request: { headers: { host: 'localhost:4200' } },
};
let page = await fastboot.visit(url, visitOpts);
let html = await page.html();
return new JSDOM(html);
}

async function fetchAsset(url: string): Promise<string> {
const origin = 'http://example.com';
let u = new URL(url, origin);
if (u.origin !== origin) {
throw new Error(`fetchAsset only supports local assets, you asked for ${url}`);
}
return readFileSync(join(app.dir, 'dist', u.pathname), 'utf-8');
}

return { visit, fetchAsset };
}

export function loadFromFixtureData(fixtureNamespace: string) {
const root = join(__dirname, '..', 'fixtures', fixtureNamespace);
const paths = globby.sync('**', { cwd: root, dot: true });
const fixtureStructure: any = {};

paths.forEach(path => {
set(fixtureStructure, path.split('/'), readFileSync(join(root, path), 'utf8'));
});

return fixtureStructure;
}
export * from './helpers/index';
52 changes: 52 additions & 0 deletions tests/scenarios/helpers/fastboot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { PreparedApp } from 'scenario-tester';
import { join } from 'path';
import { readFileSync } from 'fs';
import type { JSDOM } from 'jsdom';

export interface FastbootTestHelpers {
visit(url: string): Promise<JSDOM>;
fetchAsset(url: string): Promise<string>;
}

export async function setupFastboot(
app: PreparedApp,
environment = 'development',
envVars?: Record<string, string>
): Promise<FastbootTestHelpers> {
let result = await app.execute(`node node_modules/ember-cli/bin/ember build --environment=${environment}`, {
env: envVars,
});

if (result.exitCode !== 0) {
throw new Error(`failed to build app for fastboot: ${result.output}`);
}

const FastBoot = require('fastboot');

let fastboot = new FastBoot({
distPath: join(app.dir, 'dist'),
resilient: false,
});

async function visit(url: string) {
const jsdom = require('jsdom');
const { JSDOM } = jsdom;
let visitOpts = {
request: { headers: { host: 'localhost:4200' } },
};
let page = await fastboot.visit(url, visitOpts);
let html = await page.html();
return new JSDOM(html);
}

async function fetchAsset(url: string): Promise<string> {
const origin = 'http://example.com';
let u = new URL(url, origin);
if (u.origin !== origin) {
throw new Error(`fetchAsset only supports local assets, you asked for ${url}`);
}
return readFileSync(join(app.dir, 'dist', u.pathname), 'utf-8');
}

return { visit, fetchAsset };
}
45 changes: 45 additions & 0 deletions tests/scenarios/helpers/filesystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import fs from 'fs/promises';

export async function becomesModified({
filePath,
assert,
fn,
}: {
filePath: string;
assert: Assert;
fn: () => Promise<void>;
}) {
let oldStat = (await fs.stat(filePath)).mtimeMs;

await fn();

let newStat = (await fs.stat(filePath)).mtimeMs;

assert.notStrictEqual(
oldStat,
newStat,
`Expected ${filePath} to be modified. Latest: ${newStat}, previously: ${oldStat}`
);
}

export async function isNotModified({
filePath,
assert,
fn,
}: {
filePath: string;
assert: Assert;
fn: () => Promise<void>;
}) {
let oldStat = (await fs.stat(filePath)).mtimeMs;

await fn();

let newStat = (await fs.stat(filePath)).mtimeMs;

assert.strictEqual(
oldStat,
newStat,
`Expected ${filePath} to be unchanged. Latest: ${newStat}, and pre-fn: ${oldStat}`
);
}
16 changes: 16 additions & 0 deletions tests/scenarios/helpers/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { join } from 'path';
import { readFileSync } from 'fs';
import globby from 'globby';
import { set } from 'lodash';

export function loadFromFixtureData(fixtureNamespace: string) {
const root = join(__dirname, '..', '..', 'fixtures', fixtureNamespace);
const paths = globby.sync('**', { cwd: root, dot: true });
const fixtureStructure: any = {};

paths.forEach(path => {
set(fixtureStructure, path.split('/'), readFileSync(join(root, path), 'utf8'));
});

return fixtureStructure;
}
4 changes: 4 additions & 0 deletions tests/scenarios/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './fastboot';
export * from './fixtures';
export * from './v2-addon';
export * from './filesystem';
91 changes: 91 additions & 0 deletions tests/scenarios/helpers/v2-addon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { PreparedApp } from 'scenario-tester';
import { spawn } from 'child_process';

export class DevWatcher {
#addon: PreparedApp;
#singletonAbort?: AbortController;
#waitForBuildPromise?: Promise<unknown>;
#lastBuild?: string;

constructor(addon: PreparedApp) {
this.#addon = addon;
}

start = () => {
if (this.#singletonAbort) this.#singletonAbort.abort();

this.#singletonAbort = new AbortController();

/**
* NOTE: when running rollup in a non-TTY environemnt, the "watching for changes" message does not print.
*/
let rollupProcess = spawn('pnpm', ['start'], {
cwd: this.#addon.dir,
signal: this.#singletonAbort.signal,
stdio: ['pipe', 'pipe', 'pipe'],
// Have to disable color so our regex / string matching works easier
// Have to include process.env, so the spawned environment has access to `pnpm`
env: { ...process.env, NO_COLOR: '1' },
});

let settle: (...args: unknown[]) => void;
let error: (...args: unknown[]) => void;
this.#waitForBuildPromise = new Promise((resolve, reject) => {
settle = resolve;
error = reject;
});

if (!rollupProcess.stdout) {
throw new Error(`Failed to start process, pnpm start`);
}
if (!rollupProcess.stderr) {
throw new Error(`Failed to start process, pnpm start`);
}

let handleData = (data: Buffer) => {
let string = data.toString();
let lines = string.split('\n');

let build = lines.find(line => line.trim().match(/^created dist in (.+)$/));
let problem = lines.find(line => line.includes('Error:'));
let isAbort = lines.find(line => line.includes('AbortError:'));

if (isAbort) {
// Test may have ended, we want to kill the watcher,
// but not error, because throwing an error causes the test to fail.
return settle();
}

if (problem) {
console.error('\n!!!\n', problem, '\n!!!\n');
error(problem);
return;
}

if (build) {
this.#lastBuild = build[1];

settle?.();

this.#waitForBuildPromise = new Promise((resolve, reject) => {
settle = resolve;
error = reject;
});
}
};

// NOTE: rollup outputs to stderr only, not stdout
rollupProcess.stderr.on('data', (...args) => handleData(...args));
rollupProcess.on('error', handleData);
rollupProcess.on('close', () => settle?.());
rollupProcess.on('exit', () => settle?.());

return this.#waitForBuildPromise;
};

stop = () => this.#singletonAbort?.abort();
settled = () => this.#waitForBuildPromise;
get lastBuild() {
return this.#lastBuild;
}
}

0 comments on commit f19c012

Please sign in to comment.