Files
Netcatty/domain/customKeyBindings.test.ts
陈大猫 c30d872852 fix(settings): guard customKeyBindings sync against echo loop (closes #818) (#821)
* fix(settings): guard customKeyBindings cross-window sync against echo loop (closes #818)

customKeyBindings was the only synced setting whose two cross-window
handlers (DOM storage event + IPC onSettingsChanged) called
setCustomKeyBindings unconditionally. Every broadcast landed with a
fresh parsed object reference, so React re-rendered and the persist
effect re-broadcast, echoing across windows indefinitely.

While the echoes carry the same content, a rapid second click from
the user can arrive between the outbound broadcast and an older
in-flight echo — the echo's setState then clobbers the latest click
and the UI "bounces" from Disabled back to the original binding.
This matches the report in #818 (disable and reset operations
flicker between values when clicked in quick succession).

Fix: mirror the equality guards used by every other synced field.
Compare the incoming payload (stringified for objects) against the
current value from settingsSnapshotRef, and skip setCustomKeyBindings
when they match. Add customKeyBindings to settingsSnapshotRef so the
IPC handler has access without pulling it into the effect's closure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(settings): stop shortcut sync bounce flicker

* fix(settings): harden shortcut sync ordering

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:34:38 +08:00

141 lines
3.4 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
areCustomKeyBindingsEqual,
nextCustomKeyBindingsSyncVersion,
parseCustomKeyBindingsStorageRecord,
resetCustomKeyBinding,
serializeCustomKeyBindingsStorageRecord,
shouldApplyIncomingCustomKeyBindingsRecord,
updateCustomKeyBinding,
} from './customKeyBindings.ts';
test('parses legacy stored custom key bindings without sync metadata', () => {
const parsed = parseCustomKeyBindingsStorageRecord('{"open":{"mac":"Cmd+K"}}');
assert.deepEqual(parsed, {
version: 0,
origin: 'legacy',
bindings: {
open: { mac: 'Cmd+K' },
},
});
});
test('round-trips versioned stored custom key bindings', () => {
const raw = serializeCustomKeyBindingsStorageRecord({
version: 42,
origin: 'window-b',
bindings: {
open: { pc: 'Ctrl+K' },
},
});
assert.deepEqual(parseCustomKeyBindingsStorageRecord(raw), {
version: 42,
origin: 'window-b',
bindings: {
open: { pc: 'Ctrl+K' },
},
});
});
test('parses plain IPC custom key binding sync payloads', () => {
const parsed = parseCustomKeyBindingsStorageRecord({
version: 7,
origin: 'window-a',
bindings: {
open: { pc: 'Ctrl+K' },
},
});
assert.deepEqual(parsed, {
version: 7,
origin: 'window-a',
bindings: {
open: { pc: 'Ctrl+K' },
},
});
});
test('next sync version is monotonic even within the same millisecond', () => {
assert.equal(nextCustomKeyBindingsSyncVersion(100, 90), 101);
assert.equal(nextCustomKeyBindingsSyncVersion(100, 150), 150);
});
test('newer incoming records apply and older ones are ignored', () => {
assert.equal(
shouldApplyIncomingCustomKeyBindingsRecord(
{ version: 10, origin: 'window-a' },
{ version: 11, origin: 'window-b' },
),
true,
);
assert.equal(
shouldApplyIncomingCustomKeyBindingsRecord(
{ version: 10, origin: 'window-a' },
{ version: 10, origin: 'window-a' },
),
false,
);
assert.equal(
shouldApplyIncomingCustomKeyBindingsRecord(
{ version: 10, origin: 'window-b' },
{ version: 10, origin: 'window-a' },
),
false,
);
});
test('same-version updates converge by origin tie-breaker', () => {
assert.equal(
shouldApplyIncomingCustomKeyBindingsRecord(
{ version: 10, origin: 'window-a' },
{ version: 10, origin: 'window-b' },
),
true,
);
});
test('update custom key binding keeps other bindings intact', () => {
const prev = {
open: { mac: 'Cmd+K' },
close: { pc: 'Ctrl+W' },
};
const next = updateCustomKeyBinding(prev, 'open', 'pc', 'Ctrl+K');
assert.deepEqual(next, {
open: { mac: 'Cmd+K', pc: 'Ctrl+K' },
close: { pc: 'Ctrl+W' },
});
assert.equal(areCustomKeyBindingsEqual(prev, {
open: { mac: 'Cmd+K' },
close: { pc: 'Ctrl+W' },
}), true);
});
test('resetting one side of a shortcut does not mutate the previous bindings', () => {
const prev = {
open: { mac: 'Cmd+K', pc: 'Ctrl+K' },
};
const next = resetCustomKeyBinding(prev, 'open', 'mac');
assert.deepEqual(next, {
open: { pc: 'Ctrl+K' },
});
assert.deepEqual(prev, {
open: { mac: 'Cmd+K', pc: 'Ctrl+K' },
});
});
test('resetting the last side removes the binding entry entirely', () => {
const next = resetCustomKeyBinding({
open: { mac: 'Cmd+K' },
}, 'open', 'mac');
assert.deepEqual(next, {});
});