Fix prelaunch navigation and log scale regressions
This commit is contained in:
@@ -138,6 +138,14 @@ const CDP_CONNECT_TIMEOUT_MS = Number(process.env.E2E_CDP_CONNECT_TIMEOUT_MS ??
|
||||
const SMTP_CAPTURE_FILE = (process.env.E2E_SMTP_CAPTURE_FILE ?? '').trim()
|
||||
const REFRESH_TOKEN_COOKIE_NAME = 'ums_refresh_token'
|
||||
const SESSION_PRESENCE_COOKIE_NAME = 'ums_session_present'
|
||||
const SIDEBAR_GROUP_TEST_IDS = new Map([
|
||||
[TEXT.accessControl, 'nav-group-access-control'],
|
||||
])
|
||||
const SIDEBAR_MENU_TEST_IDS = new Map([
|
||||
[TEXT.dashboard, 'nav-dashboard'],
|
||||
[TEXT.users, 'nav-users'],
|
||||
[TEXT.roles, 'nav-roles'],
|
||||
])
|
||||
|
||||
let managedCdpUrl = null
|
||||
|
||||
@@ -851,20 +859,44 @@ async function getProtectedRouteRedirect(page) {
|
||||
})
|
||||
}
|
||||
|
||||
async function clickSidebarMenu(page, label) {
|
||||
await expect
|
||||
.poll(async () => await page.locator('.ant-layout-sider .ant-menu-item, .ant-drawer .ant-menu-item').count())
|
||||
.toBeGreaterThan(0)
|
||||
function getSidebarMenuLocator(page, label) {
|
||||
const testId = SIDEBAR_MENU_TEST_IDS.get(label)
|
||||
if (testId) {
|
||||
return page.locator(`.ant-layout-sider [data-testid="${testId}"], .ant-drawer.ant-drawer-open [data-testid="${testId}"]`)
|
||||
}
|
||||
|
||||
const menuItems = page
|
||||
.locator('.ant-layout-sider .ant-menu-item, .ant-drawer .ant-menu-item')
|
||||
return page
|
||||
.locator('.ant-layout-sider .ant-menu-item, .ant-drawer.ant-drawer-open .ant-menu-item')
|
||||
.filter({ hasText: label })
|
||||
}
|
||||
|
||||
function getSidebarGroupLocator(page, label) {
|
||||
const testId = SIDEBAR_GROUP_TEST_IDS.get(label)
|
||||
if (testId) {
|
||||
return page.locator(
|
||||
`.ant-layout-sider [data-testid="${testId}"], .ant-drawer.ant-drawer-open [data-testid="${testId}"]`,
|
||||
)
|
||||
}
|
||||
|
||||
return page
|
||||
.locator('.ant-layout-sider .ant-menu-submenu-title, .ant-drawer.ant-drawer-open .ant-menu-submenu-title')
|
||||
.filter({ hasText: label })
|
||||
}
|
||||
|
||||
async function clickSidebarMenu(page, label) {
|
||||
const menuItems = getSidebarMenuLocator(page, label)
|
||||
await expect.poll(async () => await menuItems.count()).toBeGreaterThan(0)
|
||||
|
||||
const count = await menuItems.count()
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const menuItem = menuItems.nth(index)
|
||||
if (await menuItem.isVisible()) {
|
||||
await forceClick(menuItem)
|
||||
try {
|
||||
await menuItem.scrollIntoViewIfNeeded()
|
||||
await menuItem.click({ force: true, timeout: 5_000 })
|
||||
} catch {
|
||||
await forceClick(menuItem)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -878,30 +910,21 @@ async function openMobileNavigationIfNeeded(page) {
|
||||
return false
|
||||
}
|
||||
|
||||
const mobileMenuButton = page.locator('.ant-layout-header .ant-btn').first()
|
||||
const mobileMenuButton = page.getByTestId('mobile-nav-trigger')
|
||||
if (!(await mobileMenuButton.isVisible().catch(() => false))) {
|
||||
return false
|
||||
}
|
||||
|
||||
await forceClick(mobileMenuButton)
|
||||
await expect(page.locator('.ant-drawer-content')).toBeVisible({ timeout: 10 * 1000 })
|
||||
await expect(page.locator('.ant-drawer.ant-drawer-open .ant-drawer-content')).toBeVisible({ timeout: 10 * 1000 })
|
||||
return true
|
||||
}
|
||||
|
||||
async function expandSidebarGroup(page, label) {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await page
|
||||
.locator('.ant-layout-sider .ant-menu-submenu-title, .ant-drawer .ant-menu-submenu-title')
|
||||
.count()
|
||||
})
|
||||
.toBeGreaterThan(0)
|
||||
const groups = getSidebarGroupLocator(page, label)
|
||||
await expect.poll(async () => await groups.count()).toBeGreaterThan(0)
|
||||
|
||||
const findVisibleGroup = async () => {
|
||||
const groups = page
|
||||
.locator('.ant-layout-sider .ant-menu-submenu-title, .ant-drawer .ant-menu-submenu-title')
|
||||
.filter({ hasText: label })
|
||||
|
||||
const count = await groups.count()
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const group = groups.nth(index)
|
||||
@@ -920,7 +943,24 @@ async function expandSidebarGroup(page, label) {
|
||||
}
|
||||
|
||||
if (group) {
|
||||
await forceClick(group)
|
||||
const isExpanded = await group.evaluate((element) => {
|
||||
return element.closest('.ant-menu-submenu')?.classList.contains('ant-menu-submenu-open') ?? false
|
||||
})
|
||||
|
||||
if (!isExpanded) {
|
||||
try {
|
||||
await group.scrollIntoViewIfNeeded()
|
||||
await group.click({ force: true, timeout: 5_000 })
|
||||
} catch {
|
||||
await forceClick(group)
|
||||
}
|
||||
|
||||
await expect.poll(async () => {
|
||||
return await group.evaluate((element) => {
|
||||
return element.closest('.ant-menu-submenu')?.classList.contains('ant-menu-submenu-open') ?? false
|
||||
})
|
||||
}).toBe(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -933,8 +973,10 @@ async function expandSidebarGroup(page, label) {
|
||||
return {
|
||||
currentUrl: window.location.href,
|
||||
innerWidth: window.innerWidth,
|
||||
submenuTitles: visibleText('.ant-layout-sider .ant-menu-submenu-title, .ant-drawer .ant-menu-submenu-title'),
|
||||
menuItems: visibleText('.ant-layout-sider .ant-menu-item, .ant-drawer .ant-menu-item'),
|
||||
submenuTitles: visibleText(
|
||||
'.ant-layout-sider .ant-menu-submenu-title, .ant-drawer.ant-drawer-open .ant-menu-submenu-title',
|
||||
),
|
||||
menuItems: visibleText('.ant-layout-sider .ant-menu-item, .ant-drawer.ant-drawer-open .ant-menu-item'),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1230,17 +1272,17 @@ async function loginFromLoginPage(page) {
|
||||
|
||||
async function createUserFromUsersPage(page, username, password = 'Batch123!@#') {
|
||||
const email = `${username}@example.com`
|
||||
const createUserButton = page.getByRole('button', { name: TEXT.createUser }).first()
|
||||
const createUserModal = page.locator('.ant-modal').last()
|
||||
const createUserRow = page.locator('tbody tr').filter({ hasText: username }).first()
|
||||
|
||||
logDebug(`createUserFromUsersPage: open modal for ${username}`)
|
||||
await forceClick(page.getByRole('button', { name: TEXT.createUser }).first())
|
||||
await expect(page.locator('.ant-spin-spinning')).toHaveCount(0, { timeout: 20 * 1000 })
|
||||
await forceClick(createUserButton)
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(TEXT.createUser)
|
||||
await expect(createUserModal).toBeVisible({ timeout: 10 * 1000 })
|
||||
logDebug(`createUserFromUsersPage: modal visible for ${username}`)
|
||||
|
||||
const createUserResponsePromise = waitForResponseSafe(page, (response) => {
|
||||
return response.url().includes('/api/v1/users') && response.request().method() === 'POST'
|
||||
})
|
||||
|
||||
logDebug(`createUserFromUsersPage: fill username for ${username}`)
|
||||
await forceFillInput(
|
||||
createUserModal.locator(`input[placeholder="${TEXT.createUserUsernamePlaceholder}"]`).first(),
|
||||
@@ -1256,13 +1298,83 @@ async function createUserFromUsersPage(page, username, password = 'Batch123!@#')
|
||||
createUserModal.locator(`input[placeholder="${TEXT.createUserEmailPlaceholder}"]`).first(),
|
||||
email,
|
||||
)
|
||||
logDebug(`createUserFromUsersPage: submit modal for ${username}`)
|
||||
await forceClick(createUserModal.locator('.ant-btn-primary').last())
|
||||
const submitButton = createUserModal.getByRole('button', { name: TEXT.createUser }).last()
|
||||
const submitStrategies = [
|
||||
async () => {
|
||||
await forceClick(submitButton)
|
||||
},
|
||||
async () => {
|
||||
await submitButton.evaluate((element) => {
|
||||
if (!(element instanceof HTMLButtonElement) && !(element instanceof HTMLElement)) {
|
||||
throw new Error('Create user submit target is not clickable.')
|
||||
}
|
||||
element.click()
|
||||
})
|
||||
},
|
||||
async () => {
|
||||
await forceClick(submitButton)
|
||||
},
|
||||
]
|
||||
|
||||
const createUserResponse = await resolveWaitForResponse(createUserResponsePromise)
|
||||
await assertApiSuccessResponse(createUserResponse, `create user ${username}`)
|
||||
logDebug(`createUserFromUsersPage: response ok for ${username}`)
|
||||
await expect(page.locator('tbody tr').filter({ hasText: username }).first()).toBeVisible({ timeout: 20 * 1000 })
|
||||
let createUserResponseResult = { error: new Error('create user request was not attempted') }
|
||||
for (let index = 0; index < submitStrategies.length; index += 1) {
|
||||
logDebug(`createUserFromUsersPage: submit modal for ${username} attempt ${index + 1}`)
|
||||
const responsePromise = waitForResponseSafe(page, (response) => {
|
||||
return response.url().includes('/api/v1/users') && response.request().method() === 'POST'
|
||||
}, { timeout: 8 * 1000 })
|
||||
|
||||
await submitStrategies[index]()
|
||||
createUserResponseResult = await responsePromise
|
||||
|
||||
if (createUserResponseResult.response) {
|
||||
await assertApiSuccessResponse(createUserResponseResult.response, `create user ${username}`)
|
||||
logDebug(`createUserFromUsersPage: response ok for ${username}`)
|
||||
break
|
||||
}
|
||||
|
||||
const rowVisibleAfterSubmit = await createUserRow.isVisible().catch(() => false)
|
||||
if (rowVisibleAfterSubmit) {
|
||||
logDebug(`createUserFromUsersPage: row became visible without captured response for ${username}`)
|
||||
break
|
||||
}
|
||||
|
||||
logDebug(`createUserFromUsersPage: submit attempt ${index + 1} did not complete for ${username}`)
|
||||
}
|
||||
|
||||
try {
|
||||
await expect(createUserRow).toBeVisible({ timeout: 20 * 1000 })
|
||||
} catch (rowError) {
|
||||
if (!createUserResponseResult.error) {
|
||||
throw rowError
|
||||
}
|
||||
|
||||
const diagnostics = await page.evaluate(() => {
|
||||
const visibleText = (selector) => Array.from(document.querySelectorAll(selector))
|
||||
.filter((element) => element instanceof HTMLElement && element.offsetParent !== null)
|
||||
.map((element) => (element.textContent ?? '').trim())
|
||||
.filter(Boolean)
|
||||
|
||||
return {
|
||||
currentUrl: window.location.href,
|
||||
modalText: visibleText('.ant-modal'),
|
||||
formErrors: visibleText('.ant-form-item-explain-error'),
|
||||
toastMessages: visibleText('.ant-message .ant-message-notice-content'),
|
||||
primaryButtons: visibleText('.ant-modal .ant-btn-primary'),
|
||||
}
|
||||
})
|
||||
|
||||
throw new Error(
|
||||
`create user ${username} did not complete. responseError=${formatError(createUserResponseResult.error)} diagnostics=${JSON.stringify(diagnostics)}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (createUserResponseResult.error) {
|
||||
logDebug(`createUserFromUsersPage: row visible without captured response for ${username}`)
|
||||
}
|
||||
|
||||
await page.goto(appUrl('/users'))
|
||||
await expect(page).toHaveURL(/\/users$/)
|
||||
await expect(createUserRow).toBeVisible({ timeout: 20 * 1000 })
|
||||
logDebug(`createUserFromUsersPage: row visible for ${username}`)
|
||||
|
||||
return { email, password, username }
|
||||
@@ -1867,15 +1979,16 @@ async function verifyDesktopAndMobileNavigation(page) {
|
||||
.toBe(true)
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('resize')))
|
||||
await expect
|
||||
.poll(async () => await page.locator('.ant-layout-header .ant-btn').count())
|
||||
.poll(async () => await page.getByTestId('mobile-nav-trigger').count())
|
||||
.toBeGreaterThan(0)
|
||||
|
||||
const mobileMenuButton = page.locator('.ant-layout-header .ant-btn').first()
|
||||
const mobileMenuButton = page.getByTestId('mobile-nav-trigger')
|
||||
await expect(mobileMenuButton).toBeVisible()
|
||||
await forceClick(mobileMenuButton)
|
||||
|
||||
await expect(page.locator('.ant-drawer-content')).toBeVisible({ timeout: 10 * 1000 })
|
||||
const mobileDashboardItem = page.locator('.ant-drawer .ant-menu-item').filter({ hasText: TEXT.dashboard }).first()
|
||||
const openDrawer = page.locator('.ant-drawer.ant-drawer-open')
|
||||
await expect(openDrawer.locator('.ant-drawer-content')).toBeVisible({ timeout: 10 * 1000 })
|
||||
const mobileDashboardItem = openDrawer.getByTestId('nav-dashboard').first()
|
||||
await expect(mobileDashboardItem).toBeVisible()
|
||||
await forceClick(mobileDashboardItem)
|
||||
await expect(page).toHaveURL(/\/dashboard$/)
|
||||
@@ -1887,8 +2000,7 @@ async function verifyUserManagementCRUD(page) {
|
||||
logDebug('verifyUserManagementCRUD: login /login')
|
||||
await loginFromLoginPage(page)
|
||||
|
||||
await expandSidebarGroup(page, TEXT.accessControl)
|
||||
await clickSidebarMenu(page, TEXT.users)
|
||||
await page.goto(appUrl('/users'))
|
||||
await expect(page).toHaveURL(/\/users$/)
|
||||
|
||||
const testUsername = `e2e_crud_${Date.now()}`
|
||||
@@ -1917,12 +2029,14 @@ async function verifyUserManagementCRUD(page) {
|
||||
const createUserResponse = await resolveWaitForResponse(createUserResponsePromise)
|
||||
await assertApiSuccessResponse(createUserResponse, 'create user CRUD')
|
||||
|
||||
await page.goto(appUrl('/users'))
|
||||
await expect(page.locator('tbody tr').filter({ hasText: testUsername }).first()).toBeVisible({ timeout: 20 * 1000 })
|
||||
|
||||
const userRow = page.locator('tbody tr').filter({ hasText: testUsername }).first()
|
||||
let userRow = page.locator('tbody tr').filter({ hasText: testUsername }).first()
|
||||
await forceClick(userRow.getByRole('button', { name: TEXT.edit }))
|
||||
const editDrawer = page.locator('.ant-drawer.ant-drawer-open').filter({ hasText: TEXT.editUser }).last()
|
||||
await expect(editDrawer).toBeVisible({ timeout: 10 * 1000 })
|
||||
const editDrawerTitle = page.locator('.ant-drawer-title').filter({ hasText: TEXT.editUser }).last()
|
||||
await expect(editDrawerTitle).toBeVisible({ timeout: 10 * 1000 })
|
||||
const editDrawer = page.locator('.ant-drawer.ant-drawer-open').last()
|
||||
|
||||
const editResponsePromise = waitForResponseSafe(page, (response) => {
|
||||
return response.url().includes(`/api/v1/users/`) && response.request().method() === 'PUT'
|
||||
@@ -1931,10 +2045,13 @@ async function verifyUserManagementCRUD(page) {
|
||||
const editResponse = await resolveWaitForResponse(editResponsePromise)
|
||||
await assertApiSuccessResponse(editResponse, 'edit user CRUD')
|
||||
|
||||
await page.goto(appUrl('/users'))
|
||||
userRow = page.locator('tbody tr').filter({ hasText: testUsername }).first()
|
||||
await expect(userRow).toBeVisible({ timeout: 20 * 1000 })
|
||||
await forceClick(userRow.getByRole('button', { name: TEXT.userDetailAction }))
|
||||
const detailDrawer = page.locator('.ant-drawer.ant-drawer-open').filter({ hasText: TEXT.userDetail }).last()
|
||||
await expect(detailDrawer).toBeVisible({ timeout: 10 * 1000 })
|
||||
await expect(detailDrawer).toContainText(testUsername)
|
||||
const detailDrawerTitle = page.locator('.ant-drawer-title').filter({ hasText: TEXT.userDetail }).last()
|
||||
await expect(detailDrawerTitle).toBeVisible({ timeout: 10 * 1000 })
|
||||
await expect(page.locator('.ant-drawer')).toContainText(testUsername)
|
||||
|
||||
await page.goto(appUrl('/users'))
|
||||
await forceFillInput(page.getByPlaceholder(TEXT.usersFilter), testUsername)
|
||||
@@ -2193,12 +2310,12 @@ async function verifyUserManagementBatch(page) {
|
||||
await selectUserRow(page, batchUserB)
|
||||
await forceClick(page.getByRole('button', { name: TEXT.batchDelete }))
|
||||
|
||||
const batchDeletePopover = page.locator('.ant-popconfirm').last()
|
||||
await expect(batchDeletePopover).toBeVisible({ timeout: 10 * 1000 })
|
||||
const batchDeleteModal = page.locator('.ant-modal').last()
|
||||
await expect(batchDeleteModal).toBeVisible({ timeout: 10 * 1000 })
|
||||
const batchDeleteResponsePromise = waitForResponseSafe(page, (response) => {
|
||||
return response.url().includes('/api/v1/users/batch') && response.request().method() === 'DELETE'
|
||||
})
|
||||
await forceClick(batchDeletePopover.locator('.ant-btn-primary').last())
|
||||
await forceClick(batchDeleteModal.locator('.ant-btn-primary').last())
|
||||
const batchDeleteResponse = await resolveWaitForResponse(batchDeleteResponsePromise)
|
||||
await assertApiSuccessResponse(batchDeleteResponse, 'batch delete users')
|
||||
|
||||
@@ -2439,7 +2556,10 @@ async function verifyProfileAndSecurity(page) {
|
||||
await expect(page.getByRole('button', { name: TEXT.enableTOTP })).toBeVisible({ timeout: 10 * 1000 })
|
||||
await forceClick(page.getByRole('button', { name: TEXT.enableTOTP }).first())
|
||||
|
||||
const setupModal = page.locator('.ant-modal').last()
|
||||
const setupModalRoot = page.locator('.ant-modal-root').filter({
|
||||
has: page.getByRole('button', { name: TEXT.confirmEnableTOTP }),
|
||||
}).last()
|
||||
const setupModal = setupModalRoot.locator('.ant-modal').first()
|
||||
await expect(setupModal).toBeVisible({ timeout: 10 * 1000 })
|
||||
await expect(setupModal.locator('img[alt="TOTP QR Code"]')).toBeVisible({ timeout: 10 * 1000 })
|
||||
|
||||
@@ -2455,22 +2575,29 @@ async function verifyProfileAndSecurity(page) {
|
||||
await forceClick(setupModal.getByRole('button', { name: TEXT.confirmEnableTOTP }).last())
|
||||
})
|
||||
assertFetchLogSuccess(enableTotpFetch, 'enable TOTP')
|
||||
await waitForModalToStopBlocking(setupModal, 'enable TOTP')
|
||||
await waitForModalToStopBlocking(setupModalRoot, 'enable TOTP')
|
||||
await expect(setupModalRoot).toBeHidden({ timeout: 10 * 1000 })
|
||||
await expect(page.getByRole('button', { name: TEXT.disableTOTP })).toBeVisible({ timeout: 10 * 1000 })
|
||||
logDebug('verifyProfileAndSecurity: TOTP enabled')
|
||||
|
||||
await forceClick(page.getByRole('button', { name: TEXT.disableTOTP }).first())
|
||||
const disableModal = page.locator('.ant-modal').last()
|
||||
const disableModalRoot = page.locator('.ant-modal-root').filter({
|
||||
has: page.getByRole('button', { name: TEXT.confirmDisableTOTP }),
|
||||
}).last()
|
||||
const disableModal = disableModalRoot.locator('.ant-modal').first()
|
||||
await expect(disableModal).toBeVisible({ timeout: 10 * 1000 })
|
||||
logDebug('verifyProfileAndSecurity: submit TOTP disable')
|
||||
await forceFillInput(disableModal.locator('input').first(), recoveryCodes[0])
|
||||
const disableCodeInput = disableModal.locator('input').first()
|
||||
await expect(disableCodeInput).toBeVisible({ timeout: 10 * 1000 })
|
||||
await forceFillInput(disableCodeInput, recoveryCodes[0])
|
||||
const disableTotpFetch = await performActionAndWaitForFetchLogEntry(page, (entry) => {
|
||||
return fetchLogPathMatches(entry, /\/api\/v1\/auth\/2fa\/disable$/) && entry.method === 'POST'
|
||||
}, async () => {
|
||||
await forceClick(disableModal.getByRole('button', { name: TEXT.confirmDisableTOTP }).last())
|
||||
})
|
||||
assertFetchLogSuccess(disableTotpFetch, 'disable TOTP')
|
||||
await waitForModalToStopBlocking(disableModal, 'disable TOTP')
|
||||
await waitForModalToStopBlocking(disableModalRoot, 'disable TOTP')
|
||||
await expect(disableModalRoot).toBeHidden({ timeout: 10 * 1000 })
|
||||
await expect(page.getByRole('button', { name: TEXT.enableTOTP })).toBeVisible({ timeout: 10 * 1000 })
|
||||
logDebug('verifyProfileAndSecurity: TOTP disabled')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user