diff --git a/frontend/app-seams.test.js b/frontend/app-seams.test.js index 0fa6e57..ced5ccd 100644 --- a/frontend/app-seams.test.js +++ b/frontend/app-seams.test.js @@ -76,3 +76,10 @@ test('listening tracking does not pull in the lazy Stats view',()=>{ assert.deepEqual(groups['feature:stats'].files,['/stats-core.js']); assert.match(app,/Lazy\.proxy\('feature:stats-view', \['render'\], 'StatsView'\)/); }); + +test('an unavailable Stats chunk reports an error without rejecting the UI navigation',async()=>{ + const source=app.match(/function renderStats\(\) \{[\s\S]*?\n\}/)?.[0];assert.ok(source); + const seen=[],context=require('node:vm').createContext({Lazy:{proxy:()=>({render:()=>Promise.reject(new Error('Unavailable fixture chunk'))})},toast:text=>seen.push(text)}); + require('node:vm').runInContext(source+';globalThis.render=renderStats;',context); + await context.render();assert.deepEqual(seen,['Unavailable fixture chunk']); +}); diff --git a/frontend/app.js b/frontend/app.js index e2a778b..869ed00 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -5181,7 +5181,7 @@ const StatsTrack = (() => { // Stats view // ============================================================================ function renderStats() { - return Lazy.proxy('feature:stats-view', ['render'], 'StatsView').render(); + return Lazy.proxy('feature:stats-view', ['render'], 'StatsView').render().catch(error => toast(error.message)); } @@ -7798,8 +7798,8 @@ document.querySelectorAll('.chip').forEach((c) => { $('sleepCurtain').addEventListener('click', () => $('sleepCurtain').classList.add('hidden')); $('sleepCancelBtn').addEventListener('click', () => { cancelSleepTimer(); toast('Sleep timer cancelled'); }); $('transitionBtn').addEventListener('click', () => Transition.cycle()); - $('shareMomentBtn').addEventListener('click', () => Share.open()); - $('externalBtn').addEventListener('click', () => External.open()); + $('shareMomentBtn').addEventListener('click', () => Share.open().catch(error => toast(error.message))); + $('externalBtn').addEventListener('click', () => External.open().catch(error => toast(error.message))); $('pipBtn').addEventListener('click', () => PiP.toggle()); $('eqBtn').addEventListener('click', () => EQ.open()); @@ -9005,7 +9005,7 @@ async function boot() { syncOnLaunch(); startPlaylistInboxWatch(); if(LowerThird.active()) await Lazy.load('feature:lower-third'); - await Presenter.boot(); + await Presenter.boot().catch(error => toast(error.message)); if (LowerThird.active()) return; SectionRail.boot(); if (isMediaId(new URLSearchParams(location.search).get('v'))) Share.bootFromUrl().catch(error => toast(error.message)); diff --git a/perf/seams.mjs b/perf/seams.mjs index 58d705b..39ff1e0 100644 --- a/perf/seams.mjs +++ b/perf/seams.mjs @@ -63,6 +63,10 @@ async function run(name){ if(groups.includes('feature:stats-view')) { await page.evaluate(async()=>{view={type:'stats'};render();});await page.waitForSelector('.stats-empty');checked.push('stats'); } + if(groups.includes('feature:share-external')) { + await page.evaluate(()=>{Share.open=()=>Promise.reject(new Error('Unavailable fixture chunk'));document.querySelector('#shareMomentBtn').click();}); + await page.getByText('Unavailable fixture chunk',{exact:true}).waitFor(); + } assert.equal(proxy.getStats().requestCount,before,'all warmed seam code loads from SW without network'); // Ignore only verified disconnected version/recommendation API transports in WebKit. const scriptErrors=errors.filter(e=>!(name==='webkit'&&(e==='TypeError: Load failed'||/\/api\/(?:recommendations\?[^\s]*|version)\.$/.test(e))));assert.deepEqual(scriptErrors,[]);