Skip to content

Commit cc93a1f

Browse files
authored
tests(init): remove MockTTY (#2974)
These changes simplify implementation logic of the init workflow and mock `prompt()` and `console()` functionality in a more direct manner. This makes things easier to reason about by jumping through fewer mental hoops.
1 parent 3776ef3 commit cc93a1f

2 files changed

Lines changed: 74 additions & 120 deletions

File tree

init/src/init.ts

Lines changed: 25 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// deno-lint-ignore-file no-console
12
import * as colors from "@std/fmt/colors";
23
import * as path from "@std/path";
34

@@ -7,14 +8,6 @@ const FRESH_TAILWIND_VERSION = "0.0.1-alpha.7";
78
const PREACT_VERSION = "10.26.6";
89
const PREACT_SIGNALS_VERSION = "2.0.4";
910

10-
export const enum InitStep {
11-
ProjectName = "ProjectName",
12-
Force = "Force",
13-
Tailwind = "Tailwind",
14-
VSCode = "VSCode",
15-
Docker = "Docker",
16-
}
17-
1811
function css(strs: TemplateStringsArray, ...exprs: string[]): string {
1912
let out = "";
2013

@@ -29,8 +22,8 @@ function css(strs: TemplateStringsArray, ...exprs: string[]): string {
2922

3023
export class InitError extends Error {}
3124

32-
function error(tty: MockTTY, message: string): never {
33-
tty.logError(`%cerror%c: ${message}`, "color: red; font-weight: bold", "");
25+
function error(message: string): never {
26+
console.error(`%cerror%c: ${message}`, "color: red; font-weight: bold", "");
3427
throw new InitError();
3528
}
3629

@@ -55,33 +48,12 @@ OPTIONS:
5548
--docker Setup Project to use Docker
5649
`;
5750

58-
export interface MockTTY {
59-
prompt(
60-
step: InitStep,
61-
message?: string | undefined,
62-
_default?: string | undefined,
63-
): string | null;
64-
confirm(step: InitStep, message?: string | undefined): boolean;
65-
log(...args: unknown[]): void;
66-
logError(...args: unknown[]): void;
67-
}
68-
69-
const realTTY: MockTTY = {
70-
prompt(_step, message, _default) {
71-
return prompt(message, _default);
72-
},
73-
confirm(_step, message) {
74-
return confirm(message);
75-
},
76-
log(...args) {
77-
// deno-lint-ignore no-console
78-
console.log(...args);
79-
},
80-
logError(...args) {
81-
// deno-lint-ignore no-console
82-
console.error(...args);
83-
},
84-
};
51+
export const CONFIRM_EMPTY_MESSAGE =
52+
"The target directory is not empty (files could get overwritten). Do you want to continue anyway?";
53+
export const CONFIRM_TAILWIND_MESSAGE = `Set up ${
54+
colors.cyan("Tailwind CSS")
55+
} for styling?`;
56+
export const CONFIRM_VSCODE_MESSAGE = `Do you use ${colors.cyan("VS Code")}?`;
8557

8658
export async function initProject(
8759
cwd = Deno.cwd(),
@@ -92,34 +64,29 @@ export async function initProject(
9264
tailwind?: boolean | null;
9365
vscode?: boolean | null;
9466
} = {},
95-
tty: MockTTY = realTTY,
9667
): Promise<void> {
97-
tty.log();
98-
tty.log(
68+
console.log();
69+
console.log(
9970
colors.bgRgb8(
10071
colors.rgb8(" 🍋 Fresh: The next-gen web framework. ", 0),
10172
121,
10273
),
10374
);
104-
tty.log();
75+
console.log();
10576

10677
let unresolvedDirectory = Deno.args[0];
10778
if (input.length !== 1) {
108-
const userInput = tty.prompt(
109-
InitStep.ProjectName,
79+
const userInput = prompt(
11080
"Project Name:",
11181
"fresh-project",
11282
);
11383
if (!userInput) {
114-
error(tty, HELP_TEXT);
84+
error(HELP_TEXT);
11585
}
11686

11787
unresolvedDirectory = userInput;
11888
}
11989

120-
const CONFIRM_EMPTY_MESSAGE =
121-
"The target directory is not empty (files could get overwritten). Do you want to continue anyway?";
122-
12390
const projectDir = path.resolve(cwd, unresolvedDirectory);
12491

12592
try {
@@ -128,11 +95,9 @@ export async function initProject(
12895
dir.length === 1 && dir[0].name === ".git";
12996
if (
13097
!isEmpty &&
131-
!(flags.force === null
132-
? tty.confirm(InitStep.Force, CONFIRM_EMPTY_MESSAGE)
133-
: flags.force)
98+
!(flags.force === null ? confirm(CONFIRM_EMPTY_MESSAGE) : flags.force)
13499
) {
135-
error(tty, "Directory is not empty.");
100+
error("Directory is not empty.");
136101
}
137102
} catch (err) {
138103
if (!(err instanceof Deno.errors.NotFound)) {
@@ -144,18 +109,14 @@ export async function initProject(
144109
let useTailwind = flags.tailwind || false;
145110
if (flags.tailwind == null) {
146111
if (
147-
tty.confirm(
148-
InitStep.Tailwind,
149-
`Set up ${colors.cyan("Tailwind CSS")} for styling?`,
150-
)
112+
confirm(CONFIRM_TAILWIND_MESSAGE)
151113
) {
152114
useTailwind = true;
153115
}
154116
}
155117

156-
const USE_VSCODE_MESSAGE = `Do you use ${colors.cyan("VS Code")}?`;
157118
const useVSCode = flags.vscode == null
158-
? tty.confirm(InitStep.VSCode, USE_VSCODE_MESSAGE)
119+
? confirm(CONFIRM_VSCODE_MESSAGE)
159120
: flags.vscode;
160121

161122
const writeFile = async (
@@ -715,30 +676,30 @@ This will watch the project directory and restart as necessary.`;
715676

716677
// Specifically print unresolvedDirectory, rather than resolvedDirectory in order to
717678
// not leak personal info (e.g. `/Users/MyName`)
718-
tty.log("\n%cProject initialized!\n", "color: green; font-weight: bold");
679+
console.log("\n%cProject initialized!\n", "color: green; font-weight: bold");
719680

720681
if (unresolvedDirectory !== ".") {
721-
tty.log(
682+
console.log(
722683
`Enter your project directory using %ccd ${unresolvedDirectory}%c.`,
723684
"color: cyan",
724685
"",
725686
);
726687
}
727-
tty.log(
688+
console.log(
728689
"Run %cdeno task start%c to start the project. %cCTRL-C%c to stop.",
729690
"color: cyan",
730691
"",
731692
"color: cyan",
732693
"",
733694
);
734-
tty.log();
735-
tty.log(
695+
console.log();
696+
console.log(
736697
"Stuck? Join our Discord %chttps://discord.gg/deno",
737698
"color: cyan",
738699
"",
739700
);
740-
tty.log();
741-
tty.log(
701+
console.log();
702+
console.log(
742703
"%cHappy hacking! 🦕",
743704
"color: gray",
744705
);

init/src/init_test.ts

Lines changed: 49 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
import { expect } from "@std/expect";
2-
import { initProject, InitStep, type MockTTY } from "./init.ts";
2+
import {
3+
CONFIRM_TAILWIND_MESSAGE,
4+
CONFIRM_VSCODE_MESSAGE,
5+
initProject,
6+
} from "./init.ts";
37
import * as path from "@std/path";
48
import { getStdOutput, withBrowser } from "../../tests/test_utils.tsx";
59
import { waitForText } from "../../tests/test_utils.tsx";
610
import { withChildProcessServer } from "../../tests/test_utils.tsx";
11+
import { stub } from "@std/testing/mock";
12+
13+
function stubPrompt(result: string) {
14+
return stub(globalThis, "prompt", () => result);
15+
}
16+
17+
function stubConfirm(steps: Record<string, boolean> = {}) {
18+
return stub(
19+
globalThis,
20+
"confirm",
21+
(message) => message ? steps[message] : false,
22+
);
23+
}
724

825
async function withTmpDir(fn: (dir: string) => void | Promise<void>) {
926
const hash = crypto.randomUUID().replaceAll(/-/g, "");
@@ -32,29 +49,6 @@ async function patchProject(dir: string): Promise<void> {
3249
await Deno.writeTextFile(jsonPath, JSON.stringify(json, null, 2) + "\n");
3350
}
3451

35-
function mockUserInput(steps: Record<string, unknown>) {
36-
const errorOutput: unknown[][] = [];
37-
const tty: MockTTY = {
38-
confirm(step, _msg) {
39-
return Boolean(steps[step]);
40-
},
41-
prompt(step, _msg, def) {
42-
const setting = typeof steps[step] === "string"
43-
? steps[step] as string
44-
: null;
45-
return setting ?? def ?? null;
46-
},
47-
log: () => {},
48-
logError: (...args) => {
49-
errorOutput.push(args);
50-
},
51-
};
52-
return {
53-
errorOutput,
54-
tty,
55-
};
56-
}
57-
5852
async function expectProjectFile(dir: string, pathname: string) {
5953
const filePath = path.join(dir, ...pathname.split("/").filter(Boolean));
6054
const stat = await Deno.stat(filePath);
@@ -71,15 +65,17 @@ async function readProjectFile(dir: string, pathname: string): Promise<string> {
7165

7266
Deno.test("init - new project", async () => {
7367
await withTmpDir(async (dir) => {
74-
const mock = mockUserInput({});
75-
await initProject(dir, [], {}, mock.tty);
68+
using _promptStub = stubPrompt("fresh-init");
69+
using _confirmStub = stubConfirm();
70+
await initProject(dir, [], {});
7671
});
7772
});
7873

7974
Deno.test("init - create project dir", async () => {
8075
await withTmpDir(async (dir) => {
81-
const mock = mockUserInput({ [InitStep.ProjectName]: "fresh-init" });
82-
await initProject(dir, [], {}, mock.tty);
76+
using _promptStub = stubPrompt("fresh-init");
77+
using _confirmStub = stubConfirm();
78+
await initProject(dir, [], {});
8379

8480
const root = path.join(dir, "fresh-init");
8581
await expectProjectFile(root, "deno.json");
@@ -92,11 +88,11 @@ Deno.test("init - create project dir", async () => {
9288

9389
Deno.test("init - with tailwind", async () => {
9490
await withTmpDir(async (dir) => {
95-
const mock = mockUserInput({
96-
[InitStep.ProjectName]: ".",
97-
[InitStep.Tailwind]: true,
91+
using _promptStub = stubPrompt(".");
92+
using _confirmStub = stubConfirm({
93+
[CONFIRM_TAILWIND_MESSAGE]: true,
9894
});
99-
await initProject(dir, [], {}, mock.tty);
95+
await initProject(dir, [], {});
10096

10197
const css = await readProjectFile(dir, "static/styles.css");
10298
expect(css).toMatch(/@tailwind/);
@@ -110,11 +106,11 @@ Deno.test("init - with tailwind", async () => {
110106

111107
Deno.test("init - with vscode", async () => {
112108
await withTmpDir(async (dir) => {
113-
const mock = mockUserInput({
114-
[InitStep.ProjectName]: ".",
115-
[InitStep.VSCode]: true,
109+
using _promptStub = stubPrompt(".");
110+
using _confirmStub = stubConfirm({
111+
[CONFIRM_VSCODE_MESSAGE]: true,
116112
});
117-
await initProject(dir, [], {}, mock.tty);
113+
await initProject(dir, [], {});
118114

119115
await expectProjectFile(dir, ".vscode/settings.json");
120116
await expectProjectFile(dir, ".vscode/extensions.json");
@@ -128,10 +124,9 @@ Deno.test({
128124
ignore: Deno.version.deno.includes("+"),
129125
fn: async () => {
130126
await withTmpDir(async (dir) => {
131-
const mock = mockUserInput({
132-
[InitStep.ProjectName]: ".",
133-
});
134-
await initProject(dir, [], {}, mock.tty);
127+
using _promptStub = stubPrompt(".");
128+
using _confirmStub = stubConfirm();
129+
await initProject(dir, [], {});
135130
await expectProjectFile(dir, "main.ts");
136131
await expectProjectFile(dir, "dev.ts");
137132

@@ -150,11 +145,12 @@ Deno.test({
150145

151146
Deno.test("init with tailwind - fmt, lint, and type check project", async () => {
152147
await withTmpDir(async (dir) => {
153-
const mock = mockUserInput({
154-
[InitStep.ProjectName]: ".",
155-
[InitStep.Tailwind]: true,
148+
using _promptStub = stubPrompt(".");
149+
using _confirmStub = stubConfirm({
150+
[CONFIRM_TAILWIND_MESSAGE]: true,
156151
});
157-
await initProject(dir, [], {}, mock.tty);
152+
153+
await initProject(dir, [], {});
158154
await expectProjectFile(dir, "main.ts");
159155
await expectProjectFile(dir, "dev.ts");
160156

@@ -172,10 +168,9 @@ Deno.test("init with tailwind - fmt, lint, and type check project", async () =>
172168

173169
Deno.test("init - can start dev server", async () => {
174170
await withTmpDir(async (dir) => {
175-
const mock = mockUserInput({
176-
[InitStep.ProjectName]: ".",
177-
});
178-
await initProject(dir, [], {}, mock.tty);
171+
using _promptStub = stubPrompt(".");
172+
using _confirmStub = stubConfirm();
173+
await initProject(dir, [], {});
179174
await expectProjectFile(dir, "main.ts");
180175
await expectProjectFile(dir, "dev.ts");
181176

@@ -196,10 +191,9 @@ Deno.test("init - can start dev server", async () => {
196191

197192
Deno.test("init - can start built project", async () => {
198193
await withTmpDir(async (dir) => {
199-
const mock = mockUserInput({
200-
[InitStep.ProjectName]: ".",
201-
});
202-
await initProject(dir, [], {}, mock.tty);
194+
using _promptStub = stubPrompt(".");
195+
using _confirmStub = stubConfirm();
196+
await initProject(dir, [], {});
203197
await expectProjectFile(dir, "main.ts");
204198
await expectProjectFile(dir, "dev.ts");
205199

@@ -230,10 +224,9 @@ Deno.test("init - can start built project", async () => {
230224

231225
Deno.test("init - errors on missing build cache in prod", async () => {
232226
await withTmpDir(async (dir) => {
233-
const mock = mockUserInput({
234-
[InitStep.ProjectName]: ".",
235-
});
236-
await initProject(dir, [], {}, mock.tty);
227+
using _promptStub = stubPrompt(".");
228+
using _confirmStub = stubConfirm();
229+
await initProject(dir, [], {});
237230
await expectProjectFile(dir, "main.ts");
238231
await expectProjectFile(dir, "dev.ts");
239232

0 commit comments

Comments
 (0)