Several worlds: a Worlds list with load, rename and delete; old save migrates to World 1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+81
-1
@@ -21,7 +21,77 @@ with sync_playwright() as p:
|
||||
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
|
||||
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}') if m.type == 'error' else None)
|
||||
pg.goto(url); pg.wait_for_timeout(300)
|
||||
pg.click('#btn-new'); pg.fill('#nw-seed', '777'); pg.click('#btn-generate')
|
||||
check('storage transactions roll back when a write throws', pg.evaluate("""async () => {
|
||||
try { await DB.op('readwrite', st => { st.put('partial', 'abort-test'); throw new Error('test abort'); }); }
|
||||
catch (e) { return await DB.get('abort-test') === undefined; }
|
||||
return false;
|
||||
}"""))
|
||||
# Migrate a real legacy save before starting the play-through.
|
||||
pg.evaluate("generateWorld(12345, 512, 384, null)")
|
||||
pg.evaluate("async () => { const s = serializeWorld(); await DB.put('main', { ...s, savedAt: Date.now(), playTime: 3661 }); }")
|
||||
check('a failed migration leaves the legacy save intact', pg.evaluate("""async () => {
|
||||
const get = DB.get;
|
||||
DB.get = async function (k) { const v = await get.call(this, k); return k.endsWith(':main') && v ? { ...v, savedAt: -1 } : v; };
|
||||
try { await migrateWorlds(); return false; }
|
||||
catch (e) { return !!await get.call(DB, 'main') && await get.call(DB, 'worlds') === undefined; }
|
||||
finally { DB.get = get; }
|
||||
}"""))
|
||||
pg.reload(); pg.wait_for_function("!$('title').hidden && !$('btn-continue').hidden")
|
||||
check('legacy save becomes World 1 and retires its old key', pg.evaluate("async () => { const w = await listWorlds(); return w.length === 1 && w[0].name === 'World 1' && await DB.get('main') === undefined; }"))
|
||||
pg.click('#btn-continue'); pg.wait_for_function('GAME.running')
|
||||
check('Continue loads the migrated seed and playtime', pg.evaluate('worldSeed === 12345 && GAME.playTime >= 3661 && !!GAME.worldId'))
|
||||
pg.evaluate('quitToTitle()')
|
||||
for seed in [222, 333]:
|
||||
if not pg.is_visible('#nw-seed'): pg.click('#btn-new')
|
||||
pg.fill('#nw-seed', str(seed)); pg.click('#btn-generate')
|
||||
pg.wait_for_function('GAME.running', timeout=20000)
|
||||
pg.evaluate('quitToTitle()')
|
||||
pg.wait_for_function("!$('title').hidden")
|
||||
pg.click('#btn-worlds'); pg.locator('#m-worlds').wait_for(state='visible')
|
||||
check('three independent worlds appear with their seeds', pg.locator('.world-row').count() == 3 and all(str(seed) in pg.inner_text('#worlds-list') for seed in [12345, 222, 333]))
|
||||
# Both looks, at both requested sizes; title UI shares the same styling.
|
||||
for look_shot in ['pixel', 'smooth']:
|
||||
pg.evaluate(f"SET.look = '{look_shot}'")
|
||||
for width, height, suffix in [(1280, 800, ''), (900, 700, '-900')]:
|
||||
pg.set_viewport_size({'width': width, 'height': height})
|
||||
pg.screenshot(path=str(root / 'shots' / f'worlds-{look_shot}{suffix}.png'))
|
||||
pg.set_viewport_size({'width': 1280, 'height': 800}); pg.evaluate("SET.look = 'pixel'")
|
||||
ids = pg.evaluate("async () => Object.fromEntries((await listWorlds()).map(w => [w.seed, w.id]))")
|
||||
for seed in [222, 333]:
|
||||
row = pg.locator(f'[data-world="{ids[str(seed)]}"]')
|
||||
row.get_by_role('button', name='Load', exact=True).click(); pg.wait_for_function('GAME.running')
|
||||
check(f'Load switches to seed {seed}', pg.evaluate('worldSeed') == seed and pg.evaluate('GAME.worldId') == ids[str(seed)])
|
||||
pg.evaluate('quitToTitle()'); pg.click('#btn-worlds')
|
||||
row = pg.locator(f'[data-world="{ids["222"]}"]')
|
||||
row.get_by_role('button', name='Rename', exact=True).click()
|
||||
row.locator('input').fill('<b>x</b>'); row.get_by_role('button', name='Save', exact=True).click()
|
||||
pg.wait_for_function("document.querySelector('.world-row strong') && [...document.querySelectorAll('.world-row strong')].some(e => e.textContent === '<b>x</b>')")
|
||||
check('HTML in world names is text', row.locator('strong').inner_text() == '<b>x</b>' and row.locator('strong b').count() == 0)
|
||||
pg.reload(); pg.wait_for_function("!$('title').hidden"); pg.click('#btn-worlds')
|
||||
check('rename persists after reload', row.locator('strong').inner_text() == '<b>x</b>')
|
||||
check('blank renames keep the old name', pg.evaluate("async id => { await renameWorld(id, ' '); return (await listWorlds()).find(w => w.id === id).name === '<b>x</b>'; }", ids['222']))
|
||||
row.get_by_role('button', name='Rename', exact=True).click()
|
||||
row.locator('input').evaluate("e => e.value = 'x'.repeat(60)")
|
||||
row.get_by_role('button', name='Save', exact=True).click()
|
||||
pg.wait_for_function("[...document.querySelectorAll('.world-row strong')].some(e => e.textContent === 'x'.repeat(40))")
|
||||
check('rename boundary cuts names to 40 characters', row.locator('strong').inner_text() == 'x' * 40)
|
||||
row.get_by_role('button', name='Delete', exact=True).click()
|
||||
pg.locator('#dlg-foot').get_by_role('button', name='Cancel', exact=True).click()
|
||||
check('cancelling deletion keeps the world', row.count() == 1)
|
||||
row.get_by_role('button', name='Delete', exact=True).click()
|
||||
pg.locator('#dlg-foot').get_by_role('button', name='Delete', exact=True).click()
|
||||
pg.wait_for_function("document.querySelectorAll('.world-row').length === 2")
|
||||
check('confirmed deletion removes both saves and the entry', pg.evaluate("async id => !(await listWorlds()).some(w => w.id === id) && !await DB.get(id + ':main') && !await DB.get(id + ':auto')", ids['222']))
|
||||
check('each world chooses its newer autosave without changing another', pg.evaluate("""async id => {
|
||||
const main = await DB.get(id + ':main');
|
||||
await DB.put(id + ':auto', { ...main, savedAt: main.savedAt + 1, playTime: 7777 });
|
||||
const sv = await worldNewest(id); return sv.playTime === 7777 && sv.worldId === id;
|
||||
}""", ids['333']))
|
||||
pg.keyboard.press('Escape'); pg.click('#btn-continue'); pg.wait_for_function('GAME.running')
|
||||
check('Continue chooses the most recently saved world', pg.evaluate('worldSeed') == 333)
|
||||
pg.evaluate('quitToTitle()')
|
||||
if not pg.is_visible('#nw-seed'): pg.click('#btn-new')
|
||||
pg.fill('#nw-seed', '777'); pg.click('#btn-generate')
|
||||
pg.wait_for_function('GAME.running === true', timeout=20000)
|
||||
pg.wait_for_timeout(500)
|
||||
ev = pg.evaluate
|
||||
@@ -542,10 +612,13 @@ with sync_playwright() as p:
|
||||
pg3 = ctx.new_page()
|
||||
pg3.on('pageerror', lambda e: logs.append(f'PAGEERROR(import): {e}'))
|
||||
pg3.goto(url); pg3.wait_for_timeout(300)
|
||||
worlds_before_import = ev('listWorlds().then(w => w.length)')
|
||||
pg3.set_input_files('#importfile', dl.path())
|
||||
pg3.wait_for_function('GAME.running === true', timeout=60000)
|
||||
got = pg3.evaluate('JSON.stringify([W, H, Math.round(money), Math.round(debt), bagTotal])')
|
||||
check('importing it on the title screen brings the same world back', got == want, f'{want} -> {got}')
|
||||
pg3.wait_for_function("async () => (await listWorlds()).length === " + str(worlds_before_import + 1))
|
||||
check('import adds an independent saved world', pg3.evaluate('GAME.worldId') != ev('GAME.worldId') and pg3.evaluate('listWorlds().then(w => w.length)') == worlds_before_import + 1)
|
||||
pg3.close()
|
||||
pg.keyboard.press('Escape'); settle(150)
|
||||
|
||||
@@ -562,6 +635,13 @@ with sync_playwright() as p:
|
||||
pg2.close()
|
||||
print('\n'.join(logs) or 'no console errors')
|
||||
check('every status line is short (≤ 90 characters)', len(ev('_maxStatus')) <= 90, ev('_maxStatus'))
|
||||
ev('quitToTitle()'); ev("openSettings('Game')")
|
||||
before_wipe = ev('listWorlds().then(w => w.length)')
|
||||
pg.click('#set-wipe'); pg.locator('#dlg-foot').get_by_role('button', name='Cancel', exact=True).click()
|
||||
check('cancelling Delete all worlds keeps every world', ev('listWorlds().then(w => w.length)') == before_wipe)
|
||||
pg.click('#set-wipe'); pg.locator('#dlg-foot').get_by_role('button', name='Delete all worlds', exact=True).click()
|
||||
pg.wait_for_function("async () => await DB.get('worlds') === undefined && $('btn-continue').hidden && $('btn-worlds').hidden")
|
||||
check('Delete all worlds clears the saves and index', ev("DB.op('readonly', st => st.getAllKeys()).then(keys => keys.length === 0)"))
|
||||
print('FAILS:', fails)
|
||||
b.close()
|
||||
sys.exit(1 if fails else 0)
|
||||
|
||||
Reference in New Issue
Block a user