Skip to content

Commit d128cde

Browse files
Merge pull request #1257 from monstar-lab-oss/refactor/improve-cli-test
Improve start-frontend CLI test
2 parents e589781 + 3b0ba4f commit d128cde

File tree

2 files changed

+167
-139
lines changed

2 files changed

+167
-139
lines changed

.changeset/wet-days-double.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"start-frontend": patch
3+
---
4+
5+
Improve start-frontend CLI test
Lines changed: 162 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,168 +1,191 @@
11
import { describe, test, expect, beforeAll, afterAll } from "vitest";
22
import path from "node:path";
3-
import fse from "fs-extra";
4-
import child, { ChildProcessWithoutNullStreams } from "node:child_process";
53
import util from "node:util";
6-
import concat from "concat-stream";
7-
import { spawn } from "node:child_process";
4+
import { execFile, exec, execSync } from "node:child_process";
5+
import fse from "fs-extra";
6+
7+
const START_FRONTEND = path.resolve(__dirname, "..", "dist", "index.js");
88

9-
const keys = {
9+
const KEY = {
1010
ENTER: "\x0D",
1111
DOWN: "\u001B\u005B\u0042",
12+
SPACE: "\x20",
1213
};
1314

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

17-
const testDir = "my-test" as const;
18+
let testDir: string;
1819

19-
const exe = util.promisify(child.execFile);
20+
const exe = util.promisify(execFile);
2021

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

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

25-
Usage:
26-
$ npx start-frontend [<dir>] [flags...]
27-
28-
Flags:
29-
--help, -h Show this help message
30-
--version, -v Show the version of this script`;
31-
32-
describe("start-frontend cli", () => {
33-
beforeAll(() => cleanupTestDir());
34-
afterAll(() => cleanupTestDir());
35-
36-
describe("install react boilerplate with cli", () => {
37-
test("interactively configure", async () => {
38-
const cli = spawn("node", [startFrontend], { cwd });
39-
const results = await exeInteractive(cli, [
40-
testDir,
41-
keys.ENTER,
42-
keys.ENTER,
43-
keys.ENTER,
44-
keys.ENTER,
45-
keys.ENTER,
46-
keys.ENTER,
47-
keys.ENTER,
48-
keys.ENTER,
49-
keys.ENTER,
50-
keys.ENTER,
51-
]);
29+
function nextPrompt(inputs: string[]) {
30+
if (!inputs.length) return;
5231

53-
expect(results).toContain(`start-frontend`);
54-
expect(results).toContain(`Welcome!`);
55-
expect(results).toContain(
56-
`? Where Would You like to Create Your Application?`
57-
);
58-
expect(results).toContain(`? Select a JavsScript library for UI`);
59-
expect(results).toContain(`? Select an API Solution`);
60-
expect(results).toContain(`? Select module do you want to use`);
61-
expect(results).toContain(`? Add Testing codes for Catching bugs early?`);
62-
expect(results).toContain(`? Add Vitest for Unit Testing?`);
63-
expect(results).toContain(`? Add Storybook for Visual Testing?`);
64-
expect(results).toContain(`? Add Playwright for End-To-End Testing?`);
65-
expect(results).toContain(`? Add ESLint for Code Linting?`);
66-
expect(results).toContain(`? Add Prettier for Code Formatting?`);
67-
expect(results).toContain(`Success! Created a new app at "my-test".`);
68-
});
69-
});
32+
// Write the input to the CLI process with a delay
33+
setTimeout(() => {
34+
cliProcess?.stdin?.write(inputs[0]);
35+
nextPrompt(inputs.slice(1));
36+
}, delay);
37+
}
7038

71-
// TODO: Skip testing as it is not yet implemented.
72-
describe.skip("install react boilerplate to specify dir", () => {
73-
test("install", async () => {
74-
await exe("node", [startFrontend, testDir], { cwd });
75-
76-
const expectDirs = [
77-
".env.template",
78-
".eslintrc.js",
79-
".gitignore",
80-
"README.md",
81-
"__mocks__",
82-
"babel.config.js",
83-
"index.html",
84-
"jest-setup.ts",
85-
"package.json",
86-
"public",
87-
"src",
88-
"tests",
89-
"tsconfig.json",
90-
"vite.config.ts",
91-
"yarn.lock",
92-
];
93-
94-
expect(fse.readdirSync(path.resolve(cwd, testDir))).toStrictEqual(
95-
expectDirs
96-
);
97-
});
98-
});
39+
nextPrompt(inputs);
9940

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

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

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

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

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

130-
fse.existsSync(installedTestDir) &&
131-
fse.rmSync(installedTestDir, { recursive: true });
132-
}
133-
// FIXME: Displaying loading in the process of cli execution cause test errors.
134-
function exeInteractive(
135-
cli: ChildProcessWithoutNullStreams,
136-
inputs: string[] = [],
137-
delay: number = 200
138-
) {
139-
let currentInputTimeout: NodeJS.Timeout;
140-
141-
cli.stdin.setDefaultEncoding("utf-8");
142-
143-
const loop = (inputs: string[]) => {
144-
if (!inputs.length) return void cli.stdin.end();
145-
146-
currentInputTimeout = setTimeout(() => {
147-
cli.stdin.write(inputs[0]);
148-
loop(inputs.slice(1));
149-
}, delay);
150-
};
68+
test("--help flag works", async () => {
69+
const { stdout } = await exe("node", [START_FRONTEND, "--help"]);
70+
expect(stdout.trim()).toBe(`Create a new codes for front-end app
15171
152-
return new Promise((resolve, reject) => {
153-
cli.stderr.once("data", (err) => {
154-
cli.stdin.end();
72+
Usage:
73+
$ npx start-frontend [<dir>] [flags...]
15574
156-
if (currentInputTimeout) clearTimeout(currentInputTimeout);
157-
reject(err.toString());
158-
});
75+
Flags:
76+
--help, -h Show this help message
77+
--version, -v Show the version of this script`);
78+
});
15979

160-
cli.on("error", reject);
80+
test("-h flag works", async () => {
81+
const { stdout } = await exe("node", [START_FRONTEND, "-h"]);
82+
expect(stdout.trim()).toBe(`Create a new codes for front-end app
16183
162-
loop(inputs);
84+
Usage:
85+
$ npx start-frontend [<dir>] [flags...]
16386
164-
cli.stdout.pipe(
165-
concat((result: Buffer) => resolve(result.toString("utf-8")))
166-
);
87+
Flags:
88+
--help, -h Show this help message
89+
--version, -v Show the version of this script`);
16790
});
168-
}
91+
92+
test(
93+
"handle interactive configuration on CLI",
94+
async () => {
95+
await executeCLI([
96+
// Where Would You like to Create Your Application?
97+
KEY.ENTER,
98+
// Select a JavaScript library for UI (Use arrow keys)
99+
KEY.ENTER,
100+
// Select an API Solution (Use arrow keys)
101+
KEY.ENTER,
102+
// Select module do you want to use
103+
KEY.ENTER,
104+
// Add Testing codes for Catching bugs early?
105+
KEY.ENTER,
106+
// Add Vitest for Unit Testing?
107+
KEY.ENTER,
108+
// Add Storybook for Visual Testing?
109+
KEY.ENTER,
110+
// Add Playwright for End-To-End Testing?
111+
KEY.ENTER,
112+
// Add Prettier for Code Formatting?
113+
KEY.ENTER,
114+
]);
115+
116+
// Execute tree-cli to get the directory structure and convert it to a string
117+
const result = execSync(
118+
`npx tree-cli -a -l 5 --base ${testDir}`
119+
).toString("utf-8");
120+
121+
expect(result).toContain(`
122+
├── .eslintignore
123+
├── .eslintrc.cjs
124+
├── .gitignore
125+
├── .prettierignore
126+
├── .prettierrc
127+
├── .storybook
128+
| ├── main.ts
129+
| ├── preview-head.html
130+
| └── preview.ts
131+
├── __mocks__
132+
| ├── browser.ts
133+
| ├── index.ts
134+
| ├── request-handlers.ts
135+
| └── server.ts
136+
├── __tests__
137+
| ├── About.test.ts
138+
| ├── Home.test.ts
139+
| └── utils
140+
| └── global-setup.ts
141+
├── env.d.ts
142+
├── index.html
143+
├── package.json
144+
├── playwright.config.ts
145+
├── public
146+
| ├── favicon.svg
147+
| └── mockServiceWorker.js
148+
├── src
149+
| ├── app.tsx
150+
| ├── assets
151+
| | ├── base.css
152+
| | └── main.css
153+
| ├── components
154+
| | ├── Button.tsx
155+
| | └── button.module.css
156+
| ├── context.tsx
157+
| ├── main.tsx
158+
| ├── modules
159+
| | └── restful
160+
| | ├── components
161+
| | | ├── user-form.test.tsx
162+
| | | ├── user-form.tsx
163+
| | | ├── user-list.test.tsx
164+
| | | ├── user-list.tsx
165+
| | | ├── user-view.test.tsx
166+
| | | └── user-view.tsx
167+
| | ├── hooks
168+
| | | ├── use-user.test.tsx
169+
| | | └── use-user.ts
170+
| | └── index.ts
171+
| ├── routes.tsx
172+
| └── ui
173+
| ├── nav-link.tsx
174+
| └── pages
175+
| ├── about
176+
| | └── index.tsx
177+
| ├── index.tsx
178+
| ├── layout.tsx
179+
| └── not-found
180+
| └── index.tsx
181+
├── stories
182+
| └── Button.stories.tsx
183+
├── tsconfig.json
184+
├── tsconfig.node.json
185+
├── vite.config.ts
186+
├── vitest.config.ts
187+
└── vitest.setup.ts`);
188+
},
189+
INTERACTIVE_TEST_TIMEOUT
190+
);
191+
});

0 commit comments

Comments
 (0)