chore(release): prepare v0.12.0

This commit is contained in:
2026-04-11 21:54:00 -07:00
parent 52bab1d611
commit 7ac51cd5e9
56 changed files with 466 additions and 296 deletions
+4 -1
View File
@@ -364,7 +364,10 @@ test('isYomitanPopupVisible requires visible iframe geometry', () => {
const root = {
querySelectorAll: (value: string) => {
selectors.push(value);
if (value === YOMITAN_POPUP_VISIBLE_HOST_SELECTOR || value === YOMITAN_POPUP_HOST_SELECTOR) {
if (
value === YOMITAN_POPUP_VISIBLE_HOST_SELECTOR ||
value === YOMITAN_POPUP_HOST_SELECTOR
) {
return [];
}
return [hiddenFrame, visibleFrame];
+3 -1
View File
@@ -1063,7 +1063,9 @@ test('session binding: Ctrl+Alt+S dispatches subsync action locally', async () =
testGlobals.dispatchKeydown({ key: 's', code: 'KeyS', ctrlKey: true, altKey: true });
assert.deepEqual(testGlobals.sessionActions, [{ actionId: 'triggerSubsync', payload: undefined }]);
assert.deepEqual(testGlobals.sessionActions, [
{ actionId: 'triggerSubsync', payload: undefined },
]);
} finally {
testGlobals.restore();
}
+4 -6
View File
@@ -39,12 +39,10 @@ export function createKeyboardHandlers(
let pendingLookupRefreshAfterSubtitleSeek = false;
let resetSelectionToStartOnNextSubtitleSync = false;
let lookupScanFallbackTimer: ReturnType<typeof setTimeout> | null = null;
let pendingNumericSelection:
| {
actionId: 'copySubtitleMultiple' | 'mineSentenceMultiple';
timeout: ReturnType<typeof setTimeout> | null;
}
| null = null;
let pendingNumericSelection: {
actionId: 'copySubtitleMultiple' | 'mineSentenceMultiple';
timeout: ReturnType<typeof setTimeout> | null;
} | null = null;
const CHORD_MAP = new Map<
string,
+7 -5
View File
@@ -73,11 +73,13 @@ export function createMouseHandlers(
syncOverlayMouseIgnoreState(ctx);
}
function reconcilePopupInteraction(args: {
assumeVisible?: boolean;
reclaimFocus?: boolean;
allowPause?: boolean;
} = {}): boolean {
function reconcilePopupInteraction(
args: {
assumeVisible?: boolean;
reclaimFocus?: boolean;
allowPause?: boolean;
} = {},
): boolean {
const popupVisible = syncPopupVisibilityState(args.assumeVisible === true);
if (!popupVisible) {
syncOverlayMouseIgnoreState(ctx);
+41 -35
View File
@@ -168,48 +168,54 @@ function withRuntimeOptionsModal(
test('openRuntimeOptionsModal shows loading shell before runtime options resolve', async () => {
const deferred = createDeferred<RuntimeOptionState[]>();
await withRuntimeOptionsModal(() => deferred.promise, async (input) => {
input.modal.openRuntimeOptionsModal();
await withRuntimeOptionsModal(
() => deferred.promise,
async (input) => {
input.modal.openRuntimeOptionsModal();
assert.equal(input.state.runtimeOptionsModalOpen, true);
assert.equal(input.overlayClassList.contains('interactive'), true);
assert.equal(input.modalClassList.contains('hidden'), false);
assert.equal(input.statusNode.textContent, 'Loading runtime options...');
assert.deepEqual(input.syncCalls, ['sync']);
assert.equal(input.state.runtimeOptionsModalOpen, true);
assert.equal(input.overlayClassList.contains('interactive'), true);
assert.equal(input.modalClassList.contains('hidden'), false);
assert.equal(input.statusNode.textContent, 'Loading runtime options...');
assert.deepEqual(input.syncCalls, ['sync']);
deferred.resolve([
{
id: 'anki.autoUpdateNewCards',
label: 'Auto-update new cards',
scope: 'ankiConnect',
valueType: 'boolean',
value: true,
allowedValues: [true, false],
requiresRestart: false,
},
]);
await flushAsyncWork();
deferred.resolve([
{
id: 'anki.autoUpdateNewCards',
label: 'Auto-update new cards',
scope: 'ankiConnect',
valueType: 'boolean',
value: true,
allowedValues: [true, false],
requiresRestart: false,
},
]);
await flushAsyncWork();
assert.equal(
input.statusNode.textContent,
'Use arrow keys. Click value to cycle. Enter or double-click to apply.',
);
assert.equal(input.statusNode.classList.contains('error'), false);
});
assert.equal(
input.statusNode.textContent,
'Use arrow keys. Click value to cycle. Enter or double-click to apply.',
);
assert.equal(input.statusNode.classList.contains('error'), false);
},
);
});
test('openRuntimeOptionsModal keeps modal visible when loading fails', async () => {
const deferred = createDeferred<RuntimeOptionState[]>();
await withRuntimeOptionsModal(() => deferred.promise, async (input) => {
input.modal.openRuntimeOptionsModal();
deferred.reject(new Error('boom'));
await flushAsyncWork();
await withRuntimeOptionsModal(
() => deferred.promise,
async (input) => {
input.modal.openRuntimeOptionsModal();
deferred.reject(new Error('boom'));
await flushAsyncWork();
assert.equal(input.state.runtimeOptionsModalOpen, true);
assert.equal(input.overlayClassList.contains('interactive'), true);
assert.equal(input.modalClassList.contains('hidden'), false);
assert.equal(input.statusNode.textContent, 'Failed to load runtime options');
assert.equal(input.statusNode.classList.contains('error'), true);
});
assert.equal(input.state.runtimeOptionsModalOpen, true);
assert.equal(input.overlayClassList.contains('interactive'), true);
assert.equal(input.modalClassList.contains('hidden'), false);
assert.equal(input.statusNode.textContent, 'Failed to load runtime options');
assert.equal(input.statusNode.classList.contains('error'), true);
},
);
});
+2 -1
View File
@@ -130,7 +130,8 @@ test('visible yomitan popup host keeps overlay interactive even when cached popu
},
document: {
querySelectorAll: (selector: string) =>
selector === '[data-subminer-yomitan-popup-host="true"][data-subminer-yomitan-popup-visible="true"]'
selector ===
'[data-subminer-yomitan-popup-host="true"][data-subminer-yomitan-popup-visible="true"]'
? [{ getAttribute: () => 'true' }]
: [],
},
+4 -1
View File
@@ -73,7 +73,10 @@ function queryPopupElements<T extends Element>(
}
export function isYomitanPopupVisible(root: ParentNode | null | undefined = document): boolean {
const visiblePopupHosts = queryPopupElements<HTMLElement>(root, YOMITAN_POPUP_VISIBLE_HOST_SELECTOR);
const visiblePopupHosts = queryPopupElements<HTMLElement>(
root,
YOMITAN_POPUP_VISIBLE_HOST_SELECTOR,
);
if (visiblePopupHosts.length > 0) {
return true;
}