mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
fix(desktop): release stale channel autocomplete before edit submission
Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, afterEach, before, test } from "node:test";
|
||||
import { JSDOM } from "jsdom";
|
||||
import * as React from "react";
|
||||
|
||||
import { ChannelNavigationProvider } from "@/shared/context/ChannelNavigationContext";
|
||||
import { useChannelLinks } from "./useChannelLinks.ts";
|
||||
|
||||
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
|
||||
url: "http://localhost",
|
||||
});
|
||||
|
||||
before(() => {
|
||||
Object.assign(globalThis, {
|
||||
document: dom.window.document,
|
||||
HTMLElement: dom.window.HTMLElement,
|
||||
IS_REACT_ACT_ENVIRONMENT: true,
|
||||
window: dom.window,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const { cleanup } = await import("@testing-library/react");
|
||||
cleanup();
|
||||
});
|
||||
after(() => dom.window.close());
|
||||
|
||||
const channels = [
|
||||
{ id: "general", name: "general", channelType: "stream", archivedAt: null },
|
||||
];
|
||||
|
||||
function wrapper({ children }) {
|
||||
return React.createElement(ChannelNavigationProvider, { channels }, children);
|
||||
}
|
||||
|
||||
function enterEvent() {
|
||||
return {
|
||||
key: "Enter",
|
||||
defaultPrevented: false,
|
||||
preventDefault() {
|
||||
this.defaultPrevented = true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
for (const [name, text, cursor] of [
|
||||
["replacing the loaded edit", "Edited, not deleted", 19],
|
||||
["clearing the loaded edit", "", 0],
|
||||
["moving before the channel reference", "Welcome to #general", 0],
|
||||
]) {
|
||||
test(`${name} releases Enter before the debounce expires`, async (t) => {
|
||||
const { act, renderHook } = await import("@testing-library/react");
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { result, unmount } = renderHook(useChannelLinks, { wrapper });
|
||||
act(() => result.current.updateChannelQuery("Welcome to #general", 19));
|
||||
act(() => t.mock.timers.tick(120));
|
||||
assert.equal(result.current.isChannelOpen, true);
|
||||
|
||||
act(() => result.current.updateChannelQuery(text, cursor));
|
||||
assert.equal(result.current.isChannelOpen, false);
|
||||
const event = enterEvent();
|
||||
assert.deepEqual(result.current.handleChannelKeyDown(event), {
|
||||
handled: false,
|
||||
});
|
||||
assert.equal(event.defaultPrevented, false);
|
||||
act(() => t.mock.timers.tick(120));
|
||||
assert.equal(result.current.isChannelOpen, false);
|
||||
unmount();
|
||||
});
|
||||
}
|
||||
|
||||
test("clearing a pending query cannot reopen channel completion", async (t) => {
|
||||
const { act, renderHook } = await import("@testing-library/react");
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { result, unmount } = renderHook(useChannelLinks, { wrapper });
|
||||
act(() => result.current.updateChannelQuery("#gen", 4));
|
||||
act(() => result.current.updateChannelQuery("", 0));
|
||||
act(() => t.mock.timers.tick(120));
|
||||
assert.equal(result.current.isChannelOpen, false);
|
||||
unmount();
|
||||
});
|
||||
|
||||
test("valid channel completion still debounces and consumes Enter", async (t) => {
|
||||
const { act, renderHook } = await import("@testing-library/react");
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { result, unmount } = renderHook(useChannelLinks, { wrapper });
|
||||
act(() => result.current.updateChannelQuery("See #gen", 8));
|
||||
act(() => t.mock.timers.tick(119));
|
||||
assert.equal(result.current.isChannelOpen, false);
|
||||
act(() => t.mock.timers.tick(1));
|
||||
assert.equal(result.current.isChannelOpen, true);
|
||||
const event = enterEvent();
|
||||
const selection = result.current.handleChannelKeyDown(event);
|
||||
assert.equal(selection.handled, true);
|
||||
assert.equal(event.defaultPrevented, true);
|
||||
let edit;
|
||||
act(() => {
|
||||
edit = result.current.insertChannel(selection.suggestion, 8);
|
||||
});
|
||||
assert.deepEqual(edit, {
|
||||
replaceFromOffset: 4,
|
||||
replaceToOffset: 8,
|
||||
insertText: "#general ",
|
||||
});
|
||||
assert.equal(result.current.isChannelOpen, false);
|
||||
unmount();
|
||||
});
|
||||
@@ -122,6 +122,22 @@ export function useChannelLinks() {
|
||||
|
||||
if (debounceTimerRef.current !== null) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
debounceTimerRef.current = null;
|
||||
}
|
||||
|
||||
// Losing the #query must close immediately. Debouncing dismissal leaves
|
||||
// the old suggestion owning Enter after the user replaces/clears an edit.
|
||||
if (
|
||||
!detectPrefixQuery(
|
||||
"#",
|
||||
value,
|
||||
cursorPosition,
|
||||
knownNamesLowerRef.current,
|
||||
)
|
||||
) {
|
||||
setChannelQuery(null);
|
||||
setChannelSelectedIndex(0);
|
||||
return;
|
||||
}
|
||||
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
|
||||
Reference in New Issue
Block a user