Skip to content

Commit

Permalink
WIP watch mode tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mansona committed Mar 21, 2024
1 parent 2968c5f commit 0c74a1a
Showing 1 changed file with 71 additions and 54 deletions.
125 changes: 71 additions & 54 deletions tests/scenarios/watch-mode-test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { appScenarios } from './scenarios';
import type { PreparedApp } from 'scenario-tester';
import QUnit from 'qunit';
import globby from 'globby';
import fs from 'fs/promises';
import { pathExists } from 'fs-extra';
import path from 'path';
import execa, { type Options, type ExecaChildProcess } from 'execa';
import { expectFilesAt } from '@embroider/test-support/file-assertions/qunit';

const { module: Qmodule, test } = QUnit;

Expand All @@ -16,14 +16,14 @@ let app = appScenarios.map('watch-mode', () => {
*/
});

abstract class Waiter {
readonly promise: Promise<void>;
protected _resolve!: () => void;
abstract class Waiter<T> {
readonly promise: Promise<T>;
protected _resolve!: (result: T) => void;
protected _reject!: (error: unknown) => void;
private _timeout = (timeout: number) => this.onTimeout(timeout);

constructor(timeout: number | null = DEFAULT_TIMEOUT) {
this.promise = new Promise<void>((resolve, reject) => {
this.promise = new Promise<T>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
Expand All @@ -33,14 +33,14 @@ abstract class Waiter {
}
}

abstract onOutputLine(data: string): boolean;
abstract onOutputLine(data: string): T;
abstract onExit(code: number): void;
abstract onTimeout(timeout: number): void;

protected resolve(): void {
protected resolve(resolveValue: T): void {
const resolve = this._resolve;
this._resolve = this._reject = this._timeout = () => {};
resolve();
resolve(resolveValue);
}

protected reject(error: unknown): void {
Expand All @@ -52,15 +52,16 @@ abstract class Waiter {

const DEFAULT_TIMEOUT = process.env.CI ? 90000 : 30000;

class OutputWaiter extends Waiter {
constructor(private process: EmberCLI, private output: string | RegExp, timeout?: number | null) {
class OutputWaiter extends Waiter<boolean | RegExpExecArray> {
constructor(private process: CommandWatcher, private output: string | RegExp, timeout?: number | null) {
super(timeout);
}

onOutputLine(line: string): boolean {
if (this.matchLine(line)) {
this.resolve();
return true;
onOutputLine(line: string): boolean | RegExpExecArray {
let matchLine = this.matchLine(line);
if (matchLine) {
this.resolve(matchLine);
return matchLine;
} else {
return false;
}
Expand Down Expand Up @@ -98,31 +99,37 @@ class OutputWaiter extends Waiter {
}
}

private matchLine(line: string): boolean {
private matchLine(line: string): boolean | RegExpExecArray {
if (typeof this.output === 'string') {
return this.output === line;
} else {
return this.output.test(line);
let result = this.output.exec(line);
if (!result) {
return false;
}

return result;
}
}
}

type Status = { type: 'running' } | { type: 'errored'; error: unknown } | { type: 'completed' };

class EmberCLI {
static launch(args: readonly string[], options: Options<string> = {}): EmberCLI {
return new EmberCLI(execa('ember', args, { ...options, all: true }));
class CommandWatcher {
static launch(command: string, args: readonly string[], options: Options<string> = {}): CommandWatcher {
return new CommandWatcher(execa('pnpm', [command, ...args], { ...options, all: true }));
}

readonly completed: Promise<void>;

private status: Status = { type: 'running' };
private waiters: Waiter[] = [];
private waiters: Waiter<unknown>[] = [];
private lines: string[] = [];

constructor(private process: ExecaChildProcess) {
process.all!.on('data', data => {
const lines = data.toString().split(/\r?\n/);
// console.log(lines);
this.lines.push(...lines);
for (const line of lines) {
this.waiters = this.waiters.filter(waiter => !waiter.onOutputLine(line));
Expand All @@ -137,8 +144,8 @@ class EmberCLI {
this.waiters = [];
});

const exit = new (class ExitWaiter extends Waiter {
constructor(private process: EmberCLI) {
const exit = new (class ExitWaiter extends Waiter<void> {
constructor(private process: CommandWatcher) {
super(null);
}

Expand All @@ -163,7 +170,7 @@ class EmberCLI {
onTimeout() {}
})(this);

this.waiters.push(exit);
this.waiters.push(exit as Waiter<unknown>);

this.completed = exit.promise
.then(() => {
Expand Down Expand Up @@ -191,17 +198,18 @@ class EmberCLI {
return this.lines.join('\n');
}

async waitFor(output: string | RegExp, timeout?: number | null): Promise<void> {
async waitFor(output: string | RegExp, timeout?: number | null): Promise<boolean | RegExpExecArray> {
const waiter = new OutputWaiter(this, output, timeout);

for (const line of this.lines) {
if (waiter.onOutputLine(line)) {
return;
let result = waiter.onOutputLine(line);
if (result) {
return result;
}
}

this.waiters.push(waiter);
await waiter.promise;
this.waiters.push(waiter as Waiter<unknown>);
return waiter.promise;
}

clearOutput(): void {
Expand Down Expand Up @@ -353,14 +361,14 @@ function deindent(s: string): string {
app.forEachScenario(scenario => {
Qmodule(scenario.name, function (hooks) {
let app: PreparedApp;
let server: EmberCLI;
let server: CommandWatcher;

function appFile(appPath: string): File {
let fullPath = path.join(app.dir, ...appPath.split('/'));
return new File(appPath, fullPath);
}

async function waitFor(...args: Parameters<EmberCLI['waitFor']>): Promise<void> {
async function waitFor(...args: Parameters<CommandWatcher['waitFor']>): Promise<void> {
await server.waitFor(...args);
}

Expand All @@ -376,52 +384,57 @@ app.forEachScenario(scenario => {
await waitFor(`file deleted ${path.join(...filePath.split('/'))}`);
}

async function checkScripts(distPattern: RegExp, needle: string) {
let root = app.dir;
let available = await globby('**/*', { cwd: path.join(root, 'dist') });

let matchingFiles = available.filter((item: string) => distPattern.test(item));
let matchingFileContents = await Promise.all(
matchingFiles.map(async (item: string) => {
return fs.readFile(path.join(app.dir, 'dist', item), 'utf8');
})
);
return matchingFileContents.some((item: string) => item.includes(needle));
}

hooks.beforeEach(async () => {
app = await scenario.prepare();
server = EmberCLI.launch(['serve', '--port', '0'], { cwd: app.dir });
await waitFor(/Serving on http:\/\/localhost:[0-9]+\//, DEFAULT_TIMEOUT * 2);
server = CommandWatcher.launch('vite', ['--clearScreen', 'false'], { cwd: app.dir });
console.log('waiting for serving');
const result = await waitFor(/Local: http:\/\/127.0.0.1:(\d+)\//, DEFAULT_TIMEOUT * 2);
console.log(result);
console.log('got servig');
server.clearOutput();
});

hooks.afterEach(async () => {
await server.shutdown();
});

// async function checkPath(path: string, needle: string) {
// let root = app.dir;
// let available = await globby('**/*', { cwd: path.join(root, 'dist') });

// let matchingFiles = available.filter((item: string) => distPattern.test(item));
// let matchingFileContents = await Promise.all(
// matchingFiles.map(async (item: string) => {
// return fs.readFile(path.join(app.dir, 'dist', item), 'utf8');
// })
// );
// return matchingFileContents.some((item: string) => item.includes(needle));
// }

function getFiles(assert: Assert) {
return expectFilesAt(path.join(app.dir, 'node_modules', '.embroider', 'rewritten-app'), { qunit: assert });
}

test(`ember serve`, async function (assert) {
const originalContent =
'TWO IS A GREAT NUMBER< I LKE IT A LOT< IT IS THE POWER OF ALL OF ELECTRONICS, MATH, ETC';
assert.false(await checkScripts(/js$/, originalContent), 'file has not been created yet');

getFiles(assert)('./assets/app-template.js').doesNotMatch('app-template/simple-file.js');

await appFile('app/simple-file.js').write(`export const two = "${originalContent}";`);
await added('simple-file.js');
await waitFor(/Build successful/);

assert.true(await checkScripts(/js$/, originalContent), 'the file now exists');
getFiles(assert)('./assets/app-template.js').matches('app-template/simple-file.js');
getFiles(assert)('./simple-file.js').matches(originalContent);
server.clearOutput();

const updatedContent = 'THREE IS A GREAT NUMBER TWO';
assert.false(await checkScripts(/js$/, updatedContent), 'file has not been created yet');

await appFile('app/simple-file.js').write(`export const two = "${updatedContent}";`);
await changed('simple-file.js');
await waitFor(/Build successful/);

// TODO: find a better way to test this; this seems to linger around
// assert.false(await checkScripts(/js$/, originalContent), 'the original file does not exists');
assert.true(await checkScripts(/js$/, updatedContent), 'the updated file now exists');
getFiles(assert)('./simple-file.js').matches(updatedContent);
});

Qmodule('[GH#1619] co-located components regressions', function (hooks) {
Expand Down Expand Up @@ -508,7 +521,9 @@ app.forEachScenario(scenario => {
await assertRewrittenFile('tests/integration/hello-world-test.js').includesContent('<HelloWorld />');
server.clearOutput();

let test = await EmberCLI.launch(['test', '--filter', 'hello-world'], { cwd: app.dir });
let test = await CommandWatcher.launch('ember', ['test', '--filter', 'hello-world', '--path', 'dist'], {
cwd: app.dir,
});
await test.waitFor(/^not ok .+ Integration | hello-world: it renders/, DEFAULT_TIMEOUT * 2);
await assert.rejects(test.completed);

Expand All @@ -523,7 +538,9 @@ app.forEachScenario(scenario => {
`);
await assertRewrittenFile('tests/integration/hello-world-test.js').includesContent('<HelloWorld />');

test = await EmberCLI.launch(['test', '--filter', 'hello-world'], { cwd: app.dir });
test = await CommandWatcher.launch('ember', ['test', '--filter', 'hello-world', '--path', 'dist'], {
cwd: app.dir,
});
await test.waitFor(/^ok .+ Integration | hello-world: it renders/);
await test.completed;
});
Expand Down

0 comments on commit 0c74a1a

Please sign in to comment.