diff --git a/internal/check-view-address.mjs b/internal/check-view-address.mjs index 029984700..ba33857d6 100644 --- a/internal/check-view-address.mjs +++ b/internal/check-view-address.mjs @@ -439,6 +439,14 @@ async function arms({ va, sv, counts, pw, src }, { skipF = false } = {}) { if (!/ownAddress\(stage\.value, owner\)/.test(t)) fails.push(`G: ${f} opts its column in but does not record its panes' choices (ownAddress)`) else if (!/offOwn\?\.\(\)/.test(t)) fails.push(`G: ${f} never releases ownAddress`) } + // P2b: every shell tells the shells it nests their depth below the address owner; a shell that can be nested + // (the Research Gallery, Posterverse) claims its tab WITH its depth and reads its own subtab segment. + for (const [f, t] of Object.entries(src.shells || {})) { + if (!/addressDepth = urlOwnerOf\(this\.cfg\) \? 1 : depthOf\(this\.cfg\) \+ 1/.test(t)) fails.push(`G: the shell ${f} does not pass addressDepth to the shells it nests`) + if (f.startsWith('plugin-studio/')) continue + if (!/claimView\(\{ tab: [^}]+\}, \{[^}]*\bdepth\b/.test(t)) fails.push(`G: the shell ${f} does not claim its tab with its depth`) + if (!/subtabAt\(location\.search, depthOf\(this\.cfg\)\)/.test(t)) fails.push(`G: the shell ${f} does not read its own subtab segment when nested`) + } // Each shell that owns a page records its nested composites' choices, and releases the listener on unmount // BEFORE any hot-update early return (or a hot remount stacks a second one). for (const [f, t] of Object.entries(src.shells || {})) { @@ -552,6 +560,9 @@ if (process.argv.includes('--self-test')) { ['regions-mfe without the address opt-in', { ...real, src: { ...src, mfe: src.mfe.replace('{ host: stage.value, address: { owner } }', '{ host: stage.value }') } }, /^G: regions-mfe does not opt its column in/], ['a composite that opts in without ownAddress', { ...real, src: { ...src, opted: { ...src.opted, 'plugin-designs/x/x-mfe.vue': "column = buildPaneColumn(spec.value, { host: stage.value, address: { owner } })" } } }, /^G: plugin-designs\/x\/x-mfe\.vue opts its column in but does not record/], ['no composite opted in at all', { ...real, src: { ...src, opted: {} } }, /^G: no composite opts its pane column/], + ['Plugin Studio not passing addressDepth', { ...real, src: { ...src, shells: { ...src.shells, 'plugin-studio/plugin-studio.vue': src.shells['plugin-studio/plugin-studio.vue'].replace('pc.addressDepth = urlOwnerOf(this.cfg) ? 1 : depthOf(this.cfg) + 1', 'void 0') } } }, /^G: the shell plugin-studio\/plugin-studio\.vue does not pass addressDepth/], + ['Posterverse claiming its tab without its depth', { ...real, src: { ...src, shells: { ...src.shells, 'posterverse/posterverse.vue': src.shells['posterverse/posterverse.vue'].replace(', depth: depthOf(this.cfg) })', ' })') } } }, /^G: the shell posterverse\/posterverse\.vue does not claim its tab with its depth/], + ['the Research Gallery not reading its subtab segment', { ...real, src: { ...src, shells: { ...src.shells, 'doxa-maps-spa/doxa-maps-spa.vue': src.shells['doxa-maps-spa/doxa-maps-spa.vue'].split('subtabAt(location.search, depthOf(this.cfg))').join('readViewKeys(location.search).tab') } } }, /^G: the shell doxa-maps-spa\/doxa-maps-spa\.vue does not read its own subtab/], ['Plugin Studio without ownAddress', { ...real, src: { ...src, shells: { ...src.shells, 'plugin-studio/plugin-studio.vue': src.shells['plugin-studio/plugin-studio.vue'].replace('this._offOwn = ownAddress(this.$el, () => urlOwnerOf(this.cfg))', 'this._offOwn = null') } } }, /^G: the shell plugin-studio\/plugin-studio\.vue does not record/], ['Plugin Studio releasing ownAddress after its hot-update return', { ...real, src: { ...src, shells: { ...src.shells, 'plugin-studio/plugin-studio.vue': src.shells['plugin-studio/plugin-studio.vue'].replace(' if (this._offOwn) { this._offOwn(); this._offOwn = null }\n if (import.meta.hot) return', ' if (import.meta.hot) return\n if (this._offOwn) { this._offOwn(); this._offOwn = null }') } } }, /^G: the shell plugin-studio\/plugin-studio\.vue does not release/], ['the Research Gallery without ownAddress', { ...real, src: { ...src, shells: { ...src.shells, 'doxa-maps-spa/doxa-maps-spa.vue': src.shells['doxa-maps-spa/doxa-maps-spa.vue'].replace('this._offOwn = ownAddress(', 'this._offOwn = (') } } }, /^G: the shell doxa-maps-spa/], diff --git a/package.json b/package.json index 7fe716620..abb36d0a1 100644 --- a/package.json +++ b/package.json @@ -725,8 +725,8 @@ "check:side-lanes:self-test": "a poster-render.js copy WITHOUT the lane clamp must make check:side-lanes' crossing arm FAIL on the same seeded data (it does: 36 crossings), so the arm is shown able to fail.", "check:poster-section": "the reusable poster section (): the type registry has 'country'; DX Life's /regions slugs (and every other spelling of a region) each show their countries, every country once; a withheld whole-country poster stays withheld through the type; the element's attributes map to its config; PosterSection.vue injects every name its template uses.", "check:poster-section:self-test": "a registry without 'country', a region key without \"&\" → \"and\", and a section that uses a name it does not inject must each make check:poster-section fail.", - "check:view-address": "the page's address (library/composables/useViewAddress.js: ?tab= &pane= &place=) and its ONE history write, on a simulated browser history: the id whitelist + 64 cap and the level rule; an app owner's tab change is one push that keeps history.state with current (Vue Router's field) and the hash; nothing is written when unchanged, on a partner page (no urlOwner), or by a host owner (it hands the host one composed dxm-address). Coexistence with the filters (useShareableView writes through the same commitUrl): a filter write keeps tab/pane/place, a tab/pane/place write keeps the filters, both survive Back and Forward, and re-applying the tab after Back writes nothing. Nested choices (P2): claimView writes for the owner and announces for anyone else; ownAddress records announcements only while it owns, so a pane and its selection are one Back step; pane-wrapper's opt-in ?pane= (a link opens it, a click pushes, a settled scroll replaces, Back never writes; the default pane is never written); the Regions wiring (?pane= on regions-mfe, ?place= on region-dashboards instead of its #slug). Plus the census: every history.pushState/replaceState in library/ and plugin-designs/ source is the helper or a listed writer. Pure node, no build.", - "check:view-address:self-test": "29 poisons must each make check:view-address fail. P1: a write without history.state.current, a write when not the owner, a tab change that keeps a stale place, no length cap, a write when unchanged, a host owner writing history, a dxm-address that cannot leave a shadow root, a filter write that drops tab/pane/place, the old replaceState(null) filter write, a tab write that drops the filters, a tab change written as a replace, a popstate handler that writes, a new history writer, useShareableView or region-dashboards writing history itself again. P2: a pane change that drops place, a non-owner claim that writes, an announcement carrying any key, ownAddress writing with no owner or letting a recorded announcement bubble on, pane-wrapper writing the default pane / a click's selection separately / a push where a scroll settles / on Back / ignoring a link / without the opt-in, and regions-mfe or region-dashboards wired without the address.", + "check:view-address": "the page's address (library/composables/useViewAddress.js: ?tab= &pane= &place=) and its ONE history write, on a simulated browser history: the id whitelist + 64 cap and the level rule; an app owner's tab change is one push that keeps history.state with current (Vue Router's field) and the hash; nothing is written when unchanged, on a partner page (no urlOwner), or by a host owner (it hands the host one composed dxm-address). Coexistence with the filters (useShareableView writes through the same commitUrl): a filter write keeps tab/pane/place, a tab/pane/place write keeps the filters, both survive Back and Forward, and re-applying the tab after Back writes nothing. NESTED SHELLS at any depth (P2b): a nested shell's tab is recorded as segment `depth` of ?subtab= (a path), never as the owner's tab; holes refused, the same tab a no-op, depth 1/2/3. Nested choices (P2): claimView writes for the owner and announces for anyone else; ownAddress records announcements only while it owns, so a pane and its selection are one Back step; pane-wrapper's opt-in ?pane= (a link opens it, a click pushes, a settled scroll replaces, Back never writes; the default pane is never written); the Regions wiring (?pane= on regions-mfe, ?place= on region-dashboards instead of its #slug). Plus the census: every history.pushState/replaceState in library/ and plugin-designs/ source is the helper or a listed writer. Pure node, no build.", + "check:view-address:self-test": "56 poisons must each make check:view-address fail: P1 (a write without history.state.current, a non-owner write, a stale place, no length cap, a write when unchanged or judged by the string, a host owner writing history, an uncomposed dxm-address, filter writes that drop tab/pane/place or wipe the router state, a tab write that drops the filters, a tab change as a replace, a popstate that writes, new history writers); P2 (the old pane/place level rule, non-owner claims, announcements carrying any key, ownAddress misrecording or stacking, pane-wrapper writing the default pane / splitting a click / pushing on a settle / writing on Back / ignoring or losing a fresh-load link / writing without the opt-in / not reclaiming when shown again, the shells and composites wired without ownAddress); P2b (a nested shell's tab dropped, a depth ignored, a hole accepted, the same tab not a no-op, a refused claim dropping the rest, subtab changes keeping pane/place, an owner tab change keeping subtab, paths over 6 or with an empty segment, shells not passing addressDepth / claiming without depth / not reading their segment).", "check:poster-sections": "the Poster Sections page (plugin-designs/poster-sections): every panel choice (each region or All regions, each language, open or collapsed, 1-5 per row) builds tags that the element's own attribute reader turns back into exactly those choices; All regions is one section per region; Copy embed is the bundle script plus those same tags. The presets file holds, per poster type, exactly an all-regions preset plus one per region its adapter's scopes() returns, so a region the data drops (or gains) cannot drift from the presets; a poster type not on the tree has no presets (its pages could show no posters); and the built preset pages are exactly the presets (no stale page).", "check:poster-sections:self-test": "a tag builder that drops columns, one that drops collapsed, one that repeats a region under All regions, a preset for a region the type does not have, and a built page for a preset that no longer exists must each make check:poster-sections fail.", "gen:poster-sections-presets": "writes plugin-designs/poster-sections/poster-sections.instances.json from each poster type's own scopes() (all regions + one preset per region, titled by the type's own section title, then configure); the list check:poster-sections holds the file to.",