diff --git a/tests/test_page_scripts.py b/tests/test_page_scripts.py
index f04468c..afc44bf 100644
--- a/tests/test_page_scripts.py
+++ b/tests/test_page_scripts.py
@@ -12,10 +12,13 @@ ROOT = pathlib.Path(__file__).resolve().parents[1]
# Compiles the scripts as one strict-mode block. There, functions are block scoped like let
# and const, so V8 itself rejects a name declared twice in the shared scope (however it's
# indented or declared: function, class, let, const, destructuring), while helpers with the
-# same name inside different functions stay legal.
+# same name inside different functions stay legal. var is the exception (declaring one twice
+# is allowed), so the page doesn't use var at all.
CHECK = r'''
const vm = require('vm');
const scripts = JSON.parse(require('fs').readFileSync(0, 'utf8'));
+const uses = scripts.join('\n').match(/(^|[^\w$.])var\s+[\w${[]/);
+if (uses) { console.log('declares with var: ' + uses[0].trim()); process.exit(0); }
try { new vm.Script('"use strict"; {\n' + scripts.join('\n;\n') + '\n}'); console.log('ok'); }
catch (e) { console.log(e.message); }
'''
@@ -30,7 +33,10 @@ def check(scripts):
class PageScripts(unittest.TestCase):
def test_no_top_level_name_is_declared_twice(self):
page = (ROOT / 'ui/index.html').read_text(encoding='utf-8')
- self.assertEqual(check(re.findall(r'', page, re.S)), 'ok')
+ # Every inline script, whatever its attributes (not src= ones, which aren't inline).
+ scripts = re.findall(r'', page, re.S)
+ self.assertGreaterEqual(len(scripts), 2)
+ self.assertEqual(check(scripts), 'ok')
def test_the_check_finds_what_it_should(self):
twice = {
@@ -40,6 +46,8 @@ class PageScripts(unittest.TestCase):
'later declarator': ['let x = 1;', 'const y = 2, x = 3;'],
'function and const': ['function f() {}', 'const f = 1;'],
}
+ self.assertIn('var', check(['var x = 1;', 'var x = 2;'])) # legal JavaScript, so: no var
+ self.assertEqual(check(['const css = "color: var(--blue)";']), 'ok') # CSS var() isn't one
for what, scripts in twice.items():
with self.subTest(what):
self.assertIn('already been declared', check(scripts))
diff --git a/ui/index.html b/ui/index.html
index 405b04b..2fb244b 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -1997,7 +1997,7 @@ async function capture() {
let url = null;
try {
let url_ = "/api/screenshot?view=headset";
- var shotPanel = null;
+ let shotPanel = null;
if (view === "flat") {
shotPanel = deskPanel() || (await loadPanels(), deskPanel());
if (!shotPanel) throw new Error("no app panel is open in the headset");
@@ -3314,9 +3314,10 @@ async function openStoreApp(index, install=false) {
sourceState.detail={app:{...app,offers:app.offers.map(o=>({...o}))},choice:0};
renderAppDetail();if(!$("appDetail").open)$("appDetail").showModal();$("appDetail").scrollTop=0;
// The hero's Install: the details show its progress. Already installed or installing: just
- // the details, whose button says what it does (Open in Steam would launch it).
- const o=currentOffer();
- if(install&&!sourceState.jobs.has(`${o.source}:${o.id}`))installStoreApp();
+ // the details, whose button says what it does (Open in Steam would launch it). After a
+ // failed install it tries again.
+ const o=currentOffer(), job=sourceState.jobs.get(`${o.source}:${o.id}`);
+ if(install&&(!job||job.done&&!job.result))installStoreApp();
await loadOfferDetails();
}
async function loadOfferDetails() {
@@ -3354,7 +3355,7 @@ let storeSearchTimer;
$("sourceQ").oninput=()=>{clearTimeout(storeSearchTimer);storeSearchTimer=setTimeout(searchSources,300);};
$("sourceSearch").onsubmit=e=>{e.preventDefault();clearTimeout(storeSearchTimer);searchSources();};
$("searchFilters").onclick=e=>{const b=e.target.closest('[data-filter]');if(!b)return;const f=b.dataset.filter;if(f==='all'){sourceState.vr=null;sourceState.installable=false;}else if(f==='installable')sourceState.installable=!sourceState.installable;else sourceState.vr=sourceState.vr===(f==='vr')?null:f==='vr';storeChips();searchSources();};
-$("searchGrid").onclick=e=>{const h=e.target.closest('[data-hero-install]');if(h){openStoreApp(+h.dataset.heroInstall,true);return;}const b=e.target.closest('[data-app]');if(b)openStoreApp(+b.dataset.app);if(e.target.closest('#storeReset')){$('sourceQ').value='';sourceState.vr=null;sourceState.installable=false;sourceState.source='';storeChips();searchSources();}};
+$("searchGrid").onclick=e=>{const h=e.target.closest('[data-hero-install]');if(h){openStoreApp(+h.dataset.heroInstall,true);return;}const b=e.target.closest('[data-app]');if(b)openStoreApp(+b.dataset.app);if(e.target.closest('#storeReset')){$('sourceQ').value='';sourceState.vr=null;sourceState.installable=false;storeChips();searchSources();}};
$("detailBody").onclick=e=>{if(e.target.closest('#detailInstall'))installStoreApp();const b=e.target.closest('[data-gallery]');if(b)$('detailGallery').scrollBy({left:+b.dataset.gallery*$('detailGallery').clientWidth*.86,behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'instant':'smooth'});};
$("detailBody").onchange=e=>{if(e.target.name==='storeOffer'){sourceState.detail.choice=+e.target.value;renderAppDetail();loadOfferDetails();}};
$("sourcesOpen").onclick=()=>{loadSources();$('sourcesDialog').showModal();};