* 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>
141 lines
3.4 KiB
TypeScript
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, {});
|
|
});
|