Files
openobserve/tests/ui-testing/playwright-tests/utils/wait-helpers.js
T
e2ad15f0c0 test: Framework rework Playwright e2e tests (#8118)
### **PR Type**
Tests, Enhancement


___

### **Description**
- Migrate sanity tests to enhanced framework

- Add robust SanityPage page-object methods

- Introduce logging and wait helpers

- Refactor service account tests setup


___

### Diagram Walkthrough


```mermaid
flowchart LR
  Specs["Playwright specs (sanity, serviceAccount)"]
  Utils["Enhanced test utils (fixtures, logger, waits)"]
  Pages["New page objects (SanityPage, etc.)"]
  Archives["Old specs archived (_old)"]

  Specs -- "use" --> Utils
  Specs -- "call" --> Pages
  Legacy["Legacy inline flows"] -- "moved to" --> Archives
  Legacy -- "replaced by" --> Pages
```



<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><details><summary>1
files</summary><table>
<tr>
<td><strong>sanityPage.js</strong><dd><code>New Sanity page object with
robust flows</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-a0ac0217ef875311dc536edb97deda19927a64e73cede62123b97cbe7df113b8">+669/-0</a>&nbsp;
</td>

</tr>

</table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>2
files</summary><table>
<tr>
<td><strong>sanity.spec.js</strong><dd><code>Refactor to enhanced
fixtures and page object</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-89290ca55858ff744863278ee9381e32276c37a6c0718fb3f9dcba71a6451d91">+159/-442</a></td>

</tr>

<tr>
<td><strong>serviceAccount.spec.js</strong><dd><code>Migrate service
account tests to enhanced setup</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-270e1e217b97afe4d451df3a48a0bfb14db453241adf1b7388342292707d5bb1">+114/-36</a></td>

</tr>

</table></details></td></tr><tr><td><strong>Miscellaneous</strong></td><td><details><summary>2
files</summary><table>
<tr>
<td><strong>sanity.spec_old.js</strong><dd><code>Archive legacy sanity
spec implementation</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-51cb8e6cd5047c3d1ace254fa770ae74a3c13aa97e93315132dede6c4a84739a">+498/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>sanity2.spec_old.js</strong><dd><code>Archive legacy sanity2
spec with helpers</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-0e189adff26af46cfcc03f014d522c6a37ac268f7ad3e1a3ace1267bd64161e3">[link]</a>&nbsp;
&nbsp; </td>

</tr>
</table></details></td></tr><tr><td><strong>Additional
files</strong></td><td><details><summary>31 files</summary><table>
<tr>
  <td><strong>playwright.yml</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-7afcd2d8f7b49bda74843f209eefb7b2da45f7e7803bf2e4bd636699b76aa2d3">+7/-8</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>package.json</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-a41ba7975823b7b2dc071285391f835fbefd33bfffeb5aa2fba3ae15425cc64f">+2/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>changeOrgPage.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-1061a261a5b7ae8ffd6c4b48e382bb262289aacca97207aa15f00053b822038b">+224/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>enrichmentPage.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-98a7e60352ddf3be23cf70170559e637ef448c189cd9414448e9afff897c3019">+507/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>schemaLoadPage.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-6f0791c152c7a17e60e0167d52c4032edc6384553b69b9a94b81e380e98dd1d3">+265/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>schemaPage.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-1c0c0fc2fdbebae8c8aa7bef5ba92d31dee65238272581453e6f2b9a3f135468">+397/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>userPage.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-bba64431d6702ebcfd265dd690f81d8c06a285fb09016c4823ed3490403ed777">+26/-7</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>iamPage.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-e537da3c3eaa929da94df90ba76b3b17f08792c9de268de899d310d13740efcf">+13/-5</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>page-manager.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-565eab8b3c767ae50c9d05e591621d14f66b81af84d6b5ea6efe136c1c3933df">+11/-1</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>pipelinesPage.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-d5f2771fd1139c174c46d09a9ab2da0ea4588b853e2b87e4728322d072542f49">+9/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>reportsPage.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-bc6b666f3862dce898d9ca68d036f317305b3f3d063d6eda423f2d864d36be0f">+8/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>changeOrg.spec.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-d5557230e5f003c7c0129aab00855dc94501cc12a06b1f7814ff85d9fe389f47">+188/-149</a></td>

</tr>

<tr>
  <td><strong>enrichment.spec.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-f52a06db75e7593aee95da4e0b255cb0001aa6c2b416d30358858e2f39b0cdcd">+106/-319</a></td>

</tr>

<tr>
  <td><strong>schema.spec.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-72dd04280e3c77242b982bd7853ffa447428c0f9419c37a71d867ec7cd28d4b8">+121/-265</a></td>

</tr>

<tr>
  <td><strong>schemaload.spec.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-5184069ec159ab301454535a5f45967635972e108095b82401ba96cb92856c5b">+114/-184</a></td>

</tr>

<tr>
  <td><strong>usersOrg.spec.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-0bf40521e88d2cf427617e49affa14d4ebe469b6da4ea4aeef4fe3e246eb24c7">+177/-77</a></td>

</tr>

<tr>
  <td><strong>enhanced-baseFixtures.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-3657cfe42414b97b6676be58109d3c3f92e8ba2f52bb09840b65ac9f9c889997">+137/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>framework-test.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-d3d8309ee6bdfb8edd0e39b4f8d2008ffe07775f130882a60b0509b29ae1a15d">+33/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>global-setup.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-06323dad7214f86b41b62e121e716eeea0054bc4d950c6bcf4a0efe53894acc8">+147/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>global-teardown.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-f3a6f320a6dd08450cf34d96f07847b9324fff8415f768963252ce0e1e35315e">+30/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>slack-reporter.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-29fdf4df416fe8e9965be16bc0419787175c227c385ac37560da40dc935328f5">+162/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>test-logger.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-9ad67a2a3dbfce3994655b30eb07c0a1b795ce0dc10ae9785481f9472394ce5e">+202/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>wait-helpers.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-fdf1e3fd5f38875694acd5e3f0a140ed89326ce8a2910f07b18d1004fd5d2165">+331/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>playwright.config.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-91cb9615068914b2e4f5d66fcbed50d3e9771ea2d882f8e8048bbe54eab3c414">+19/-7</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>changeOrg.spec_old.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-1a658a67b111f3f391520406aed0a66793d0e91c3157dca683a6e3603ff37bc6">+239/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>enrichment.spec_old.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-92bb2446bdc69b0b0a88ea39a9ae169f5266e6021df70ea50149f0c624f1b7f9">+325/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>sanity2_consolidated.spec_old.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-59079f7e0748f2f8d484ea6537936f8bfc488793799fa118c5f92c710645e175">+76/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>schema.spec_old.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-61f5d68bba432d1c3c14d155e24088dd5849e90df5f4e636bf79c6477947105e">+275/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>schemaload.spec_old.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-f230c8cf8362435b93ca2346369054471f362221d4209308616eb256970f8c8e">+188/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>serviceAccount.spec_old.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-fed6804e49039a3e8ad282d4db4ddf9da8afc954bdb4c4dbc0cdeccbe1f3f1c0">+138/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>usersOrg.spec_old.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8118/files#diff-e7b7c318f5ca80a33d8351d60134f6ff3983d89e76e517eb0d8e94ca9b30fac9">+211/-0</a>&nbsp;
</td>

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

</details>

___

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Neha P <144308844+neha00290@users.noreply.github.com>
Co-authored-by: ktx-akshay <akshay.patil@kiara.tech>
2025-08-29 00:30:23 +05:30

335 lines
10 KiB
JavaScript

const testLogger = require('./test-logger.js');
/**
* Enhanced wait utilities with double assurance (locator + page state)
* Replaces all hard waits with proper Playwright waits
*/
export class WaitHelpers {
constructor(page) {
this.page = page;
}
/**
* Wait for element to be visible with page state assurance
* @param {string|Locator} selector - CSS selector or Playwright locator
* @param {Object} options - Wait options
*/
async waitForElementVisible(selector, options = {}) {
const { timeout = 30000, description = 'element to be visible' } = options;
testLogger.wait('Element Visible', description, { selector: selector.toString?.() || selector });
try {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
// Double assurance: locator + page state
await Promise.all([
locator.waitFor({ state: 'visible', timeout }),
this.page.waitForLoadState('domcontentloaded', { timeout: Math.min(timeout, 10000) })
]);
testLogger.debug(`Element visible: ${description}`);
return locator;
} catch (error) {
testLogger.error(`Element not visible: ${description}`, { error: error.message });
throw error;
}
}
/**
* Wait for element to be clickable (visible + enabled)
* @param {string|Locator} selector - CSS selector or Playwright locator
* @param {Object} options - Wait options
*/
async waitForElementClickable(selector, options = {}) {
const { timeout = 30000, description = 'element to be clickable' } = options;
testLogger.wait('Element Clickable', description, { selector: selector.toString?.() || selector });
try {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
// Wait for visible and enabled states
await locator.waitFor({ state: 'visible', timeout });
await locator.waitFor({ state: 'attached', timeout });
// For locator objects, we'll just skip the disabled check since locator.click() handles it
if (typeof selector !== 'string') {
// Locator objects handle disabled state automatically
testLogger.debug(`Element clickable (locator): ${description}`);
return locator;
}
// Only do disabled check for string selectors
await this.page.waitForFunction(
(sel) => {
const element = document.querySelector(sel);
return element && !element.disabled && !element.hasAttribute('disabled');
},
selector,
{ timeout }
);
testLogger.debug(`Element clickable: ${description}`);
return locator;
} catch (error) {
testLogger.error(`Element not clickable: ${description}`, { error: error.message });
throw error;
}
}
/**
* Wait for API response with enhanced logging
* @param {string|RegExp} urlPattern - URL pattern to match
* @param {Object} options - Wait options
*/
async waitForApiResponse(urlPattern, options = {}) {
const {
timeout = 30000,
status = [200],
description = 'API response',
method = null
} = options;
testLogger.wait('API Response', description, { urlPattern, expectedStatus: status });
try {
const startTime = Date.now();
const response = await this.page.waitForResponse(
(response) => {
const url = response.url();
const matchesUrl = typeof urlPattern === 'string' ?
url.includes(urlPattern) :
urlPattern.test(url);
const matchesMethod = !method || response.request().method() === method;
const matchesStatus = Array.isArray(status) ?
status.includes(response.status()) :
response.status() === status;
return matchesUrl && matchesMethod && matchesStatus;
},
{ timeout }
);
const duration = Date.now() - startTime;
testLogger.apiCall(
response.request().method(),
response.url(),
response.status(),
duration,
{ description }
);
return response;
} catch (error) {
testLogger.error(`API response timeout: ${description}`, {
urlPattern,
expectedStatus: status,
error: error.message
});
throw error;
}
}
/**
* Wait for page navigation to complete
* @param {Function} action - Function that triggers navigation
* @param {Object} options - Wait options
*/
async waitForNavigation(action, options = {}) {
const { timeout = 30000, description = 'navigation' } = options;
testLogger.wait('Navigation', description);
try {
await Promise.all([
this.page.waitForLoadState('networkidle', { timeout }),
action()
]);
testLogger.navigation(this.page.url(), { description });
} catch (error) {
testLogger.error(`Navigation failed: ${description}`, { error: error.message });
throw error;
}
}
/**
* Wait for text content to appear in element
* @param {string|Locator} selector - CSS selector or Playwright locator
* @param {string|RegExp} text - Expected text or pattern
* @param {Object} options - Wait options
*/
async waitForTextContent(selector, text, options = {}) {
const { timeout = 30000, description = 'text content' } = options;
testLogger.wait('Text Content', description, {
selector: selector.toString?.() || selector,
expectedText: text
});
try {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
if (typeof text === 'string') {
await locator.getByText(text).waitFor({ timeout });
} else {
// For RegExp, use waitForFunction
await this.page.waitForFunction(
(sel, pattern) => {
const element = typeof sel === 'string' ?
document.querySelector(sel) :
sel;
return element && new RegExp(pattern).test(element.textContent);
},
selector,
text.source,
{ timeout }
);
}
testLogger.debug(`Text content found: ${description}`);
return locator;
} catch (error) {
testLogger.error(`Text content not found: ${description}`, { error: error.message });
throw error;
}
}
/**
* Wait for element to disappear
* @param {string|Locator} selector - CSS selector or Playwright locator
* @param {Object} options - Wait options
*/
async waitForElementHidden(selector, options = {}) {
const { timeout = 30000, description = 'element to be hidden' } = options;
testLogger.wait('Element Hidden', description, { selector: selector.toString?.() || selector });
try {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
await locator.waitFor({ state: 'hidden', timeout });
testLogger.debug(`Element hidden: ${description}`);
} catch (error) {
testLogger.error(`Element still visible: ${description}`, { error: error.message });
throw error;
}
}
/**
* Smart wait for form submission (handles both navigation and AJAX)
* @param {Function} submitAction - Function that submits the form
* @param {Object} options - Wait options
*/
async waitForFormSubmission(submitAction, options = {}) {
const {
timeout = 30000,
waitForResponse = null,
waitForNavigation = false,
description = 'form submission'
} = options;
testLogger.wait('Form Submission', description);
try {
const promises = [submitAction()];
if (waitForNavigation) {
promises.push(this.page.waitForLoadState('networkidle', { timeout }));
}
if (waitForResponse) {
promises.push(this.waitForApiResponse(waitForResponse, { timeout }));
}
await Promise.all(promises);
testLogger.debug(`Form submitted: ${description}`);
} catch (error) {
testLogger.error(`Form submission failed: ${description}`, { error: error.message });
throw error;
}
}
/**
* Wait with retries for flaky elements
* @param {Function} action - Action to retry
* @param {Object} options - Retry options
*/
async waitWithRetry(action, options = {}) {
const {
retries = 3,
delay = 1000,
description = 'action with retry'
} = options;
testLogger.wait('Retry Action', description, { maxRetries: retries });
for (let attempt = 1; attempt <= retries; attempt++) {
try {
await action();
testLogger.debug(`Action succeeded on attempt ${attempt}: ${description}`);
return;
} catch (error) {
if (attempt === retries) {
testLogger.error(`Action failed after ${retries} attempts: ${description}`, {
error: error.message
});
throw error;
}
testLogger.warn(`Attempt ${attempt} failed, retrying: ${description}`, {
error: error.message,
nextRetryIn: `${delay}ms`
});
// Use intelligent wait instead of hard timeout
if (delay <= 1000) {
await this.page.waitForLoadState('domcontentloaded');
} else {
await this.page.waitForLoadState('networkidle', { timeout: Math.min(delay, 3000) });
}
}
}
}
}
/**
* Enhanced utility functions for common wait patterns
*/
const waitUtils = {
/**
* Replace page.waitForTimeout with smart waiting
* @param {import('@playwright/test').Page} page
* @param {number} ms - Milliseconds (will be converted to smart wait)
* @param {string} reason - Reason for waiting
*/
async smartWait(page, ms, reason = 'timeout replacement') {
// Convert common timeout patterns to smart waits
if (ms <= 1000) {
// Short waits - usually for UI updates
await page.waitForLoadState('domcontentloaded');
} else if (ms <= 5000) {
// Medium waits - usually for network/API
await page.waitForLoadState('networkidle');
} else {
// Long waits - fallback to shorter intelligent wait
await page.waitForLoadState('networkidle', { timeout: Math.min(ms, 10000) });
}
},
/**
* Create wait helpers instance for a page
* @param {import('@playwright/test').Page} page
*/
create(page) {
return new WaitHelpers(page);
}
};
module.exports = {
WaitHelpers,
waitUtils
};