Takes the flat module count from 66 to 42, continuing what admin/ and
calendar/ started.
lib/mailbox/ archiveDate, emptyFolder, folderMove, labelTree,
mailboxName, mailboxRoute
lib/sieve/ sieve, sieveApply, sieveFolders
lib/input/ keyboard, swipe, touch, listSelection, dropUpload
lib/notify/ notify, webpush, webpushEnable
lib/sw/ swCache, swFacts, staleBuild
lib/text/ html, markdown, text, emlName
FOUR THINGS THE FILENAMES GET WRONG, each checked by reading the file
rather than trusting what it is called:
- appFolder is not a mailbox. It is the `ihasmail` folder in JMAP
*Files*, where the client keeps signature images and synced settings.
It stays flat.
- format holds no formatting of text. It re-exports the date and clock
formatters, so it belongs with dates/datetime, not with text/.
- preview is the file viewer deciding what it can show without
downloading, and source is where to point someone asking for this
instance's AGPL source. Neither is about text.
- notify is not Web Push. It is the tab title, the favicon badge and
the new-mail sound -- in-app notification, which is why it sits with
webpush rather than under sw/ with the service worker's own concerns.
threadScroll stays flat too: it decides where a conversation opens, which
is view state rather than a gesture, and input/ is honest only if
everything in it interprets something the reader did.
No behavior change. Almost every reference was on the @/ alias; eight
relative imports in files that did not move, or that moved away from a
sibling, needed rewriting by hand.
102 lines
3.3 KiB
TypeScript
102 lines
3.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { keyboard } from "@/lib/input/keyboard";
|
|
|
|
/*
|
|
* Two-key sequences against the single keys they start with.
|
|
*
|
|
* "Go to folder" is `g o` while `o` on its own opens a conversation (#233), so
|
|
* the whole feature rests on a pending prefix being tried before a bare key.
|
|
* That was true when it was written and nothing said so out loud, which is the
|
|
* kind of thing a later refactor quietly reverses.
|
|
*/
|
|
|
|
const press = (key: string) => {
|
|
const e = new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true });
|
|
window.dispatchEvent(e);
|
|
return e;
|
|
};
|
|
|
|
let pop: (() => void) | null = null;
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
pop?.();
|
|
pop = null;
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("a sequence sharing its second key with a single binding", () => {
|
|
it("runs the sequence, not the single key", () => {
|
|
const seq = vi.fn();
|
|
const single = vi.fn();
|
|
pop = keyboard.pushScope("t", [
|
|
{ keys: "g o", description: "Go to folder", group: "Navigation", handler: seq },
|
|
{ keys: "o", description: "Open", group: "Mail", handler: single },
|
|
]);
|
|
press("g");
|
|
press("o");
|
|
expect(seq).toHaveBeenCalledOnce();
|
|
expect(single).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("runs the single key when no prefix is pending", () => {
|
|
const seq = vi.fn();
|
|
const single = vi.fn();
|
|
pop = keyboard.pushScope("t", [
|
|
{ keys: "g o", description: "Go to folder", group: "Navigation", handler: seq },
|
|
{ keys: "o", description: "Open", group: "Mail", handler: single },
|
|
]);
|
|
press("o");
|
|
expect(single).toHaveBeenCalledOnce();
|
|
expect(seq).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("forgets the prefix after a pause, so a later key means itself again", () => {
|
|
const seq = vi.fn();
|
|
const single = vi.fn();
|
|
pop = keyboard.pushScope("t", [
|
|
{ keys: "g o", description: "Go to folder", group: "Navigation", handler: seq },
|
|
{ keys: "o", description: "Open", group: "Mail", handler: single },
|
|
]);
|
|
press("g");
|
|
vi.advanceTimersByTime(2000);
|
|
press("o");
|
|
expect(seq).not.toHaveBeenCalled();
|
|
expect(single).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("swallows the prefix rather than letting it act on its own", () => {
|
|
// `g` is not a binding by itself; pressing it must not fall through to
|
|
// anything, or holding it would type into the page.
|
|
const seq = vi.fn();
|
|
pop = keyboard.pushScope("t", [
|
|
{ keys: "g o", description: "Go to folder", group: "Navigation", handler: seq },
|
|
]);
|
|
const e = press("g");
|
|
expect(e.defaultPrevented).toBe(true);
|
|
expect(seq).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("lets a key that completes no sequence still act as itself", () => {
|
|
/*
|
|
* `g` then `z`, where `g z` is nothing. The prefix is dropped and `z` runs
|
|
* on that same press rather than being eaten — so a mistyped prefix costs
|
|
* the prefix and not the keystroke after it.
|
|
*/
|
|
const seq = vi.fn();
|
|
const single = vi.fn();
|
|
pop = keyboard.pushScope("t", [
|
|
{ keys: "g o", description: "Go to folder", group: "Navigation", handler: seq },
|
|
{ keys: "z", description: "Zed", group: "Mail", handler: single },
|
|
]);
|
|
press("g");
|
|
press("z");
|
|
expect(seq).not.toHaveBeenCalled();
|
|
expect(single).toHaveBeenCalledOnce();
|
|
});
|
|
});
|