mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 08:09:27 +00:00
## Summary
- Replaced every bare `waitForLoadState('networkidle')` with `{ timeout:
10000 }.catch(() => {})` across **56 files** (35 spec files, 21 page
objects)
- OpenObserve pages maintain persistent RUM/replay/WebSocket connections
that prevent `networkidle` from ever resolving, causing 90s timeout
crashes
- Added `fetchWithRetry` helper for transient 502/proxy errors in SDR
multi-pattern `beforeAll`
- Bumped SDR multi-pattern test timeout from 300s to 420s for CI
## Files changed
**Page objects (21 files):** alertsPage, alertManagement,
alertCreationWizard, alertTemplatesPage, alertBulkOperations, logsPage,
streamsPage, streamAssociationPage, sdrPatternsPage,
sdrVerificationPage, dashboardPage, functionsPage, pipelinesPage,
enrichmentPage, sanityPage, schemaPage, loginPage, commonActions, and
more
**Spec files (35 files):** All SDR specs, all Traces specs, all Logs
specs, all RegressionSet specs, Alerts specs, Pipelines specs,
Dashboards specs, Functions specs, GeneralTests specs, wait-helpers
utility
## Test plan
- [x] Verify zero bare `networkidle` calls remain in `tests/ui-testing/`
- [ ] CI passes for SDR, Streams, Logs Regression, and other test suites
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
432 lines
13 KiB
JavaScript
432 lines
13 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safely wait for element to be hidden (won't fail test if timeout)
|
|
* Use this for optional UI cleanup waits where failure is acceptable
|
|
* @param {string|Locator} selector - CSS selector or Playwright locator
|
|
* @param {Object} options - Wait options
|
|
* @returns {boolean} - true if element became hidden, false if timeout
|
|
*/
|
|
async safeWaitForHidden(selector, options = {}) {
|
|
const { timeout = 5000, description = 'element to hide' } = options;
|
|
|
|
try {
|
|
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
|
|
await locator.waitFor({ state: 'hidden', timeout });
|
|
testLogger.debug(`[Safe] Element hidden: ${description}`);
|
|
return true;
|
|
} catch {
|
|
testLogger.debug(`[Safe] Element hide timeout (acceptable): ${description}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safely wait for network to be idle (won't fail test if timeout)
|
|
* Use this for optional network settling where failure is acceptable
|
|
* @param {Object} options - Wait options
|
|
* @returns {boolean} - true if network became idle, false if timeout
|
|
*/
|
|
async safeWaitForNetworkIdle(options = {}) {
|
|
const { timeout = 3000, description = 'network idle' } = options;
|
|
|
|
try {
|
|
await this.page.waitForLoadState('networkidle', { timeout });
|
|
testLogger.debug(`[Safe] Network idle: ${description}`);
|
|
return true;
|
|
} catch {
|
|
testLogger.debug(`[Safe] Network idle timeout (acceptable): ${description}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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', { timeout: 10000 }).catch(() => {});
|
|
} else {
|
|
// Long waits - fallback to shorter intelligent wait
|
|
await page.waitForLoadState('networkidle', { timeout: Math.min(ms, 10000) }).catch(() => {});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create wait helpers instance for a page
|
|
* @param {import('@playwright/test').Page} page
|
|
*/
|
|
create(page) {
|
|
return new WaitHelpers(page);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Standalone safe wait functions (for easy import without class instantiation)
|
|
* These replace the anti-pattern: .catch(() => {})
|
|
*/
|
|
|
|
/**
|
|
* Safely wait for element to be hidden
|
|
* @param {import('@playwright/test').Page} page
|
|
* @param {string} selector - CSS selector
|
|
* @param {Object} options - Wait options
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async function safeWaitForHidden(page, selector, options = {}) {
|
|
const { timeout = 5000 } = options;
|
|
try {
|
|
await page.locator(selector).waitFor({ state: 'hidden', timeout });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safely wait for network to be idle
|
|
* @param {import('@playwright/test').Page} page
|
|
* @param {Object} options - Wait options
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async function safeWaitForNetworkIdle(page, options = {}) {
|
|
const { timeout = 3000 } = options;
|
|
try {
|
|
await page.waitForLoadState('networkidle', { timeout });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safely wait for DOM content to be loaded
|
|
* @param {import('@playwright/test').Page} page
|
|
* @param {Object} options - Wait options
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async function safeWaitForDOMContentLoaded(page, options = {}) {
|
|
const { timeout = 5000 } = options;
|
|
try {
|
|
await page.waitForLoadState('domcontentloaded', { timeout });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
WaitHelpers,
|
|
waitUtils,
|
|
safeWaitForHidden,
|
|
safeWaitForNetworkIdle,
|
|
safeWaitForDOMContentLoaded
|
|
}; |