import { test as base, expect, type Page } from '@playwright/test'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export interface AuthCreds { account: string; password: string; } export const DEFAULT_CREDS: AuthCreds = { account: process.env.AIDOP_E2E_ACCOUNT ?? 'superAdmin.NET', password: process.env.AIDOP_E2E_PASSWORD ?? '1234567890dop', }; export const STORAGE_STATE_PATH = path.resolve( __dirname, '../.auth/storage-state.json', ); async function selectFirstTenantIfPresent(page: Page): Promise { const tenantSelect = page.locator('.login-content-form .el-select').first(); const visible = await tenantSelect.isVisible().catch(() => false); if (!visible) return; const wrapper = tenantSelect.locator('.el-select__wrapper').first(); await wrapper.click({ force: true }); const option = page .locator('.el-select-dropdown:visible .el-select-dropdown__item:not(.is-disabled)') .first(); await expect(option).toBeVisible({ timeout: 15000 }); await option.click(); } /** * Drive the login page end-to-end. Leaves `page` on /#/dashboard/home with a valid session. * Does NOT navigate further — callers decide where to go next. */ export async function performLogin(page: Page, creds: AuthCreds = DEFAULT_CREDS): Promise { await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60000 }); await page.waitForLoadState('networkidle', { timeout: 60000 }).catch(() => {}); const loginButton = page.getByRole('button', { name: '登 录' }); await expect(loginButton).toBeVisible({ timeout: 30000 }); await page.getByPlaceholder('请输入账号').fill(creds.account); await page.getByPlaceholder('请输入密码').fill(creds.password); await selectFirstTenantIfPresent(page); const loginRespPromise = page .waitForResponse( (r) => r.url().includes('/api/sysAuth/login') && r.request().method() === 'POST', { timeout: 30000 }, ) .catch(() => null); await loginButton.click(); const rotateVerifyVisible = await page .locator('.el-dialog') .filter({ hasText: '请按住滑块拖动' }) .isVisible() .catch(() => false); if (rotateVerifyVisible) { throw new Error('登录触发了旋转验证,fixture 未处理该交互'); } const loginResp = await loginRespPromise; if (!loginResp) { throw new Error('点击登录后 30s 内未捕获到 /api/sysAuth/login'); } if (loginResp.status() !== 200) { throw new Error(`登录失败,HTTP ${loginResp.status()}: ${await loginResp.text()}`); } await page.waitForURL(/#\/(dashboard|)/, { timeout: 30000 }).catch(() => {}); await page.waitForLoadState('networkidle', { timeout: 60000 }).catch(() => {}); } /** * Ensure storageState file exists. If not, drive a real browser login once and persist it. * Called by global-setup; tests themselves just consume `authedPage`. */ export async function ensureStorageState( browserFactory: () => Promise, ): Promise { if (fs.existsSync(STORAGE_STATE_PATH)) { return STORAGE_STATE_PATH; } fs.mkdirSync(path.dirname(STORAGE_STATE_PATH), { recursive: true }); const browser = await browserFactory(); try { const ctx = await browser.newContext({ baseURL: 'http://localhost:8888' }); const page = await ctx.newPage(); await performLogin(page); await ctx.storageState({ path: STORAGE_STATE_PATH }); await ctx.close(); } finally { await browser.close(); } return STORAGE_STATE_PATH; } type AuthFixtures = { authedPage: Page; }; /** * `test` extended with an `authedPage` fixture. `authedPage` is a page that has been * navigated to '/' under the shared storageState — the session has already been hydrated. */ export const test = base.extend({ authedPage: async ({ page }, use) => { // storageState is applied at project level via playwright.config.ts. await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60000 }); await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {}); // If storageState expired and we land on /#/login, re-login inline to self-heal. if (/\/#\/login/.test(page.url())) { await performLogin(page); } await use(page); }, }); export { expect };