fix: preserve analysis results and flag stale user data
Publish GitHub Pages output / publish (push) Successful in 51s
Publish GitHub Pages output / publish (push) Successful in 51s
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createContext, runInContext } from 'node:vm';
|
||||
import ts from 'typescript';
|
||||
import { compile } from 'svelte/compiler';
|
||||
|
||||
const page = readFileSync(new URL('../../routes/analysis/+page.svelte', import.meta.url), 'utf8');
|
||||
const script = page.match(/<script lang="ts">([\s\S]*?)<\/script>/)![1];
|
||||
const source = ts.createSourceFile('analysis.ts', script, ts.ScriptTarget.Latest, true);
|
||||
const names = new Set(['updateAwakening', 'updateFixedReduction', 'toggleAllBlueNodes', 'updateGenerationBonus',
|
||||
'updateSongScoreBonusOverride', 'updateCard', 'updateLeader', 'updateOutfit', 'applyPreset']);
|
||||
const snippets: string[] = [];
|
||||
function collect(node: ts.Node): void {
|
||||
if (ts.isFunctionDeclaration(node) && node.name && names.has(node.name.text)) snippets.push(node.getText(source));
|
||||
if (ts.isBinaryExpression(node) && node.left.getText(source) === 'worker.onmessage') snippets.push(node.getText(source) + ';');
|
||||
if (ts.isVariableDeclaration(node) && node.name.getText(source) === 'syncStorage') snippets.push(`const ${node.getText(source)};`);
|
||||
ts.forEachChild(node, collect);
|
||||
}
|
||||
collect(source);
|
||||
const code = ts.transpileModule(snippets.join('\n'), { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText;
|
||||
|
||||
test('편성·성장·프리셋을 수정해도 이전 결과와 확률을 유지한다', () => {
|
||||
const result = { theoreticalMaxScore: 123 }, capture = { title: 'old song' }, probabilities = { maximum: 0.1 };
|
||||
const context = createContext({ result, resultCapture: capture, achievementProbabilities: probabilities, worker: {},
|
||||
selectedAwakenings: [1], fixedCooldownReductions: [null], allBlueNodes: [false], generationBonusOverrides: [null],
|
||||
songScoreBonusOverride: null, selectedCardIds: ['old'], collection: {}, leaderCharacterId: 'old', leaderOutfitCardId: '',
|
||||
fiveStarCards: [{ id: 'outfit', characterId: 'new', details: { costume: {} } }],
|
||||
normalizeAwakening: Number, normalizeGenerationBonusOverride: Number, restoreAnalysisState: () => {}, errorMessage: '' });
|
||||
runInContext(code, context);
|
||||
for (const update of ['updateAwakening(0, 4)', 'updateFixedReduction(0, 12)', 'toggleAllBlueNodes(0)',
|
||||
'updateGenerationBonus(0, "900")', 'updateSongScoreBonusOverride({value: "10"})', 'updateCard(0, "new")',
|
||||
'updateLeader("new")', 'updateOutfit("outfit")', 'applyPreset({detail: {}})']) {
|
||||
runInContext(update, context);
|
||||
assert.equal(context.result, result, update);
|
||||
assert.equal(context.resultCapture, capture, update);
|
||||
assert.equal(context.achievementProbabilities, probabilities, update);
|
||||
}
|
||||
});
|
||||
|
||||
test('계산 완료는 현재 입력이 아닌 시작 당시 조건·시뮬레이션과 결과를 함께 교체한다', () => {
|
||||
const next = { theoreticalMaxScore: 456 }, capture = { title: 'requested song' }, simulation = { chart: 'requested chart' };
|
||||
const context = createContext({ worker: {}, result: { theoreticalMaxScore: 123 }, pendingCapture: capture,
|
||||
pendingInputKey: 'requested conditions', currentAnalysisKey: 'edited while running', pendingSimulationContext: simulation,
|
||||
resultCapture: null, resultInputKey: '', simulationContext: { chart: 'old chart' }, achievementProbabilities: null, running: true, next });
|
||||
runInContext(code, context);
|
||||
runInContext('worker.onmessage({data: {type: "result", result: next, achievementProbabilities: {maximum: 0.2}}})', context);
|
||||
assert.equal(context.result, next);
|
||||
assert.equal(context.resultCapture, capture);
|
||||
assert.equal(context.resultInputKey, 'requested conditions');
|
||||
assert.notEqual(context.resultInputKey, context.currentAnalysisKey);
|
||||
assert.equal(context.simulationContext, simulation);
|
||||
assert.equal(context.running, false);
|
||||
});
|
||||
|
||||
test('이전 결과 안내는 입력 변화에 반응하고 타임라인은 저장된 결과의 악곡 길이를 사용한다', () => {
|
||||
const code = compile(page, { filename: 'analysis.svelte', generate: 'client' }).js.code;
|
||||
const effects = code.split('$.legacy_pre_effect(');
|
||||
assert.ok(effects.find(part => part.includes('$.set(resultStale,') && part.includes('$.get(currentAnalysisKey)')));
|
||||
assert.ok(effects.find(part => part.includes('$.set(currentAnalysisKey,') && part.includes('$.get(selectedSongId)') && part.includes('$.get(difficulty)')));
|
||||
assert.ok(effects.find(part => part.includes('$.set(timelineDuration,') && part.includes('$.get(resultCapture)?.durationSeconds')));
|
||||
});
|
||||
|
||||
test('다른 탭의 성장 변경과 재분석 오류가 기존 결과·기록·시뮬레이션을 지우지 않는다', () => {
|
||||
const result = { theoreticalMaxScore: 123 }, capture = { title: 'old' }, simulation = { chart: 'old' };
|
||||
const storage = { getItem: () => '{"memoryCount":30}' };
|
||||
let loaded = '';
|
||||
const context = createContext({ worker: {}, result, resultCapture: capture, simulationContext: simulation,
|
||||
localStorage: storage, USER_DATA_STORAGE_KEY: 'growth', applyStoredUserData: (raw: string) => { loaded = raw; },
|
||||
running: true, errorMessage: '' });
|
||||
runInContext(code, context);
|
||||
runInContext('syncStorage({key: "growth", storageArea: localStorage, newValue: "stale event"})', context);
|
||||
assert.equal(loaded, '{"memoryCount":30}');
|
||||
runInContext('worker.onmessage({data: {type: "error", message: "retry"}})', context);
|
||||
assert.equal(context.result, result);
|
||||
assert.equal(context.resultCapture, capture);
|
||||
assert.equal(context.simulationContext, simulation);
|
||||
assert.equal(context.running, false);
|
||||
assert.equal(context.errorMessage, 'retry');
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createContext, runInContext } from 'node:vm';
|
||||
import ts from 'typescript';
|
||||
|
||||
// Exercise the page's actual storage handler without a browser or expensive workers.
|
||||
const page = readFileSync(new URL('../../routes/combination/+page.svelte', import.meta.url), 'utf8');
|
||||
const script = page.match(/<script lang="ts">([\s\S]*?)<\/script>/)![1];
|
||||
const source = ts.createSourceFile('combination.ts', script, ts.ScriptTarget.Latest, true);
|
||||
const snippets: string[] = [];
|
||||
function collect(node: ts.Node): void {
|
||||
if (ts.isFunctionDeclaration(node) && node.name?.text === 'loadUserData') snippets.push(node.getText(source));
|
||||
if (ts.isVariableDeclaration(node) && node.name.getText(source) === 'syncStorage') {
|
||||
snippets.push(`const ${node.getText(source)};`);
|
||||
}
|
||||
ts.forEachChild(node, collect);
|
||||
}
|
||||
collect(source);
|
||||
assert.equal(snippets.length, 2);
|
||||
const code = ts.transpileModule(snippets.join('\n'), { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText;
|
||||
|
||||
test('다른 탭의 데이터 변경은 계산 중인 스냅샷과 기존 결과를 보존하고 안내만 표시한다', () => {
|
||||
let raw = JSON.stringify({ collection: { a: { awakening: 1 } }, memberBoards: { a: ['B1'] }, memoryCount: 1 });
|
||||
const storage = { getItem: () => raw };
|
||||
const result = { theoreticalMaxScore: 123 };
|
||||
const context = createContext({ localStorage: storage, USER_DATA_STORAGE_KEY: 'growth',
|
||||
loadedUserData: undefined, userDataChanged: false, collection: {}, memberBoards: {}, boardCards: {},
|
||||
memoryCount: 0, memberPowerUpPercent: 0, result, rankedResults: [result], running: true });
|
||||
runInContext(code + '\nloadUserData();', context);
|
||||
raw = JSON.stringify({ collection: { a: { awakening: 5 } }, memberBoards: { a: ['B1', 'B2'] }, memoryCount: 30 });
|
||||
runInContext('syncStorage({key: "growth", storageArea: localStorage});', context);
|
||||
assert.equal(context.userDataChanged, true);
|
||||
assert.equal(context.collection.a.awakening, 1);
|
||||
assert.equal(context.memoryCount, 1);
|
||||
assert.equal(context.memberBoards.a.length, 1);
|
||||
assert.equal(context.result, result);
|
||||
assert.equal(context.rankedResults[0], result);
|
||||
assert.equal(context.running, true);
|
||||
|
||||
// Only the next explicit search loads the new snapshot.
|
||||
runInContext('loadUserData();', context);
|
||||
assert.equal(context.collection.a.awakening, 5);
|
||||
assert.equal(context.memoryCount, 30);
|
||||
assert.equal(context.memberBoards.a.length, 2);
|
||||
assert.equal(context.userDataChanged, false);
|
||||
|
||||
runInContext('syncStorage({key: "growth", newValue: "outdated queued event", storageArea: localStorage});', context);
|
||||
assert.equal(context.userDataChanged, false);
|
||||
raw = 'null';
|
||||
runInContext('syncStorage({key: "other", storageArea: localStorage});', context);
|
||||
assert.equal(context.userDataChanged, false);
|
||||
runInContext('syncStorage({key: "growth", storageArea: {}});', context);
|
||||
assert.equal(context.userDataChanged, false);
|
||||
runInContext('syncStorage({key: null, storageArea: localStorage});', context);
|
||||
assert.equal(context.userDataChanged, true);
|
||||
assert.equal(context.result, result);
|
||||
});
|
||||
Reference in New Issue
Block a user