wrangler 2.6.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/bin/wrangler.js +9 -1
  2. package/miniflare-dist/index.mjs +1 -1
  3. package/package.json +12 -10
  4. package/src/__tests__/api-dev.test.ts +65 -36
  5. package/src/__tests__/api-devregistry.test.js +14 -6
  6. package/src/__tests__/configuration.test.ts +2 -31
  7. package/src/__tests__/{d1.test.ts → d1/d1.test.ts} +48 -5
  8. package/src/__tests__/d1/splitter.test.ts +255 -0
  9. package/src/__tests__/delete.test.ts +5 -2
  10. package/src/__tests__/deployments.test.ts +20 -6
  11. package/src/__tests__/dev.test.tsx +52 -19
  12. package/src/__tests__/generate.test.ts +7 -4
  13. package/src/__tests__/helpers/mock-auth-domain.ts +20 -0
  14. package/src/__tests__/helpers/mock-cfetch.ts +2 -57
  15. package/src/__tests__/helpers/mock-dialogs.ts +70 -86
  16. package/src/__tests__/helpers/mock-oauth-flow.ts +64 -49
  17. package/src/__tests__/helpers/mock-process.ts +8 -13
  18. package/src/__tests__/helpers/msw/blob-worker.cjs +19 -0
  19. package/src/__tests__/helpers/msw/read-file-sync.js +61 -0
  20. package/src/__tests__/index.test.ts +46 -42
  21. package/src/__tests__/init.test.ts +782 -522
  22. package/src/__tests__/jest.setup.ts +20 -24
  23. package/src/__tests__/kv.test.ts +286 -173
  24. package/src/__tests__/logout.test.ts +1 -1
  25. package/src/__tests__/metrics.test.ts +5 -7
  26. package/src/__tests__/middleware.scheduled.test.ts +40 -30
  27. package/src/__tests__/middleware.test.ts +144 -120
  28. package/src/__tests__/pages.test.ts +1618 -1161
  29. package/src/__tests__/publish.test.ts +174 -125
  30. package/src/__tests__/r2.test.ts +2 -2
  31. package/src/__tests__/secret.test.ts +183 -126
  32. package/src/__tests__/tail.test.ts +6 -0
  33. package/src/__tests__/tsconfig-sanity.ts +12 -0
  34. package/src/__tests__/tsconfig.json +8 -0
  35. package/src/__tests__/tsconfig.tsbuildinfo +1 -0
  36. package/src/__tests__/whoami.test.tsx +1 -96
  37. package/src/api/dev.ts +78 -41
  38. package/src/api/index.ts +1 -1
  39. package/src/{bundle-reporter.tsx → bundle-reporter.ts} +0 -0
  40. package/src/cfetch/index.ts +0 -2
  41. package/src/cfetch/internal.ts +16 -18
  42. package/src/cli.ts +2 -2
  43. package/src/config/index.ts +2 -1
  44. package/src/config/validation.ts +1 -2
  45. package/src/create-worker-upload-form.ts +2 -2
  46. package/src/d1/{delete.tsx → delete.ts} +0 -0
  47. package/src/d1/execute.tsx +8 -37
  48. package/src/d1/migrations/apply.tsx +32 -19
  49. package/src/d1/migrations/{index.tsx → index.ts} +0 -0
  50. package/src/d1/splitter.ts +161 -0
  51. package/src/d1/{types.tsx → types.ts} +0 -0
  52. package/src/delete.ts +3 -8
  53. package/src/deployments.ts +6 -0
  54. package/src/deprecated/index.ts +2 -295
  55. package/src/dev/dev.tsx +2 -2
  56. package/src/dev/{get-local-persistence-path.tsx → get-local-persistence-path.ts} +0 -0
  57. package/src/dev/local.tsx +16 -4
  58. package/src/dev/remote.tsx +28 -1
  59. package/src/dev/start-server.ts +19 -11
  60. package/src/dev/use-esbuild.ts +1 -1
  61. package/src/{dev-registry.tsx → dev-registry.ts} +0 -0
  62. package/src/dev.tsx +35 -11
  63. package/src/dialogs.ts +136 -0
  64. package/src/dispatch-namespace.ts +1 -1
  65. package/src/docs/index.ts +97 -0
  66. package/src/environment-variables/factory.ts +88 -0
  67. package/src/environment-variables/misc-variables.ts +30 -0
  68. package/src/generate/index.ts +300 -0
  69. package/src/{index.tsx → index.ts} +16 -10
  70. package/src/init.ts +106 -60
  71. package/src/jest.d.ts +4 -0
  72. package/src/logger.ts +15 -3
  73. package/src/metrics/metrics-config.ts +1 -1
  74. package/src/metrics/send-event.ts +2 -1
  75. package/src/miniflare-cli/assets.ts +4 -0
  76. package/src/miniflare-cli/index.ts +1 -5
  77. package/src/miniflare-cli/tsconfig.json +9 -0
  78. package/src/miniflare-cli/tsconfig.tsbuildinfo +1 -0
  79. package/src/miniflare-cli/types.ts +11 -0
  80. package/src/pages/{build.tsx → build.ts} +0 -0
  81. package/src/pages/{deployment-tails.tsx → deployment-tails.ts} +0 -0
  82. package/src/pages/{dev.tsx → dev.ts} +53 -55
  83. package/src/pages/functions/buildWorker.ts +1 -1
  84. package/src/pages/functions/tsconfig.json +8 -0
  85. package/src/pages/functions/tsconfig.tsbuildinfo +1 -0
  86. package/src/pages/{functions.tsx → functions.ts} +0 -0
  87. package/src/pages/{hash.tsx → hash.ts} +0 -0
  88. package/src/pages/{index.tsx → index.ts} +0 -0
  89. package/src/pages/projects.tsx +3 -5
  90. package/src/pages/publish.tsx +16 -5
  91. package/src/pages/upload.tsx +27 -6
  92. package/src/publish/publish.ts +9 -7
  93. package/src/pubsub/{pubsub-commands.tsx → pubsub-commands.ts} +1 -1
  94. package/src/secret/index.ts +1 -1
  95. package/src/{sites.tsx → sites.ts} +0 -0
  96. package/src/tail/index.ts +2 -3
  97. package/src/tsconfig-sanity.ts +16 -0
  98. package/src/user/access.ts +0 -1
  99. package/src/user/auth-variables.ts +113 -0
  100. package/src/user/choose-account.tsx +1 -31
  101. package/src/user/index.ts +0 -1
  102. package/src/user/{user.tsx → user.ts} +107 -73
  103. package/src/{whoami.tsx → whoami.ts} +37 -71
  104. package/templates/__tests__/tsconfig-sanity.ts +12 -0
  105. package/templates/__tests__/tsconfig.json +8 -0
  106. package/templates/__tests__/tsconfig.tsbuildinfo +1 -0
  107. package/templates/d1-beta-facade.js +36 -0
  108. package/templates/facade.d.ts +14 -0
  109. package/templates/first-party-worker-module-facade.ts +4 -3
  110. package/templates/format-dev-errors.ts +7 -6
  111. package/templates/init-tests/test-jest-new-worker.js +3 -5
  112. package/templates/init-tests/test-vitest-new-worker.js +3 -5
  113. package/templates/init-tests/test-vitest-new-worker.ts +25 -0
  114. package/templates/middleware/loader-modules.ts +0 -2
  115. package/templates/middleware/loader-sw.ts +6 -0
  116. package/templates/pages-dev-pipeline.ts +4 -1
  117. package/templates/pages-shim.ts +4 -1
  118. package/templates/pages-template-plugin.ts +12 -7
  119. package/templates/serve-static-assets.ts +16 -14
  120. package/templates/tsconfig-sanity.ts +11 -0
  121. package/templates/tsconfig.init.json +106 -0
  122. package/templates/tsconfig.json +5 -103
  123. package/templates/tsconfig.tsbuildinfo +1 -0
  124. package/wrangler-dist/cli.d.ts +58 -60
  125. package/wrangler-dist/cli.js +34498 -55459
  126. package/wrangler-dist/wasm-sync.wasm +0 -0
  127. package/src/__tests__/dialogs.test.tsx +0 -40
  128. package/src/dialogs.tsx +0 -168
  129. package/src/environment-variables.ts +0 -50
  130. package/src/user/env-vars.ts +0 -46
@@ -2,17 +2,20 @@ import { rest } from "msw";
2
2
  import { mockAccountId, mockApiToken } from "./helpers/mock-account-id";
3
3
  import { mockConsoleMethods } from "./helpers/mock-console";
4
4
  import { mockConfirm } from "./helpers/mock-dialogs";
5
+ import { useMockIsTTY } from "./helpers/mock-istty";
5
6
  import { msw } from "./helpers/msw";
6
7
  import { runInTempDir } from "./helpers/run-in-tmp";
7
8
  import { runWrangler } from "./helpers/run-wrangler";
8
9
  import writeWranglerToml from "./helpers/write-wrangler-toml";
9
10
  import type { KVNamespaceInfo } from "../kv/helpers";
10
-
11
11
  describe("delete", () => {
12
12
  mockAccountId();
13
13
  mockApiToken();
14
14
  runInTempDir();
15
-
15
+ const { setIsTTY } = useMockIsTTY();
16
+ beforeEach(() => {
17
+ setIsTTY(true);
18
+ });
16
19
  const std = mockConsoleMethods();
17
20
 
18
21
  it("should delete an entire service by name", async () => {
@@ -1,5 +1,7 @@
1
1
  // import * as fs from "fs";
2
2
  // import * as TOML from "@iarna/toml";
3
+ import * as fs from "node:fs";
4
+ import * as TOML from "@iarna/toml";
3
5
  import { rest } from "msw";
4
6
  import { mockAccountId, mockApiToken } from "./helpers/mock-account-id";
5
7
  import { mockConsoleMethods } from "./helpers/mock-console";
@@ -26,11 +28,7 @@ describe("deployments", () => {
26
28
  ...mswSuccessUserHandlers,
27
29
  rest.get(
28
30
  "*/accounts/:accountId/workers/services/:scriptName",
29
- (request, response, context) => {
30
- expect(["undefined", "somethingElse"]).toContain(
31
- request.params.scriptName
32
- );
33
-
31
+ (_, response, context) => {
34
32
  return response.once(
35
33
  context.status(200),
36
34
  context.json({
@@ -52,6 +50,16 @@ describe("deployments", () => {
52
50
  });
53
51
 
54
52
  it("should log deployments", async () => {
53
+ fs.writeFileSync(
54
+ "./wrangler.toml",
55
+ TOML.stringify({
56
+ compatibility_date: "2022-01-12",
57
+ name: "test-script-name",
58
+ first_party_worker: true,
59
+ }),
60
+ "utf-8"
61
+ );
62
+
55
63
  await runWrangler("deployments");
56
64
  expect(std.out).toMatchInlineSnapshot(`
57
65
  "🚧\`wrangler deployments\` is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose
@@ -71,7 +79,7 @@ describe("deployments", () => {
71
79
  });
72
80
 
73
81
  it("should log deployments for script with passed in name option", async () => {
74
- await runWrangler("deployments --name somethingElse");
82
+ await runWrangler("deployments --name something-else");
75
83
  expect(std.out).toMatchInlineSnapshot(`
76
84
  "🚧\`wrangler deployments\` is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose
77
85
 
@@ -88,4 +96,10 @@ describe("deployments", () => {
88
96
  🟩 Active"
89
97
  `);
90
98
  });
99
+
100
+ it("should error on missing script name", async () => {
101
+ await expect(runWrangler("deployments")).rejects.toMatchInlineSnapshot(
102
+ `[Error: Required Worker name missing. Please specify the Worker name in wrangler.toml, or pass it as an argument with \`--name\`]`
103
+ );
104
+ });
91
105
  });
@@ -1,11 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import getPort from "get-port";
3
+ import { rest } from "msw";
3
4
  import patchConsole from "patch-console";
4
5
  import dedent from "ts-dedent";
5
6
  import Dev from "../dev/dev";
6
7
  import { CI } from "../is-ci";
7
8
  import { mockAccountId, mockApiToken } from "./helpers/mock-account-id";
8
- import { setMockResponse, unsetAllMocks } from "./helpers/mock-cfetch";
9
9
  import { mockConsoleMethods } from "./helpers/mock-console";
10
10
  import {
11
11
  msw,
@@ -33,7 +33,7 @@ describe("wrangler dev", () => {
33
33
  afterEach(() => {
34
34
  (Dev as jest.Mock).mockClear();
35
35
  patchConsole(() => {});
36
- unsetAllMocks();
36
+ msw.resetHandlers();
37
37
  });
38
38
 
39
39
  describe("authorization", () => {
@@ -439,16 +439,44 @@ describe("wrangler dev", () => {
439
439
  main: "index.js",
440
440
  });
441
441
  fs.writeFileSync("index.js", `export default {};`);
442
- mockGetZones("111.222.333.some-host.com", []);
443
- mockGetZones("222.333.some-host.com", []);
444
- mockGetZones("333.some-host.com", [{ id: "some-zone-id" }]);
445
- await runWrangler("dev --host 111.222.333.some-host.com");
446
- expect((Dev as jest.Mock).mock.calls[0][0]).toEqual(
447
- expect.objectContaining({
448
- host: "111.222.333.some-host.com",
449
- zone: "some-zone-id",
442
+
443
+ msw.use(
444
+ rest.get("*/zones", (req, res, ctx) => {
445
+ let zone: [] | [{ id: "some-zone-id" }] = [];
446
+ if (
447
+ req.url.searchParams.get("name") === "111.222.333.some-host.com"
448
+ ) {
449
+ zone = [];
450
+ } else if (
451
+ req.url.searchParams.get("name") === "222.333.some-host.com"
452
+ ) {
453
+ zone = [];
454
+ } else if (req.url.searchParams.get("name") === "333.some-host.com") {
455
+ zone = [{ id: "some-zone-id" }];
456
+ }
457
+
458
+ return res(
459
+ ctx.status(200),
460
+ ctx.json({
461
+ success: true,
462
+ errors: [],
463
+ messages: [],
464
+ result: zone,
465
+ })
466
+ );
450
467
  })
451
468
  );
469
+
470
+ await runWrangler("dev --host 111.222.333.some-host.com");
471
+
472
+ const devMockCall = (Dev as jest.Mock).mock.calls[0][0];
473
+
474
+ expect(devMockCall).toHaveProperty("host", "111.222.333.some-host.com");
475
+ expect(devMockCall).toHaveProperty(
476
+ "localUpstream",
477
+ "111.222.333.some-host.com"
478
+ );
479
+ expect(devMockCall).toHaveProperty("zone", "some-zone-id");
452
480
  });
453
481
 
454
482
  it("should, in order, use args.host/config.dev.host/args.routes/(config.route|config.routes)", async () => {
@@ -1540,14 +1568,19 @@ describe("wrangler dev", () => {
1540
1568
  });
1541
1569
 
1542
1570
  function mockGetZones(domain: string, zones: { id: string }[] = []) {
1543
- const removeMock = setMockResponse(
1544
- "/zones",
1545
- "GET",
1546
- (_urlPieces, _init, queryParams) => {
1547
- expect([...queryParams.entries()]).toEqual([["name", domain]]);
1548
- // Because the API URL `/zones` is the same for each request, we can get into a situation where earlier mocks get triggered for later requests. So, we simply clear the mock on every trigger.
1549
- removeMock();
1550
- return zones;
1551
- }
1571
+ msw.use(
1572
+ rest.get("*/zones", (req, res, ctx) => {
1573
+ expect([...req.url.searchParams.entries()]).toEqual([["name", domain]]);
1574
+
1575
+ return res(
1576
+ ctx.status(200),
1577
+ ctx.json({
1578
+ success: true,
1579
+ errors: [],
1580
+ messages: [],
1581
+ result: zones,
1582
+ })
1583
+ );
1584
+ })
1552
1585
  );
1553
1586
  }
@@ -1,18 +1,21 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { mockConsoleMethods } from "./helpers/mock-console";
4
- import { mockConfirm, clearConfirmMocks } from "./helpers/mock-dialogs";
4
+ import { mockConfirm } from "./helpers/mock-dialogs";
5
+ import { useMockIsTTY } from "./helpers/mock-istty";
5
6
  import { runInTempDir } from "./helpers/run-in-tmp";
6
7
  import { runWrangler } from "./helpers/run-wrangler";
7
8
 
8
9
  describe("generate", () => {
9
10
  runInTempDir();
11
+ const { setIsTTY } = useMockIsTTY();
10
12
  const std = mockConsoleMethods();
13
+ beforeEach(() => {
14
+ setIsTTY(true);
15
+ });
11
16
 
12
17
  describe("cli functionality", () => {
13
- afterEach(() => {
14
- clearConfirmMocks();
15
- });
18
+ afterEach(() => {});
16
19
 
17
20
  it("defers to `wrangler init` when no template is given", async () => {
18
21
  mockConfirm(
@@ -0,0 +1,20 @@
1
+ const ORIGINAL_WRANGLER_AUTH_DOMAIN = process.env.WRANGLER_AUTH_DOMAIN;
2
+
3
+ /**
4
+ * Mock the Auth URL domain so that we can control where we attempt to login.
5
+ *
6
+ * Note that you can remove any API token from the environment by setting the value to `null`.
7
+ * This is useful if a higher `describe()` block has already called `mockAuthDomain()`.
8
+ */
9
+ export function mockAuthDomain({ domain }: { domain: string | null }) {
10
+ beforeEach(() => {
11
+ if (domain === null) {
12
+ delete process.env.WRANGLER_AUTH_DOMAIN;
13
+ } else {
14
+ process.env.WRANGLER_AUTH_DOMAIN = domain;
15
+ }
16
+ });
17
+ afterEach(() => {
18
+ process.env.WRANGLER_AUTH_DOMAIN = ORIGINAL_WRANGLER_AUTH_DOMAIN;
19
+ });
20
+ }
@@ -2,7 +2,7 @@ import { Readable } from "node:stream";
2
2
  import { URL, URLSearchParams } from "node:url";
3
3
  import { pathToRegexp } from "path-to-regexp";
4
4
  import { Response } from "undici";
5
- import { getCloudflareApiBaseUrl } from "../../cfetch";
5
+ import { getCloudflareApiBaseUrl } from "../../environment-variables/misc-variables";
6
6
  import type { FetchResult, FetchError } from "../../cfetch";
7
7
  import type {
8
8
  fetchInternal,
@@ -175,6 +175,7 @@ export function setMockResponse<ResponseType>(
175
175
 
176
176
  /**
177
177
  * A helper to make it easier to create `FetchResult` objects in tests.
178
+ * TODO: Hijack this for MSW response objects. - JACOB
178
179
  */
179
180
  export async function createFetchResult<ResponseType>(
180
181
  result: ResponseType | Promise<ResponseType>,
@@ -219,31 +220,6 @@ const kvGetMocks = new Map<string, string | Buffer>();
219
220
  const r2GetMocks = new Map<string, string | undefined>();
220
221
  const dashScriptMocks = new Map<string, string | undefined>();
221
222
 
222
- /**
223
- * @mocked typeof fetchKVGetValue
224
- */
225
- export function mockFetchKVGetValue(
226
- accountId: string,
227
- namespaceId: string,
228
- key: string
229
- ) {
230
- const mapKey = `${accountId}/${namespaceId}/${key}`;
231
- if (kvGetMocks.has(mapKey)) {
232
- const value = kvGetMocks.get(mapKey);
233
- if (value !== undefined) return Promise.resolve(value);
234
- }
235
- throw new Error(`no mock value found for \`kv:key get\` - ${mapKey}`);
236
- }
237
-
238
- export function setMockFetchKVGetValue(
239
- accountId: string,
240
- namespaceId: string,
241
- key: string,
242
- value: string | Buffer
243
- ) {
244
- kvGetMocks.set(`${accountId}/${namespaceId}/${key}`, value);
245
- }
246
-
247
223
  /**
248
224
  * @mocked typeof fetchR2Objects
249
225
  */
@@ -300,34 +276,3 @@ export function unsetSpecialMockFns() {
300
276
  r2GetMocks.clear();
301
277
  dashScriptMocks.clear();
302
278
  }
303
-
304
- /**
305
- * @mocked typeof fetchDashScript
306
- * multipart/form-data is the response for modules and raw text for the Script endpoint.
307
- */
308
- export async function mockFetchDashScript(resource: string): Promise<string> {
309
- if (dashScriptMocks.has(resource)) {
310
- return dashScriptMocks.get(resource) ?? "";
311
- }
312
- throw new Error(`no mock found for \`init from-dash\` - ${resource}`);
313
- }
314
-
315
- /**
316
- * Mock setter for usage within test blocks, companion helper to `mockFetchDashScript`
317
- */
318
- export function setMockFetchDashScript({
319
- accountId,
320
- fromDashScriptName,
321
- environment,
322
- mockResponse,
323
- }: {
324
- accountId: string;
325
- fromDashScriptName: string;
326
- environment: string;
327
- mockResponse?: string;
328
- }) {
329
- dashScriptMocks.set(
330
- `/accounts/${accountId}/workers/services/${fromDashScriptName}/environments/${environment}/content`,
331
- mockResponse
332
- );
333
- }
@@ -1,12 +1,12 @@
1
- import { confirm, prompt, select } from "../../dialogs";
2
- import { normalizeSlashes } from "./mock-console";
3
-
1
+ import prompts from "prompts";
4
2
  /**
5
3
  * The expected values for a confirmation request.
6
4
  */
7
5
  export interface ConfirmExpectation {
8
6
  /** The text expected to be seen in the confirmation dialog. */
9
7
  text: string;
8
+
9
+ options?: { defaultValue: boolean };
10
10
  /** The mock response send back from the confirmation dialog. */
11
11
  result: boolean;
12
12
  }
@@ -19,35 +19,21 @@ export interface ConfirmExpectation {
19
19
  * then an error is thrown.
20
20
  */
21
21
  export function mockConfirm(...expectations: ConfirmExpectation[]) {
22
- (confirm as jest.Mock).mockImplementation((text: string) => {
23
- for (const expectation of expectations) {
24
- if (normalizeSlashes(text) === normalizeSlashes(expectation.text)) {
25
- expectations = expectations.filter((e) => e !== expectation);
26
- return Promise.resolve(expectation.result);
22
+ for (const expectation of expectations) {
23
+ (prompts as unknown as jest.Mock).mockImplementationOnce(
24
+ ({ type, name, message, initial }) => {
25
+ expect({ type, name, message }).toStrictEqual({
26
+ type: "confirm",
27
+ name: "value",
28
+ message: expectation.text,
29
+ });
30
+ if (expectation.options) {
31
+ expect(initial).toStrictEqual(expectation.options?.defaultValue);
32
+ }
33
+ return Promise.resolve({ value: expectation.result });
27
34
  }
28
- }
29
- throw new Error(`Unexpected confirmation message: ${text}`);
30
- });
31
- return () => {
32
- if (expectations.length > 0) {
33
- throw new Error(
34
- "The following expected confirmation dialogs were not used:\n" +
35
- expectations.map((e) => `- "${e.text}"`).join("\n")
36
- );
37
- }
38
- };
39
- }
40
-
41
- export function clearConfirmMocks() {
42
- (confirm as jest.Mock).mockReset();
43
- // Because confirm was originally a spy, calling mockReset will simply reset
44
- // it as a function with no return value (!), so we need to accitionally reset
45
- // the mock implementation to the one that throws (from jest.setup.js).
46
- (confirm as jest.Mock).mockImplementation((text: string) => {
47
- throw new Error(
48
- `Unexpected call to \`confirm("${text}")\`.\nYou should use \`mockConfirm()\` to mock calls to \`confirm()\` with expectations. Search the codebase for \`mockConfirm\` to learn more.`
49
35
  );
50
- });
36
+ }
51
37
  }
52
38
 
53
39
  /**
@@ -57,7 +43,10 @@ export interface PromptExpectation {
57
43
  /** The text expected to be seen in the prompt dialog. */
58
44
  text: string;
59
45
  /** The type of the prompt. */
60
- type: "text" | "password";
46
+ options?: {
47
+ defaultValue?: string;
48
+ isSecret?: boolean;
49
+ };
61
50
  /** The mock response send back from the prompt dialog. */
62
51
  result: string;
63
52
  }
@@ -70,45 +59,44 @@ export interface PromptExpectation {
70
59
  * then an error is thrown.
71
60
  */
72
61
  export function mockPrompt(...expectations: PromptExpectation[]) {
73
- (prompt as jest.Mock).mockImplementation(
74
- (text: string, type: "text" | "password") => {
75
- for (const expectation of expectations) {
76
- if (text === expectation.text && type == expectation.type) {
77
- expectations = expectations.filter((e) => e !== expectation);
78
- return Promise.resolve(expectation.result);
62
+ for (const expectation of expectations) {
63
+ (prompts as unknown as jest.Mock).mockImplementationOnce(
64
+ ({ type, name, message, initial, style }) => {
65
+ expect({ type, name, message }).toStrictEqual({
66
+ type: "text",
67
+ name: "value",
68
+ message: expectation.text,
69
+ });
70
+ if (expectation.options) {
71
+ expect(initial).toStrictEqual(expectation.options?.defaultValue);
72
+ expect(style).toStrictEqual(
73
+ expectation.options?.isSecret ? "password" : "default"
74
+ );
79
75
  }
76
+ return Promise.resolve({ value: expectation.result });
80
77
  }
81
- throw new Error(`Unexpected confirmation message: ${text}`);
82
- }
83
- );
84
- return () => {
85
- if (expectations.length > 0) {
86
- throw new Error(
87
- "The following expected prompt dialogs were not used:\n" +
88
- expectations.map((e) => `- "${e.text}"`).join("\n")
89
- );
90
- }
91
- };
78
+ );
79
+ }
92
80
  }
93
81
 
94
- export function clearPromptMocks() {
95
- (prompt as jest.Mock).mockReset();
96
- // Because prompt was originally a spy, calling mockReset will simply reset
97
- // it as a function with no return value (!), so we need to accitionally reset
98
- // the mock implementation to the one that throws (from jest.setup.js).
99
- (prompt as jest.Mock).mockImplementation((text: string) => {
100
- throw new Error(
101
- `Unexpected call to \`prompt(${text}, ...)\`.\nYou should use \`mockPrompt()\` to mock calls to \`prompt()\` with expectations. Search the codebase for \`mockPrompt\` to learn more.`
102
- );
103
- });
82
+ interface SelectOptions<Values> {
83
+ choices: SelectOption<Values>[];
84
+ defaultOption?: number;
104
85
  }
105
86
 
87
+ interface SelectOption<Values> {
88
+ title: string;
89
+ description?: string;
90
+ value: Values;
91
+ }
106
92
  /**
107
93
  * The expected values for a select request.
108
94
  */
109
- export interface SelectExpectation {
95
+ export interface SelectExpectation<Values> {
110
96
  /** The text expected to be seen in the select dialog. */
111
97
  text: string;
98
+
99
+ options?: SelectOptions<Values>;
112
100
  /** The mock response send back from the select dialog. */
113
101
  result: string;
114
102
  }
@@ -120,34 +108,30 @@ export interface SelectExpectation {
120
108
  * If there is a call to `select()` that does not match any of the expectations
121
109
  * then an error is thrown.
122
110
  */
123
- export function mockSelect(...expectations: SelectExpectation[]) {
124
- (select as jest.Mock).mockImplementation((text: string) => {
125
- for (const expectation of expectations) {
126
- if (normalizeSlashes(text) === normalizeSlashes(expectation.text)) {
127
- expectations = expectations.filter((e) => e !== expectation);
128
- return Promise.resolve(expectation.result);
111
+ export function mockSelect<Values>(
112
+ ...expectations: SelectExpectation<Values>[]
113
+ ) {
114
+ for (const expectation of expectations) {
115
+ (prompts as unknown as jest.Mock).mockImplementationOnce(
116
+ ({ type, name, message, choices, initial }) => {
117
+ expect({ type, name, message }).toStrictEqual({
118
+ type: "select",
119
+ name: "value",
120
+ message: expectation.text,
121
+ });
122
+ if (expectation.options) {
123
+ expect(choices).toStrictEqual(expectation.options?.choices);
124
+ expect(initial).toStrictEqual(expectation.options?.defaultOption);
125
+ }
126
+ return Promise.resolve({ value: expectation.result });
129
127
  }
130
- }
131
- throw new Error(`Unexpected select message: ${text}`);
132
- });
133
- return () => {
134
- if (expectations.length > 0) {
135
- throw new Error(
136
- "The following expected select dialogs were not used:\n" +
137
- expectations.map((e) => `- "${e.text}"`).join("\n")
138
- );
139
- }
140
- };
128
+ );
129
+ }
141
130
  }
142
131
 
143
- export function clearSelectMocks() {
144
- (select as jest.Mock).mockReset();
145
- // Because select was originally a spy, calling mockReset will simply reset
146
- // it as a function with no return value (!), so we need to additionally reset
147
- // the mock implementation to the one that throws (from jest.setup.js).
148
- (select as jest.Mock).mockImplementation((text: string) => {
149
- throw new Error(
150
- `Unexpected call to \`select("${text}")\`.\nYou should use \`mockSelect()\` to mock calls to \`select()\` with expectations. Search the codebase for \`mockSelect\` to learn more.`
151
- );
152
- });
132
+ export function clearDialogs() {
133
+ // No dialog mocks should be left after each test, and so calling the dialog methods should throw
134
+ expect(() => prompts({ type: "select", name: "unknown" })).toThrow(
135
+ "Unexpected call to "
136
+ );
153
137
  }