diff --git a/tests/test_page_startup.py b/tests/test_page_startup.py new file mode 100644 index 0000000..69bd5c1 --- /dev/null +++ b/tests/test_page_startup.py @@ -0,0 +1,55 @@ +"""The page calls showPage() while its first script is still running, before names declared +later (in that script or the second one) exist. Touching one of them there throws, and the +rest of the page's setup never runs: opening Frame Control at #games did exactly that.""" +import json +import pathlib +import re +import shutil +import subprocess +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +STUBS = ['$', 'toggleLive', 'scrollToY', 'loadMacView', 'loadPanels', 'loadPanelSwitcher', 'loadTitles'] + +RUN = r''' +const vm = require('vm'); +const {code, hashes} = JSON.parse(require('fs').readFileSync(0, 'utf8')); +const failures = []; +for (const hash of hashes) { + const context = vm.createContext({ + location: {hash}, document: {querySelectorAll: () => [], title: ''}, live: false, + }); + try { vm.runInContext(code, context); } + catch (e) { failures.push(`${hash}: ${e.message}`); } +} +console.log(JSON.stringify(failures)); +''' + + +@unittest.skipUnless(shutil.which('node'), 'Node runs the page code') +class PageStartup(unittest.TestCase): + def test_opening_any_page_or_section_at_startup_runs(self): + page = (ROOT / 'ui/index.html').read_text(encoding='utf-8') + first, second = re.findall(r'', page, re.S)[:2] + tables = first[first.index('const PAGES ='):first.index('let page =')] + start = first.index('function showPage(') + show = first[start:first.index('\n}\n', start) + 3] + # Everything declared after the startup call is still uninitialised when it runs. + call = re.search(r'^showPage\(\);', first, re.M).end() + later = re.findall(r'^(?:const|let)\s+(\w+)', first[call:] + second, re.M) + later = [n for n in dict.fromkeys(later) if n not in STUBS and n not in ('PAGES', 'SECTION_PAGE', 'page')] + self.assertIn('link', later) # the name that broke #games + stubs = ''.join(f'function {n}() {{ return {{ classList: {{ toggle() {{}} }}, scrollIntoView() {{}} }}; }}\n' + for n in STUBS if n != '$') + code = ('const $ = id => id === "nowhere" ? null : { classList: { toggle() {} }, scrollIntoView() {} };\n' + stubs + tables + 'let page = "home";\n' + show + + 'showPage();\n' + ''.join(f'let {n};\n' for n in later)) + hashes = ['', '#home', '#games', '#android', '#tools', '#settings', '#devices', '#nowhere'] + hashes += ['#' + k for k in re.findall(r'(\w+): "', tables)] + r = subprocess.run(['node', '-e', RUN], input=json.dumps({'code': code, 'hashes': hashes}), + capture_output=True, text=True) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertEqual(json.loads(r.stdout), []) + + +if __name__ == '__main__': + unittest.main() diff --git a/ui/index.html b/ui/index.html index d924968..707867a 100644 --- a/ui/index.html +++ b/ui/index.html @@ -3827,7 +3827,7 @@ const SECTION_PAGE = { view: "home", device: "home", shots: "home", comfort: "ho transfer: "tools", apps: "tools", power: "tools", macview: "tools", media: "tools", panels: "tools", privacy: "settings", artworkSettings: "settings", assistant: "settings" }; let page = "home"; -function showPage() { +function showPage(e) { const id = location.hash.slice(1); const next = PAGES.includes(id) ? id : SECTION_PAGE[id] || "home"; if (next !== page && page === "home" && live) toggleLive(false); // don't stream video nobody is watching @@ -3840,8 +3840,9 @@ function showPage() { const section = !PAGES.includes(id) && id && $(id); if (section) section.scrollIntoView(); else scrollToY(0); if (page === "tools") { loadMacView(); loadPanels(); } - // Another computer may have added or removed a title: phones have no Refresh button. - if (page === "games" && prev !== "games" && link.s && link.s.phase === "connected") loadTitles(); + // Another computer may have added or removed a title: phones have no Refresh button. Only + // on navigating here (e: the hashchange); at startup the first load is already on its way. + if (e && page === "games" && prev !== "games") loadTitles(); } window.addEventListener("hashchange", showPage); // While a text field has focus, phones hide the bottom tab bar (see body.typing in the CSS).