diff --git a/.changeset/wet-days-double.md b/.changeset/wet-days-double.md
new file mode 100644
index 000000000..6354d9e74
--- /dev/null
+++ b/.changeset/wet-days-double.md
@@ -0,0 +1,5 @@
+---
+"start-frontend": patch
+---
+
+Improve start-frontend CLI test
diff --git a/packages/start-frontend/__tests__/cli.test.ts b/packages/start-frontend/__tests__/cli.test.ts
index 37efe4c98..ea7ac5476 100644
--- a/packages/start-frontend/__tests__/cli.test.ts
+++ b/packages/start-frontend/__tests__/cli.test.ts
@@ -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";
+
+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 [
] [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".`);
- });
- });
+ // 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 [] [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 [] [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
+ );
+});