diff --git a/src/settings-onboarding-page.js b/src/settings-onboarding-page.js index 8c3c3a99..a5fda669 100644 --- a/src/settings-onboarding-page.js +++ b/src/settings-onboarding-page.js @@ -32,7 +32,7 @@ export const SERVICE_SCRIPT = `"use strict"; (() => { const $ = (id) => document.getElementById(id); if (!$('services-section')) return; let spotifyFlow=null, spotifyGeneration=0, hostedFlow=false, hostedGeneration=0, hostedStarting=false, hostedExpiresAt=0; -const say=(id,text)=>{ $(id).textContent=text; }; +const say=(id,text)=>{ const out=$(id); if (id==='hosted-service-result' && document.activeElement && out.contains(document.activeElement)) $('hosted-connect').focus(); out.textContent=text; }; async function api(path,method='GET',body) { const r=await fetch(path,{method,cache:'no-store',headers:{Accept:'application/json',...(method==='POST'?{'Content-Type':'application/json'}:{})},...(body?{body:JSON.stringify(body)}:{})}); const d=await r.json().catch(()=>({})); if (!r.ok) throw Error(d.error||d.status||'request_failed'); return d; } async function load() { try { const s=await api('/api/settings/services'); say('spotify-account',s.spotify ? 'Connected as '+s.spotify.name : 'Not connected'); $('spotify-remove').hidden=!s.spotify; if(s.spotify) $('spotify-client-id').value=s.spotify.clientId; say('hosted-account',s.hosted.login ? 'Signed in as '+s.hosted.login : 'Not signed in'); $('hosted-address').value=s.hosted.url; } catch { say('spotify-service-result','Could not load connected services.'); } } $('spotify-connect').addEventListener('click',async()=>{ const generation=++spotifyGeneration; spotifyFlow=null; $('spotify-open-link').replaceChildren(); $('spotify-open-link').hidden=true; try { const d=await api('/api/setup/spotify','POST',{action:'start',clientId:$('spotify-client-id').value.trim()}); if(generation!==spotifyGeneration)return; spotifyFlow=d.flowId; if(/^https:\\/\\/accounts\\.spotify\\.com\\//.test(d.authUrl||'')) { const out=$('spotify-open-link'); out.replaceChildren(); const a=document.createElement('a'); a.href=d.authUrl; a.target='_blank'; a.rel='noopener noreferrer'; a.textContent='Open Spotify sign-in'; out.replaceChildren(a); out.hidden=false; window.open(d.authUrl,'_blank','noopener'); } say('spotify-service-result', $('spotify-open-link').hidden ? 'Spotify sign-in link is unavailable. Try again.' : 'Use the link below if a Spotify tab did not open.'); pollSpotify(generation); } catch(e) { if(generation!==spotifyGeneration)return; say('spotify-service-result','Spotify sign-in could not start: '+e.message); } }); diff --git a/test/settings-hosted-signin-focus.test.js b/test/settings-hosted-signin-focus.test.js new file mode 100644 index 00000000..4dc63962 --- /dev/null +++ b/test/settings-hosted-signin-focus.test.js @@ -0,0 +1,16 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import {runInNewContext} from "node:vm"; +import {SERVICE_SCRIPT} from "../src/settings-onboarding-page.js"; +const tick=()=>new Promise(resolve=>setImmediate(resolve)); +for(const outcome of ["signed_in","expired"]) test(`hosted sign-in ${outcome} preserves focus when its verification link is replaced`,async()=>{ + const fetches=[], timers=[], nodes=new Map();const document={activeElement:null,getElementById:id=>{if(!nodes.has(id))nodes.set(id,create(id));return nodes.get(id);},createElement:tag=>create(null)}; + function create(id){return {id,value:"https://cards.example",children:[],hidden:false,_text:"",get textContent(){return this._text;},set textContent(t){this._text=t;this.children=[];},addEventListener(e,fn){this[e]=fn;},replaceChildren(...c){this.children=c;},contains(other){return this===other||this.children.some(c=>typeof c==='object'&&c.contains?.(other));},focus(){document.activeElement=this;}}} + const node=document.getElementById; + runInNewContext(SERVICE_SCRIPT,{document,fetch:(path,options)=>new Promise(resolve=>fetches.push({path,options,resolve:body=>resolve({ok:true,json:async()=>body})})),setTimeout:fn=>timers.push(fn),performance:{now:()=>0},window:{open(){}}}); + fetches[0].resolve({spotify:null,hosted:{url:"https://cards.example"}});await tick(); + const start=node("hosted-connect").click();fetches[1].resolve({status:"started",verificationUri:"https://github.com/login/device",userCode:"TEST",interval:5});await start; + node("hosted-service-result").children[1].focus();const poll=timers.shift()();fetches[2].resolve({status:outcome,cardUrl:"https://cards.example/u/test.svg"});await tick(); + if(outcome==="signed_in"){fetches[3].resolve({spotify:null,hosted:{url:"https://cards.example",login:"test"}});await tick();}await poll; + assert.equal(node("hosted-service-result").children.length,0);assert.equal(document.activeElement,node("hosted-connect")); +});