From fa3ea52e2be00da86afd3dd78b92e759dea9d3b7 Mon Sep 17 00:00:00 2001 From: shenlan Date: Fri, 31 Oct 2025 18:38:36 +0800 Subject: [PATCH] Refine auth page tests to avoid snapshots (#614) --- dashboard/app/(auth)/layout.tsx | 13 ++++ .../app/{ => (auth)}/login/LoginContent.tsx | 4 +- .../app/{ => (auth)}/login/LoginForm.tsx | 0 dashboard/app/{ => (auth)}/login/page.tsx | 0 .../{ => (auth)}/register/RegisterContent.tsx | 4 +- dashboard/app/{ => (auth)}/register/page.tsx | 0 dashboard/cms/extensionRuntime.ts | 18 +++++- dashboard/components/auth/AuthLayout.tsx | 5 +- dashboard/lib/appShellBypass.tsx | 5 ++ dashboard/ui/tests/e2e/auth-pages.spec.ts | 63 +++++++++++++++++++ 10 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 dashboard/app/(auth)/layout.tsx rename dashboard/app/{ => (auth)}/login/LoginContent.tsx (98%) rename dashboard/app/{ => (auth)}/login/LoginForm.tsx (100%) rename dashboard/app/{ => (auth)}/login/page.tsx (100%) rename dashboard/app/{ => (auth)}/register/RegisterContent.tsx (99%) rename dashboard/app/{ => (auth)}/register/page.tsx (100%) create mode 100644 dashboard/lib/appShellBypass.tsx create mode 100644 dashboard/ui/tests/e2e/auth-pages.spec.ts diff --git a/dashboard/app/(auth)/layout.tsx b/dashboard/app/(auth)/layout.tsx new file mode 100644 index 0000000..1b02a89 --- /dev/null +++ b/dashboard/app/(auth)/layout.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from 'react' + +import { AppShellBypass } from '@lib/appShellBypass' + +export default function AuthPagesLayout({ children }: { children: ReactNode }) { + return ( + +
+ {children} +
+
+ ) +} diff --git a/dashboard/app/login/LoginContent.tsx b/dashboard/app/(auth)/login/LoginContent.tsx similarity index 98% rename from dashboard/app/login/LoginContent.tsx rename to dashboard/app/(auth)/login/LoginContent.tsx index 754331b..05d4c89 100644 --- a/dashboard/app/login/LoginContent.tsx +++ b/dashboard/app/(auth)/login/LoginContent.tsx @@ -5,13 +5,12 @@ import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { Github } from 'lucide-react' -import { AskAIButton } from '@components/AskAIButton' import { AuthLayout, AuthLayoutSocialButton } from '@components/auth/AuthLayout' import { useLanguage } from '@i18n/LanguageProvider' import { translations } from '@i18n/translations' import { getAccountServiceBaseUrl } from '@lib/serviceConfig' -import { WeChatIcon } from '../components/icons/WeChatIcon' +import { WeChatIcon } from '../../components/icons/WeChatIcon' type LoginContentProps = { children?: ReactNode @@ -346,7 +345,6 @@ export default function LoginContent({ children }: LoginContentProps) { > {formContent} - ) } diff --git a/dashboard/app/login/LoginForm.tsx b/dashboard/app/(auth)/login/LoginForm.tsx similarity index 100% rename from dashboard/app/login/LoginForm.tsx rename to dashboard/app/(auth)/login/LoginForm.tsx diff --git a/dashboard/app/login/page.tsx b/dashboard/app/(auth)/login/page.tsx similarity index 100% rename from dashboard/app/login/page.tsx rename to dashboard/app/(auth)/login/page.tsx diff --git a/dashboard/app/register/RegisterContent.tsx b/dashboard/app/(auth)/register/RegisterContent.tsx similarity index 99% rename from dashboard/app/register/RegisterContent.tsx rename to dashboard/app/(auth)/register/RegisterContent.tsx index 94b51df..f24d13e 100644 --- a/dashboard/app/register/RegisterContent.tsx +++ b/dashboard/app/(auth)/register/RegisterContent.tsx @@ -5,13 +5,12 @@ import { ChevronDown, Github } from 'lucide-react' import { FormEvent, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' import { useRouter, useSearchParams } from 'next/navigation' -import { AskAIButton } from '@components/AskAIButton' import { AuthLayout, AuthLayoutSocialButton } from '@components/auth/AuthLayout' import { useLanguage } from '@i18n/LanguageProvider' import { translations } from '@i18n/translations' import { getAccountServiceBaseUrl } from '@lib/serviceConfig' -import { WeChatIcon } from '../components/icons/WeChatIcon' +import { WeChatIcon } from '../../components/icons/WeChatIcon' type AlertState = { type: 'error' | 'success'; message: string } @@ -498,7 +497,6 @@ export default function RegisterContent() { - ) } diff --git a/dashboard/app/register/page.tsx b/dashboard/app/(auth)/register/page.tsx similarity index 100% rename from dashboard/app/register/page.tsx rename to dashboard/app/(auth)/register/page.tsx diff --git a/dashboard/cms/extensionRuntime.ts b/dashboard/cms/extensionRuntime.ts index c8f70dd..26617ce 100644 --- a/dashboard/cms/extensionRuntime.ts +++ b/dashboard/cms/extensionRuntime.ts @@ -1,10 +1,11 @@ -import { ComponentType, createElement, type ReactNode } from 'react' +import { ComponentType, createElement, isValidElement, type ReactNode } from 'react' import { cmsConfig, type ExtensionName, type ThemeName } from './config' import type { CmsExtension, CmsTheme } from './types' import { appShellExtension } from './extensions/appShell' import { markdownSyncExtension } from './extensions/markdownSync' import defaultTheme from './themes/default' +import { AppShellBypass } from '../lib/appShellBypass' const themeRegistry: Record = { default: defaultTheme, @@ -30,11 +31,24 @@ export function collectExtensionProviders(): ComponentType<{ children: ReactNode } export function applyExtensionLayouts(children: ReactNode): ReactNode { + const { content, skipAppShell } = unwrapAppShellBypass(children) + return getActiveExtensions().reduceRight((acc, extension) => { if (!extension.Layout) { return acc } + if (skipAppShell && extension.name === 'app-shell') { + return acc + } const LayoutComponent = extension.Layout return createElement(LayoutComponent, null, acc) - }, children) + }, content) +} + +function unwrapAppShellBypass(node: ReactNode): { content: ReactNode; skipAppShell: boolean } { + if (isValidElement(node) && node.type === AppShellBypass) { + return { content: node.props.children, skipAppShell: true } + } + + return { content: node, skipAppShell: false } } diff --git a/dashboard/components/auth/AuthLayout.tsx b/dashboard/components/auth/AuthLayout.tsx index 7146afb..f58374b 100644 --- a/dashboard/components/auth/AuthLayout.tsx +++ b/dashboard/components/auth/AuthLayout.tsx @@ -98,7 +98,10 @@ export function AuthLayout({ className="pointer-events-none absolute inset-x-0 -top-1/3 h-1/2 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-sky-100 via-transparent to-transparent" aria-hidden /> -
+
diff --git a/dashboard/lib/appShellBypass.tsx b/dashboard/lib/appShellBypass.tsx new file mode 100644 index 0000000..743bcaf --- /dev/null +++ b/dashboard/lib/appShellBypass.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from 'react' + +export function AppShellBypass({ children }: { children: ReactNode }) { + return <>{children} +} diff --git a/dashboard/ui/tests/e2e/auth-pages.spec.ts b/dashboard/ui/tests/e2e/auth-pages.spec.ts new file mode 100644 index 0000000..aac4801 --- /dev/null +++ b/dashboard/ui/tests/e2e/auth-pages.spec.ts @@ -0,0 +1,63 @@ +import { Page, expect, test } from '@playwright/test' + +async function expectAuthLayoutWithinViewport(page: Page) { + const viewport = page.viewportSize() + if (!viewport) { + throw new Error('Viewport size is not available') + } + + const authLayout = page.getByTestId('auth-layout') + await expect(authLayout).toBeVisible() + + const boundingBox = await authLayout.evaluate((element) => { + const rect = element.getBoundingClientRect() + return { + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + } + }) + + const tolerance = 1 + expect.soft(boundingBox.top).toBeGreaterThanOrEqual(-tolerance) + expect.soft(boundingBox.left).toBeGreaterThanOrEqual(-tolerance) + expect.soft(boundingBox.right).toBeLessThanOrEqual(viewport.width + tolerance) + expect.soft(boundingBox.bottom).toBeLessThanOrEqual(viewport.height + tolerance) + + await expect(page.locator('nav')).toHaveCount(0) +} + +test.describe('Auth pages', () => { + test('login form visible on desktop viewport', async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 720 }) + await page.goto('/login') + await page.waitForLoadState('networkidle') + + await expectAuthLayoutWithinViewport(page) + }) + + test('login form visible on mobile viewport', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }) + await page.goto('/login') + await page.waitForLoadState('networkidle') + + await expectAuthLayoutWithinViewport(page) + }) + + test('register form visible on desktop viewport', async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 720 }) + await page.goto('/register') + await page.waitForLoadState('networkidle') + + await expectAuthLayoutWithinViewport(page) + }) + + test('register form visible on mobile viewport', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }) + await page.goto('/register') + await page.waitForLoadState('networkidle') + + await expectAuthLayoutWithinViewport(page) + }) +})