mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
test: extract computeWordClass tests to dedicated file
- Move computeWordClass tests from subtitle-render.test.ts to subtitle-render-word-class.test.ts - Extract createToken helper into subtitle-render-test-helpers.ts for reuse - Register new test file in test:core:src and test:core:dist scripts
This commit is contained in:
+2
-2
@@ -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: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: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: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: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/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: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: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: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",
|
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { MergedToken } from '../types';
|
||||||
|
import { PartOfSpeech } from '../types.js';
|
||||||
|
|
||||||
|
export function createToken(overrides: Partial<MergedToken>): MergedToken {
|
||||||
|
return {
|
||||||
|
surface: '',
|
||||||
|
reading: '',
|
||||||
|
headword: '',
|
||||||
|
startPos: 0,
|
||||||
|
endPos: 0,
|
||||||
|
partOfSpeech: PartOfSpeech.other,
|
||||||
|
isMerged: true,
|
||||||
|
isKnown: false,
|
||||||
|
isNPlusOneTarget: false,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
@@ -4,11 +4,9 @@ import fs from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import type { MergedToken } from '../types';
|
import type { MergedToken } from '../types';
|
||||||
import { PartOfSpeech } from '../types.js';
|
|
||||||
import {
|
import {
|
||||||
alignTokensToSourceText,
|
alignTokensToSourceText,
|
||||||
buildSubtitleTokenHoverRanges,
|
buildSubtitleTokenHoverRanges,
|
||||||
computeWordClass,
|
|
||||||
createSubtitleRenderer,
|
createSubtitleRenderer,
|
||||||
getFrequencyRankLabelForToken,
|
getFrequencyRankLabelForToken,
|
||||||
getJlptLevelLabelForToken,
|
getJlptLevelLabelForToken,
|
||||||
@@ -16,6 +14,7 @@ import {
|
|||||||
sanitizeSubtitleHoverTokenColor,
|
sanitizeSubtitleHoverTokenColor,
|
||||||
shouldRenderTokenizedSubtitle,
|
shouldRenderTokenizedSubtitle,
|
||||||
} from './subtitle-render.js';
|
} from './subtitle-render.js';
|
||||||
|
import { createToken } from './subtitle-render-test-helpers.js';
|
||||||
import { createRendererState } from './state.js';
|
import { createRendererState } from './state.js';
|
||||||
|
|
||||||
class FakeTextNode {
|
class FakeTextNode {
|
||||||
@@ -134,21 +133,6 @@ function collectWordNodes(root: FakeElement): FakeElement[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createToken(overrides: Partial<MergedToken>): 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 {
|
function extractClassBlock(cssText: string, selector: string): string {
|
||||||
const ruleRegex = /([^{}]+)\{([^}]*)\}/g;
|
const ruleRegex = /([^{}]+)\{([^}]*)\}/g;
|
||||||
let match: RegExpExecArray | null = null;
|
let match: RegExpExecArray | null = null;
|
||||||
@@ -242,121 +226,6 @@ function buildJlptColorSelector(level: number): string {
|
|||||||
return `#subtitleRoot .word.word-jlpt-n${level}:not(:is(${higherPriorityClasses}))`;
|
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', () => {
|
test('applySubtitleStyle sets subtitle name-match color variable', () => {
|
||||||
const restoreDocument = installFakeDocument();
|
const restoreDocument = installFakeDocument();
|
||||||
try {
|
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', () => {
|
test('getFrequencyRankLabelForToken returns rank only for frequency-colored tokens', () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user