ファイル
btrc-hub/frontend/src/pages/GekanatorPage.test.tsx
T
みてるぞ ffd28c0f9e グカネータ スコア補正 (#376) (#377)
Reviewed-on: #377
Co-authored-by: miteruzo <miteruzo@naver.com>
Co-committed-by: miteruzo <miteruzo@naver.com>
2026-06-18 01:15:54 +09:00

241 行
9.3 KiB
TypeScript

import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { isQuestionHardFilteredAfterAnswers } from '@/lib/gekanatorQuestionFilters'
import type {
GekanatorAnswerLog,
GekanatorAnswerValue,
GekanatorQuestion,
GekanatorQuestionCondition,
} from '@/lib/gekanator'
const question = (
condition: GekanatorQuestionCondition,
): GekanatorQuestion => ({
id: `${ condition.type }:candidate`,
text: 'candidate?',
kind: condition.type === 'source'
? 'source'
: condition.type.startsWith ('original-')
? 'original_date'
: condition.type.startsWith ('title-')
? 'title'
: 'tag',
condition,
source: 'default',
priorityWeight: 1,
test: () => false,
})
const answer = (
condition: GekanatorQuestionCondition,
value: GekanatorAnswerValue,
): GekanatorAnswerLog => ({
questionId: 'previous',
questionText: 'previous?',
questionCondition: condition,
answer: value,
originalAnswer: value,
})
const blocked = (
candidate: GekanatorQuestionCondition,
previous: GekanatorQuestionCondition,
value: GekanatorAnswerValue,
): boolean =>
isQuestionHardFilteredAfterAnswers (question (candidate), [answer (previous, value)])
const gekanatorPageSource = readFileSync (
resolve (process.cwd (), 'src/pages/GekanatorPage.tsx'),
'utf8')
const gekanatorBackdropSource = gekanatorPageSource.slice (
gekanatorPageSource.indexOf ('const GekanatorBackdrop'),
gekanatorPageSource.indexOf ('const expectedAnswerFor'))
const gekanatorChooseQuestionSource = gekanatorPageSource.slice (
gekanatorPageSource.indexOf ('const chooseQuestion'),
gekanatorPageSource.indexOf ('const winningRunPriorityFor'))
const gekanatorFallbackQuestionSource = gekanatorPageSource.slice (
gekanatorPageSource.indexOf ('const chooseFallbackQuestion'),
gekanatorPageSource.indexOf ('const shouldEnterGuessPhase'))
describe('GekanatorBackdrop regression structure', () => {
it('keeps displayedBackdropMode as the render-time source of truth', () => {
expect(gekanatorBackdropSource).not.toContain ('isLeavingGuessBackdrop')
expect(gekanatorBackdropSource).not.toContain ('renderBackdropMode')
expect(gekanatorBackdropSource).not.toContain ('renderWinningRunCount')
expect(gekanatorBackdropSource).not.toContain ('renderThumbnails')
expect(gekanatorBackdropSource).not.toContain ('renderIsCrossfading')
expect(gekanatorBackdropSource).toContain (
"const renderedSettings = settingsForMode (displayedBackdropMode)")
expect(gekanatorBackdropSource).toContain (
'scaleForMode (displayedBackdropMode, displayedWinningRunCount)')
expect(gekanatorBackdropSource).toContain (
"backdropMode === 'guess' || displayedBackdropMode === 'guess'")
})
it('does not split guess into a separate renderer or force a remount', () => {
expect(gekanatorBackdropSource).not.toContain ('renderStaticGuessBackdrop')
expect(gekanatorBackdropSource).not.toContain ('guessZoomAnimationKey')
expect(gekanatorBackdropSource).not.toContain ('shouldAnimateGuessZoomIn')
expect(gekanatorBackdropSource).not.toContain ('previousBackdropModeRef')
expect(gekanatorBackdropSource).not.toContain (
'if (isGuessPresentation && guessThumbnail)')
})
it('keeps tile keys independent from backdrop mode', () => {
expect(gekanatorBackdropSource).toContain ('key={duplicate}')
expect(gekanatorBackdropSource).toContain ('key={`${ duplicate }:${ index }`}')
expect(gekanatorBackdropSource).not.toMatch (/key=\{`\$\{\s*mode/)
expect(gekanatorBackdropSource).not.toMatch (/key=\{`\$\{\s*displayedBackdropMode/)
})
it('keeps guess on the shared scale, x, and y animation path', () => {
expect(gekanatorBackdropSource).toContain ('animate={{ scale: renderedScale')
expect(gekanatorBackdropSource).toContain (
"x: displayedBackdropMode === 'guess' ? guessFocusOffset.x : '0%'")
expect(gekanatorBackdropSource).toContain (
"y: displayedBackdropMode === 'guess' ? guessFocusOffset.y : '0%'")
})
})
describe('Gekanator question selection regression structure', () => {
it('prefers normal questions after user_suggested quota has been met', () => {
const normalFallbackIndex = gekanatorChooseQuestionSource.indexOf (
'else if (normalPool.length > 0)')
const effectiveFallbackIndex = gekanatorChooseQuestionSource.indexOf (
'else if (effectiveUserSuggestedPool.length > 0)')
expect(normalFallbackIndex).toBeGreaterThan(0)
expect(effectiveFallbackIndex).toBeGreaterThan(0)
expect(normalFallbackIndex).toBeLessThan(effectiveFallbackIndex)
})
it('does not let fallback questions bypass user_suggested purpose tracking', () => {
expect(gekanatorFallbackQuestionSource).toContain (
"question.source !== 'user_suggested'")
})
it('does not show a fixed extra-question count in the extra learning UI', () => {
expect(gekanatorPageSource).not.toContain ('追加で 2 問まで答えてください。')
expect(gekanatorPageSource).toContain ('追加で質問に答えてください。')
})
})
describe('isQuestionHardFilteredAfterAnswers', () => {
it('blocks only contradictory or redundant month questions after a yes answer', () => {
const previous: GekanatorQuestionCondition = { type: 'original-month', month: 12 }
expect(blocked ({ type: 'original-month', month: 12 }, previous, 'yes')).toBe(true)
expect(blocked ({ type: 'original-month', month: 2 }, previous, 'yes')).toBe(true)
expect(blocked ({ type: 'original-month-day', monthDay: '2-14' }, previous, 'yes'))
.toBe(true)
expect(blocked ({ type: 'original-month-day', monthDay: '12-25' }, previous, 'yes'))
.toBe(false)
expect(blocked ({ type: 'original-year', year: 2024 }, previous, 'yes')).toBe(false)
expect(blocked ({ type: 'source', host: 'example.com' }, previous, 'yes')).toBe(false)
expect(blocked ({ type: 'tag', key: 'character:喜多郁代' }, previous, 'yes')).toBe(false)
})
it('blocks same-month facts after a no answer', () => {
const previous: GekanatorQuestionCondition = { type: 'original-month', month: 12 }
expect(blocked ({ type: 'original-month', month: 12 }, previous, 'no')).toBe(true)
expect(blocked ({ type: 'original-month', month: 2 }, previous, 'no')).toBe(false)
expect(blocked ({ type: 'original-month-day', monthDay: '12-25' }, previous, 'no'))
.toBe(true)
expect(blocked ({ type: 'original-month-day', monthDay: '2-14' }, previous, 'no'))
.toBe(false)
})
it('blocks all month and month-day questions after a month-day yes answer', () => {
const previous: GekanatorQuestionCondition = {
type: 'original-month-day',
monthDay: '12-25',
}
expect(blocked ({ type: 'original-month', month: 12 }, previous, 'yes')).toBe(true)
expect(blocked ({ type: 'original-month', month: 2 }, previous, 'yes')).toBe(true)
expect(blocked ({ type: 'original-month-day', monthDay: '12-25' }, previous, 'yes'))
.toBe(true)
expect(blocked ({ type: 'original-month-day', monthDay: '12-26' }, previous, 'yes'))
.toBe(true)
})
it('blocks the same month-day only after a month-day no answer', () => {
const previous: GekanatorQuestionCondition = {
type: 'original-month-day',
monthDay: '12-25',
}
expect(blocked ({ type: 'original-month-day', monthDay: '12-25' }, previous, 'no'))
.toBe(true)
expect(blocked ({ type: 'original-month-day', monthDay: '12-26' }, previous, 'no'))
.toBe(false)
expect(blocked ({ type: 'original-month', month: 12 }, previous, 'no')).toBe(false)
})
it('blocks year and source as single-value facts', () => {
expect(blocked (
{ type: 'original-year', year: 2025 },
{ type: 'original-year', year: 2024 },
'yes',
)).toBe(true)
expect(blocked (
{ type: 'original-year', year: 2024 },
{ type: 'original-year', year: 2024 },
'no',
)).toBe(true)
expect(blocked (
{ type: 'source', host: 'b.example' },
{ type: 'source', host: 'a.example' },
'yes',
)).toBe(true)
expect(blocked (
{ type: 'source', host: 'b.example' },
{ type: 'source', host: 'a.example' },
'no',
)).toBe(false)
})
it('does not hard-filter partial, probably_no, or unknown fact answers', () => {
const previous: GekanatorQuestionCondition = { type: 'original-month', month: 12 }
const candidate: GekanatorQuestionCondition = { type: 'original-month', month: 2 }
expect(blocked (candidate, previous, 'partial')).toBe(false)
expect(blocked (candidate, previous, 'probably_no')).toBe(false)
expect(blocked (candidate, previous, 'unknown')).toBe(false)
})
it('keeps title-length hard redundancy for yes and no only', () => {
const previous: GekanatorQuestionCondition = {
type: 'title-length-at-least',
length: 30,
}
expect(blocked ({ type: 'title-length-at-least', length: 20 }, previous, 'yes'))
.toBe(true)
expect(blocked ({ type: 'title-length-at-least', length: 40 }, previous, 'yes'))
.toBe(false)
expect(blocked ({ type: 'title-length-at-least', length: 40 }, previous, 'no'))
.toBe(true)
expect(blocked ({ type: 'title-length-at-least', length: 20 }, previous, 'no'))
.toBe(false)
expect(blocked ({ type: 'title-length-at-least', length: 20 }, previous, 'partial'))
.toBe(false)
})
})