diff --git a/tests/test_comfort_ui.py b/tests/test_comfort_ui.py index c990bc2..4ed21e9 100644 --- a/tests/test_comfort_ui.py +++ b/tests/test_comfort_ui.py @@ -67,8 +67,10 @@ const log = ()=>{}, toast = ()=>{}; const assert = require('node:assert/strict'); const elements = new Map(); const $ = id => { - if (!elements.has(id)) elements.set(id, {textContent:'', hidden:true, disabled:false, value:'30', checked:true, - type: /Alert$/.test(id) ? 'checkbox' : 'number'}); + // The form's own defaults, as in the page's HTML. + const value = {sessionMinutes:'30', breakMinutes:'20', stillMinutes:'30'}[id] || ''; + if (!elements.has(id)) elements.set(id, {textContent:'', hidden:true, disabled:false, value, defaultValue:value, + checked:true, defaultChecked:true, type: /Alert$/.test(id) ? 'checkbox' : 'number'}); return elements.get(id); }; const window = {frameApp:{notify:async()=>{}}}; @@ -78,13 +80,23 @@ const log = ()=>{}, toast = ()=>{}; renderComfort({id:'old', active:false, time:100, options:{minutes:1,breakMinutes:0,stillMinutes:0,batteryAlert:false,heatAlert:false}, events:[{id:'old:1',kind:'finished',time:90,message:'Session ended'}]}); assert.equal($('sessionMinutes').value, '30'); -assert.equal($('breakMinutes').value, '30'); +assert.equal($('breakMinutes').value, '20'); assert.equal($('batteryAlert').checked, true); assert.match($('comfortEvents').textContent, /Session ended/); assert.notEqual($('comfortEvents').textContent, 'Session ended'); // it carries a time renderComfort({id:'new', active:true, time:200, remaining:600, options:{minutes:45,breakMinutes:15,stillMinutes:20,batteryAlert:true,heatAlert:false}, events:[]}); assert.equal($('sessionMinutes').value, 45); // a running session shows its own settings assert.equal($('heatAlert').checked, false); +// The page stays open while that session ends: the next one starts from the defaults again. +renderComfort({id:'new', active:false, time:900, options:{minutes:45,breakMinutes:15,stillMinutes:20,batteryAlert:true,heatAlert:false}, + events:[{id:'new:1',kind:'finished',time:899,message:'Session ended'}]}); +assert.equal($('sessionMinutes').value, '30'); +assert.equal($('breakMinutes').value, '20'); +assert.equal($('heatAlert').checked, true); +renderComfort({id:'new', active:false, time:905, options:{minutes:45}, events:[]}); +$('sessionMinutes').value = '50'; // what's typed for the next session +renderComfort({id:'new', active:false, time:910, options:{minutes:45}, events:[]}); +assert.equal($('sessionMinutes').value, '50'); // later polls leave it alone ''' result = subprocess.run(['node', '-e', setup + code + checks], capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr) diff --git a/ui/index.html b/ui/index.html index 9ac7a9b..6112d3e 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1649,7 +1649,7 @@ $("vrUtilities").onclick = async e => { }; // The Frame owns the clock. Poll independently of status and the selected tab. -let comfortBusy = false, comfortSeen = new Set(), comfortSession = null, comfortHydrated = false; +let comfortBusy = false, comfortSeen = new Set(), comfortSession = null, comfortHydrated = false, comfortActive = false; async function localNotification(message, request = false) { try { if (window.frameApp?.notify) await window.frameApp.notify(message, request); @@ -1684,6 +1684,12 @@ function renderComfort(s) { if (el.type === "checkbox") el.checked = value; else el.value = value; } } + // When the running session ends, the form goes back to the page's defaults for the next one. + if (comfortActive && !s.active) for (const id of ["sessionMinutes", "breakMinutes", "stillMinutes", "batteryAlert", "heatAlert"]) { + const el = $(id); + if (el.type === "checkbox") el.checked = el.defaultChecked; else el.value = el.defaultValue; + } + comfortActive = !!s.active; let statusText = s.error || (s.active ? `${Math.ceil(s.remaining / 60)} min until Steam Home ยท ${s.activity == null ? "activity unknown" : s.activity === 3 ? "headset in standby" : "monitoring"}` : "No session running."); @@ -2697,7 +2703,11 @@ $("clipMac").onclick = () => act($("clipMac").textContent, sendComputerClipboard const drop = $("drop"); drop.onclick = () => $("fileInput").click(); drop.onkeydown = e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); $("fileInput").click(); } }; -$("fileInput").onchange = () => sendFiles([...$("fileInput").files]); +$("fileInput").onchange = () => { + // Chosen from Home: show Tools' file panel, where the progress bar is (phones have no Activity bar). + if ($("fileInput").files.length && page !== "tools") location.hash = "#transfer"; + sendFiles([...$("fileInput").files]); +}; // Files can be dropped anywhere in the window; an overlay says what happens. // Dialogs keep their own drops (none take files today), so the overlay stays out of them. let dragDepth = 0; @@ -3821,6 +3831,7 @@ function showPage() { 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 + const prev = page; page = next; document.querySelectorAll(".page").forEach(p => p.classList.toggle("on", p.dataset.page === page)); document.querySelectorAll("nav a").forEach(a => a.classList.toggle("on", a.getAttribute("href") === "#" + page)); @@ -3829,6 +3840,8 @@ 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(); } window.addEventListener("hashchange", showPage); // While a text field has focus, phones hide the bottom tab bar (see body.typing in the CSS).