-
Notifications
You must be signed in to change notification settings - Fork 1
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
Changes from 6 commits
24b2ccb
65d88c1
dc7f22c
688770e
6ebf40f
e2943c8
2661d0a
a1d5bd7
3b0ba4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"start-frontend": patch | ||
--- | ||
|
||
Improve start-frontend CLI test |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,168 +1,190 @@ | ||
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 TEST_DIR; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's not a constant, I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 2661d0a |
||
|
||
const exe = util.promisify(child.execFile); | ||
const exe = util.promisify(execFile); | ||
|
||
const startFrontend = path.resolve(__dirname, "../dist/index.js"); | ||
async function cleanupTestDir() { | ||
fse.existsSync(TEST_DIR) && fse.rmSync(TEST_DIR, { 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} ${TEST_DIR}`); | ||
cliProcess.stdin?.setDefaultEncoding("utf-8"); | ||
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
TEST_DIR = | ||
// eslint-disable-next-line turbo/no-undeclared-env-vars | ||
process.env.RUNNER_TEMP || | ||
ptrkdan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
execSync("mktemp -d -t my-test").toString("utf-8"); | ||
ptrkdan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 ${TEST_DIR}` | ||
).toString("utf-8"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used tree-cli because it provides a visually clear folder structure. Do you have any other recommendations for a shell tree tool that works on macOS, Linux, and (if possible) Windows? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've only ever used tree-cli myself. 🤔 |
||
|
||
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 | ||
); | ||
}); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. 👍🏼
Yeah, it would be nice if we could ensure this with a formatter, like
FYI @mvn-luanvu-hn @seiya0914