Files
Nexus/frontend/e2e/helpers.mjs
T

95 lines
3.3 KiB
JavaScript
Raw Normal View History

/**
* Shared Playwright helpers for Nexus SPA E2E.
*/
import { expect } from '@playwright/test'
export const BASE = process.env.NEXUS_E2E_BASE || 'https://api.synaglobal.vip'
export const USER = process.env.NEXUS_E2E_USER || 'admin'
export const PASS = process.env.NEXUS_E2E_PASSWORD || ''
export const APP = `${BASE.replace(/\/$/, '')}/app/`
const SIDEBAR_LINKS = {
'/': '仪表盘',
'/servers': '服务器',
'/terminal': '终端',
'/files': '文件管理',
'/push': '推送',
'/scripts': '脚本库',
'/executions': '执行记录',
'/schedules': '调度',
'/retries': '重试队列',
'/commands': '命令日志',
'/alerts': '告警中心',
'/audit': '审计日志',
}
const E2E_ACCESS_TOKEN = process.env.NEXUS_E2E_ACCESS_TOKEN || ''
/** Mock refresh + profile so Pinia bootstraps with a pre-minted JWT (bypass login IP allowlist). */
export async function loginWithAccessToken(page, accessToken = E2E_ACCESS_TOKEN) {
if (!accessToken) {
throw new Error('Set NEXUS_E2E_ACCESS_TOKEN for token-based E2E login')
}
await page.route('**/api/auth/refresh', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ access_token: accessToken }),
})
})
await page.route('**/api/auth/me', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 1,
username: USER,
totp_enabled: false,
created_at: '2020-01-01T00:00:00Z',
}),
})
})
await page.goto(`${APP}#/`, { waitUntil: 'domcontentloaded' })
await expect(page).not.toHaveURL(/#\/login/, { timeout: 20000 })
await expect(page.getByRole('button', { name: USER })).toBeVisible({ timeout: 15000 })
}
/** Restore session from worker refresh cookie (avoids parallel UI logins / lockout). */
export async function login(page) {
if (E2E_ACCESS_TOKEN) {
return loginWithAccessToken(page, E2E_ACCESS_TOKEN)
}
await page.goto(`${APP}#/`, { waitUntil: 'domcontentloaded' })
await expect(page).not.toHaveURL(/#\/login/, { timeout: 20000 })
await expect(page.getByRole('button', { name: 'admin' })).toBeVisible({ timeout: 15000 })
}
/** Navigate via drawer clicks (reliable with Vue keep-alive; hash goto alone is flaky). */
export async function nav(page, hash) {
const path = hash.startsWith('/') ? hash : `/${hash}`
const routePath = path.split('?')[0]
const escaped = routePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
if (routePath === '/settings') {
await page.locator('.v-navigation-drawer').getByText('设置', { exact: true }).click()
} else {
const label = SIDEBAR_LINKS[routePath]
if (!label) throw new Error(`Unknown route for nav(): ${path}`)
await page.getByRole('link', { name: label, exact: true }).click()
}
await expect(page).toHaveURL(new RegExp(`#${escaped}(?:\\?|$)`), { timeout: 20000 })
if (path.includes('?')) {
await page.evaluate((p) => {
window.location.hash = `#${p}`
}, path)
await expect(page).toHaveURL(new RegExp(`#${escaped}\\?`), { timeout: 10000 })
}
await page.waitForLoadState('networkidle').catch(() => {})
}
/** ISO UTC bare string e.g. 2026-06-08T12:00:00Z — should not appear in display columns */
export const ISO_UTC_RE = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/