diff --git a/internal/check-view-address.mjs b/internal/check-view-address.mjs index 1e3a95f0f..a5ab21674 100644 --- a/internal/check-view-address.mjs +++ b/internal/check-view-address.mjs @@ -296,6 +296,24 @@ async function arms({ va, sv, counts, pw, src }, { skipF = false } = {}) { c.destroy() delete globalThis.ResizeObserver } + if (!skipF) { + // The same fresh load, where the observer NEVER fires again after layout (a pane's height arrived after the + // column's last resize): the bounded rAF loop must still open the link. + const ros = [] + globalThis.ResizeObserver = class { constructor(cb) { this.cb = cb; ros.push(this) } observe() {} disconnect() { this.off = true } } + globalThis.requestAnimationFrame = (cb) => setTimeout(cb, 5) + const b = browser('https://x.test/upg-100-mfe?pane=dashboard') + const host = new FakeEl() + FakeEl.h0 = 0 + const c = pw.buildPaneColumn(SPEC, { host, address: { owner: () => 'app' } }) + FakeEl.h0 = undefined + const col = host.children[0] + col.clientHeight = 100; await settle() + if (col.scrollTop !== 100) fails.push(`F: with an observer that never fires again, the bounded rAF did not open the link (scrollTop ${col.scrollTop})`) + if (b.writes.push + b.writes.replace) fails.push('F: the rAF-opened link wrote the address') + c.destroy() + delete globalThis.ResizeObserver; delete globalThis.requestAnimationFrame + } if (!skipF) { // A KEPT-ALIVE column whose tab is hidden and shown again (ResizeObserver: no height, then height). const ros = [] @@ -442,6 +460,7 @@ if (process.argv.includes('--self-test')) { ['pane-wrapper writing on Back (the pane it is leaving)', { ...real, pw: await pwVariant("show(known(pane) ? pane : column[0].id, 'smooth')", "(claimPane(current(), true), show(known(pane) ? pane : column[0].id, 'smooth'))") }, /^F: Back did not show the address's pane, or it WROTE/], ['pane-wrapper opening a link at build time only (before the column has a layout)', { ...real, pw: await pwVariant("let pending = known(linked) && linked !== column[0].id ? linked : ''", "let pending = ''; if (known(linked)) show(linked, 'instant')") }, /^F: a FRESH LOAD with \?pane=dashboard did not open/], ['pane-wrapper whose settle overwrites a link still waiting to open', { ...real, pw: await pwVariant('if (col.clientHeight && !pending) claimPane(current(), false)', 'if (!pending || true) claimPane(current(), false)') }, /^F: before its layout, the column overwrote the link/], + ['pane-wrapper retrying a pending link by rAF only when there is no observer', { ...real, pw: await pwVariant("if (pending && typeof requestAnimationFrame !== 'undefined') {", "if (pending && typeof ResizeObserver === 'undefined' && typeof requestAnimationFrame !== 'undefined') {") }, /^F: with an observer that never fires again/], ['pane-wrapper ignoring a link', { ...real, pw: await pwVariant("let pending = known(linked) && linked !== column[0].id ? linked : ''", "let pending = ''") }, /^F: a link to \?pane=dashboard did not open/], ['pane-wrapper writing without the opt-in', { ...real, pw: await pwVariant("=> address && claimView({ pane: id === column[0].id ? '' : id, ...changes }, { owner: address.owner(),", "=> claimView({ pane: id === column[0].id ? '' : id, ...changes }, { owner: address ? address.owner() : 'app',") }, /^F: a column WITHOUT the address opt-in/], ['ownAddress that lets a nested element switch the tab', { ...real, va: { ...va, ownAddress: (el, g) => { const h = (e) => { const o = g(); if (o) { e.stopPropagation(); va.writeViewKeys(e.detail.changes, { owner: o, el, push: e.detail.push }) } }; el.addEventListener(va.VIEW_EVENT, h); return () => el.removeEventListener(va.VIEW_EVENT, h) } } }, /^E: a nested announcement switched the OWNER's tab/], diff --git a/library/layouts/pane-wrapper/pane-wrapper.js b/library/layouts/pane-wrapper/pane-wrapper.js index 99e51ea05..710824490 100644 --- a/library/layouts/pane-wrapper/pane-wrapper.js +++ b/library/layouts/pane-wrapper/pane-wrapper.js @@ -201,7 +201,10 @@ export function buildPaneColumn(spec, { host, mountPane, address }) { pending = '' } openLink() - if (pending && typeof ResizeObserver === 'undefined' && typeof requestAnimationFrame !== 'undefined') { + // Retried by the ResizeObserver below, by every settled scroll, and by a bounded rAF loop that runs even WITH an + // observer: the observer fires only when the COLUMN resizes, and a pane's height can arrive later (styles adopted + // late in a shadow root). + if (pending && typeof requestAnimationFrame !== 'undefined') { let n = 0 const tick = () => { openLink(); if (pending && ++n < 120) requestAnimationFrame(tick) } requestAnimationFrame(tick) @@ -210,7 +213,7 @@ export function buildPaneColumn(spec, { host, mountPane, address }) { // step per scroll. A column with no height (a hidden tab) has no "where", and a link still waiting to open must // not be overwritten, so neither writes. let t = null - const settle = () => { if (t) clearTimeout(t); t = setTimeout(() => { if (col.clientHeight && !pending) claimPane(current(), false) }, 120) } + const settle = () => { if (t) clearTimeout(t); t = setTimeout(() => { openLink(); if (col.clientHeight && !pending) claimPane(current(), false) }, 120) } col.addEventListener('scroll', settle, { passive: true }) offs.push(() => { if (t) clearTimeout(t); col.removeEventListener('scroll', settle) }) offs.push(onAddressChange(({ pane }) => show(known(pane) ? pane : column[0].id, 'smooth')))