auth.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import { test as base, expect, type Page } from '@playwright/test';
  2. import fs from 'node:fs';
  3. import path from 'node:path';
  4. import { fileURLToPath } from 'node:url';
  5. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  6. export interface AuthCreds {
  7. account: string;
  8. password: string;
  9. }
  10. export const DEFAULT_CREDS: AuthCreds = {
  11. account: process.env.AIDOP_E2E_ACCOUNT ?? 'superAdmin.NET',
  12. password: process.env.AIDOP_E2E_PASSWORD ?? '1234567890dop',
  13. };
  14. export const STORAGE_STATE_PATH = path.resolve(
  15. __dirname,
  16. '../.auth/storage-state.json',
  17. );
  18. async function selectFirstTenantIfPresent(page: Page): Promise<void> {
  19. const tenantSelect = page.locator('.login-content-form .el-select').first();
  20. const visible = await tenantSelect.isVisible().catch(() => false);
  21. if (!visible) return;
  22. const wrapper = tenantSelect.locator('.el-select__wrapper').first();
  23. await wrapper.click({ force: true });
  24. const option = page
  25. .locator('.el-select-dropdown:visible .el-select-dropdown__item:not(.is-disabled)')
  26. .first();
  27. await expect(option).toBeVisible({ timeout: 15000 });
  28. await option.click();
  29. }
  30. /**
  31. * Drive the login page end-to-end. Leaves `page` on /#/dashboard/home with a valid session.
  32. * Does NOT navigate further — callers decide where to go next.
  33. */
  34. export async function performLogin(page: Page, creds: AuthCreds = DEFAULT_CREDS): Promise<void> {
  35. await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60000 });
  36. await page.waitForLoadState('networkidle', { timeout: 60000 }).catch(() => {});
  37. const loginButton = page.getByRole('button', { name: '登 录' });
  38. await expect(loginButton).toBeVisible({ timeout: 30000 });
  39. await page.getByPlaceholder('请输入账号').fill(creds.account);
  40. await page.getByPlaceholder('请输入密码').fill(creds.password);
  41. await selectFirstTenantIfPresent(page);
  42. const loginRespPromise = page
  43. .waitForResponse(
  44. (r) => r.url().includes('/api/sysAuth/login') && r.request().method() === 'POST',
  45. { timeout: 30000 },
  46. )
  47. .catch(() => null);
  48. await loginButton.click();
  49. const rotateVerifyVisible = await page
  50. .locator('.el-dialog')
  51. .filter({ hasText: '请按住滑块拖动' })
  52. .isVisible()
  53. .catch(() => false);
  54. if (rotateVerifyVisible) {
  55. throw new Error('登录触发了旋转验证,fixture 未处理该交互');
  56. }
  57. const loginResp = await loginRespPromise;
  58. if (!loginResp) {
  59. throw new Error('点击登录后 30s 内未捕获到 /api/sysAuth/login');
  60. }
  61. if (loginResp.status() !== 200) {
  62. throw new Error(`登录失败,HTTP ${loginResp.status()}: ${await loginResp.text()}`);
  63. }
  64. await page.waitForURL(/#\/(dashboard|)/, { timeout: 30000 }).catch(() => {});
  65. await page.waitForLoadState('networkidle', { timeout: 60000 }).catch(() => {});
  66. }
  67. /**
  68. * Ensure storageState file exists. If not, drive a real browser login once and persist it.
  69. * Called by global-setup; tests themselves just consume `authedPage`.
  70. */
  71. export async function ensureStorageState(
  72. browserFactory: () => Promise<import('@playwright/test').Browser>,
  73. ): Promise<string> {
  74. if (fs.existsSync(STORAGE_STATE_PATH)) {
  75. return STORAGE_STATE_PATH;
  76. }
  77. fs.mkdirSync(path.dirname(STORAGE_STATE_PATH), { recursive: true });
  78. const browser = await browserFactory();
  79. try {
  80. const ctx = await browser.newContext({ baseURL: 'http://localhost:8888' });
  81. const page = await ctx.newPage();
  82. await performLogin(page);
  83. await ctx.storageState({ path: STORAGE_STATE_PATH });
  84. await ctx.close();
  85. } finally {
  86. await browser.close();
  87. }
  88. return STORAGE_STATE_PATH;
  89. }
  90. type AuthFixtures = {
  91. authedPage: Page;
  92. };
  93. /**
  94. * `test` extended with an `authedPage` fixture. `authedPage` is a page that has been
  95. * navigated to '/' under the shared storageState — the session has already been hydrated.
  96. */
  97. export const test = base.extend<AuthFixtures>({
  98. authedPage: async ({ page }, use) => {
  99. // storageState is applied at project level via playwright.config.ts.
  100. await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60000 });
  101. await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
  102. // If storageState expired and we land on /#/login, re-login inline to self-heal.
  103. if (/\/#\/login/.test(page.url())) {
  104. await performLogin(page);
  105. }
  106. await use(page);
  107. },
  108. });
  109. export { expect };