From 0ec733b1acd1a50d3ab38bc001acadf2ec20a966 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sun, 31 Mar 2024 17:43:50 -0700 Subject: [PATCH] Add interactive mode to our test runner --- CONTRIBUTING.md | 14 +++++++------- src/tests/TestRunner.ts | 28 ++++++++++++++++++++++++---- src/tests/test.ts | 5 +++-- src/tests/yesNo.ts | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 src/tests/yesNo.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ae52f66..6b32b6d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,13 +11,13 @@ case and compare the output to corresponding `.expected.ts` file. If the output does not match the expected output, the test runner will fail. The tests in `src/tests/fixtures` are unit tests that test the behavior of the -extraction. They extract GraphQL SDL from the file and write that as output. +extraction and code generation. They extract GraphQL SDL, generated `schema.ts` or any associated errors and code actions from the file and write that as output. If the test includes a line like `// Locate: User.name` of `// Locate: SomeType` then the test runner will instead locate the given entity and write the location as output. -The tests in `src/tests/integrationFixtures` are integration tests that test the _runtime_ behavior of the tool. They expect each file to be a `.ts` file with `@gql` docblock tags which exports a root query class as the named export `Query` and a GraphQL query text under the named export `query`. The test runner will execute the query against the root query class and emit the returned response JSON as the test output. +The tests in `src/tests/integrationFixtures` are integration tests that test the _runtime_ behavior of the generated code. Each directory contains an `index.ts` file with `@gql` docblock tags which exports a root query class as the named export `Query` and a GraphQL query text under the named export `query`. The test runner will execute the query against the root query class and emit the returned response JSON as the test output. ``` @@ -29,25 +29,25 @@ To run a specific test case, you can use the `--filter` flag and provide a substring match for the test fixture's path. ``` - pnpm run test --filter=import - ``` To update fixture files, you can use the `--write` flag. ``` - pnpm run test --write +``` +Interactive mode will prompt you to update the fixture files one by one for each failing fixture test. + +``` +pnpm run test --interactive ``` You an also get help with the CLI flags: ``` - pnpm run test --help - ``` All changes that affect the behavior of the tool, either new features of bug diff --git a/src/tests/TestRunner.ts b/src/tests/TestRunner.ts index b789dc06..7cc519cf 100644 --- a/src/tests/TestRunner.ts +++ b/src/tests/TestRunner.ts @@ -1,6 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import { diff } from "jest-diff"; +import { ask } from "./yesNo"; type Transformer = ( code: string, @@ -47,9 +48,9 @@ export default class TestRunner { } // Returns true if the test passed - async run(): Promise { + async run({ interactive }: { interactive: boolean }): Promise { for (const fixture of this._testFixtures) { - await this._testFixture(fixture); + await this._testFixture(fixture, { interactive }); } console.log(""); @@ -80,7 +81,10 @@ export default class TestRunner { return true; } - async _testFixture(fixture: string) { + async _testFixture( + fixture: string, + { interactive }: { interactive: boolean }, + ) { const expectedFileName = fixture + ".expected"; const expectedFilePath = path.join(this._fixturesDir, expectedFileName); if (this._otherFiles.has(expectedFileName)) { @@ -115,7 +119,23 @@ OUTPUT ${actual}`; if (actualOutput !== expectedContent) { - if (this._write) { + if (interactive) { + console.error("FAILURE: " + displayName); + console.log(diff(expectedContent, actualOutput)); + console.log("Fixture did not match."); + console.log( + `(You can rerun just this test with: \`pnpm run test --filter=${fixture}\`)`, + ); + const write = await ask( + "Would you like to update this fixture file? (y/n)", + ); + if (write) { + console.error("UPDATED: " + displayName); + fs.writeFileSync(expectedFilePath, actualOutput, "utf-8"); + } else { + this._failureCount++; + } + } else if (this._write) { console.error("UPDATED: " + displayName); fs.writeFileSync(expectedFilePath, actualOutput, "utf-8"); } else { diff --git a/src/tests/test.ts b/src/tests/test.ts index 9a397ee0..8d2c9a36 100644 --- a/src/tests/test.ts +++ b/src/tests/test.ts @@ -45,7 +45,8 @@ program "-f, --filter ", "A regex to filter the tests to run. Only tests with a file path matching the regex will be run.", ) - .action(async ({ filter, write }) => { + .option("-i, --interactive", "Run tests in interactive mode.") + .action(async ({ filter, write, interactive }) => { const filterRegex = filter ?? null; let failures = false; for (const { @@ -62,7 +63,7 @@ program ignoreFilePattern, transformer, ); - failures = !(await runner.run()) || failures; + failures = !(await runner.run({ interactive })) || failures; } if (failures) { process.exit(1); diff --git a/src/tests/yesNo.ts b/src/tests/yesNo.ts new file mode 100644 index 00000000..bfdfa49c --- /dev/null +++ b/src/tests/yesNo.ts @@ -0,0 +1,38 @@ +// Adapted from https://github.com/tcql/node-yesno/blob/master/yesno.js + +import * as readline from "readline"; + +const options = { + yes: ["yes", "y"], + no: ["no", "n"], +}; + +export async function ask(question: string): Promise { + const yValues = options.yes.map((v) => v.toLowerCase()); + const nValues = options.no.map((v) => v.toLowerCase()); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise(function (resolve) { + rl.question(question + " ", async function (answer) { + rl.close(); + + const cleaned = answer.trim().toLowerCase(); + + if (yValues.indexOf(cleaned) >= 0) return resolve(true); + + if (nValues.indexOf(cleaned) >= 0) return resolve(false); + + process.stdout.write("\nInvalid Response.\n"); + process.stdout.write( + "Answer either yes : (" + yValues.join(", ") + ") \n", + ); + process.stdout.write("Or no: (" + nValues.join(", ") + ") \n\n"); + const result = await ask(question); + resolve(result); + }); + }); +}