-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1257 from monstar-lab-oss/refactor/improve-cli-test
Improve start-frontend CLI test
- Loading branch information
Showing
2 changed files
with
167 additions
and
139 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"start-frontend": patch | ||
--- | ||
|
||
Improve start-frontend CLI test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
|
||
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".`); | ||
}); | ||
}); | ||
// 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 | ||
); | ||
}); |