diff --git a/neode-ui/src/views/dashboard/DashboardRouterView.vue b/neode-ui/src/views/dashboard/DashboardRouterView.vue index b8135c53..c0356a83 100644 --- a/neode-ui/src/views/dashboard/DashboardRouterView.vue +++ b/neode-ui/src/views/dashboard/DashboardRouterView.vue @@ -1,126 +1,68 @@ diff --git a/neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts b/neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts index e10fc9a1..35c13707 100644 --- a/neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts +++ b/neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts @@ -101,6 +101,56 @@ describe('DashboardRouterView keep-alive behavior', () => { expect(shouldKeepAlive({ path: '/dashboard/marketplace' })).toBe(true) expect(shouldKeepAlive({ path: '/dashboard/marketplace/abc' })).toBe(false) }) + + // Visual contract (Task 3 checkpoint regression): the padded default-shape + // wrapper must render INSIDE `div.view-wrapper`, never the other way around. + // dashboard-styles.css scopes every transition as a compound selector + // (`.slide-up-enter-active.view-wrapper`, …) and `.view-wrapper` is + // `absolute inset-0` — if `view-wrapper` ever moves onto the view root + // inside the padded wrapper, page margins break and the slide/depth + // transitions stop firing. This test pins the structure so that regression + // cannot silently return. + it('renders the padded default wrapper inside div.view-wrapper', async () => { + const router = makeRouter() + router.push('/dashboard/other') + await router.isReady() + + const wrapper = mount(DashboardRouterView, { + props: { mobileTabPaddingTop: null, needsMobileBackButtonSpace: false }, + global: { plugins: [router] }, + }) + await flushPromises() + + const viewWrapper = wrapper.find('.view-wrapper') + expect(viewWrapper.exists()).toBe(true) + + // view-wrapper is the bare transition surface — no padding classes on it. + expect(viewWrapper.classes()).not.toContain('px-4') + expect(viewWrapper.classes()).not.toContain('view-container') + + // Its first child is the padded scroll container carrying the page margins. + const padded = viewWrapper.element.firstElementChild as HTMLElement + expect(padded).not.toBeNull() + for (const cls of [ + 'absolute', 'inset-0', 'px-4', 'pt-4', 'md:pt-8', 'md:px-8', + 'overflow-y-auto', 'mobile-safe-top', 'dashboard-scroll-panel', 'mobile-scroll-pad', + ]) { + expect(padded.classList.contains(cls), `padded wrapper missing ${cls}`).toBe(true) + } + + // The routed view sits inside the padded wrapper with its per-component + // classes, followed by the scroll-clearance spacer. + const view = wrapper.find('.other-stub') + expect(view.exists()).toBe(true) + expect(view.classes()).toContain('view-container') + expect(view.classes()).toContain('flex-none') + expect(padded.contains(view.element)).toBe(true) + const spacer = padded.querySelector('[aria-hidden="true"]') + expect(spacer).not.toBeNull() + expect(spacer!.classList.contains('shrink-0')).toBe(true) + + wrapper.unmount() + }) }) describe('RefreshIndicator', () => { diff --git a/neode-ui/src/views/dashboard/dashboardViewWrappers.ts b/neode-ui/src/views/dashboard/dashboardViewWrappers.ts new file mode 100644 index 00000000..001be55e Binary files /dev/null and b/neode-ui/src/views/dashboard/dashboardViewWrappers.ts differ