diff --git a/package.json b/package.json index 957b6695..7af57ee8 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,8 @@ "test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua", "test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts", "test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src", - "test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/settings/settings-model.test.ts src/settings/settings-field-layout.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/ipc.test.ts src/core/services/anki-jimaku-ipc.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/main/runtime/autoplay-tokenization-warm-release.test.ts src/main/runtime/autoplay-subtitle-primer.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts stats/src/hooks/useExcludedWords.test.ts", - "test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/settings/settings-model.test.js dist/settings/settings-field-layout.test.js dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/main/runtime/autoplay-tokenization-warm-release.test.js dist/main/runtime/autoplay-subtitle-primer.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js", + "test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/settings/settings-model.test.ts src/settings/settings-field-layout.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/ipc.test.ts src/core/services/anki-jimaku-ipc.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/main/runtime/autoplay-tokenization-warm-release.test.ts src/main/runtime/autoplay-subtitle-primer.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/subtitle-render-word-class.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts stats/src/hooks/useExcludedWords.test.ts", + "test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/settings/settings-model.test.js dist/settings/settings-field-layout.test.js dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/main/runtime/autoplay-tokenization-warm-release.test.js dist/main/runtime/autoplay-subtitle-primer.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/subtitle-render-word-class.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js", "test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js", "test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist", "test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts", diff --git a/src/renderer/subtitle-render-test-helpers.ts b/src/renderer/subtitle-render-test-helpers.ts new file mode 100644 index 00000000..8f7818da --- /dev/null +++ b/src/renderer/subtitle-render-test-helpers.ts @@ -0,0 +1,17 @@ +import type { MergedToken } from '../types'; +import { PartOfSpeech } from '../types.js'; + +export function createToken(overrides: Partial): MergedToken { + return { + surface: '', + reading: '', + headword: '', + startPos: 0, + endPos: 0, + partOfSpeech: PartOfSpeech.other, + isMerged: true, + isKnown: false, + isNPlusOneTarget: false, + ...overrides, + }; +} diff --git a/src/renderer/subtitle-render-word-class.test.ts b/src/renderer/subtitle-render-word-class.test.ts new file mode 100644 index 00000000..ece64e53 --- /dev/null +++ b/src/renderer/subtitle-render-word-class.test.ts @@ -0,0 +1,206 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import type { MergedToken } from '../types'; +import { computeWordClass } from './subtitle-render.js'; +import { createToken } from './subtitle-render-test-helpers.js'; + +test('computeWordClass preserves known and n+1 classes while adding JLPT classes', () => { + const knownJlpt = createToken({ + isKnown: true, + jlptLevel: 'N1', + surface: '猫', + }); + const nPlusOneJlpt = createToken({ + isNPlusOneTarget: true, + jlptLevel: 'N2', + surface: '犬', + }); + + assert.equal(computeWordClass(knownJlpt), 'word word-known word-jlpt-n1'); + assert.equal(computeWordClass(nPlusOneJlpt), 'word word-n-plus-one word-jlpt-n2'); +}); + +test('computeWordClass applies name-match class ahead of known, n+1, frequency, and JLPT classes when enabled', () => { + const token = createToken({ + isKnown: true, + isNPlusOneTarget: true, + jlptLevel: 'N2', + frequencyRank: 10, + surface: 'アクア', + }) as MergedToken & { isNameMatch?: boolean }; + token.isNameMatch = true; + + assert.equal( + computeWordClass(token, { + nameMatchEnabled: true, + enabled: true, + topX: 100, + mode: 'single', + singleColor: '#000000', + bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, + }), + 'word word-name-match', + ); +}); + +test('computeWordClass skips name-match class by default', () => { + const token = createToken({ + surface: 'アクア', + }) as MergedToken & { isNameMatch?: boolean }; + token.isNameMatch = true; + + assert.equal(computeWordClass(token), 'word'); +}); + +test('computeWordClass skips name-match class when disabled', () => { + const token = createToken({ + surface: 'アクア', + }) as MergedToken & { isNameMatch?: boolean }; + token.isNameMatch = true; + + assert.equal( + computeWordClass(token, { + nameMatchEnabled: false, + enabled: true, + topX: 100, + mode: 'single', + singleColor: '#000000', + bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, + }), + 'word', + ); +}); + +test('computeWordClass keeps known and N+1 color classes exclusive over frequency classes', () => { + const known = createToken({ + isKnown: true, + frequencyRank: 10, + surface: '既知', + }); + const nPlusOne = createToken({ + isNPlusOneTarget: true, + frequencyRank: 10, + surface: '目標', + }); + const frequency = createToken({ + frequencyRank: 10, + surface: '頻度', + }); + + assert.equal( + computeWordClass(known, { + enabled: true, + topX: 100, + mode: 'single', + singleColor: '#000000', + bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, + }), + 'word word-known', + ); + assert.equal( + computeWordClass(nPlusOne, { + enabled: true, + topX: 100, + mode: 'single', + singleColor: '#000000', + bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, + }), + 'word word-n-plus-one', + ); + assert.equal( + computeWordClass(frequency, { + enabled: true, + topX: 100, + mode: 'single', + singleColor: '#000000', + bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, + }), + 'word word-frequency-single', + ); +}); + +test('computeWordClass adds frequency class for single mode when rank is within topX', () => { + const token = createToken({ + surface: '猫', + frequencyRank: 50, + }); + + const actual = computeWordClass(token, { + enabled: true, + topX: 100, + mode: 'single', + singleColor: '#000000', + bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, + }); + + assert.equal(actual, 'word word-frequency-single'); +}); + +test('computeWordClass adds frequency class when rank equals topX', () => { + const token = createToken({ + surface: '水', + frequencyRank: 100, + }); + + const actual = computeWordClass(token, { + enabled: true, + topX: 100, + mode: 'single', + singleColor: '#000000', + bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, + }); + + assert.equal(actual, 'word word-frequency-single'); +}); + +test('computeWordClass adds frequency class for banded mode', () => { + const token = createToken({ + surface: '犬', + frequencyRank: 250, + }); + + const actual = computeWordClass(token, { + enabled: true, + topX: 1000, + mode: 'banded', + singleColor: '#000000', + bandedColors: ['#111111', '#222222', '#333333', '#444444', '#555555'] as const, + }); + + assert.equal(actual, 'word word-frequency-band-2'); +}); + +test('computeWordClass uses configured band count for banded mode', () => { + const token = createToken({ + surface: '犬', + frequencyRank: 2, + }); + + const actual = computeWordClass(token, { + enabled: true, + topX: 4, + mode: 'banded', + singleColor: '#000000', + bandedColors: ['#111111', '#222222', '#333333', '#444444', '#555555'], + } as any); + + assert.equal(actual, 'word word-frequency-band-3'); +}); + +test('computeWordClass skips frequency class when rank is out of topX', () => { + const token = createToken({ + surface: '犬', + frequencyRank: 1200, + }); + + const actual = computeWordClass(token, { + enabled: true, + topX: 1000, + mode: 'single', + singleColor: '#000000', + bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, + }); + + assert.equal(actual, 'word'); +}); diff --git a/src/renderer/subtitle-render.test.ts b/src/renderer/subtitle-render.test.ts index 443a22e4..e3b71c36 100644 --- a/src/renderer/subtitle-render.test.ts +++ b/src/renderer/subtitle-render.test.ts @@ -4,11 +4,9 @@ import fs from 'node:fs'; import path from 'node:path'; import type { MergedToken } from '../types'; -import { PartOfSpeech } from '../types.js'; import { alignTokensToSourceText, buildSubtitleTokenHoverRanges, - computeWordClass, createSubtitleRenderer, getFrequencyRankLabelForToken, getJlptLevelLabelForToken, @@ -16,6 +14,7 @@ import { sanitizeSubtitleHoverTokenColor, shouldRenderTokenizedSubtitle, } from './subtitle-render.js'; +import { createToken } from './subtitle-render-test-helpers.js'; import { createRendererState } from './state.js'; class FakeTextNode { @@ -134,21 +133,6 @@ function collectWordNodes(root: FakeElement): FakeElement[] { ); } -function createToken(overrides: Partial): MergedToken { - return { - surface: '', - reading: '', - headword: '', - startPos: 0, - endPos: 0, - partOfSpeech: PartOfSpeech.other, - isMerged: true, - isKnown: false, - isNPlusOneTarget: false, - ...overrides, - }; -} - function extractClassBlock(cssText: string, selector: string): string { const ruleRegex = /([^{}]+)\{([^}]*)\}/g; let match: RegExpExecArray | null = null; @@ -242,121 +226,6 @@ function buildJlptColorSelector(level: number): string { return `#subtitleRoot .word.word-jlpt-n${level}:not(:is(${higherPriorityClasses}))`; } -test('computeWordClass preserves known and n+1 classes while adding JLPT classes', () => { - const knownJlpt = createToken({ - isKnown: true, - jlptLevel: 'N1', - surface: '猫', - }); - const nPlusOneJlpt = createToken({ - isNPlusOneTarget: true, - jlptLevel: 'N2', - surface: '犬', - }); - - assert.equal(computeWordClass(knownJlpt), 'word word-known word-jlpt-n1'); - assert.equal(computeWordClass(nPlusOneJlpt), 'word word-n-plus-one word-jlpt-n2'); -}); - -test('computeWordClass applies name-match class ahead of known, n+1, frequency, and JLPT classes when enabled', () => { - const token = createToken({ - isKnown: true, - isNPlusOneTarget: true, - jlptLevel: 'N2', - frequencyRank: 10, - surface: 'アクア', - }) as MergedToken & { isNameMatch?: boolean }; - token.isNameMatch = true; - - assert.equal( - computeWordClass(token, { - nameMatchEnabled: true, - enabled: true, - topX: 100, - mode: 'single', - singleColor: '#000000', - bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, - }), - 'word word-name-match', - ); -}); - -test('computeWordClass skips name-match class by default', () => { - const token = createToken({ - surface: 'アクア', - }) as MergedToken & { isNameMatch?: boolean }; - token.isNameMatch = true; - - assert.equal(computeWordClass(token), 'word'); -}); - -test('computeWordClass skips name-match class when disabled', () => { - const token = createToken({ - surface: 'アクア', - }) as MergedToken & { isNameMatch?: boolean }; - token.isNameMatch = true; - - assert.equal( - computeWordClass(token, { - nameMatchEnabled: false, - enabled: true, - topX: 100, - mode: 'single', - singleColor: '#000000', - bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, - }), - 'word', - ); -}); - -test('computeWordClass keeps known and N+1 color classes exclusive over frequency classes', () => { - const known = createToken({ - isKnown: true, - frequencyRank: 10, - surface: '既知', - }); - const nPlusOne = createToken({ - isNPlusOneTarget: true, - frequencyRank: 10, - surface: '目標', - }); - const frequency = createToken({ - frequencyRank: 10, - surface: '頻度', - }); - - assert.equal( - computeWordClass(known, { - enabled: true, - topX: 100, - mode: 'single', - singleColor: '#000000', - bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, - }), - 'word word-known', - ); - assert.equal( - computeWordClass(nPlusOne, { - enabled: true, - topX: 100, - mode: 'single', - singleColor: '#000000', - bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, - }), - 'word word-n-plus-one', - ); - assert.equal( - computeWordClass(frequency, { - enabled: true, - topX: 100, - mode: 'single', - singleColor: '#000000', - bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, - }), - 'word word-frequency-single', - ); -}); - test('applySubtitleStyle sets subtitle name-match color variable', () => { const restoreDocument = installFakeDocument(); try { @@ -632,91 +501,6 @@ test('annotated subtitle tokens inherit configured base subtitle typography', () } }); -test('computeWordClass adds frequency class for single mode when rank is within topX', () => { - const token = createToken({ - surface: '猫', - frequencyRank: 50, - }); - - const actual = computeWordClass(token, { - enabled: true, - topX: 100, - mode: 'single', - singleColor: '#000000', - bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, - }); - - assert.equal(actual, 'word word-frequency-single'); -}); - -test('computeWordClass adds frequency class when rank equals topX', () => { - const token = createToken({ - surface: '水', - frequencyRank: 100, - }); - - const actual = computeWordClass(token, { - enabled: true, - topX: 100, - mode: 'single', - singleColor: '#000000', - bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, - }); - - assert.equal(actual, 'word word-frequency-single'); -}); - -test('computeWordClass adds frequency class for banded mode', () => { - const token = createToken({ - surface: '犬', - frequencyRank: 250, - }); - - const actual = computeWordClass(token, { - enabled: true, - topX: 1000, - mode: 'banded', - singleColor: '#000000', - bandedColors: ['#111111', '#222222', '#333333', '#444444', '#555555'] as const, - }); - - assert.equal(actual, 'word word-frequency-band-2'); -}); - -test('computeWordClass uses configured band count for banded mode', () => { - const token = createToken({ - surface: '犬', - frequencyRank: 2, - }); - - const actual = computeWordClass(token, { - enabled: true, - topX: 4, - mode: 'banded', - singleColor: '#000000', - bandedColors: ['#111111', '#222222', '#333333', '#444444', '#555555'], - } as any); - - assert.equal(actual, 'word word-frequency-band-3'); -}); - -test('computeWordClass skips frequency class when rank is out of topX', () => { - const token = createToken({ - surface: '犬', - frequencyRank: 1200, - }); - - const actual = computeWordClass(token, { - enabled: true, - topX: 1000, - mode: 'single', - singleColor: '#000000', - bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const, - }); - - assert.equal(actual, 'word'); -}); - test('getFrequencyRankLabelForToken returns rank only for frequency-colored tokens', () => { const settings = { enabled: true,