From 96ece8355096fa27b93d59aabd19b78e9cc4b5eb Mon Sep 17 00:00:00 2001 From: HuuThanh1610 Date: Sun, 16 Aug 2026 21:28:12 +0700 Subject: [PATCH] fix: landing page content --- apps/web/e2e-dev/landing-navigation.spec.ts | 25 ++++++++++++++++ apps/web/package.json | 4 +-- apps/web/playwright.config.ts | 4 +-- apps/web/playwright.dev.config.ts | 2 +- .../web/src/features/landing/landing-page.tsx | 16 ++++++---- apps/web/test/landing-page.test.tsx | 30 +++++++++++++++++++ prototypes/databreeze-landing/script.js | 4 +++ 7 files changed, 75 insertions(+), 10 deletions(-) diff --git a/apps/web/e2e-dev/landing-navigation.spec.ts b/apps/web/e2e-dev/landing-navigation.spec.ts index e0508683..e7c5898c 100644 --- a/apps/web/e2e-dev/landing-navigation.spec.ts +++ b/apps/web/e2e-dev/landing-navigation.spec.ts @@ -4,6 +4,7 @@ const landingAnchors = [ { label: 'Sản phẩm', id: 'flow' }, { label: 'AI có kiểm chứng', id: 'intelligence' }, { label: 'Chế độ dữ liệu', id: 'modes' }, + { label: 'Bảng giá', id: 'pricing' }, ] as const; test('landing navigation reveals and offsets each destination [WEB-013/WEB-014]', async ({ @@ -11,13 +12,27 @@ test('landing navigation reveals and offsets each destination [WEB-013/WEB-014]' }) => { await page.goto('/vi-VN'); + await expect + .poll(() => + page + .locator('.interactive-surface') + .evaluateAll((surfaces) => + surfaces.every( + (surface) => surface.querySelectorAll(':scope > .surface-glow').length === 1, + ), + ), + ) + .toBe(true); + const headerHeight = await page .locator('[data-header]') .evaluate((element) => Math.round(element.getBoundingClientRect().height)); + const visitedDestinationIds: string[] = []; for (const { label, id } of landingAnchors) { await page.getByRole('link', { name: label, exact: true }).click(); await expect(page).toHaveURL(new RegExp(`#${id}$`)); + visitedDestinationIds.push(id); const destination = page.locator(`#${id}`); await expect(destination.locator('[data-reveal]').first()).toHaveClass(/is-visible/); @@ -26,6 +41,16 @@ test('landing navigation reveals and offsets each destination [WEB-013/WEB-014]' destination.evaluate((element) => Math.round(element.getBoundingClientRect().top)), ) .toBeGreaterThanOrEqual(headerHeight); + + for (const visitedId of visitedDestinationIds) { + await expect + .poll(() => + page + .locator(`#${visitedId} [data-reveal]`) + .evaluateAll((items) => items.every((item) => item.classList.contains('is-visible'))), + ) + .toBe(true); + } } }); diff --git a/apps/web/package.json b/apps/web/package.json index d4077ba0..db037a46 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,11 +6,11 @@ "scripts": { "browser:install:ci": "playwright install --with-deps chromium", "budget": "node scripts/check-bundle-budget.mjs", - "build": "tsc --build && vite build && corepack pnpm budget", + "build": "tsc --build && vite build && pnpm budget", "dev": "vite", "preview": "vite preview", "test": "vitest run", - "test:e2e": "corepack pnpm test:e2e:preview && corepack pnpm test:e2e:dev", + "test:e2e": "pnpm test:e2e:preview && pnpm test:e2e:dev", "test:e2e:dev": "playwright test --config playwright.dev.config.ts", "test:e2e:preview": "playwright test", "test:watch": "vitest", diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index e56aa2e5..cdd96611 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -5,8 +5,8 @@ const browserChannel = process.env['PLAYWRIGHT_BROWSER_CHANNEL']; const browser = browserChannel === 'chrome' ? { channel: 'chrome' as const } : {}; const previewCommand = process.env['PLAYWRIGHT_SKIP_BUILD'] === 'true' - ? `corepack pnpm preview --host 127.0.0.1 --port ${previewPort} --strictPort` - : `corepack pnpm build && corepack pnpm preview --host 127.0.0.1 --port ${previewPort} --strictPort`; + ? `pnpm preview --host 127.0.0.1 --port ${previewPort} --strictPort` + : `pnpm build && pnpm preview --host 127.0.0.1 --port ${previewPort} --strictPort`; export default defineConfig({ timeout: 30_000, diff --git a/apps/web/playwright.dev.config.ts b/apps/web/playwright.dev.config.ts index 47491aec..46d4b44e 100644 --- a/apps/web/playwright.dev.config.ts +++ b/apps/web/playwright.dev.config.ts @@ -18,7 +18,7 @@ export default defineConfig({ trace: 'retain-on-failure', }, webServer: { - command: `corepack pnpm dev --host 127.0.0.1 --port ${devPort} --strictPort`, + command: `pnpm dev --host 127.0.0.1 --port ${devPort} --strictPort`, env: { VITE_DATABREEZE_DEMO_MODE: 'true' }, reuseExistingServer: false, timeout: 120_000, diff --git a/apps/web/src/features/landing/landing-page.tsx b/apps/web/src/features/landing/landing-page.tsx index 8781184e..1946097e 100644 --- a/apps/web/src/features/landing/landing-page.tsx +++ b/apps/web/src/features/landing/landing-page.tsx @@ -28,6 +28,10 @@ export function LandingPage({ }), [locale], ); + // Keep the innerHTML prop stable while only the route hash changes. The + // landing script progressively adds reveal state and interaction layers to + // this DOM; replacing it on every navbar jump would discard that state. + const markupProperty = useMemo(() => ({ __html: markup }), [markup]); useEffect(() => { if (import.meta.env.MODE === 'test') return undefined; @@ -79,10 +83,12 @@ export function LandingPage({ useEffect(() => { const root = document.querySelector('.teammate-landing-root'); - const pricingSection = root?.querySelector('[data-pricing-section]'); - if (root === null || pricingSection === null) return; + if (root === null) return; - const pricingLocale = pricingSection.dataset.pricingLocale === 'en' ? 'en-US' : 'vi-VN'; + const pricingSection = root.querySelector('[data-pricing-section]'); + if (pricingSection === null) return; + + const pricingLocale = pricingSection.dataset['pricingLocale'] === 'en' ? 'en-US' : 'vi-VN'; const formatPricingAmount = (value: number) => `${new Intl.NumberFormat(pricingLocale).format(value)} ₫`; @@ -92,7 +98,7 @@ export function LandingPage({ const button = target.closest('[data-pricing-cycle]'); if (button === null || !root.contains(button)) return; - const cycle = button.dataset.pricingCycle; + const cycle = button.dataset['pricingCycle']; if (cycle !== 'monthly' && cycle !== 'annual') return; root.querySelectorAll('[data-pricing-cycle]').forEach((item) => { @@ -129,7 +135,7 @@ export function LandingPage({ return ( <> -
+
); } diff --git a/apps/web/test/landing-page.test.tsx b/apps/web/test/landing-page.test.tsx index bb171f84..553fe3ba 100644 --- a/apps/web/test/landing-page.test.tsx +++ b/apps/web/test/landing-page.test.tsx @@ -108,6 +108,36 @@ describe('landing page stylesheet [WEB-013]', () => { }, ); + it('preserves previously revealed sections across consecutive navbar hash navigation', async () => { + const router = createAppRouter({ + authenticationState: 'signed-out', + initialEntries: ['/vi-VN'], + }); + + render(); + await screen.findByRole('heading', { name: /DataBreeze Dữ liệu biết cất lời\./u }); + + await router.navigate('/vi-VN#flow'); + const flowHeading = await waitFor(() => { + const heading = document.querySelector('#flow [data-reveal]'); + expect(heading?.classList.contains('is-visible')).toBe(true); + return heading; + }); + + await router.navigate('/vi-VN#intelligence'); + await waitFor(() => { + expect( + document + .querySelector('#intelligence [data-reveal]') + ?.classList.contains('is-visible'), + ).toBe(true); + }); + + const preservedFlowHeading = document.querySelector('#flow [data-reveal]'); + expect(preservedFlowHeading).toBe(flowHeading); + expect(preservedFlowHeading?.classList.contains('is-visible')).toBe(true); + }); + it('removes the landing stylesheet when navbar routing leaves the page', async () => { const router = createAppRouter({ authenticationState: 'signed-out', diff --git a/prototypes/databreeze-landing/script.js b/prototypes/databreeze-landing/script.js index 465e73c2..45a1a802 100644 --- a/prototypes/databreeze-landing/script.js +++ b/prototypes/databreeze-landing/script.js @@ -1,6 +1,10 @@ (() => { 'use strict'; + const landingRoot = document.querySelector('.teammate-landing-root') ?? document.body; + if (landingRoot.dataset.landingEnhanced === 'true') return; + landingRoot.dataset.landingEnhanced = 'true'; + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const header = document.querySelector('[data-header]'); const revealItems = document.querySelectorAll('[data-reveal]');