|
1 | 1 | import { describe, test, expect, beforeAll, afterAll } from "vitest";
|
2 | 2 | import path from "node:path";
|
3 |
| -import fse from "fs-extra"; |
4 |
| -import child, { ChildProcessWithoutNullStreams } from "node:child_process"; |
5 | 3 | 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"); |
8 | 8 |
|
9 |
| -const keys = { |
| 9 | +const KEY = { |
10 | 10 | ENTER: "\x0D",
|
11 | 11 | DOWN: "\u001B\u005B\u0042",
|
| 12 | + SPACE: "\x20", |
12 | 13 | };
|
13 | 14 |
|
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; |
16 | 17 |
|
17 |
| -const testDir = "my-test" as const; |
| 18 | +let testDir: string; |
18 | 19 |
|
19 |
| -const exe = util.promisify(child.execFile); |
| 20 | +const exe = util.promisify(execFile); |
20 | 21 |
|
21 |
| -const startFrontend = path.resolve(__dirname, "../dist/index.js"); |
| 22 | +async function cleanupTestDir() { |
| 23 | + fse.existsSync(testDir) && fse.rmSync(testDir, { recursive: true }); |
| 24 | +} |
22 | 25 |
|
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}`); |
24 | 28 |
|
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; |
52 | 31 |
|
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 | + } |
70 | 38 |
|
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); |
99 | 40 |
|
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 | +} |
105 | 43 |
|
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(); |
110 | 54 | });
|
111 | 55 |
|
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); |
118 | 57 |
|
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+)$/); |
124 | 61 | });
|
125 |
| -}); |
126 | 62 |
|
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 | + }); |
129 | 67 |
|
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 |
151 | 71 |
|
152 |
| - return new Promise((resolve, reject) => { |
153 |
| - cli.stderr.once("data", (err) => { |
154 |
| - cli.stdin.end(); |
| 72 | + Usage: |
| 73 | + $ npx start-frontend [<dir>] [flags...] |
155 | 74 |
|
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 | + }); |
159 | 79 |
|
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 |
161 | 83 |
|
162 |
| - loop(inputs); |
| 84 | + Usage: |
| 85 | + $ npx start-frontend [<dir>] [flags...] |
163 | 86 |
|
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`); |
167 | 90 | });
|
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