| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- 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<void> {
- 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<void> {
- 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<import('@playwright/test').Browser>,
- ): Promise<string> {
- 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<AuthFixtures>({
- 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 };
|