ffd28c0f9e
Reviewed-on: #377 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
241 行
9.3 KiB
TypeScript
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)
|
|
})
|
|
})
|