Files
openobserve/tests/ui-testing/pages/generalPages/createOrgPage.js
T
Neha P 50490671db test: create org testcases (#8362)
### **PR Type**
Tests, Enhancement


___

### **Description**
- Add comprehensive org CRUD Playwright tests

- Implement org delete via API and UI

- Add helper to extract org identifier

- Fix verifyOrgNotExists usage and CI suite


___

### Diagram Walkthrough


```mermaid
flowchart LR
  Tests["New Playwright org tests"] -- "uses" --> CreateOrgPage["CreateOrgPage page object"]
  CreateOrgPage -- "adds" --> DeleteAPI["deleteOrgViaAPI()"]
  CreateOrgPage -- "adds" --> DeleteUI["deleteOrgViaUI()"]
  CreateOrgPage -- "adds" --> GetIdentifier["getOrgIdentifierFromTable()"]
  Workflow["GitHub Actions workflow"] -- "runs" --> Tests
```



<details> <summary><h3> File Walkthrough</h3></summary>

<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>createOrgPage.js</strong><dd><code>Enhance
CreateOrgPage with delete and identifier utilities</code></dd></summary>
<hr>

tests/ui-testing/pages/generalPages/createOrgPage.js

<ul><li>Import Playwright <code>expect</code>.<br> <li> Remove param
from <code>verifyOrgNotExists</code>.<br> <li> Add
<code>deleteOrgViaAPI</code> with Basic Auth DELETE call.<br> <li> Add
<code>deleteOrgViaUI</code> with fallback selectors and confirm
flow.<br> <li> Add <code>getOrgIdentifierFromTable</code> utility
parsing table cells.</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8362/files#diff-e3ff7268323b35f1798acb81fcd3541e0d2ee7c283124a064854ac9f6e67beb8">+113/-3</a>&nbsp;
</td>

</tr>
</table></td></tr><tr><td><strong>Tests</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>org.spec.js</strong><dd><code>New Playwright org
management E2E tests</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

tests/ui-testing/playwright-tests/GeneralTests/org.spec.js

<ul><li>Add full CRUD and validation test suite.<br> <li> Cover API/UI
creation, search, negatives, special chars.<br> <li> Implement cleanup
via API with UI fallback.<br> <li> Use test logger and base navigation
setup.</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8362/files#diff-ab00b9371adadcb16226e3afb422dcab9e6fb0b99a3b43766d4bdda6b12f4378">+556/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>usersOrg.spec.js</strong><dd><code>Fix
verifyOrgNotExists usage in user-org tests</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

tests/ui-testing/playwright-tests/GeneralTests/usersOrg.spec.js

<ul><li>Update <code>verifyOrgNotExists</code> call signature.<br> <li>
Remove redundant <code>expect</code> argument.</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8362/files#diff-0bf40521e88d2cf427617e49affa14d4ebe469b6da4ea4aeef4fe3e246eb24c7">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>playwright.yml</strong><dd><code>Run organization tests
in CI workflow</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.github/workflows/playwright.yml

<ul><li>Add <code>org.spec.js</code> to CI run files.<br> <li> Ensure
org tests run in workflow.</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8362/files#diff-7afcd2d8f7b49bda74843f209eefb7b2da45f7e7803bf2e4bd636699b76aa2d3">+1/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr></tr></tbody></table>

</details>

___
2025-09-10 19:09:08 +05:30

321 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { expect } = require('@playwright/test');
export class CreateOrgPage {
constructor(page) {
this.page = page;
}
async createOrg(newOrgName) {
const basicAuthCredentials = Buffer.from(`${process.env["ZO_ROOT_USER_EMAIL"]}:${process.env["ZO_ROOT_USER_PASSWORD"]}`).toString('base64');
const headers = {
"Authorization": `Basic ${basicAuthCredentials}`,
"Content-Type": "application/json",
};
let newOrgBody = {
"name": newOrgName,
}
const fetchResponse = await fetch(
`${process.env.INGESTION_URL}/api/organizations`,
{
method: "POST",
headers: headers,
body: JSON.stringify(newOrgBody),
}
);
try {
const response = await fetchResponse;
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
const jsonData = await response.json();
console.log(response.status);
console.log(jsonData);
return jsonData.identifier;
// Process JSON data here
} else {
const textData = await response.text();
console.log("Response is not JSON:", textData);
return textData;
// Handle the non-JSON response here
}
} catch (error) {
console.error("Failed to parse JSON response:", error);
return "";
}
}
async navigateToOrg() {
await this.page.locator('[data-test="menu-link-\\/iam-item"]').click();
await this.page.locator('[data-test="iam-organizations-tab"]').click();
}
async clickAddOrg() {
await this.page.locator('[data-test="Add Organization"]').click();
}
async fillOrgName(orgName) {
//check if element is visible before filling
const orgNameField = this.page.locator('[data-test="org-name"]');
await orgNameField.waitFor({ state: 'visible' });
await orgNameField.fill(orgName);
}
async clickSaveOrg() {
await this.page.locator('[data-test="add-org"]').click();
}
async checkSaveEnabled() {
const saveButton = this.page.locator('[data-test="add-org"]');
// Check if the button is enabled
const isEnabled = await saveButton.isEnabled();
if (!isEnabled) {
console.error('The "Add Organization" button is not enabled.');
return false; // Return false to indicate the button is not enabled
}
return true; // Return true if the button is enabled
}
async clickCancelButton() {
const cancelButton = this.page.locator('[data-test="cancel-organizations-modal"]');
// Check if the button is visible and enabled before clicking
const isVisible = await cancelButton.isVisible();
const isEnabled = await cancelButton.isEnabled();
if (isVisible && isEnabled) {
await cancelButton.click();
} else {
console.error('The "Cancel" button is not visible or enabled.');
throw new Error('Attempted to click "Cancel" button, but it is not visible or enabled.');
}
}
async searchOrg(orgName) {
await this.page.getByPlaceholder('Search Organization').click();
await this.page.getByPlaceholder('Search Organization').fill(orgName);
await this.page.waitForTimeout(5000);
}
async verifyOrgNotExists() {
const mainSection = this.page.locator('[data-test="iam-page"]').getByRole('main');
const textContent = await mainSection.textContent();
console.log('Main section text:', textContent); // Debugging line
await expect(mainSection).toContainText('No data available');
}
async verifyOrgExists(orgName) {
await expect(this.page.locator('tbody')).toContainText(orgName);
}
async deleteOrgViaAPI(orgIdentifier) {
console.log(`⚠️ WARNING: Organization deletion may not be supported in this environment`);
const basicAuthCredentials = Buffer.from(`${process.env["ZO_ROOT_USER_EMAIL"]}:${process.env["ZO_ROOT_USER_PASSWORD"]}`).toString('base64');
const headers = {
"Authorization": `Basic ${basicAuthCredentials}`,
"Content-Type": "application/json",
};
try {
const fetchResponse = await fetch(
`${process.env.INGESTION_URL}/api/organizations/${orgIdentifier}`,
{
method: "DELETE",
headers: headers,
}
);
console.log(`Delete org API response - Status: ${fetchResponse.status}`);
const responseText = await fetchResponse.text();
console.log(`Delete org API response - Body: ${responseText}`);
if (fetchResponse.ok) {
console.log(`✓ Organization ${orgIdentifier} deletion API call succeeded`);
return true;
} else if (fetchResponse.status === 403) {
console.log(`✗ Organization deletion forbidden (403) - deletion not allowed in this environment`);
return false;
} else if (fetchResponse.status === 404) {
console.log(`ℹ Organization ${orgIdentifier} not found (404) - may already be deleted`);
return true; // Consider 404 as success since org doesn't exist
} else {
console.log(`✗ Failed to delete organization ${orgIdentifier}: Status ${fetchResponse.status} - ${responseText}`);
return false;
}
} catch (error) {
console.error(`✗ Error during organization deletion API call:`, error);
return false;
}
}
async deleteOrgViaUI(orgName) {
console.log(`⚠️ WARNING: UI deletion may not be supported - delete buttons might not exist`);
try {
// Search for the organization first
await this.searchOrg(orgName);
await this.page.waitForTimeout(1000);
// Check if any delete buttons exist in the table at all
const anyDeleteButtons = this.page.locator('[data-test*="delete"]', '[title*="delete" i]', 'button:has-text("Delete")');
const deleteButtonCount = await anyDeleteButtons.count();
console.log(`Found ${deleteButtonCount} delete buttons in the organization table`);
if (deleteButtonCount === 0) {
console.log(`✗ No delete buttons found in organization table - deletion likely not supported in UI`);
return false;
}
// Look for delete button in the organization row
const deleteButton = this.page.locator(`[data-test="org-delete-${orgName}"]`).or(
this.page.locator('tbody tr').filter({ hasText: orgName }).locator('[data-test*="delete"]')
);
if (await deleteButton.isVisible({ timeout: 5000 })) {
console.log(`Found delete button for organization ${orgName}, attempting to click...`);
await deleteButton.click();
await this.page.waitForTimeout(1000);
// Confirm deletion if confirmation dialog appears
const confirmButton = this.page.locator('[data-test="confirm-delete"]').or(
this.page.getByRole('button', { name: /delete|confirm|yes/i })
);
if (await confirmButton.isVisible({ timeout: 3000 })) {
console.log(`Confirmation dialog appeared, confirming deletion...`);
await confirmButton.click();
await this.page.waitForTimeout(2000);
// Check if organization is actually gone
await this.searchOrg(orgName);
const stillExists = await this.page.locator('tbody tr').filter({ hasText: orgName }).isVisible({ timeout: 2000 });
if (stillExists) {
console.log(`✗ Organization ${orgName} still exists after UI deletion - deletion may be restricted`);
return false;
} else {
console.log(`✓ Organization ${orgName} successfully deleted via UI`);
return true;
}
} else {
console.log(`✗ No confirmation dialog appeared for ${orgName} deletion`);
return false;
}
} else {
console.log(`✗ Delete button not found for organization ${orgName}`);
return false;
}
} catch (error) {
console.error(`✗ Failed to delete organization ${orgName} via UI:`, error);
return false;
}
}
async getOrgIdentifierFromTable(orgName) {
const maxRetries = 3;
const retryDelay = 1000;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`Attempting to get identifier for ${orgName} (attempt ${attempt}/${maxRetries})`);
// Ensure we search for the organization first to make it visible
await this.searchOrg(orgName);
await this.page.waitForLoadState('networkidle', { timeout: 5000 });
// Wait for table to be stable
await this.page.locator('tbody tr').first().waitFor({ state: 'visible', timeout: 5000 });
// Find the row containing the organization name with exact match
const orgRow = this.page.locator('tbody tr').filter({ hasText: new RegExp(`\\b${orgName}\\b`) });
if (!(await orgRow.isVisible({ timeout: 3000 }))) {
console.log(`Organization row not visible for ${orgName} on attempt ${attempt}`);
if (attempt < maxRetries) {
await this.page.waitForTimeout(retryDelay);
continue;
}
return null;
}
// Get all cell text content at once for analysis
const cellTexts = await orgRow.locator('td').allTextContents();
console.log(`Row data for ${orgName}:`, cellTexts);
// Find identifier using multiple strategies with improved patterns
let identifier = null;
// Strategy 1: Look for UUID pattern (32 chars, alphanumeric)
for (let i = 0; i < cellTexts.length; i++) {
const text = cellTexts[i]?.trim();
if (!text || text === orgName) continue;
// Check for standard UUID pattern (32 chars, alphanumeric)
if (/^[a-zA-Z0-9]{32}$/.test(text)) {
identifier = text;
console.log(`Found UUID identifier: ${identifier}`);
break;
}
}
// Strategy 2: Look for shorter hash pattern (16-24 chars)
if (!identifier) {
for (let i = 0; i < cellTexts.length; i++) {
const text = cellTexts[i]?.trim();
if (!text || text === orgName) continue;
// Check for shorter hash pattern
if (/^[a-zA-Z0-9]{16,24}$/.test(text)) {
identifier = text;
console.log(`Found hash identifier: ${identifier}`);
break;
}
}
}
// Strategy 3: Use positional approach - identifier typically in second column
if (!identifier && cellTexts.length >= 2) {
const secondCol = cellTexts[1]?.trim();
// Validate it's not a common non-identifier value
if (secondCol &&
secondCol !== orgName &&
secondCol.length >= 8 &&
secondCol.length <= 50 &&
!/^(active|inactive|enabled|disabled|default|admin|user|root|true|false|\d{4}-\d{2}-\d{2})$/i.test(secondCol) &&
!/\s/.test(secondCol)) {
identifier = secondCol;
console.log(`Using positional identifier: ${identifier}`);
}
}
if (identifier) {
console.log(`Successfully found identifier for ${orgName}: ${identifier}`);
return identifier;
}
console.log(`No suitable identifier found for ${orgName} on attempt ${attempt}`);
if (attempt < maxRetries) {
await this.page.waitForTimeout(retryDelay);
}
} catch (error) {
console.error(`Error getting identifier for ${orgName} on attempt ${attempt}:`, error.message);
if (attempt < maxRetries) {
await this.page.waitForTimeout(retryDelay);
}
}
}
console.warn(`Failed to get identifier for ${orgName} after ${maxRetries} attempts`);
return null;
}
}