refactor(app): extract keybind settings controller (#39226)
This commit is contained in:
parent
87db7a7f0a
commit
9c8060d96d
2 changed files with 396 additions and 61 deletions
|
|
@ -30,6 +30,8 @@ type KeybindMeta = {
|
||||||
|
|
||||||
type KeybindMap = Record<string, string | undefined>
|
type KeybindMap = Record<string, string | undefined>
|
||||||
type CommandContext = ReturnType<typeof useCommand>
|
type CommandContext = ReturnType<typeof useCommand>
|
||||||
|
type LanguageContext = ReturnType<typeof useLanguage>
|
||||||
|
type SettingsContext = ReturnType<typeof useSettings>
|
||||||
|
|
||||||
const GROUPS: KeybindGroup[] = ["General", "Session", "Navigation", "Model and agent", "Terminal", "Prompt"]
|
const GROUPS: KeybindGroup[] = ["General", "Session", "Navigation", "Model and agent", "Terminal", "Prompt"]
|
||||||
|
|
||||||
|
|
@ -122,7 +124,7 @@ function keybinds(value: unknown): KeybindMap {
|
||||||
return value as KeybindMap
|
return value as KeybindMap
|
||||||
}
|
}
|
||||||
|
|
||||||
function listFor(command: CommandContext, map: KeybindMap, palette: string) {
|
function listFor(command: Pick<CommandContext, "catalog" | "options">, map: KeybindMap, palette: string) {
|
||||||
const out = new Map<string, KeybindMeta>()
|
const out = new Map<string, KeybindMeta>()
|
||||||
out.set(PALETTE_ID, { title: palette, group: "General" })
|
out.set(PALETTE_ID, { title: palette, group: "General" })
|
||||||
|
|
||||||
|
|
@ -262,7 +264,274 @@ function useKeyCapture(input: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createKeybindSettingsController(
|
||||||
|
input: {
|
||||||
|
command: Pick<CommandContext, "catalog" | "options" | "keybinds">
|
||||||
|
settings: {
|
||||||
|
current: { keybinds: unknown }
|
||||||
|
keybinds: Pick<SettingsContext["keybinds"], "get" | "set" | "resetAll">
|
||||||
|
}
|
||||||
|
target?: Document
|
||||||
|
notify?: (toast: { title: string; description: string }) => void
|
||||||
|
},
|
||||||
|
language: Pick<LanguageContext, "locale" | "t"> = useLanguage(),
|
||||||
|
) {
|
||||||
|
const [store, setStore] = createStore({ active: null as string | null })
|
||||||
|
const overrides = createMemo(() => keybinds(input.settings.current.keybinds))
|
||||||
|
const list = createMemo(() => {
|
||||||
|
language.locale()
|
||||||
|
return listFor(input.command, overrides(), language.t("command.palette"))
|
||||||
|
})
|
||||||
|
const grouped = createMemo(() => groupedFor(list()))
|
||||||
|
const title = (id: string) => list().get(id)?.title ?? ""
|
||||||
|
const effective = (id: string) => {
|
||||||
|
if (id === PALETTE_ID) return input.settings.keybinds.get(id) ?? DEFAULT_PALETTE_KEYBIND
|
||||||
|
|
||||||
|
const custom = input.settings.keybinds.get(id)
|
||||||
|
if (typeof custom === "string") return custom
|
||||||
|
|
||||||
|
const live = input.command.options.find((item) => item.id === id)
|
||||||
|
if (live?.keybind) return live.keybind
|
||||||
|
return input.command.catalog.find((item) => item.id === id)?.keybind
|
||||||
|
}
|
||||||
|
const used = createMemo(() => {
|
||||||
|
const value = new Map<string, { id: string; title: string }[]>()
|
||||||
|
|
||||||
|
for (const id of list().keys()) {
|
||||||
|
for (const signature of signatures(effective(id))) {
|
||||||
|
const items = value.get(signature)
|
||||||
|
if (items) {
|
||||||
|
items.push({ id, title: title(id) })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value.set(signature, [{ id, title: title(id) }])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
const stop = () => {
|
||||||
|
if (!store.active) return
|
||||||
|
setStore("active", null)
|
||||||
|
input.command.keybinds(true)
|
||||||
|
}
|
||||||
|
const toggle = (id: string) => {
|
||||||
|
if (store.active === id) {
|
||||||
|
stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (store.active) stop()
|
||||||
|
setStore("active", id)
|
||||||
|
input.command.keybinds(false)
|
||||||
|
}
|
||||||
|
const notify = input.notify ?? ((toast: { title: string; description: string }) => showToast(toast))
|
||||||
|
|
||||||
|
const handle = (event: KeyboardEvent) => {
|
||||||
|
const id = store.active
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
event.stopImmediatePropagation()
|
||||||
|
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear =
|
||||||
|
(event.key === "Backspace" || event.key === "Delete") &&
|
||||||
|
!event.ctrlKey &&
|
||||||
|
!event.metaKey &&
|
||||||
|
!event.altKey &&
|
||||||
|
!event.shiftKey
|
||||||
|
if (clear) {
|
||||||
|
input.settings.keybinds.set(id, "none")
|
||||||
|
stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = recordKeybind(event)
|
||||||
|
if (!next) return
|
||||||
|
|
||||||
|
const conflicts = new Map<string, string>()
|
||||||
|
for (const signature of signatures(next)) {
|
||||||
|
for (const item of used().get(signature) ?? []) {
|
||||||
|
if (item.id === id) continue
|
||||||
|
conflicts.set(item.id, item.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conflicts.size > 0) {
|
||||||
|
notify({
|
||||||
|
title: language.t("settings.shortcuts.conflict.title"),
|
||||||
|
description: language.t("settings.shortcuts.conflict.description", {
|
||||||
|
keybind: formatKeybind(next, language.t),
|
||||||
|
titles: [...conflicts.values()].join(", "),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
input.settings.keybinds.set(id, next)
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = input.target ?? (typeof document === "object" ? document : undefined)
|
||||||
|
if (target) makeEventListener(target, "keydown", handle, { capture: true })
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (store.active) input.command.keybinds(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
catalog: {
|
||||||
|
groups: GROUPS,
|
||||||
|
filtered: (query: string) =>
|
||||||
|
filteredFor(query, list(), grouped(), (id) => formatKeybind(effective(id) ?? "", language.t)),
|
||||||
|
title,
|
||||||
|
keybind: (id: string) => formatKeybind(effective(id) ?? "", language.t),
|
||||||
|
},
|
||||||
|
capture: {
|
||||||
|
active: () => store.active,
|
||||||
|
toggle,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
hasOverrides: () => Object.values(overrides()).some((value) => typeof value === "string"),
|
||||||
|
reset: () => {
|
||||||
|
stop()
|
||||||
|
input.settings.keybinds.resetAll()
|
||||||
|
notify({
|
||||||
|
title: language.t("settings.shortcuts.reset.toast.title"),
|
||||||
|
description: language.t("settings.shortcuts.reset.toast.description"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsKeybindsV2() {
|
||||||
|
const command = useCommand()
|
||||||
|
const settings = useSettings()
|
||||||
|
const controller = createKeybindSettingsController({
|
||||||
|
command,
|
||||||
|
settings,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsKeybindsV2View
|
||||||
|
groups={controller.catalog.groups}
|
||||||
|
filtered={controller.catalog.filtered}
|
||||||
|
title={controller.catalog.title}
|
||||||
|
keybind={controller.catalog.keybind}
|
||||||
|
active={controller.capture.active}
|
||||||
|
onCapture={controller.capture.toggle}
|
||||||
|
hasOverrides={controller.settings.hasOverrides}
|
||||||
|
onReset={controller.settings.reset}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsKeybindsV2View(props: {
|
||||||
|
groups: KeybindGroup[]
|
||||||
|
filtered: (query: string) => Map<KeybindGroup, string[]>
|
||||||
|
title: (id: string) => string
|
||||||
|
keybind: (id: string) => string
|
||||||
|
active: () => string | null
|
||||||
|
onCapture: (id: string) => void
|
||||||
|
hasOverrides: () => boolean
|
||||||
|
onReset: () => void
|
||||||
|
}) {
|
||||||
|
const language = useLanguage()
|
||||||
|
const [store, setStore] = createStore({ filter: "" })
|
||||||
|
const filtered = createMemo(() => props.filtered(store.filter))
|
||||||
|
const hasResults = createMemo(() => props.groups.some((group) => (filtered().get(group)?.length ?? 0) > 0))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="settings-v2-tab-header settings-v2-tab-header--stacked">
|
||||||
|
<div class="settings-v2-tab-header-row">
|
||||||
|
<h2 class="settings-v2-tab-title">{language.t("settings.shortcuts.title")}</h2>
|
||||||
|
<ButtonV2 variant="ghost" onClick={props.onReset} disabled={!props.hasOverrides()}>
|
||||||
|
{language.t("settings.shortcuts.reset.button")}
|
||||||
|
</ButtonV2>
|
||||||
|
</div>
|
||||||
|
<div class="settings-v2-tab-search">
|
||||||
|
<TextInputV2
|
||||||
|
type="search"
|
||||||
|
appearance="base"
|
||||||
|
value={store.filter}
|
||||||
|
onInput={(event) => setStore("filter", event.currentTarget.value)}
|
||||||
|
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
||||||
|
spellcheck={false}
|
||||||
|
autocorrect="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
aria-label={language.t("settings.shortcuts.search.placeholder")}
|
||||||
|
/>
|
||||||
|
<Show when={store.filter}>
|
||||||
|
<IconButtonV2
|
||||||
|
type="button"
|
||||||
|
variant="ghost-muted"
|
||||||
|
size="small"
|
||||||
|
class="settings-v2-tab-search-clear"
|
||||||
|
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
|
||||||
|
onClick={() => setStore("filter", "")}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-v2-tab-body">
|
||||||
|
<div class="settings-v2-shortcuts flex flex-col gap-8">
|
||||||
|
<For each={props.groups}>
|
||||||
|
{(group) => (
|
||||||
|
<Show when={(filtered().get(group) ?? []).length > 0}>
|
||||||
|
<div class="settings-v2-section">
|
||||||
|
<h3 class="settings-v2-section-title">{language.t(groupKey[group])}</h3>
|
||||||
|
<SettingsListV2>
|
||||||
|
<For each={filtered().get(group) ?? []}>
|
||||||
|
{(id) => (
|
||||||
|
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||||
|
<span>{props.title(id)}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-keybind-id={id}
|
||||||
|
classList={{
|
||||||
|
"settings-v2-keybind-button": true,
|
||||||
|
"settings-v2-keybind-button--active": props.active() === id,
|
||||||
|
}}
|
||||||
|
onClick={() => props.onCapture(id)}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={props.active() === id}
|
||||||
|
fallback={props.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
||||||
|
>
|
||||||
|
{language.t("settings.shortcuts.pressKeys")}
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</SettingsListV2>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<Show when={store.filter && !hasResults()}>
|
||||||
|
<div class="settings-v2-shortcuts-status">
|
||||||
|
<span>{language.t("settings.shortcuts.search.empty")}</span>
|
||||||
|
<span class="settings-v2-shortcuts-status-filter">"{store.filter}"</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const SettingsKeybinds: Component<{ v2?: boolean }> = (props) => {
|
export const SettingsKeybinds: Component<{ v2?: boolean }> = (props) => {
|
||||||
|
if (props.v2) return <SettingsKeybindsV2 />
|
||||||
|
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
@ -476,78 +745,37 @@ export const SettingsKeybinds: Component<{ v2?: boolean }> = (props) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
when={props.v2}
|
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||||
fallback={
|
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
||||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.shortcuts.title")}</h2>
|
||||||
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
<Button size="small" variant="secondary" onClick={resetAll} disabled={!hasOverrides()}>
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.shortcuts.title")}</h2>
|
|
||||||
<Button size="small" variant="secondary" onClick={resetAll} disabled={!hasOverrides()}>
|
|
||||||
{language.t("settings.shortcuts.reset.button")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
|
|
||||||
<Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
|
|
||||||
<TextField
|
|
||||||
variant="ghost"
|
|
||||||
type="text"
|
|
||||||
value={store.filter}
|
|
||||||
onChange={(v) => setStore("filter", v)}
|
|
||||||
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
|
||||||
spellcheck={false}
|
|
||||||
autocorrect="off"
|
|
||||||
autocomplete="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
class="flex-1"
|
|
||||||
/>
|
|
||||||
<Show when={store.filter}>
|
|
||||||
<IconButton icon="circle-x" variant="ghost" onClick={() => setStore("filter", "")} />
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{groups}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
<div class="settings-v2-tab-header settings-v2-tab-header--stacked">
|
|
||||||
<div class="settings-v2-tab-header-row">
|
|
||||||
<h2 class="settings-v2-tab-title">{language.t("settings.shortcuts.title")}</h2>
|
|
||||||
<ButtonV2 variant="ghost" onClick={resetAll} disabled={!hasOverrides()}>
|
|
||||||
{language.t("settings.shortcuts.reset.button")}
|
{language.t("settings.shortcuts.reset.button")}
|
||||||
</ButtonV2>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-v2-tab-search">
|
|
||||||
<TextInputV2
|
<div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
|
||||||
type="search"
|
<Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
|
||||||
appearance="base"
|
<TextField
|
||||||
|
variant="ghost"
|
||||||
|
type="text"
|
||||||
value={store.filter}
|
value={store.filter}
|
||||||
onInput={(event) => setStore("filter", event.currentTarget.value)}
|
onChange={(v) => setStore("filter", v)}
|
||||||
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
aria-label={language.t("settings.shortcuts.search.placeholder")}
|
class="flex-1"
|
||||||
/>
|
/>
|
||||||
<Show when={store.filter}>
|
<Show when={store.filter}>
|
||||||
<IconButtonV2
|
<IconButton icon="circle-x" variant="ghost" onClick={() => setStore("filter", "")} />
|
||||||
type="button"
|
|
||||||
variant="ghost-muted"
|
|
||||||
size="small"
|
|
||||||
class="settings-v2-tab-search-clear"
|
|
||||||
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
|
|
||||||
onClick={() => setStore("filter", "")}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-v2-tab-body">{groups}</div>
|
</div>
|
||||||
</>
|
{groups}
|
||||||
</Show>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
107
packages/app/test-browser/settings-keybinds.test.ts
Normal file
107
packages/app/test-browser/settings-keybinds.test.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { createRoot } from "solid-js"
|
||||||
|
import { createKeybindSettingsController } from "../src/components/settings-keybinds"
|
||||||
|
|
||||||
|
function setup(overrides: Record<string, string> = {}) {
|
||||||
|
const changes: [string, string][] = []
|
||||||
|
const suppression: boolean[] = []
|
||||||
|
const notifications: { title: string; description?: string }[] = []
|
||||||
|
let resets = 0
|
||||||
|
let controller: ReturnType<typeof createKeybindSettingsController>
|
||||||
|
|
||||||
|
const dispose = createRoot((dispose) => {
|
||||||
|
controller = createKeybindSettingsController(
|
||||||
|
{
|
||||||
|
command: {
|
||||||
|
catalog: [
|
||||||
|
{ id: "session.alpha", title: "Alpha", keybind: "mod+a" },
|
||||||
|
{ id: "session.beta", title: "Beta", keybind: "mod+b" },
|
||||||
|
],
|
||||||
|
options: [],
|
||||||
|
keybinds: (enabled) => suppression.push(enabled),
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
current: { keybinds: overrides },
|
||||||
|
keybinds: {
|
||||||
|
get: (id) => overrides[id],
|
||||||
|
set: (id, value) => {
|
||||||
|
overrides[id] = value
|
||||||
|
changes.push([id, value])
|
||||||
|
},
|
||||||
|
resetAll: () => {
|
||||||
|
resets++
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
notify: (toast) => notifications.push(toast),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
locale: () => "en",
|
||||||
|
t: (key, params) => {
|
||||||
|
if (params) return `${key}:${Object.values(params).join("|")}`
|
||||||
|
if (key === "common.key.alt") return "Alt"
|
||||||
|
return String(key)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return dispose
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
controller: controller!,
|
||||||
|
changes,
|
||||||
|
suppression,
|
||||||
|
notifications,
|
||||||
|
resets: () => resets,
|
||||||
|
dispose,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function modKey(key: string) {
|
||||||
|
const mac = /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||||
|
return new KeyboardEvent("keydown", { key, ctrlKey: !mac, metaKey: mac, bubbles: true, cancelable: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("keybind settings controller", () => {
|
||||||
|
test("derives the catalog, effective bindings, and filtered groups", () => {
|
||||||
|
const state = setup({ "session.beta": "alt+k" })
|
||||||
|
|
||||||
|
expect(state.controller.catalog.title("session.alpha")).toBe("Alpha")
|
||||||
|
expect(state.controller.catalog.keybind("session.beta")).toBe("Alt+K")
|
||||||
|
expect(state.controller.catalog.filtered("alt k").get("Session")).toEqual(["session.beta"])
|
||||||
|
expect(state.controller.settings.hasOverrides()).toBe(true)
|
||||||
|
|
||||||
|
state.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("captures bindings, rejects conflicts, and restores command handling", () => {
|
||||||
|
const state = setup()
|
||||||
|
|
||||||
|
state.controller.capture.toggle("session.beta")
|
||||||
|
document.dispatchEvent(modKey("a"))
|
||||||
|
expect(state.changes).toEqual([])
|
||||||
|
expect(state.notifications).toHaveLength(1)
|
||||||
|
expect(state.controller.capture.active()).toBe("session.beta")
|
||||||
|
|
||||||
|
document.dispatchEvent(modKey("x"))
|
||||||
|
expect(state.changes).toEqual([["session.beta", "mod+x"]])
|
||||||
|
expect(state.suppression).toEqual([false, true])
|
||||||
|
expect(state.controller.capture.active()).toBeNull()
|
||||||
|
|
||||||
|
state.controller.capture.toggle("session.alpha")
|
||||||
|
state.dispose()
|
||||||
|
expect(state.suppression).toEqual([false, true, false, true])
|
||||||
|
document.dispatchEvent(modKey("z"))
|
||||||
|
expect(state.changes).toEqual([["session.beta", "mod+x"]])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("resets persisted overrides and reports success", () => {
|
||||||
|
const state = setup({ "session.alpha": "none" })
|
||||||
|
|
||||||
|
state.controller.settings.reset()
|
||||||
|
expect(state.resets()).toBe(1)
|
||||||
|
expect(state.notifications[0]?.title).toBe("settings.shortcuts.reset.toast.title")
|
||||||
|
|
||||||
|
state.dispose()
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue