diff --git a/apps/desktop/e2e/fixtures.ts b/apps/desktop/e2e/fixtures.ts index 94ad73983ba..dc8d4492882 100644 --- a/apps/desktop/e2e/fixtures.ts +++ b/apps/desktop/e2e/fixtures.ts @@ -339,6 +339,7 @@ async function withE2eWindow( export const test = base.extend<{ window: Page; + onboardingWindow: Page; gitReviewWindow: { page: Page; projectRoot: string }; invocableSkillsWindow: Page; linkColorWindow: Page; @@ -350,6 +351,14 @@ export const test = base.extend<{ window: async ({}, use) => { await withE2eWindow({ seed: true, readinessSelector: COMPOSER_INPUT, locale: 'zh' }, use); }, + onboardingWindow: async ({}, use) => { + await withE2eWindow({ + seed: false, + readinessSelector: '[data-maka-contract="onboarding-card"]', + locale: 'zh', + showWindow: true, + }, use); + }, gitReviewWindow: async ({}, use) => { await withE2eWindow( { diff --git a/apps/desktop/e2e/onboarding-viewport.spec.ts b/apps/desktop/e2e/onboarding-viewport.spec.ts new file mode 100644 index 00000000000..56776701b62 --- /dev/null +++ b/apps/desktop/e2e/onboarding-viewport.spec.ts @@ -0,0 +1,98 @@ +import { expect, test } from './fixtures'; + +test('first-run onboarding stays within the chat viewport', async ({ onboardingWindow }) => { + const geometry = await onboardingWindow.evaluate(() => { + const pageRoot = document.scrollingElement; + const scrollContainer = document.querySelector('[data-chat-scroll-container="true"]'); + const surface = document.querySelector('[data-maka-contract="onboarding-surface"]'); + const card = document.querySelector('[data-maka-contract="onboarding-card"]'); + if (!pageRoot || !scrollContainer || !surface || !card) { + throw new Error('Onboarding geometry is unavailable'); + } + + const scrollRect = scrollContainer.getBoundingClientRect(); + const surfaceRect = surface.getBoundingClientRect(); + const cardRect = card.getBoundingClientRect(); + return { + pageClientHeight: pageRoot.clientHeight, + pageScrollHeight: pageRoot.scrollHeight, + clientHeight: scrollContainer.clientHeight, + scrollHeight: scrollContainer.scrollHeight, + surfaceTop: surfaceRect.top, + surfaceBottom: surfaceRect.bottom, + viewportTop: scrollRect.top, + viewportBottom: scrollRect.bottom, + cardTop: cardRect.top, + cardBottom: cardRect.bottom, + }; + }); + + expect(geometry.pageScrollHeight, JSON.stringify(geometry, null, 2)).toBe( + geometry.pageClientHeight, + ); + expect(geometry.scrollHeight, JSON.stringify(geometry, null, 2)).toBe(geometry.clientHeight); + expect(geometry.surfaceTop).toBeGreaterThanOrEqual(geometry.viewportTop); + expect(geometry.surfaceBottom).toBeLessThanOrEqual(geometry.viewportBottom); + expect(geometry.cardTop).toBeGreaterThanOrEqual(geometry.viewportTop); + expect(geometry.cardBottom).toBeLessThanOrEqual(geometry.viewportBottom); +}); + +test('first-run onboarding remains reachable at the minimum window size', async ({ + onboardingWindow, +}) => { + await onboardingWindow.setViewportSize({ width: 480, height: 320 }); + + const surface = onboardingWindow.locator('[data-maka-contract="onboarding-surface"]'); + const initialScrollTop = await surface.evaluate((element) => element.scrollTop); + await surface.hover(); + await onboardingWindow.mouse.wheel(0, 10_000); + await expect.poll(() => surface.evaluate((element) => element.scrollTop)).toBeGreaterThan( + initialScrollTop, + ); + + const geometry = await onboardingWindow.evaluate(() => { + const pageRoot = document.scrollingElement; + const scrollContainer = document.querySelector('[data-chat-scroll-container="true"]'); + const surface = document.querySelector('[data-maka-contract="onboarding-surface"]'); + const content = surface?.querySelector('.maka-onboarding-center'); + if (!pageRoot || !scrollContainer || !surface || !content) { + throw new Error('Onboarding geometry is unavailable'); + } + + const scrollRect = scrollContainer.getBoundingClientRect(); + const surfaceRect = surface.getBoundingClientRect(); + const contentRect = content.getBoundingClientRect(); + + return { + windowInnerWidth: window.innerWidth, + windowInnerHeight: window.innerHeight, + pageClientHeight: pageRoot.clientHeight, + pageScrollHeight: pageRoot.scrollHeight, + viewportClientHeight: scrollContainer.clientHeight, + viewportScrollHeight: scrollContainer.scrollHeight, + viewportTop: scrollRect.top, + viewportBottom: scrollRect.bottom, + surfaceClientHeight: surface.clientHeight, + surfaceScrollHeight: surface.scrollHeight, + surfaceTop: surfaceRect.top, + surfaceBottom: surfaceRect.bottom, + finalScrollTop: surface.scrollTop, + contentBottom: contentRect.bottom, + }; + }); + + expect(geometry.windowInnerWidth).toBe(480); + expect(geometry.windowInnerHeight).toBe(320); + expect(geometry.pageScrollHeight, JSON.stringify(geometry, null, 2)).toBe( + geometry.pageClientHeight, + ); + expect(geometry.viewportScrollHeight, JSON.stringify(geometry, null, 2)).toBe( + geometry.viewportClientHeight, + ); + expect(geometry.surfaceScrollHeight).toBeGreaterThan(geometry.surfaceClientHeight); + expect(initialScrollTop).toBe(0); + expect(geometry.finalScrollTop).toBeGreaterThan(initialScrollTop); + expect(geometry.surfaceTop).toBeGreaterThanOrEqual(geometry.viewportTop); + expect(geometry.surfaceBottom).toBeLessThanOrEqual(geometry.viewportBottom); + expect(geometry.contentBottom).toBeLessThanOrEqual(geometry.surfaceBottom); +}); diff --git a/apps/desktop/src/renderer/styles/onboarding.css b/apps/desktop/src/renderer/styles/onboarding.css index cbb2dd6d41b..07b8166cbec 100644 --- a/apps/desktop/src/renderer/styles/onboarding.css +++ b/apps/desktop/src/renderer/styles/onboarding.css @@ -33,6 +33,25 @@ overscroll-behavior: contain; } +/* The first-run screen is a viewport, not a transcript. ChatMessageList keeps + an empty flex spacer before its empty state so ordinary chat heroes settle + low in the message flow; here that spacer gives the full-height onboarding + surface only half of the available block size. The surface then overflows + ChatLayout and its self-scroll root exposes a phantom page scrollbar. */ +.maka-chat-layout:has(.maka-onboarding-surface) { + overflow-y: hidden; +} + +.maka-chat-layout:has(.maka-onboarding-surface) > :first-child { + flex: 1 1 auto; + padding-block-end: 0; + overflow: hidden; +} + +.maka-chat-message-list:has(.maka-onboarding-surface) > div > [aria-hidden]:has(+ :last-child) { + display: none; +} + /* ChatLayout always paints its frosted composer dock, even when the Composer itself is hidden. Onboarding owns the whole empty surface, so that dock has no content to support and would blur the card actions behind it. Derive the @@ -62,8 +81,11 @@ display: grid; place-items: center; width: 100%; - min-height: calc(100dvh - 84px); + height: 100%; + min-height: 0; padding: var(--space-8) var(--space-6); + overflow-y: auto; + overscroll-behavior: contain; } @media (max-width: 560px) {