Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve start-frontend CLI test #1257

Merged
merged 9 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wet-days-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"start-frontend": patch
---

Improve start-frontend CLI test
301 changes: 162 additions & 139 deletions packages/start-frontend/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,168 +1,191 @@
import { describe, test, expect, beforeAll, afterAll } from "vitest";
import path from "node:path";
import fse from "fs-extra";
import child, { ChildProcessWithoutNullStreams } from "node:child_process";
import util from "node:util";
import concat from "concat-stream";
import { spawn } from "node:child_process";
import { execFile, exec, execSync } from "node:child_process";
import fse from "fs-extra";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor suggestion, but what do you think about sorting the imports? (Opt + Shift + O on Mac VS Code)

It would be nice if there's a way to include it in the pre-commit script.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opt + Shift + O on Mac VS Code

Personally, I've been using Helix as my editor recently 🙏🏼
But for import order, I think your suggestion to handle it with a precommit hook could work. 👍🏼

It would be nice if there's a way to include it in the pre-commit script.

Yeah, it would be nice if we could ensure this with a formatter, like

FYI @mvn-luanvu-hn @seiya0914


const START_FRONTEND = path.resolve(__dirname, "..", "dist", "index.js");

const keys = {
const KEY = {
ENTER: "\x0D",
DOWN: "\u001B\u005B\u0042",
SPACE: "\x20",
};

// outside monorepo
const cwd = path.resolve(__dirname, "../../../..");
// Timeout duration for interactive tests, to allow for code stub downloads
const INTERACTIVE_TEST_TIMEOUT = 10000;

const testDir = "my-test" as const;
let testDir: string;

const exe = util.promisify(child.execFile);
const exe = util.promisify(execFile);

const startFrontend = path.resolve(__dirname, "../dist/index.js");
async function cleanupTestDir() {
fse.existsSync(testDir) && fse.rmSync(testDir, { recursive: true });
}

const EXPECTED_HELP = `Create a new codes for front-end app
async function executeCLI(inputs: string[], delay = 500) {
const cliProcess = exec(`node ${START_FRONTEND} ${testDir}`);

Usage:
$ npx start-frontend [<dir>] [flags...]

Flags:
--help, -h Show this help message
--version, -v Show the version of this script`;

describe("start-frontend cli", () => {
beforeAll(() => cleanupTestDir());
afterAll(() => cleanupTestDir());

describe("install react boilerplate with cli", () => {
test("interactively configure", async () => {
const cli = spawn("node", [startFrontend], { cwd });
const results = await exeInteractive(cli, [
testDir,
keys.ENTER,
keys.ENTER,
keys.ENTER,
keys.ENTER,
keys.ENTER,
keys.ENTER,
keys.ENTER,
keys.ENTER,
keys.ENTER,
keys.ENTER,
]);
function nextPrompt(inputs: string[]) {
if (!inputs.length) return;

expect(results).toContain(`start-frontend`);
expect(results).toContain(`Welcome!`);
expect(results).toContain(
`? Where Would You like to Create Your Application?`
);
expect(results).toContain(`? Select a JavsScript library for UI`);
expect(results).toContain(`? Select an API Solution`);
expect(results).toContain(`? Select module do you want to use`);
expect(results).toContain(`? Add Testing codes for Catching bugs early?`);
expect(results).toContain(`? Add Vitest for Unit Testing?`);
expect(results).toContain(`? Add Storybook for Visual Testing?`);
expect(results).toContain(`? Add Playwright for End-To-End Testing?`);
expect(results).toContain(`? Add ESLint for Code Linting?`);
expect(results).toContain(`? Add Prettier for Code Formatting?`);
expect(results).toContain(`Success! Created a new app at "my-test".`);
Comment on lines -53 to -67
Copy link
Collaborator Author

@jinmayamashita jinmayamashita Jun 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the wording of the CLI prompts is likely to change frequnetly, I thought it would be better to test the contests of the files generated by running the CLI rather than the working itself.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the file contents may change a bit after this, since we're reorganizing the templates and how the projects are generated 😅

We'll just have to keep this in mind and modify the tests in order to accommodate any changes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right. Even if we change the tests to check the file structure from the messages, the file structure might change frequently for a while, so we'll need to update the tests each time. 😶‍🌫️

In the future, it might be more flexible to focus on whether the generated files can be built, rather than their structure. 🤔

});
});
// Write the input to the CLI process with a delay
setTimeout(() => {
cliProcess?.stdin?.write(inputs[0]);
nextPrompt(inputs.slice(1));
}, delay);
}

// TODO: Skip testing as it is not yet implemented.
describe.skip("install react boilerplate to specify dir", () => {
test("install", async () => {
await exe("node", [startFrontend, testDir], { cwd });

const expectDirs = [
".env.template",
".eslintrc.js",
".gitignore",
"README.md",
"__mocks__",
"babel.config.js",
"index.html",
"jest-setup.ts",
"package.json",
"public",
"src",
"tests",
"tsconfig.json",
"vite.config.ts",
"yarn.lock",
];

expect(fse.readdirSync(path.resolve(cwd, testDir))).toStrictEqual(
expectDirs
);
});
});
nextPrompt(inputs);

describe("printing help message", () => {
test("--help flag works", async () => {
const { stdout } = await exe("node", [startFrontend, "--help"]);
expect(stdout.trim()).toBe(EXPECTED_HELP);
});
return new Promise((resolve) => cliProcess.on("exit", resolve));
}

test("-h flag works", async () => {
const { stdout } = await exe("node", [startFrontend, "-h"]);
expect(stdout.trim()).toBe(EXPECTED_HELP);
});
describe("start-frontend", () => {
beforeAll(() => {
// Initialize TEST_DIR before all tests
testDir =
// Use the default GitHub Actions temporary directory for development in the CI environment.
// refs: https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
// eslint-disable-next-line turbo/no-undeclared-env-vars
process.env.RUNNER_TEMP ||
execSync("mktemp -d -t my-test").toString("utf-8");
cleanupTestDir();
});

describe("printing version", () => {
test("--version flag works", async () => {
const { stdout } = await exe("node", [startFrontend, "--version"]);
// eslint-disable-next-line turbo/no-undeclared-env-vars
expect(stdout.trim()).toBe(process.env.npm_package_version);
});
afterAll(cleanupTestDir);

test("-v flag works", async () => {
const { stdout } = await exe("node", [startFrontend, "-v"]);
// eslint-disable-next-line turbo/no-undeclared-env-vars
expect(stdout.trim()).toBe(process.env.npm_package_version);
});
test("--version works", async () => {
const { stdout } = await exe("node", [START_FRONTEND, "--version"]);
expect(stdout.trim()).toMatch(/^(\d+\.)?(\d+\.)?(\*|\d+)$/);
});
});

function cleanupTestDir() {
const installedTestDir = path.resolve(cwd, testDir);
test("-v flag works", async () => {
const { stdout } = await exe("node", [START_FRONTEND, "-v"]);
expect(stdout.trim()).toMatch(/^(\d+\.)?(\d+\.)?(\*|\d+)$/);
});

fse.existsSync(installedTestDir) &&
fse.rmSync(installedTestDir, { recursive: true });
}
// FIXME: Displaying loading in the process of cli execution cause test errors.
function exeInteractive(
cli: ChildProcessWithoutNullStreams,
inputs: string[] = [],
delay: number = 200
) {
let currentInputTimeout: NodeJS.Timeout;

cli.stdin.setDefaultEncoding("utf-8");

const loop = (inputs: string[]) => {
if (!inputs.length) return void cli.stdin.end();

currentInputTimeout = setTimeout(() => {
cli.stdin.write(inputs[0]);
loop(inputs.slice(1));
}, delay);
};
test("--help flag works", async () => {
const { stdout } = await exe("node", [START_FRONTEND, "--help"]);
expect(stdout.trim()).toBe(`Create a new codes for front-end app

return new Promise((resolve, reject) => {
cli.stderr.once("data", (err) => {
cli.stdin.end();
Usage:
$ npx start-frontend [<dir>] [flags...]

if (currentInputTimeout) clearTimeout(currentInputTimeout);
reject(err.toString());
});
Flags:
--help, -h Show this help message
--version, -v Show the version of this script`);
});

cli.on("error", reject);
test("-h flag works", async () => {
const { stdout } = await exe("node", [START_FRONTEND, "-h"]);
expect(stdout.trim()).toBe(`Create a new codes for front-end app

loop(inputs);
Usage:
$ npx start-frontend [<dir>] [flags...]

cli.stdout.pipe(
concat((result: Buffer) => resolve(result.toString("utf-8")))
);
Flags:
--help, -h Show this help message
--version, -v Show the version of this script`);
});
}

test(
"handle interactive configuration on CLI",
async () => {
await executeCLI([
// Where Would You like to Create Your Application?
KEY.ENTER,
// Select a JavaScript library for UI (Use arrow keys)
KEY.ENTER,
// Select an API Solution (Use arrow keys)
KEY.ENTER,
// Select module do you want to use
KEY.ENTER,
// Add Testing codes for Catching bugs early?
KEY.ENTER,
// Add Vitest for Unit Testing?
KEY.ENTER,
// Add Storybook for Visual Testing?
KEY.ENTER,
// Add Playwright for End-To-End Testing?
KEY.ENTER,
// Add Prettier for Code Formatting?
KEY.ENTER,
]);

// Execute tree-cli to get the directory structure and convert it to a string
const result = execSync(
`npx tree-cli -a -l 5 --base ${testDir}`
).toString("utf-8");

expect(result).toContain(`
├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .storybook
| ├── main.ts
| ├── preview-head.html
| └── preview.ts
├── __mocks__
| ├── browser.ts
| ├── index.ts
| ├── request-handlers.ts
| └── server.ts
├── __tests__
| ├── About.test.ts
| ├── Home.test.ts
| └── utils
| └── global-setup.ts
├── env.d.ts
├── index.html
├── package.json
├── playwright.config.ts
├── public
| ├── favicon.svg
| └── mockServiceWorker.js
├── src
| ├── app.tsx
| ├── assets
| | ├── base.css
| | └── main.css
| ├── components
| | ├── Button.tsx
| | └── button.module.css
| ├── context.tsx
| ├── main.tsx
| ├── modules
| | └── restful
| | ├── components
| | | ├── user-form.test.tsx
| | | ├── user-form.tsx
| | | ├── user-list.test.tsx
| | | ├── user-list.tsx
| | | ├── user-view.test.tsx
| | | └── user-view.tsx
| | ├── hooks
| | | ├── use-user.test.tsx
| | | └── use-user.ts
| | └── index.ts
| ├── routes.tsx
| └── ui
| ├── nav-link.tsx
| └── pages
| ├── about
| | └── index.tsx
| ├── index.tsx
| ├── layout.tsx
| └── not-found
| └── index.tsx
├── stories
| └── Button.stories.tsx
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
├── vitest.config.ts
└── vitest.setup.ts`);
},
INTERACTIVE_TEST_TIMEOUT
);
});
Loading