mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
### **PR Type**
Enhancement, Tests
___
### **Description**
- Add helper to change variable and monitor dependencies
- Refactor dependency tests to use helper function
- Migrate variable-helpers.js to ES module exports
- Enable dependency tests in CI workflow
___
<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>dashboard-variables-scoped.js</strong><dd><code>Add
changeVariableValueAndMonitorDependencies helper</code>
</dd></summary>
<hr>
tests/ui-testing/pages/dashboardPages/dashboard-variables-scoped.js
<ul><li>Added <code>changeVariableValueAndMonitorDependencies</code>
method<br> <li> Dynamically import and use
<code>monitorVariableAPICalls</code><br> <li> Sequence dropdown
interaction and API monitoring<br> <li> Error handling for missing
options and load failures</ul>
</details>
</td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10200/files#diff-86abb9f90fabd982dedfdd9a21a052a47b6984edc7bd29a51ee104c8c473aec4">+101/-0</a>
</td>
</tr>
</table></td></tr><tr><td><strong>Tests</strong></td><td><table>
<tr>
<td>
<details>
<summary><strong>dashboard-variables-dependency.spec.js</strong><dd><code>Refactor
tests to use new dependency helper</code>
</dd></summary>
<hr>
tests/ui-testing/playwright-tests/Dashboards/dashboard-variables-dependency.spec.js
<ul><li>Replaced manual API monitor and dropdown logic with helper<br>
<li> Updated <code>expectedAPICalls</code> and assertion on
<code>result.success</code><br> <li> Skipped unstable error state test
(<code>test.skip</code>)<br> <li> Extended timeouts and stabilized
selectors</ul>
</details>
</td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10200/files#diff-ad5a7685fbf4477a81657949b7dd4aa4ada6e66a4576d0c61bacb388095d0b52">+53/-157</a></td>
</tr>
</table></td></tr><tr><td><strong>Refactoring</strong></td><td><table>
<tr>
<td>
<details>
<summary><strong>variable-helpers.js</strong><dd><code>Migrate
variable-helpers to ES module</code>
</dd></summary>
<hr>
tests/ui-testing/playwright-tests/utils/variable-helpers.js
<ul><li>Converted CommonJS require to ES <code>import</code><br> <li>
Removed <code>module.exports</code> in favor of named exports</ul>
</details>
</td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10200/files#diff-2e2771d2582ab60dfcbaeef480226887677e58b881edbb244e0ffa4396ffbb5c">+1/-15</a>
</td>
</tr>
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
<td>
<details>
<summary><strong>playwright.yml</strong><dd><code>Enable dependency
tests in CI workflow</code>
</dd></summary>
<hr>
.github/workflows/playwright.yml
<ul><li>Uncommented <code>Dashboards-Variables-Dependency</code>
testfolder<br> <li> Enabled dependency spec in CI Chrome job</ul>
</details>
</td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10200/files#diff-7afcd2d8f7b49bda74843f209eefb7b2da45f7e7803bf2e4bd636699b76aa2d3">+6/-6</a>
</td>
</tr>
</table></td></tr></tr></tbody></table>
</details>
___
432 lines
14 KiB
JavaScript
432 lines
14 KiB
JavaScript
/**
|
|
* Variable Testing Helper Utilities
|
|
* Provides common methods for variable API monitoring, assertions, and state validation
|
|
*/
|
|
|
|
import testLogger from './test-logger.js';
|
|
|
|
/**
|
|
* Monitor variable values API calls and track completion
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {Object} options - Configuration options
|
|
* @param {number} options.expectedCount - Expected number of API calls (default: 1)
|
|
* @param {number} options.timeout - Timeout in milliseconds (default: 15000)
|
|
* @returns {Promise<Object>} - {success, actualCount, calls, timedOut}
|
|
*/
|
|
export async function monitorVariableAPICalls(page, options = {}) {
|
|
const { expectedCount = 1, timeout = 15000 } = options;
|
|
const startTime = Date.now();
|
|
const apiCalls = [];
|
|
|
|
return new Promise((resolve) => {
|
|
const responseHandler = async (response) => {
|
|
const url = response.url();
|
|
|
|
if (url.includes('_values_stream') || url.includes('/values')) {
|
|
try {
|
|
const body = await response.text();
|
|
const status = response.status();
|
|
|
|
const callInfo = {
|
|
url,
|
|
status,
|
|
timestamp: Date.now(),
|
|
duration: Date.now() - startTime,
|
|
completed: body.includes('[[DONE]]') || body.includes('"type":"end"'),
|
|
hasData: body.length > 0,
|
|
};
|
|
|
|
apiCalls.push(callInfo);
|
|
|
|
testLogger.info(`Variable API call ${apiCalls.length}/${expectedCount}: ${status} - ${callInfo.completed ? 'Completed' : 'Incomplete'}`);
|
|
|
|
if (callInfo.completed && apiCalls.length >= expectedCount) {
|
|
page.off('response', responseHandler);
|
|
resolve({
|
|
success: true,
|
|
actualCount: apiCalls.length,
|
|
calls: apiCalls,
|
|
timedOut: false,
|
|
totalDuration: Date.now() - startTime
|
|
});
|
|
}
|
|
} catch (error) {
|
|
testLogger.debug(`Error reading response body: ${error.message}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
page.on('response', responseHandler);
|
|
|
|
setTimeout(() => {
|
|
page.off('response', responseHandler);
|
|
testLogger.warn(`Variable API monitoring timed out: ${apiCalls.length}/${expectedCount} calls completed`);
|
|
resolve({
|
|
success: apiCalls.length >= expectedCount,
|
|
actualCount: apiCalls.length,
|
|
calls: apiCalls,
|
|
timedOut: true,
|
|
totalDuration: timeout
|
|
});
|
|
}, timeout);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wait for variable to load by checking its state
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} variableName - Variable name
|
|
* @param {Object} options - Configuration options
|
|
* @returns {Promise<Object>} - {success, state, error}
|
|
*/
|
|
export async function waitForVariableToLoad(page, variableName, options = {}) {
|
|
const { timeout = 10000 } = options;
|
|
|
|
try {
|
|
// Wait for variable to be visible
|
|
await page.locator(`[data-test="dashboard-variable-${variableName}"]`).waitFor({
|
|
state: 'visible',
|
|
timeout
|
|
});
|
|
|
|
// Check if variable has error state
|
|
const hasError = await page.locator(`[data-test="dashboard-variable-${variableName}-error"]`).isVisible().catch(() => false);
|
|
|
|
if (hasError) {
|
|
return { success: false, state: 'error', error: 'Variable shows error state' };
|
|
}
|
|
|
|
// Check if variable is still loading
|
|
const isLoading = await page.locator(`[data-test="dashboard-variable-${variableName}-loading"]`).isVisible().catch(() => false);
|
|
|
|
if (isLoading) {
|
|
// Wait for loading to finish
|
|
await page.locator(`[data-test="dashboard-variable-${variableName}-loading"]`).waitFor({
|
|
state: 'hidden',
|
|
timeout
|
|
});
|
|
}
|
|
|
|
return { success: true, state: 'loaded', error: null };
|
|
} catch (error) {
|
|
return { success: false, state: 'timeout', error: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify variable API was called when clicking dropdown
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {Function} action - Action to trigger API call (e.g., clicking dropdown)
|
|
* @param {Object} options - Configuration options
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
export async function verifyVariableAPITriggered(page, action, options = {}) {
|
|
const { timeout = 10000 } = options;
|
|
let apiCalled = false;
|
|
|
|
const responseHandler = async (response) => {
|
|
const url = response.url();
|
|
if (url.includes('_values_stream') || url.includes('/values')) {
|
|
apiCalled = true;
|
|
testLogger.info(`Variable API triggered: ${url}`);
|
|
}
|
|
};
|
|
|
|
page.on('response', responseHandler);
|
|
|
|
try {
|
|
await action();
|
|
await page.waitForTimeout(timeout);
|
|
} finally {
|
|
page.off('response', responseHandler);
|
|
}
|
|
|
|
return apiCalled;
|
|
}
|
|
|
|
/**
|
|
* Check if refresh button indicates changes are pending
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} level - 'global' or panel ID
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
export async function hasRefreshIndicator(page, level = 'global') {
|
|
try {
|
|
if (level === 'global') {
|
|
const refreshBtn = page.locator('[data-test="dashboard-refresh-btn"]');
|
|
// Check if button has warning color class
|
|
// When Quasar applies :color="warning" with :outline="false", it adds q-btn--warning class
|
|
// When :outline="true", it's just an outlined button with no warning
|
|
const classList = await refreshBtn.getAttribute('class');
|
|
// Check for Quasar warning classes (q-btn--warning) or custom refresh indicator classes
|
|
return classList && (
|
|
classList.includes('q-btn--warning') ||
|
|
classList.includes('bg-warning') ||
|
|
classList.includes('text-warning') ||
|
|
classList.includes('refresh-needed') ||
|
|
classList.includes('needs-refresh') ||
|
|
classList.includes('highlighted')
|
|
);
|
|
} else {
|
|
const panelRefreshBtn = page.locator(`[data-panel="dashboard-panel-refresh-panel-btn"]`);
|
|
const classList = await panelRefreshBtn.getAttribute('class');
|
|
return classList && (
|
|
classList.includes('q-btn--warning') ||
|
|
classList.includes('bg-warning') ||
|
|
classList.includes('text-warning') ||
|
|
classList.includes('refresh-needed') ||
|
|
classList.includes('needs-refresh') ||
|
|
classList.includes('highlighted')
|
|
);
|
|
}
|
|
} catch (error) {
|
|
testLogger.debug(`Error checking refresh indicator: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify panel shows refresh needed indicator
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} panelId - Panel ID
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
export async function panelNeedsRefresh(page, panelId) {
|
|
try {
|
|
const warningIcon = page.locator(`[data-test="dashboard-panel-refresh-panel-btn"]`);
|
|
return await warningIcon.isVisible({ timeout: 3000 });
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Track panel reload by monitoring query API calls
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} panelId - Panel ID
|
|
* @param {Function} action - Action that triggers panel reload
|
|
* @param {number} timeout - Timeout in milliseconds
|
|
* @returns {Promise<Object>} - {reloaded, queryCount}
|
|
*/
|
|
export async function trackPanelReload(page, panelId, action, timeout = 10000) {
|
|
const queryCalls = [];
|
|
const startTime = Date.now();
|
|
|
|
const responseHandler = async (response) => {
|
|
const url = response.url();
|
|
if (url.includes('_search') || url.includes('_multi_search')) {
|
|
queryCalls.push({
|
|
url,
|
|
timestamp: Date.now(),
|
|
panelId: extractPanelIdFromURL(url)
|
|
});
|
|
testLogger.info(`Panel query detected: ${queryCalls.length} calls`);
|
|
}
|
|
};
|
|
|
|
page.on('response', responseHandler);
|
|
|
|
try {
|
|
await action();
|
|
await page.waitForTimeout(timeout);
|
|
} finally {
|
|
page.off('response', responseHandler);
|
|
}
|
|
|
|
return {
|
|
reloaded: queryCalls.length > 0,
|
|
queryCount: queryCalls.length,
|
|
calls: queryCalls,
|
|
duration: Date.now() - startTime
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract panel ID from URL (helper function)
|
|
*/
|
|
function extractPanelIdFromURL(url) {
|
|
const match = url.match(/panelId=([^&]+)/);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
/**
|
|
* Verify variable value persists across tab switches
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} variableName - Variable name
|
|
* @param {string} tabId - Tab ID
|
|
* @param {string} expectedValue - Expected value
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
export async function verifyVariableValuePersists(page, variableName, tabId, expectedValue) {
|
|
try {
|
|
// Switch to the tab
|
|
await page.locator(`[data-test="dashboard-tab-${tabId}"]`).click();
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Get variable value
|
|
const variableInput = page.locator(`[data-test="dashboard-variable-${variableName}"] input`);
|
|
const actualValue = await variableInput.inputValue();
|
|
|
|
testLogger.info(`Variable ${variableName} in tab ${tabId}: expected="${expectedValue}", actual="${actualValue}"`);
|
|
|
|
return actualValue === expectedValue;
|
|
} catch (error) {
|
|
testLogger.error(`Error verifying variable value: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify variable loading sequence with dependencies
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {Array} variableNames - Array of variable names in expected load order
|
|
* @param {number} timeout - Timeout in milliseconds
|
|
* @returns {Promise<Object>} - {success, loadOrder, errors}
|
|
*/
|
|
export async function verifyVariableLoadSequence(page, variableNames, timeout = 20000) {
|
|
const loadOrder = [];
|
|
const errors = [];
|
|
const startTime = Date.now();
|
|
|
|
const responseHandler = async (response) => {
|
|
const url = response.url();
|
|
|
|
if (url.includes('_values_stream') || url.includes('/values')) {
|
|
try {
|
|
const body = await response.text();
|
|
if (body.includes('[[DONE]]') || body.includes('"type":"end"')) {
|
|
// Extract variable name from URL if possible
|
|
const variableName = extractVariableNameFromURL(url);
|
|
if (variableName && variableNames.includes(variableName)) {
|
|
loadOrder.push({
|
|
name: variableName,
|
|
timestamp: Date.now(),
|
|
order: loadOrder.length + 1,
|
|
duration: Date.now() - startTime
|
|
});
|
|
testLogger.info(`Variable ${variableName} loaded (${loadOrder.length}/${variableNames.length})`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
errors.push({ error: error.message, timestamp: Date.now() });
|
|
}
|
|
}
|
|
};
|
|
|
|
page.on('response', responseHandler);
|
|
|
|
try {
|
|
await page.waitForTimeout(timeout);
|
|
} finally {
|
|
page.off('response', responseHandler);
|
|
}
|
|
|
|
return {
|
|
success: loadOrder.length === variableNames.length,
|
|
loadOrder,
|
|
errors,
|
|
totalDuration: Date.now() - startTime
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract variable name from URL (helper function)
|
|
*/
|
|
function extractVariableNameFromURL(url) {
|
|
const match = url.match(/variable=([^&]+)/);
|
|
return match ? decodeURIComponent(match[1]) : null;
|
|
}
|
|
|
|
/**
|
|
* Verify URL contains correct variable parameters
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {Object} expectedVariables - Expected variables {name: value, ...}
|
|
* @param {string} scope - 'global', 'tab', or 'panel'
|
|
* @param {string} scopeId - Tab or panel ID (if applicable)
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
export async function verifyVariablesInURL(page, expectedVariables, scope = 'global', scopeId = null) {
|
|
const currentURL = page.url();
|
|
|
|
for (const [name, value] of Object.entries(expectedVariables)) {
|
|
let expectedParam;
|
|
|
|
if (scope === 'global') {
|
|
expectedParam = `v-${name}=${encodeURIComponent(value)}`;
|
|
} else if (scope === 'tab') {
|
|
expectedParam = `v-${name}.t.${scopeId}=${encodeURIComponent(value)}`;
|
|
} else if (scope === 'panel') {
|
|
expectedParam = `v-${name}.p.${scopeId}=${encodeURIComponent(value)}`;
|
|
}
|
|
|
|
if (!currentURL.includes(expectedParam)) {
|
|
testLogger.error(`URL missing expected parameter: ${expectedParam}`);
|
|
testLogger.debug(`Current URL: ${currentURL}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Wait for all panels to finish loading
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {number} panelCount - Expected number of panels
|
|
* @param {number} timeout - Timeout in milliseconds
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
export async function waitForAllPanelsToLoad(page, panelCount, timeout = 30000) {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
await page.waitForFunction(
|
|
(count) => {
|
|
const loadedPanels = document.querySelectorAll('[data-test*="dashboard-panel-"][data-state="loaded"]');
|
|
return loadedPanels.length >= count;
|
|
},
|
|
panelCount,
|
|
{ timeout, polling: 500 }
|
|
);
|
|
|
|
testLogger.info(`All ${panelCount} panels loaded in ${Date.now() - startTime}ms`);
|
|
return true;
|
|
} catch (error) {
|
|
testLogger.error(`Panels failed to load within ${timeout}ms`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert variable API loading states
|
|
* @param {Object} monitorResult - Result from monitorVariableAPICalls
|
|
* @param {Object} expectations - {success: true, minCalls: 1, maxDuration: 10000}
|
|
* @throws {Error} if expectations are not met
|
|
*/
|
|
export function assertVariableAPILoading(monitorResult, expectations = {}) {
|
|
const {
|
|
success = true,
|
|
minCalls = 1,
|
|
maxDuration = 15000,
|
|
shouldTimeout = false
|
|
} = expectations;
|
|
|
|
if (expectations.success !== undefined && monitorResult.success !== success) {
|
|
throw new Error(`Expected success=${success}, got ${monitorResult.success}`);
|
|
}
|
|
|
|
if (monitorResult.actualCount < minCalls) {
|
|
throw new Error(`Expected at least ${minCalls} API calls, got ${monitorResult.actualCount}`);
|
|
}
|
|
|
|
if (monitorResult.totalDuration > maxDuration) {
|
|
throw new Error(`Loading took ${monitorResult.totalDuration}ms, expected < ${maxDuration}ms`);
|
|
}
|
|
|
|
if (shouldTimeout !== monitorResult.timedOut) {
|
|
throw new Error(`Expected timedOut=${shouldTimeout}, got ${monitorResult.timedOut}`);
|
|
}
|
|
|
|
testLogger.info(`Variable API assertion passed: ${monitorResult.actualCount} calls in ${monitorResult.totalDuration}ms`);
|
|
}
|