The settings shell painted the onboarding overlay (opaque stage, mask, #root inert) the moment a step was registered and not locally completed, while every step still had to load its private join before deciding whether to show — rendering null could not suppress the shell-owned chrome. Every reload on the hero therefore flashed a full-screen opaque layer (white in the light palette) for one settings/credential RPC round-trip after the session list turned ready. The chrome now belongs to the step: a new zero-cordis OnboardingSurface primitive (ui-primitives) renders the body-portaled overlay/mask/stage verbatim from the former SettingsRoot stylesheet and holds #root inert for exactly its own lifetime. WelcomeNotice and DeepSeekOnboardingDialog wrap only their visible branch in it, so their existing null branches paint and block nothing by construction. SettingsRoot keeps the coordinator unchanged but renders the elected step bare, and the settings.onboarding contract now names the surface wrap as the registrant's obligation. The onboarding e2e gains a held-join reload scenario pinning that a configured world never mounts the takeover chrome or inerts the app.
48 lines
1.7 KiB
TypeScript
48 lines
1.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { cleanup, render } from '@testing-library/react'
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { OnboardingSurface } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
|
|
let appRoot: HTMLDivElement
|
|
|
|
beforeEach(() => {
|
|
appRoot = document.createElement('div')
|
|
appRoot.id = 'root'
|
|
document.body.appendChild(appRoot)
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
appRoot.remove()
|
|
})
|
|
|
|
describe('OnboardingSurface', () => {
|
|
it('portals the overlay chrome to document.body around its content', () => {
|
|
const view = render(<OnboardingSurface><p>step content</p></OnboardingSurface>)
|
|
// Portaled: the overlay is a body child, not inside the render container.
|
|
expect(view.container.querySelector('[class*="onboardingOverlay"]')).toBeNull()
|
|
const overlay = document.body.querySelector('[class*="onboardingOverlay"]')
|
|
expect(overlay).not.toBeNull()
|
|
// The onboarding e2e pins the mask by class substring; the stage carries
|
|
// the content.
|
|
expect(overlay!.querySelector('[class*="onboardingMask"]')).not.toBeNull()
|
|
const stage = overlay!.querySelector('[class*="onboardingStage"]')
|
|
expect(stage).not.toBeNull()
|
|
expect(stage!.textContent).toBe('step content')
|
|
})
|
|
|
|
it('holds #root inert for exactly its own lifetime', () => {
|
|
const view = render(<OnboardingSurface>x</OnboardingSurface>)
|
|
expect(appRoot.inert).toBe(true)
|
|
view.unmount()
|
|
expect(appRoot.inert).toBe(false)
|
|
})
|
|
|
|
it('renders without an #root element (compositions that mount elsewhere)', () => {
|
|
appRoot.remove()
|
|
const view = render(<OnboardingSurface>x</OnboardingSurface>)
|
|
expect(document.body.querySelector('[class*="onboardingStage"]')!.textContent).toBe('x')
|
|
view.unmount()
|
|
})
|
|
})
|