-
Notifications
You must be signed in to change notification settings - Fork 613
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
React-free hotkeys implementation (#6149)
* initial implementation * implement higher-level interface for hotkeys automatically compute instructions * extract onKeyPress to own module for mocking * add tests * add changeset * add --x-dev-env flagged run of fixtures/interactive-dev-tests * add test for handlers throwing errors * chore: cleanup teardown * fix: onKeyPress keeps the process alive after unregistering Listening for events on process.stdin (eg .on('keypress')) causes it to go into 'old mode' which keeps this nodejs process alive even after calling .off('keypress') WORKAROUND: piping stdin via a transform stream allows us to call stream.destroy() which then allows this nodejs process to close cleanly * hotkeys integration * implement Logger bottom float * use registerGlobalBottomFloat for floating hotkeys instructions * move registerGlobalBottomFloat calls into hotkeys implementation * add test steps for unregistered hotkeys + update mock to return unregisterHotKeys mock * fix lints * setIsTTY(true) in tests * add docs link to comment * unregisterHotKeys before devEnv.teardown * enable modifier key detection in hotkeys api * fix: store previousBottomFloatLineCount in case the line count changes + ensures no existing logs (from before registerGlobalBottomFloat was called) are cleared * fix: usages of console.dir/table -> logger.dir/table + use static methods of miniflare Log which are now stateful -- rather than reimplement in Wrangler's Logger not sharing state * mark methods with unsafe_ * fixup * use setIsTTY inside beforeEach * use readline.moveCursor(stdout,...) instead of stdout.moveCursor(...) * abstract knowledge of bototm float from logger implementations * fix lints * use readline.clearScreenDown * handle forceLocal (disabled hotkey options) * refactor * update changeset type to refactor * implement logger.console * unregister hotkeys during auth prompt * update AbortError checks * fix logger.console types * change getAccountId to requireAuth * check isTTY in unregister keypress * fix RemoteRuntimeController shutdown log * use isNonInteractiveOrCI util * hookify the hotkeys disabled option * use isInteractive() instead of !isNonIteractiveOrCI()
- Loading branch information
Showing
23 changed files
with
570 additions
and
41 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 @@ | ||
--- | ||
"wrangler": patch | ||
--- | ||
|
||
refactor: React-free hotkeys implementation, behind the `--x-dev-env` flag |
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
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
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,203 @@ | ||
import { setTimeout } from "node:timers/promises"; | ||
import { vitest } from "vitest"; | ||
import registerHotKeys from "../cli-hotkeys"; | ||
import { logger } from "../logger"; | ||
import { mockConsoleMethods } from "./helpers/mock-console"; | ||
import { useMockIsTTY } from "./helpers/mock-istty"; | ||
import type { KeypressEvent } from "../utils/onKeyPress"; | ||
|
||
const writeToMockedStdin = (input: string) => | ||
_internalKeyPressCallback({ | ||
name: input, | ||
sequence: input, | ||
ctrl: false, | ||
meta: false, | ||
shift: false, | ||
}); | ||
let _internalKeyPressCallback: (input: KeypressEvent) => void; | ||
vitest.mock("../utils/onKeyPress", async () => { | ||
return { | ||
onKeyPress(callback: () => void) { | ||
_internalKeyPressCallback = callback; | ||
|
||
return () => {}; | ||
}, | ||
}; | ||
}); | ||
|
||
describe("Hot Keys", () => { | ||
const std = mockConsoleMethods(); | ||
const { setIsTTY } = useMockIsTTY(); | ||
beforeEach(() => { | ||
setIsTTY(true); | ||
}); | ||
|
||
describe("callbacks", () => { | ||
it("calls handlers when a key is pressed", async () => { | ||
const handlerA = vi.fn(); | ||
const handlerB = vi.fn(); | ||
const handlerC = vi.fn(); | ||
const options = [ | ||
{ keys: ["a"], label: "first option", handler: handlerA }, | ||
{ keys: ["b"], label: "second option", handler: handlerB }, | ||
{ keys: ["c"], label: "third option", handler: handlerC }, | ||
]; | ||
|
||
registerHotKeys(options); | ||
|
||
writeToMockedStdin("a"); | ||
expect(handlerA).toHaveBeenCalled(); | ||
expect(handlerB).not.toHaveBeenCalled(); | ||
expect(handlerC).not.toHaveBeenCalled(); | ||
handlerA.mockClear(); | ||
|
||
writeToMockedStdin("b"); | ||
expect(handlerA).not.toHaveBeenCalled(); | ||
expect(handlerB).toHaveBeenCalled(); | ||
expect(handlerC).not.toHaveBeenCalled(); | ||
handlerB.mockClear(); | ||
|
||
writeToMockedStdin("c"); | ||
expect(handlerA).not.toHaveBeenCalled(); | ||
expect(handlerB).not.toHaveBeenCalled(); | ||
expect(handlerC).toHaveBeenCalled(); | ||
handlerC.mockClear(); | ||
}); | ||
|
||
it("handles CAPSLOCK", async () => { | ||
const handlerA = vi.fn(); | ||
const options = [ | ||
{ keys: ["a"], label: "first option", handler: handlerA }, | ||
]; | ||
|
||
registerHotKeys(options); | ||
|
||
writeToMockedStdin("a"); | ||
expect(handlerA).toHaveBeenCalled(); | ||
handlerA.mockClear(); | ||
|
||
writeToMockedStdin("A"); | ||
expect(handlerA).toHaveBeenCalled(); | ||
handlerA.mockClear(); | ||
}); | ||
|
||
it("ignores unbound keys", async () => { | ||
const handlerA = vi.fn(); | ||
const handlerD = vi.fn(); | ||
const options = [ | ||
{ keys: ["a"], label: "first option", handler: handlerA }, | ||
{ keys: ["d"], label: "disabled", disabled: true, handler: handlerD }, | ||
]; | ||
|
||
registerHotKeys(options); | ||
|
||
writeToMockedStdin("z"); | ||
expect(handlerA).not.toHaveBeenCalled(); | ||
|
||
writeToMockedStdin("d"); | ||
expect(handlerD).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("calls handler if any additional key bindings are pressed", async () => { | ||
const handlerA = vi.fn(); | ||
const options = [ | ||
{ keys: ["a", "b", "c"], label: "first option", handler: handlerA }, | ||
]; | ||
|
||
registerHotKeys(options); | ||
|
||
writeToMockedStdin("a"); | ||
expect(handlerA).toHaveBeenCalled(); | ||
handlerA.mockClear(); | ||
|
||
writeToMockedStdin("b"); | ||
expect(handlerA).toHaveBeenCalled(); | ||
handlerA.mockClear(); | ||
|
||
writeToMockedStdin("c"); | ||
expect(handlerA).toHaveBeenCalled(); | ||
handlerA.mockClear(); | ||
}); | ||
|
||
it("surfaces errors in handlers", async () => { | ||
const handlerA = vi.fn().mockImplementation(() => { | ||
throw new Error("sync error"); | ||
}); | ||
const handlerB = vi.fn().mockRejectedValue("async error"); | ||
const options = [ | ||
{ keys: ["a"], label: "first option", handler: handlerA }, | ||
{ keys: ["b"], label: "second option", handler: handlerB }, | ||
]; | ||
|
||
registerHotKeys(options); | ||
|
||
writeToMockedStdin("a"); | ||
expect(std.err).toMatchInlineSnapshot(` | ||
"[31mX [41;31m[[41;97mERROR[41;31m][0m [1mError while handling hotkey [a][0m | ||
" | ||
`); | ||
|
||
writeToMockedStdin("b"); | ||
await setTimeout(0); // | ||
expect(std.err).toMatchInlineSnapshot(` | ||
"[31mX [41;31m[[41;97mERROR[41;31m][0m [1mError while handling hotkey [a][0m | ||
[31mX [41;31m[[41;97mERROR[41;31m][0m [1mError while handling hotkey [b][0m | ||
" | ||
`); | ||
}); | ||
}); | ||
|
||
describe("instructions", () => { | ||
it("provides formatted instructions to Wrangler's & Miniflare's logger implementations", async () => { | ||
const handlerA = vi.fn(); | ||
const handlerB = vi.fn(); | ||
const handlerC = vi.fn(); | ||
const handlerD = vi.fn(); | ||
const options = [ | ||
{ keys: ["a"], label: "first option", handler: handlerA }, | ||
{ keys: ["b"], label: "second option", handler: handlerB }, | ||
{ keys: ["c"], label: () => "third option", handler: handlerC }, | ||
{ keys: ["d"], label: "disabled", disabled: true, handler: handlerD }, | ||
]; | ||
|
||
// should print instructions immediately | ||
const unregisterHotKeys = registerHotKeys(options); | ||
|
||
expect(std.out).toMatchInlineSnapshot(` | ||
"╭─────────────────────────────────────────────────────────╮ | ||
│ [a] first option, [b] second option, [c] third option │ | ||
╰─────────────────────────────────────────────────────────╯" | ||
`); | ||
|
||
logger.log("something 1"); | ||
|
||
expect(std.out).toMatchInlineSnapshot(` | ||
"╭─────────────────────────────────────────────────────────╮ | ||
│ [a] first option, [b] second option, [c] third option │ | ||
╰─────────────────────────────────────────────────────────╯ | ||
something 1 | ||
╭─────────────────────────────────────────────────────────╮ | ||
│ [a] first option, [b] second option, [c] third option │ | ||
╰─────────────────────────────────────────────────────────╯" | ||
`); | ||
|
||
unregisterHotKeys(); | ||
logger.log("something 2"); | ||
|
||
expect(std.out).toMatchInlineSnapshot(` | ||
"╭─────────────────────────────────────────────────────────╮ | ||
│ [a] first option, [b] second option, [c] third option │ | ||
╰─────────────────────────────────────────────────────────╯ | ||
something 1 | ||
╭─────────────────────────────────────────────────────────╮ | ||
│ [a] first option, [b] second option, [c] third option │ | ||
╰─────────────────────────────────────────────────────────╯ | ||
something 2" | ||
`); | ||
}); | ||
}); | ||
}); |
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
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
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
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
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
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
Oops, something went wrong.