Files
openobserve/tests/ui-testing/pages/iamPages/iamFormValidationPage.js
T

374 lines
20 KiB
JavaScript

// Copyright 2026 OpenObserve Inc.
export class IamFormValidationPage {
/**
* @param {import('@playwright/test').Page} page
*/
constructor(page) {
this.page = page;
// ── Navigation ───────────────────────────────────────────────────────
this.iamMenuLink = '[data-test="menu-link-\\/iam-item"]';
this.iamOrganizationsTab = '[data-test="iam-organizations-tab"]';
this.iamGroupsTab = '[data-test="iam-groups-tab"]';
this.iamRolesTab = '[data-test="iam-roles-tab"]';
this.iamServiceAccountsTab = '[data-test="iam-service-accounts-tab"]';
// ── Organization form ─────────────────────────────────────────────────
// Add org button uses the literal label from ListOrganizations.vue
this.addOrgButton = '[data-test="Add Organization"]';
// ODialog wrapper
this.orgDialog = '[data-test="add-update-organization-dialog"]';
// OInput — OInput auto-generates -field (native <input>) and -error (message span)
this.orgNameInput = '[data-test="org-name-field"]';
this.orgNameError = '[data-test="org-name-error"]';
// ODialog built-in primary / secondary buttons scoped to dialog
this.orgSubmitBtn = '[data-test="add-update-organization-dialog"] [data-test="o-dialog-primary-btn"]';
this.orgCancelBtn = '[data-test="add-update-organization-dialog"] [data-test="o-dialog-secondary-btn"]';
// ── Group form ────────────────────────────────────────────────────────
this.addGroupButton = '[data-test="iam-groups-add-group-btn"]';
this.groupDialog = '[data-test="add-group-dialog"]';
// OInput data-test="add-group-groupname-input-btn" → -field / -error
this.groupNameInput = '[data-test="add-group-groupname-input-btn-field"]';
this.groupNameError = '[data-test="add-group-groupname-input-btn-error"]';
this.groupSubmitBtn = '[data-test="add-group-dialog"] [data-test="o-dialog-primary-btn"]';
this.groupCancelBtn = '[data-test="add-group-dialog"] [data-test="o-dialog-secondary-btn"]';
// ── Role form ─────────────────────────────────────────────────────────
this.addRoleButton = '[data-test="iam-roles-add-role-btn"]';
this.roleDialog = '[data-test="add-role-dialog"]';
// OInput data-test="add-role-rolename-input-btn" → -field / -error
this.roleNameInput = '[data-test="add-role-rolename-input-btn-field"]';
this.roleNameError = '[data-test="add-role-rolename-input-btn-error"]';
this.roleSubmitBtn = '[data-test="add-role-dialog"] [data-test="o-dialog-primary-btn"]';
this.roleCancelBtn = '[data-test="add-role-dialog"] [data-test="o-dialog-secondary-btn"]';
// ── Service Account form ──────────────────────────────────────────────
this.addServiceAccountBtn = '[data-test="service-accounts-add-btn"]';
this.saDialog = '[data-test="add-service-account-dialog"]';
// Service accounts are created from a NAME (slug) now, not an email —
// the identifier is synthesized as <name>.<org>@sa.internal.
// OInput data-test="iam-add-service-account-name-input" → -field / -error
this.saNameInput = '[data-test="iam-add-service-account-name-input-field"]';
this.saNameError = '[data-test="iam-add-service-account-name-input-error"]';
this.saSubmitBtn = '[data-test="add-service-account-dialog"] [data-test="o-dialog-primary-btn"]';
this.saCancelBtn = '[data-test="add-service-account-dialog"] [data-test="o-dialog-secondary-btn"]';
// ── Add User form ─────────────────────────────────────────────────────
this.iamUsersTab = '[data-test="iam-users-tab"]';
this.addBasicUserBtn = '[data-test="add-basic-user"]';
this.addUserDialog = '[data-test="add-user-dialog"]';
// AddUser.vue uses data-test="user-email-field", "user-password-field", "user-role-field"
// OInput auto-generates -field / -error suffixes
// OInput data-test="user-email-field" → error span: data-test="user-email-field-error"
this.userEmailInput = '[data-test="user-email-field-field"]';
this.userEmailError = '[data-test="user-email-field-error"]';
// OInput data-test="user-password-field" → error span: data-test="user-password-field-error"
this.userPasswordInput = '[data-test="user-password-field-field"]';
this.userPasswordError = '[data-test="user-password-field-error"]';
// OSelect data-test="user-role-field" → error span: data-test="user-role-field-error"
this.userRoleError = '[data-test="user-role-field-error"]';
this.addUserSubmitBtn = '[data-test="add-user-submit-btn"]';
this.addUserCancelBtn = '[data-test="add-user-dialog"] [data-test="o-dialog-secondary-btn"]';
// ── UpdateRole (edit user) dialog ─────────────────────────────────────
// Trigger: row action button data-test="edit-basic-user-${index}" which,
// in OSS / enterprise (non-cloud) mode, opens the AddUser dialog in update
// mode (data-test="add-user-dialog"). The legacy UpdateRole.vue dialog is
// not reachable from this edit button in the current build.
this.editBasicUserFirstBtn = '[data-test^="edit-basic-user-"]';
this.updateRoleDialog = '[data-test="add-user-dialog"]';
// OSelect data-test="user-role-field" → -trigger to open, -error span on validation
this.updateRoleSelect = '[data-test="user-role-field-trigger"]';
this.updateRoleError = '[data-test="user-role-field-error"]';
this.updateRoleSaveBtn = '[data-test="add-user-submit-btn"]';
this.updateRoleCancelBtn = '[data-test="add-user-dialog"] [data-test="o-dialog-secondary-btn"]';
// ── Add Role "Start from" preset (new) ────────────────────────────────
this.roleStartFromSelect = '[data-test="add-role-start-from-select-trigger"]';
this.roleStartFromOption = (value) => `[data-test="add-role-start-from-select-option"][data-test-value="${value}"]`;
// ── Edit Role / Edit Group pages (auto-route + dirty state, new) ───────
this.editRolePage = '[data-test="edit-role-page"]';
this.editRolePermsSection = '[data-test="edit-role-permissions-section"]';
this.editRolePermsTab = '[data-test="tab-permissions"]';
this.editRolePermsDirtyDot = '[data-test="tab-permissions-dirty-dot"]';
this.editGroupSection = '[data-test="edit-group-section"]';
this.editGroupRolesTab = '[data-test="tab-roles"]';
this.editGroupRolesDirtyDot= '[data-test="tab-roles-dirty-dot"]';
// ── Confirm dialog (shared) — delete blast-radius warning banner (new) ─
this.confirmDialog = '[data-test="confirm-dialog"]';
this.confirmDialogOkBtn = '[data-test="confirm-dialog"] [data-test="o-dialog-primary-btn"]';
this.confirmDialogCancelBtn= '[data-test="confirm-dialog"] [data-test="o-dialog-secondary-btn"]';
// ── Toast / success messages ──────────────────────────────────────────
this.toastSuccess = '[data-test-variant="success"]';
this.toastMessage = '[data-test="o-toast-message"]';
}
// ── Navigation helpers ────────────────────────────────────────────────────
async navigateToIam() {
await this.page.locator(this.iamMenuLink).click();
await this.page.locator('[data-test="iam-page"]').waitFor({ state: 'visible', timeout: 10000 });
}
async navigateToOrganizationsTab() {
await this.navigateToIam();
await this.page.locator(this.iamOrganizationsTab).click();
}
/**
* Navigates to the IAM Groups tab and reports whether the feature mounted.
* Groups (RBAC) is an ENTERPRISE-only feature — the tab and add button are
* absent on the OSS binary. Returns true when the feature rendered (so a
* caller can gate-skip enterprise-only assertions on OSS), false otherwise.
* @returns {Promise<boolean>}
*/
async navigateToGroupsTab() {
await this.navigateToIam();
try {
await this.page.locator(this.iamGroupsTab).waitFor({ state: 'visible', timeout: 15000 });
await this.page.locator(this.iamGroupsTab).click();
await this.page.locator(this.addGroupButton).waitFor({ state: 'visible', timeout: 15000 });
return true;
} catch {
return false;
}
}
/**
* Navigates to the IAM Roles tab and reports whether the feature mounted.
* Roles (RBAC) is an ENTERPRISE-only feature — the tab and add button are
* absent on the OSS binary. Returns true when the feature rendered, false
* otherwise, so callers can gate-skip enterprise-only assertions on OSS.
* @returns {Promise<boolean>}
*/
async navigateToRolesTab() {
await this.navigateToIam();
try {
await this.page.locator(this.iamRolesTab).waitFor({ state: 'visible', timeout: 15000 });
await this.page.locator(this.iamRolesTab).click();
await this.page.locator(this.addRoleButton).waitFor({ state: 'visible', timeout: 15000 });
return true;
} catch {
return false;
}
}
async navigateToServiceAccountsTab() {
await this.navigateToIam();
await this.page.locator(this.iamServiceAccountsTab).waitFor({ state: 'visible', timeout: 10000 });
await this.page.locator(this.iamServiceAccountsTab).click();
}
// ── Organization form actions ─────────────────────────────────────────────
async openOrganizationForm() {
await this.page.locator(this.addOrgButton).click();
await this.page.locator(this.orgDialog).waitFor({ state: 'visible', timeout: 8000 });
}
async fillOrgName(name) {
await this.page.locator(this.orgNameInput).fill(name);
}
async clearOrgName() {
await this.page.locator(this.orgNameInput).clear();
}
async submitOrgForm() {
await this.page.locator(this.orgSubmitBtn).click();
}
async cancelOrgForm() {
await this.page.locator(this.orgCancelBtn).click();
}
// ── Group form actions ────────────────────────────────────────────────────
async openGroupForm() {
await this.page.locator(this.addGroupButton).click();
await this.page.locator(this.groupDialog).waitFor({ state: 'visible', timeout: 8000 });
}
async fillGroupName(name) {
await this.page.locator(this.groupNameInput).fill(name);
}
async submitGroupForm() {
await this.page.locator(this.groupSubmitBtn).click();
}
async cancelGroupForm() {
await this.page.locator(this.groupCancelBtn).click();
}
// ── Role form actions ─────────────────────────────────────────────────────
async openRoleForm() {
await this.page.locator(this.addRoleButton).click();
await this.page.locator(this.roleDialog).waitFor({ state: 'visible', timeout: 8000 });
}
async fillRoleName(name) {
await this.page.locator(this.roleNameInput).fill(name);
}
async submitRoleForm() {
await this.page.locator(this.roleSubmitBtn).click();
}
async cancelRoleForm() {
await this.page.locator(this.roleCancelBtn).click();
}
async selectRoleStartFrom(value) {
await this.page.locator(this.roleStartFromSelect).click();
await this.page.locator(this.roleStartFromOption(value)).click();
}
async selectRoleStartFromReadonly() {
await this.selectRoleStartFrom('readonly');
}
// Delete the first role/group in the list via its row trash icon, keyed by name.
async clickDeleteRole(roleName) {
await this.page
.locator(`[data-test="iam-roles-delete-${roleName}-role-icon"]`)
.click();
}
async clickDeleteGroup(groupName) {
await this.page
.locator(`[data-test="iam-groups-delete-${groupName}-role-icon"]`)
.click();
}
// Toggle the first available permission checkbox on the Edit Role Permissions
// tab (used to dirty the tab). Returns true if a checkbox was found+clicked.
async toggleFirstPermissionCheckbox() {
const checkbox = this.page
.locator('[data-test*="-permissions-table-body-row-"][data-test$="-checkbox"]')
.first();
try {
await checkbox.waitFor({ state: 'visible', timeout: 15000 });
await checkbox.click();
return true;
} catch {
return false;
}
}
// ── Service Account form actions ──────────────────────────────────────────
async openServiceAccountForm() {
await this.page.locator(this.addServiceAccountBtn).click();
await this.page.locator(this.saDialog).waitFor({ state: 'visible', timeout: 8000 });
}
async fillSaName(name) {
await this.page.locator(this.saNameInput).fill(name);
}
async submitSaForm() {
await this.page.locator(this.saSubmitBtn).click();
}
async cancelSaForm() {
await this.page.locator(this.saCancelBtn).click();
}
// ── Add User form actions ─────────────────────────────────────────────────
async openAddUserForm() {
await this.page.locator(this.addBasicUserBtn).click();
await this.page.locator(this.addUserDialog).waitFor({ state: 'visible', timeout: 8000 });
}
async fillUserEmail(email) {
await this.page.locator(this.userEmailInput).fill(email);
}
async fillUserPassword(password) {
await this.page.locator(this.userPasswordInput).fill(password);
}
async submitAddUserForm() {
await this.page.locator(this.addUserSubmitBtn).click();
}
async cancelAddUserForm() {
await this.page.locator(this.addUserCancelBtn).click();
}
// ── Assertion-ready locator getters ──────────────────────────────────────
getOrgNameErrorLocator() { return this.page.locator(this.orgNameError); }
getOrgSubmitBtnLocator() { return this.page.locator(this.orgSubmitBtn); }
getGroupNameErrorLocator() { return this.page.locator(this.groupNameError); }
getGroupSubmitBtnLocator() { return this.page.locator(this.groupSubmitBtn); }
getRoleNameErrorLocator() { return this.page.locator(this.roleNameError); }
getRoleSubmitBtnLocator() { return this.page.locator(this.roleSubmitBtn); }
getSaNameErrorLocator() { return this.page.locator(this.saNameError); }
getToastMessageLocator() { return this.page.locator(this.toastMessage); }
getOrgDialogLocator() { return this.page.locator(this.orgDialog); }
getGroupDialogLocator() { return this.page.locator(this.groupDialog); }
getRoleDialogLocator() { return this.page.locator(this.roleDialog); }
getSaDialogLocator() { return this.page.locator(this.saDialog); }
getAddUserDialogLocator() { return this.page.locator(this.addUserDialog); }
// New-behavior getters (start-from, auto-route, delete banner, dirty dots)
getRoleStartFromSelectLocator() { return this.page.locator(this.roleStartFromSelect); }
getRoleStartFromOptionLocator(value) { return this.page.locator(this.roleStartFromOption(value)); }
getEditRolePageLocator() { return this.page.locator(this.editRolePage); }
getEditRolePermsSectionLocator() { return this.page.locator(this.editRolePermsSection); }
getEditRolePermsDirtyDotLocator() { return this.page.locator(this.editRolePermsDirtyDot); }
getEditGroupSectionLocator() { return this.page.locator(this.editGroupSection); }
getConfirmDialogLocator() { return this.page.locator(this.confirmDialog); }
getConfirmDialogOkBtnLocator() { return this.page.locator(this.confirmDialogOkBtn); }
getConfirmDialogCancelBtnLocator(){ return this.page.locator(this.confirmDialogCancelBtn); }
getUserEmailErrorLocator() { return this.page.locator(this.userEmailError); }
getUserPasswordErrorLocator() { return this.page.locator(this.userPasswordError); }
getUserRoleErrorLocator() { return this.page.locator(this.userRoleError); }
// ── UpdateRole locator getters ────────────────────────────────────────────
getUpdateRoleDialogLocator() { return this.page.locator(this.updateRoleDialog); }
getUpdateRoleSelectLocator() { return this.page.locator(this.updateRoleSelect); }
getUpdateRoleErrorLocator() { return this.page.locator(this.updateRoleError); }
getUpdateRoleSaveBtnLocator() { return this.page.locator(this.updateRoleSaveBtn); }
getUpdateRoleCancelBtnLocator(){ return this.page.locator(this.updateRoleCancelBtn); }
getToastSuccessLocator() { return this.page.locator(this.toastSuccess); }
// Open the UpdateRole (edit user) dialog for the first user visible in the list
async openUpdateRoleDialogForFirstUser() {
const editBtn = this.page.locator(this.editBasicUserFirstBtn).first();
await editBtn.waitFor({ state: 'visible', timeout: 10000 });
await editBtn.click();
await this.page.locator(this.updateRoleDialog).waitFor({ state: 'visible', timeout: 10000 });
}
// Count of user-row edit buttons currently rendered in the Users list
async getEditUserButtonCount() {
return this.page.locator(this.editBasicUserFirstBtn).count();
}
async clickFirstEditUserButton() {
await this.page.locator(this.editBasicUserFirstBtn).first().click();
}
getUrlWithUserActionPattern() { return /[?&]action=(update|add)\b|[?&]email=/; }
async navigateToUsersTab() {
await this.navigateToIam();
const tab = this.page.locator(this.iamUsersTab);
await tab.waitFor({ state: 'visible', timeout: 10000 });
await tab.click();
await this.page.locator(this.addBasicUserBtn).waitFor({ state: 'visible', timeout: 10000 });
// Wait for the user rows to render so edit-button presence checks are stable
await this.page.locator(this.editBasicUserFirstBtn).first().waitFor({ state: 'visible', timeout: 10000 });
}
}