Served with isolation headers; start-screen capability notices; tests on the real GPU (t88 phase 1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
"""An isolated Firefox title screen and three seconds of a new pixel world."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'tools'))
|
||||
from use_venv import ensure_playwright
|
||||
ensure_playwright()
|
||||
from browser import capabilities, consume_gpu_option, installed, launch
|
||||
|
||||
consume_gpu_option()
|
||||
if not installed('firefox'):
|
||||
print('SKIP firefox_smoke.py: Playwright Firefox is not installed')
|
||||
sys.exit(0)
|
||||
|
||||
with launch(browser_name='firefox') as (_, page, url):
|
||||
errors = []
|
||||
page.on('pageerror', lambda e: errors.append(str(e)))
|
||||
page.add_init_script("localStorage.setItem('deepgrain.settings', JSON.stringify({look:'pixel', sv:2}));")
|
||||
page.goto(url)
|
||||
caps = capabilities(page)
|
||||
print('Firefox: ' + json.dumps(caps))
|
||||
assert caps['isolated']
|
||||
if not caps['webgpu']:
|
||||
assert page.locator('#cap-notice').is_visible()
|
||||
text = page.inner_text('#cap-notice')
|
||||
assert 'about:config' in text and 'dom.webgpu.enabled' in text and 'Firefox Nightly' in text
|
||||
print('PASS Firefox isolation and capability notice')
|
||||
page.click('#btn-new')
|
||||
page.fill('#nw-seed', '424242')
|
||||
page.select_option('#nw-size', '2048x1024')
|
||||
page.click('#btn-generate')
|
||||
page.wait_for_function('GAME.running && !GAME.loading', timeout=60000)
|
||||
before = page.evaluate('tick')
|
||||
page.wait_for_timeout(3000)
|
||||
assert page.evaluate('tick') > before
|
||||
assert not errors, errors
|
||||
print('PASS Firefox new world runs for three seconds without page errors')
|
||||
+28
-15
@@ -4,20 +4,19 @@
|
||||
import json, sys, pathlib, re
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools'))
|
||||
from use_venv import ensure_playwright; ensure_playwright()
|
||||
from playwright.sync_api import sync_playwright
|
||||
from browser import launch, consume_gpu_option
|
||||
consume_gpu_option()
|
||||
root = pathlib.Path(__file__).resolve().parent.parent
|
||||
out = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else root / 'shots' / 'play'); out.mkdir(parents=True, exist_ok=True)
|
||||
url = (root / 'dist' / 'index.html').as_uri()
|
||||
fails = []
|
||||
def check(name, cond, extra=''):
|
||||
print(('PASS ' if cond else 'FAIL ') + name + (f' [{extra}]' if extra != '' else ''))
|
||||
if not cond: fails.append(name)
|
||||
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch()
|
||||
ctx = b.new_context(viewport={'width': 1280, 'height': 800})
|
||||
pg = ctx.new_page()
|
||||
with launch(viewport={'width': 1280, 'height': 800}) as (b, pg, url):
|
||||
ctx = pg.context
|
||||
logs = []
|
||||
pg.add_init_script("if (!localStorage.getItem('deepgrain.settings')) localStorage.setItem('deepgrain.settings', JSON.stringify({look:'pixel', sv:2}));")
|
||||
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.evaluate("SET.fog = false"); pg.wait_for_timeout(300)
|
||||
@@ -61,13 +60,13 @@ with sync_playwright() as p:
|
||||
check('Continue loads the migrated seed and playtime', pg.evaluate('worldSeed === 12345 && GAME.playTime >= 3661 && !!GAME.worldId'))
|
||||
pg.evaluate("""() => {
|
||||
window.__pack = packBuffers;
|
||||
packBuffers = async b => { await new Promise(r => setTimeout(r, 600)); return __pack(b); };
|
||||
packBuffers = async b => { await new Promise(r => { window.__finishSave = r; }); return __pack(b); };
|
||||
window.__saving = saveWorld('main', true);
|
||||
}""")
|
||||
pg.locator('#saving').wait_for(state='visible')
|
||||
pg.screenshot(path=str(out / 'saving-progress.png'))
|
||||
check('slow save shows the saving chip', pg.is_visible('#saving'))
|
||||
pg.evaluate('async () => { await __saving; packBuffers = __pack; }')
|
||||
pg.evaluate('async () => { __finishSave(); await __saving; packBuffers = __pack; }')
|
||||
check('saving chip hides when the save ends', not pg.is_visible('#saving'))
|
||||
pg.evaluate('quitToTitle()')
|
||||
names = {}
|
||||
@@ -248,7 +247,8 @@ with sync_playwright() as p:
|
||||
led = ev(camera_offset)
|
||||
check('camera leads toward the pointer when enabled', all(b - a > 2 for a, b in zip(centred, led)), led)
|
||||
pg.mouse.move(*mouse)
|
||||
ev(f'GAME.paused = {json.dumps(paused)}; saveSettings()')
|
||||
# Keep world-to-screen targets fixed after checking both camera modes.
|
||||
ev(f'GAME.paused = {json.dumps(paused)}; SET.cameraLead = false; saveSettings(); snapCamera()')
|
||||
def cell_to_screen(x, y):
|
||||
return ev(f'(() => {{ const dpr = R.canvas.width / innerWidth; const s = worldToScreenPx({x}+0.5, {y}+0.5); return [s.x/dpr, s.y/dpr]; }})()')
|
||||
def blk(bx, by): return cell_to_screen(bx * 4 + 2, by * 4 + 2)
|
||||
@@ -281,7 +281,11 @@ with sync_playwright() as p:
|
||||
# 1. drill: rock breaks loose, nothing enters the bag
|
||||
pg.mouse.move(*cell_to_screen(bx + 6, gy + 3)); settle(900)
|
||||
sx, sy = cell_to_screen(bx + 6, gy + 3)
|
||||
hold(sx, sy, 3500)
|
||||
pg.mouse.move(sx, sy); pg.mouse.down()
|
||||
try:
|
||||
pg.wait_for_function('stats.dug > 20', timeout=15000)
|
||||
finally:
|
||||
pg.mouse.up()
|
||||
settle(700)
|
||||
check('drill digs with the mouse', ev('stats.dug') > 20, ev('stats.dug'))
|
||||
check('drill does not fill the bag', ev('bagTotal') == 0, ev('bagTotal'))
|
||||
@@ -307,7 +311,8 @@ with sync_playwright() as p:
|
||||
|
||||
ev("SET.cardSeconds = 0; $('cards').replaceChildren()") # centred cards cover the mouse work area; discovery was checked above
|
||||
# 2. vacuum, left button: lock onto what you click
|
||||
pg.keyboard.press('2'); settle(100)
|
||||
pg.keyboard.press('2')
|
||||
pg.wait_for_function("GAME.tool === 1 && $('selcard').querySelector('.name')?.textContent === 'Pneumatic nozzle'")
|
||||
check('Pneumatic nozzle card title and width match the selected tool', ev("GAME.tool === 1 && $('selcard').querySelector('.name').textContent === 'Pneumatic nozzle' && $('selcard').firstElementChild.querySelector('.chip').textContent === `width ${GAME.brush[1]}` && !$('selcard').querySelector('.sub .chip')"))
|
||||
check('Pneumatic nozzle name appears in the tool list and hotbar title', ev("TOOLS[1].name === 'Pneumatic nozzle' && $('hotbar').children[1].title.startsWith('Pneumatic nozzle')"))
|
||||
tgt = ev(f'''(() => {{ for (let r = 0; r < 30; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {{
|
||||
@@ -492,6 +497,7 @@ with sync_playwright() as p:
|
||||
check('Shift lights every loose bag row', ev("blowAllList().every(m => [...$('baglist').querySelectorAll('.bagrow.on .nm')].some(e => e.textContent === MD[m].name))"))
|
||||
pg.screenshot(path=str(out / '03b-blow-all-preview.png'))
|
||||
pg.keyboard.up('Shift'); settle(150)
|
||||
pg.wait_for_function("!$('selcard').querySelector('.vacchoice').textContent.includes('Everything') && document.querySelectorAll('#baglist .bagrow.on').length === 1")
|
||||
check('releasing Shift restores the chosen blow material', ev("$('selcard').querySelector('.vacchoice').textContent.includes(MD[COAL].name) && !$('selcard').querySelector('.vacchoice').textContent.includes('Everything')"))
|
||||
check('releasing Shift lights only coal', ev("(() => { const rows = [...$('baglist').querySelectorAll('.bagrow.on')]; return rows.length === 1 && rows[0].querySelector('.nm').textContent === MD[COAL].name; })()"))
|
||||
bot_at = ev('[bot.x, bot.y]')
|
||||
@@ -1156,6 +1162,12 @@ with sync_playwright() as p:
|
||||
navigator.getGamepads = () => [window.__pad];
|
||||
}''')
|
||||
pg.keyboard.press('1'); settle(100)
|
||||
# The preceding play-through can leave only air below the bot.
|
||||
ev('''() => {
|
||||
GAME.drillTune = 0;
|
||||
const x0 = Math.floor(bot.x + bot.w / 2), y0 = Math.ceil(bot.y + bot.h) + 2;
|
||||
for (let y = y0; y < y0 + 8; y++) for (let x = x0 - 4; x <= x0 + 4; x++) setCell(y * W + x, DIRT, 20, F_NAT);
|
||||
}''')
|
||||
d0 = ev('stats.dug')
|
||||
ev("__pad.axes[3] = 0.9; __pad.buttons[7].pressed = true; __pad.buttons[7].value = 1;")
|
||||
settle(1500)
|
||||
@@ -1206,10 +1218,11 @@ with sync_playwright() as p:
|
||||
pg.keyboard.press('Escape'); settle(150)
|
||||
|
||||
# Fog has its own paused scene; existing play checks keep the whole world visible.
|
||||
fb = p.chromium.launch(args=['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'])
|
||||
fp = fb.new_page(viewport={'width': 1280, 'height': 800})
|
||||
fog_context = b.new_context(viewport={'width': 1280, 'height': 800})
|
||||
fp = fog_context.new_page()
|
||||
if pg.evaluate('!!window.__allowSoftwareGL'):
|
||||
fp.add_init_script('window.__allowSoftwareGL = true')
|
||||
fp.on('pageerror', lambda e: logs.append(f'PAGEERROR(fog): {e}'))
|
||||
fp.add_init_script('window.__allowSoftwareGL = true')
|
||||
fp.goto(url)
|
||||
fp.evaluate('startNewWorld(4242, 512, 384)')
|
||||
fp.wait_for_function('GAME.running')
|
||||
@@ -1318,7 +1331,7 @@ with sync_playwright() as p:
|
||||
check('fog draws in the smooth look', fp.evaluate('R.smoothOn && SM.ok'))
|
||||
check('unseen smooth grains have no terrain shape', fp.evaluate("(() => {const k=(235-R.vy0)*R.bw+300-R.vx0;return R.px[k]===0x04000000 && R.backPx[k]===0 && R.treePx[k]===0 && R.fieldPx[k]===255})()"))
|
||||
fp.screenshot(path=str(out / 'fog-smooth.png'))
|
||||
fb.close()
|
||||
fog_context.close()
|
||||
|
||||
# 11. the largest world from the title screen
|
||||
pg2 = ctx.new_page()
|
||||
|
||||
+7
-1
@@ -30,6 +30,9 @@ def run_py(name):
|
||||
|
||||
|
||||
def main():
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
from browser import consume_gpu_option
|
||||
consume_gpu_option()
|
||||
args = [a for a in sys.argv[1:] if not a.startswith('--')]
|
||||
browser = '--browser' in sys.argv or '--browser-only' in sys.argv
|
||||
node = '--browser-only' not in sys.argv
|
||||
@@ -42,7 +45,8 @@ def main():
|
||||
if browser:
|
||||
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True)
|
||||
# one at a time: two headless browsers at once can starve each other and flake
|
||||
for name in ['play.py', 't_smooth.py', 't_dev_console.py']: results.append(run_py(name))
|
||||
for name in ['play.py', 't_smooth.py', 't_dev_console.py', 't_caps.py', 'firefox_smoke.py']:
|
||||
results.append(run_py(name))
|
||||
bad = 0
|
||||
for name, code, out, secs in results:
|
||||
fails = [l for l in out.splitlines() if l.startswith('FAIL') or 'Error' in l or l.startswith('FAILS: [') and l != 'FAILS: []']
|
||||
@@ -50,6 +54,8 @@ def main():
|
||||
ok = code == 0 and not [l for l in fails if l.startswith('FAIL ') or 'Error' in l]
|
||||
bad += not ok
|
||||
print(f"{'ok ' if ok else 'FAIL'} {name:22s} {passes:3d} checks passed {secs:5.1f}s")
|
||||
for line in out.splitlines():
|
||||
if line.startswith('SKIP '): print(' ' + line)
|
||||
if not ok:
|
||||
for l in (fails or out.splitlines()[-15:]): print(' ' + l)
|
||||
print('ALL PASSED' if not bad else f'{bad} SUITE(S) FAILED')
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Capability detection, title warnings and the future startup gate."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'tools'))
|
||||
from use_venv import ensure_playwright
|
||||
ensure_playwright()
|
||||
from browser import capabilities, consume_gpu_option, launch
|
||||
|
||||
consume_gpu_option()
|
||||
for case in ['native', 'absent', 'null', 'reject', 'firefox', 'required']:
|
||||
with launch() as (_, page, url):
|
||||
errors = []
|
||||
page.on('pageerror', lambda e: errors.append(str(e)))
|
||||
if case != 'native':
|
||||
gpu = 'undefined' if case == 'absent' else "{requestAdapter: async () => null}"
|
||||
if case == 'reject': gpu = "{requestAdapter: async () => {throw new Error('adapter denied')}}"
|
||||
page.add_init_script(f"Object.defineProperty(navigator, 'gpu', {{value: {gpu}}});")
|
||||
page.add_init_script("Object.defineProperty(self, 'crossOriginIsolated', {value: false});")
|
||||
if case == 'firefox':
|
||||
page.add_init_script("Object.defineProperty(navigator, 'userAgent', {value: 'Firefox/157.0'});")
|
||||
if case == 'required':
|
||||
def require(route):
|
||||
response = route.fetch()
|
||||
route.fulfill(response=response, body=response.text().replace(
|
||||
'const REQUIRE_GPU_AND_THREADS = false;', 'const REQUIRE_GPU_AND_THREADS = true;'))
|
||||
page.route('**/index.html', require)
|
||||
page.goto(url)
|
||||
caps = capabilities(page)
|
||||
page.wait_for_function("!$('title').hidden")
|
||||
if case == 'native':
|
||||
assert caps['isolated']
|
||||
assert page.locator('#cap-notice').is_hidden() == caps['webgpu']
|
||||
else:
|
||||
assert not caps['webgpu'] and caps['adapterInfo'] is None
|
||||
notice = page.inner_text('#cap-notice')
|
||||
assert 'python3 tools/serve.py' in notice and "opened from a file it can't" in notice
|
||||
assert ('Firefox Nightly' in notice and 'dom.webgpu.enabled' in notice) if case == 'firefox' else 'Chrome or Edge' in notice
|
||||
if case == 'required':
|
||||
assert not page.evaluate('canStartGame()')
|
||||
page.evaluate('startNewWorld(424242, 512, 384)')
|
||||
assert not page.evaluate('GAME.running || GAME.loading')
|
||||
assert not page.evaluate('startFromSave({})')
|
||||
else:
|
||||
assert page.evaluate('canStartGame()')
|
||||
if case == 'absent':
|
||||
Path('shots').mkdir(exist_ok=True)
|
||||
page.screenshot(path='shots/t88-p1-notice.png')
|
||||
page.evaluate("SET.look='pixel'; startNewWorld(424242, 512, 384)")
|
||||
page.wait_for_function('GAME.running && !GAME.loading', timeout=60000)
|
||||
assert not errors, errors
|
||||
print(f'PASS capabilities: {case}')
|
||||
@@ -2,7 +2,8 @@
|
||||
import pathlib, sys
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools'))
|
||||
from use_venv import ensure_playwright; ensure_playwright()
|
||||
from playwright.sync_api import sync_playwright
|
||||
from browser import launch, consume_gpu_option
|
||||
consume_gpu_option()
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
SHOTS = ROOT / 'shots'
|
||||
@@ -12,13 +13,11 @@ def check(name, ok):
|
||||
print(('PASS ' if ok else 'FAIL ') + name)
|
||||
if not ok: fails.append(name)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
page = browser.new_page(viewport={'width': 1280, 'height': 760})
|
||||
with launch() as (browser, page, url):
|
||||
errors = []
|
||||
page.on('pageerror', lambda e: errors.append(str(e)))
|
||||
page.add_init_script("localStorage.setItem('deepgrain.settings', JSON.stringify({look:'pixel', sv:2}));")
|
||||
page.goto((ROOT / 'dist/index.html').as_uri())
|
||||
page.goto(url)
|
||||
page.wait_for_function("!$('title').hidden")
|
||||
page.evaluate("startNewWorld(424242, 512, 384, 'Console test')")
|
||||
page.wait_for_function('GAME.running && !GAME.loading')
|
||||
|
||||
+13
-13
@@ -2,21 +2,20 @@
|
||||
import sys, pathlib
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools'))
|
||||
from use_venv import ensure_playwright; ensure_playwright()
|
||||
from playwright.sync_api import sync_playwright
|
||||
from browser import launch, consume_gpu_option
|
||||
consume_gpu_option()
|
||||
root = pathlib.Path(__file__).resolve().parent.parent
|
||||
out = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else root / 'shots' / 'smooth'); out.mkdir(parents=True, exist_ok=True)
|
||||
fails = []
|
||||
def check(name, cond, extra=''):
|
||||
print(('PASS ' if cond else 'FAIL ') + name + (f' [{extra}]' if extra != '' else ''))
|
||||
if not cond: fails.append(name)
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(args=['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'])
|
||||
pg = b.new_page(viewport={'width': 1024, 'height': 640})
|
||||
with launch(viewport={'width': 1024, 'height': 640}) as (b, pg, url):
|
||||
logs = []
|
||||
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 in ('error', 'warning') else None)
|
||||
pg.add_init_script('window.__allowSoftwareGL = true; localStorage.clear();')
|
||||
pg.goto((root / 'dist' / 'index.html').as_uri()); pg.evaluate('SET.fog = false'); pg.wait_for_timeout(300)
|
||||
pg.add_init_script('localStorage.clear();')
|
||||
pg.goto(url); pg.evaluate('SET.fog = false'); pg.wait_for_timeout(300)
|
||||
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(1500)
|
||||
ev = pg.evaluate
|
||||
@@ -283,15 +282,16 @@ with sync_playwright() as p:
|
||||
}""")
|
||||
check('filter drag previews pass stored travel bits to the join rule',
|
||||
bool(filter_plans) and all(v == (128 if c == ev('BP_FILTER_DOWN') else 0) for c, v in filter_plans), filter_plans)
|
||||
# a browser without hardware drawing gets the pixel look and a note
|
||||
pg2 = b.new_page(viewport={'width': 800, 'height': 500})
|
||||
pg2.add_init_script('localStorage.clear();')
|
||||
pg2.goto((root / 'dist' / 'index.html').as_uri()); pg2.evaluate('SET.fog = false'); pg2.wait_for_timeout(300)
|
||||
pg2.click('#btn-new'); pg2.fill('#nw-seed', '5'); pg2.click('#btn-generate')
|
||||
pg2.wait_for_function('GAME.running === true', timeout=20000); pg2.wait_for_timeout(1200)
|
||||
check('software rendering falls back to pixels with a note', pg2.evaluate("SET.look === 'pixel' && !R.smoothOn && SM.why === 'software'") and pg2.locator('.toast', has_text='graphics card').count() >= 1, pg2.evaluate('[SET.look, SM.why]'))
|
||||
errs = [l for l in logs if 'PAGEERROR' in l or l.startswith('error')]
|
||||
check('no errors', not errs, errs[:3])
|
||||
b.close()
|
||||
|
||||
# a browser without hardware drawing gets the pixel look and a note
|
||||
with launch(gpu='soft', viewport={'width': 800, 'height': 500}) as (_, pg2, soft_url):
|
||||
pg2.add_init_script('window.__allowSoftwareGL = false; localStorage.clear();')
|
||||
pg2.goto(soft_url); pg2.evaluate('SET.fog = false'); pg2.wait_for_timeout(300)
|
||||
pg2.click('#btn-new'); pg2.fill('#nw-seed', '5'); pg2.click('#btn-generate')
|
||||
pg2.wait_for_function('GAME.running === true', timeout=20000); pg2.wait_for_timeout(1200)
|
||||
check('software rendering falls back to pixels with a note', pg2.evaluate("SET.look === 'pixel' && !R.smoothOn && SM.why === 'software'") and pg2.locator('.toast', has_text='graphics card').count() >= 1, pg2.evaluate('[SET.look, SM.why]'))
|
||||
print('FAILS:', fails)
|
||||
sys.exit(1 if fails else 0)
|
||||
|
||||
Reference in New Issue
Block a user