mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 00:09:35 +00:00
test: add regression tests for query inspector, sorting, and logs expand (#10586)
## Summary Add Playwright regression tests for logs functionality: - **Query Inspector**: Verify readonly flag in buildSearch doesn't mutate logs state - **Sorting Logs**: Verify logs are sorted correctly when response has order_by_metadata - **Logs Expand**: Verify last log maintains highlighting when expanding other log entries Tests use Page Object Model pattern with methods added to logsPage.js. Tests for https://github.com/openobserve/openobserve/pull/10580 --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -237,6 +237,14 @@ export class LogsPage {
|
||||
this.patternLoadingSpinner = '.q-spinner-hourglass';
|
||||
this.patternLoadingText = 'text=Extracting patterns from logs...';
|
||||
this.patternEmptyState = 'text=No patterns found';
|
||||
|
||||
// ===== V0.40 REGRESSION TEST LOCATORS =====
|
||||
this.logsSearchResultTableRows = '[data-test="logs-search-result-logs-table"] tbody tr';
|
||||
this.tableRowExpandMenu = '[data-test="table-row-expand-menu"]';
|
||||
this.logDetailsIncludeExcludeBtn = '[data-test="log-details-include-exclude-field-btn"]';
|
||||
this.timestampCells = '[data-test^="log-table-column-"][data-test$="-_timestamp"]';
|
||||
this.searchResultText = '[data-test="logs-search-search-result"]';
|
||||
this.logDetailPanel = '.q-dialog, [data-test*="log-detail"]';
|
||||
}
|
||||
|
||||
|
||||
@@ -3773,6 +3781,22 @@ export class LogsPage {
|
||||
return await this.page.locator(`${this.logsTable} tbody tr`).count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get log table row texts as an array (one string per row)
|
||||
* @param {number} limit - Maximum number of rows to return
|
||||
* @returns {Promise<string[]>} Array of row text content
|
||||
*/
|
||||
async getLogsTableRowTexts(limit = 10) {
|
||||
const rows = this.page.locator(`${this.logsTable} tbody tr`);
|
||||
const count = Math.min(await rows.count(), limit);
|
||||
const texts = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const text = await rows.nth(i).textContent();
|
||||
if (text) texts.push(text.trim());
|
||||
}
|
||||
return texts;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Stream display methods for multi-stream scenarios
|
||||
// ============================================================================
|
||||
@@ -5257,15 +5281,6 @@ export class LogsPage {
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logs table content as text
|
||||
* @returns {Promise<string>} The table content text
|
||||
*/
|
||||
async getLogsTableContent() {
|
||||
const table = this.page.locator(this.logsTable);
|
||||
return await table.textContent().catch(() => '');
|
||||
}
|
||||
|
||||
// ========== BUG REGRESSION TEST METHODS ==========
|
||||
|
||||
/**
|
||||
@@ -5987,4 +6002,158 @@ export class LogsPage {
|
||||
await this.page.getByText(`Pattern ${expectedIndex} of`).waitFor({ state: 'visible', timeout: 5000 });
|
||||
testLogger.info(`Pattern details showing pattern ${expectedIndex}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// V0.40 REGRESSION TEST METHODS
|
||||
// VRL fields, Query Inspector, Sorting, and Highlight tests
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get the logs table element
|
||||
* @returns {Locator} - The logs table locator
|
||||
*/
|
||||
getLogsTable() {
|
||||
return this.page.locator(this.logsSearchResultLogsTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for logs table to be visible
|
||||
* @param {number} timeout - Timeout in milliseconds
|
||||
*/
|
||||
async waitForLogsTable(timeout = 30000) {
|
||||
await this.page.locator(this.logsSearchResultLogsTable).waitFor({ state: 'visible', timeout });
|
||||
testLogger.info('Logs table is visible');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of log rows in the table
|
||||
* @returns {Promise<number>} - Number of log rows
|
||||
*/
|
||||
async getLogRowCount() {
|
||||
const rows = this.page.locator(this.logsSearchResultTableRows);
|
||||
const count = await rows.count();
|
||||
testLogger.info(`Log row count: ${count}`);
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all log rows as locators
|
||||
* @returns {Locator} - All log row locators
|
||||
*/
|
||||
getLogRows() {
|
||||
return this.page.locator(this.logsSearchResultTableRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the first expand menu in the logs table
|
||||
*/
|
||||
async clickFirstExpandMenu() {
|
||||
const expandMenus = this.page.locator(this.tableRowExpandMenu);
|
||||
await expandMenus.first().click();
|
||||
testLogger.info('Clicked first expand menu');
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the last expand menu in the logs table
|
||||
*/
|
||||
async clickLastExpandMenu() {
|
||||
const expandMenus = this.page.locator(this.tableRowExpandMenu);
|
||||
await expandMenus.last().click();
|
||||
testLogger.info('Clicked last expand menu');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the first expand menu is visible
|
||||
* @returns {Promise<boolean>} - Whether the first expand menu is visible
|
||||
*/
|
||||
async isFirstExpandMenuVisible() {
|
||||
const expandMenus = this.page.locator(this.tableRowExpandMenu);
|
||||
const count = await expandMenus.count();
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timestamp cell values from the logs table
|
||||
* @param {number} limit - Maximum number of timestamps to return
|
||||
* @returns {Promise<string[]>} - Array of timestamp values
|
||||
*/
|
||||
async getTimestampCellValues(limit = 5) {
|
||||
const cells = this.page.locator(this.timestampCells);
|
||||
const count = Math.min(await cells.count(), limit);
|
||||
const values = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
let text = await cells.nth(i).textContent();
|
||||
text = text?.trim() || '';
|
||||
// Strip expand button icon text that appears before the timestamp
|
||||
// The cell contains both the expand icon ("chevron_right" or "expand_more") and the timestamp
|
||||
text = text.replace(/^(chevron_right|expand_more|chevron_left|expand_less)/, '').trim();
|
||||
values.push(text);
|
||||
}
|
||||
testLogger.info(`Got ${values.length} timestamp values`);
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of timestamp cells in the logs table
|
||||
* @returns {Promise<number>} - Count of timestamp cells
|
||||
*/
|
||||
async getTimestampCellCount() {
|
||||
const count = await this.page.locator(this.timestampCells).count();
|
||||
testLogger.info(`Timestamp cell count: ${count}`);
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get search result text
|
||||
* @returns {Promise<string>} - Search result text
|
||||
*/
|
||||
async getSearchResultText() {
|
||||
const text = await this.page.locator(this.searchResultText).textContent().catch(() => '');
|
||||
testLogger.info(`Search result text: ${text?.substring(0, 50)}`);
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Press Escape key to close any open dialog
|
||||
*/
|
||||
async pressEscapeToCloseDialog() {
|
||||
await this.page.keyboard.press('Escape');
|
||||
await this.page.waitForTimeout(300);
|
||||
testLogger.info('Pressed Escape to close dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last row in the logs table
|
||||
* @returns {Locator} - The last row locator
|
||||
*/
|
||||
getLastRow() {
|
||||
return this.page.locator(this.logsSearchResultTableRows).last();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first row expand menu
|
||||
* @returns {Locator} - The first expand menu locator
|
||||
*/
|
||||
getFirstRowExpandMenu() {
|
||||
return this.page.locator(this.tableRowExpandMenu).first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if log detail panel is visible
|
||||
* @returns {Promise<boolean>} - Whether panel is visible
|
||||
*/
|
||||
async isLogDetailPanelVisible() {
|
||||
const visible = await this.page.locator(this.logDetailPanel).isVisible().catch(() => false);
|
||||
testLogger.info(`Log detail panel visible: ${visible}`);
|
||||
return visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a locator is visible
|
||||
* @param {Locator} locator - The locator to check
|
||||
*/
|
||||
async expectVisible(locator) {
|
||||
await expect(locator).toBeVisible();
|
||||
testLogger.info('Element is visible');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1203,14 +1203,320 @@ test.describe("Logs Regression Bugs", () => {
|
||||
testLogger.info('✓ Bug #9724 verification complete: Log detail sidebar opens with JSON tab by default');
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
testLogger.info('Logs regression test completed');
|
||||
// ============================================================================
|
||||
// Query Inspector: readonly flag in buildSearch to avoid mutating logs state
|
||||
// Feature: Query inspector should not modify the original logs state
|
||||
// ============================================================================
|
||||
test('should not mutate logs state when using query inspector @queryInspector @P1 @regression @main', async ({ page }) => {
|
||||
testLogger.info('Test: Query inspector should not mutate logs state');
|
||||
|
||||
// Note: Test data cleanup is handled implicitly through time-based filtering.
|
||||
// Tests use recent time ranges (Last 1 hour, Last 15 min) which naturally
|
||||
// exclude old test data. For Bug #9475, test data is ingested with unique
|
||||
// timestamps each run, preventing false positives from previous runs.
|
||||
// Future improvement: Consider using unique stream names per test run
|
||||
// (e.g., `e2e_automate_${Date.now()}`) for complete isolation.
|
||||
// Navigate to logs page
|
||||
await pm.logsPage.clickMenuLinkLogsItem();
|
||||
await pm.logsPage.selectStream('e2e_automate');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Set time range and run initial query
|
||||
await pm.logsPage.clickDateTimeButton();
|
||||
await pm.logsPage.clickRelative15MinButton();
|
||||
await pm.logsPage.clickRefreshButton();
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Capture initial logs state (first N row texts as array)
|
||||
await pm.logsPage.waitForLogsTable(10000);
|
||||
const initialRowTexts = await pm.logsPage.getLogsTableRowTexts(5);
|
||||
expect(initialRowTexts.length, 'Initial table should have rows before opening query inspector').toBeGreaterThan(0);
|
||||
testLogger.info(`Initial rows captured: ${initialRowTexts.length}`);
|
||||
|
||||
// Store initial row count
|
||||
const initialRowCount = await pm.logsPage.getLogRowCount();
|
||||
testLogger.info(`Initial row count: ${initialRowCount}`);
|
||||
|
||||
// Open query inspector/show query toggle
|
||||
await pm.logsPage.clickShowQueryToggle();
|
||||
await page.waitForTimeout(1000);
|
||||
testLogger.info('Opened query inspector');
|
||||
|
||||
// Interact with query inspector - view the query
|
||||
await pm.logsPage.waitForQueryEditorVisible(5000);
|
||||
|
||||
// Click on query editor (readonly interaction)
|
||||
await pm.logsPage.clickQueryEditor();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Close query inspector
|
||||
await pm.logsPage.clickShowQueryToggle();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// PRIMARY ASSERTION 1: Logs table should still be visible after query inspector interaction
|
||||
await pm.logsPage.expectLogsTableVisible();
|
||||
testLogger.info('✓ Logs table remains visible after query inspector interaction');
|
||||
|
||||
// PRIMARY ASSERTION 2: Compare same-index rows to detect mutation
|
||||
const finalRowTexts = await pm.logsPage.getLogsTableRowTexts(5);
|
||||
expect(finalRowTexts.length, 'Logs table should have rows after query inspector').toBeGreaterThan(0);
|
||||
|
||||
// Compare same-index rows to detect reordering or content mutation
|
||||
const rowsToCompare = Math.min(initialRowTexts.length, finalRowTexts.length);
|
||||
let sameIndexMatches = 0;
|
||||
for (let i = 0; i < rowsToCompare; i++) {
|
||||
if (initialRowTexts[i] === finalRowTexts[i]) {
|
||||
sameIndexMatches++;
|
||||
}
|
||||
}
|
||||
// Most rows at same index should match (some tolerance for streaming)
|
||||
const matchRatio = sameIndexMatches / rowsToCompare;
|
||||
expect(matchRatio, `Row content was mutated. Only ${sameIndexMatches}/${rowsToCompare} rows match at same index`).toBeGreaterThanOrEqual(0.8);
|
||||
testLogger.info(`✓ Content integrity: ${sameIndexMatches}/${rowsToCompare} rows match at same index (${Math.round(matchRatio * 100)}%)`);
|
||||
|
||||
// Also verify row count didn't decrease
|
||||
const finalRowCount = await pm.logsPage.getLogRowCount();
|
||||
expect(finalRowCount).toBeGreaterThanOrEqual(initialRowCount);
|
||||
testLogger.info(`✓ Row count preserved: initial=${initialRowCount}, final=${finalRowCount}`);
|
||||
|
||||
testLogger.info('✓ PRIMARY CHECK PASSED: Query inspector does not mutate logs state (readonly flag working)');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Sorting Logs: Sort logs when response has order_by_metadata
|
||||
// Feature: Logs should be correctly sorted when API response contains order_by_metadata
|
||||
// ============================================================================
|
||||
test('should sort logs correctly when response has order_by_metadata @sorting @P1 @regression @main', async ({ page }) => {
|
||||
testLogger.info('Test: Logs sorting with order_by_metadata');
|
||||
|
||||
// Navigate to logs page
|
||||
await pm.logsPage.clickMenuLinkLogsItem();
|
||||
await pm.logsPage.selectStream('e2e_automate');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Turn off quick mode if enabled
|
||||
await pm.logsPage.ensureQuickModeState(false);
|
||||
|
||||
// Set time range
|
||||
await pm.logsPage.clickDateTimeButton();
|
||||
await pm.logsPage.clickRelative15MinButton();
|
||||
|
||||
// Run query
|
||||
await pm.logsPage.clickRefreshButton();
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// Wait for results
|
||||
await pm.logsPage.waitForLogsTable(15000);
|
||||
|
||||
// Get timestamps from visible log rows to verify sorting
|
||||
const timestampCount = await pm.logsPage.getTimestampCellCount();
|
||||
testLogger.info(`Found ${timestampCount} timestamp cells`);
|
||||
|
||||
// GUARD ASSERTION: Must have at least 2 timestamps to verify sorting - fail fast if not enough data
|
||||
expect(timestampCount).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const timestamps = await pm.logsPage.getTimestampCellValues(5);
|
||||
testLogger.info(`First ${timestamps.length} timestamps: ${JSON.stringify(timestamps)}`);
|
||||
|
||||
// PRIMARY ASSERTION: Timestamps should be in descending order (newest first) by default
|
||||
// Logs with order_by_metadata should maintain proper sort order
|
||||
|
||||
// Helper to parse timestamps in OpenObserve's format "MMM DD, YYYY HH:mm:ss.SSS Z" or similar
|
||||
// Falls back to native Date.parse if the format doesn't match
|
||||
const MONTH_MAP = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };
|
||||
const parseTimestamp = (str) => {
|
||||
if (!str || typeof str !== 'string') return NaN;
|
||||
// Try native Date.parse first (handles ISO 8601 and common formats)
|
||||
const native = new Date(str).getTime();
|
||||
if (!isNaN(native)) return native;
|
||||
// Try to parse "MMM DD, YYYY HH:mm:ss[.SSS] [+/-]ZZZZ" format
|
||||
// Example: "Jan 15, 2024 10:30:45.123 +0000" or "Jan 15, 2024 10:30:45 +0000"
|
||||
const match = str.match(/^(\w{3})\s+(\d{1,2}),\s+(\d{4})\s+(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?\s*([+-]\d{4})?/);
|
||||
if (match) {
|
||||
const [, mon, day, year, hour, min, sec, ms, tz] = match;
|
||||
const month = MONTH_MAP[mon];
|
||||
if (month !== undefined) {
|
||||
const date = new Date(Date.UTC(+year, month, +day, +hour, +min, +sec, ms ? +ms.slice(0, 3) : 0));
|
||||
// Adjust for timezone offset if present (e.g., +0000, -0500)
|
||||
if (tz) {
|
||||
const tzHours = parseInt(tz.slice(0, 3), 10);
|
||||
const tzMins = parseInt(tz.slice(0, 1) + tz.slice(3), 10);
|
||||
date.setUTCMinutes(date.getUTCMinutes() - tzHours * 60 - tzMins);
|
||||
}
|
||||
return date.getTime();
|
||||
}
|
||||
}
|
||||
return NaN;
|
||||
};
|
||||
|
||||
let isDescending = true;
|
||||
let validTimestampCount = 0;
|
||||
const nanIndices = new Set(); // Track indices to avoid duplicates
|
||||
for (let i = 1; i < timestamps.length; i++) {
|
||||
const prev = parseTimestamp(timestamps[i - 1]);
|
||||
const curr = parseTimestamp(timestamps[i]);
|
||||
// Collect NaN timestamp indices (use Set to avoid duplicates when same index is prev and curr)
|
||||
if (isNaN(prev)) nanIndices.add(i - 1);
|
||||
if (isNaN(curr)) nanIndices.add(i);
|
||||
if (isNaN(prev) || isNaN(curr)) {
|
||||
testLogger.warn(`Timestamp parsing failed: "${timestamps[i-1]}" -> ${prev}, "${timestamps[i]}" -> ${curr}`);
|
||||
continue; // Skip this comparison but continue checking others
|
||||
}
|
||||
validTimestampCount++;
|
||||
if (prev < curr) {
|
||||
isDescending = false;
|
||||
testLogger.warn(`Sort order broken at index ${i}: ${timestamps[i-1]} > ${timestamps[i]}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Convert to array with values for error message
|
||||
const nanTimestamps = [...nanIndices].map(idx => ({ index: idx, value: timestamps[idx] }));
|
||||
|
||||
// ASSERTION: No timestamps should be unparseable (fail with descriptive message)
|
||||
expect(nanTimestamps.length, `Unparseable timestamps: ${JSON.stringify(nanTimestamps)}`).toBe(0);
|
||||
|
||||
// ASSERTION: Must have at least 2 timestamps to verify sorting
|
||||
expect(timestamps.length).toBeGreaterThanOrEqual(2);
|
||||
// ASSERTION: At least one valid timestamp comparison must have been made
|
||||
expect(validTimestampCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// PRIMARY ASSERTION: Logs must be sorted in descending order (newest first)
|
||||
expect(isDescending).toBe(true);
|
||||
testLogger.info(`✓ Logs are correctly sorted in descending order (${validTimestampCount} comparisons made)`);
|
||||
|
||||
// PRIMARY ASSERTION: Results should be displayed
|
||||
const resultText = await pm.logsPage.getSearchResultText();
|
||||
expect(resultText).toBeTruthy();
|
||||
testLogger.info(`✓ Results displayed: ${resultText?.substring(0, 50)}`);
|
||||
|
||||
testLogger.info('✓ PRIMARY CHECK PASSED: Logs sorting works with order_by_metadata');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Logs Expand: Last log should maintain highlighting when expanding
|
||||
// Feature: When expanding a log entry, the last log should not lose its highlighting
|
||||
// ============================================================================
|
||||
test('should maintain highlighting on last log when expanding log entries @expand @highlight @P1 @regression @main', async ({ page }) => {
|
||||
testLogger.info('Test: Last log highlighting on expand');
|
||||
|
||||
// Navigate to logs page
|
||||
await pm.logsPage.clickMenuLinkLogsItem();
|
||||
await pm.logsPage.selectStream('e2e_automate');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Turn off quick mode if enabled
|
||||
await pm.logsPage.ensureQuickModeState(false);
|
||||
|
||||
// Set time range and run query
|
||||
await pm.logsPage.clickDateTimeButton();
|
||||
await pm.logsPage.clickRelative15MinButton();
|
||||
await pm.logsPage.clickRefreshButton();
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// Wait for logs table
|
||||
await pm.logsPage.waitForLogsTable(15000);
|
||||
|
||||
// Get all log rows
|
||||
const rowCount = await pm.logsPage.getLogRowCount();
|
||||
testLogger.info(`Found ${rowCount} log rows`);
|
||||
|
||||
if (rowCount < 2) {
|
||||
testLogger.warn('Not enough log rows to test highlighting - skipping');
|
||||
test.skip(true, 'Not enough log rows');
|
||||
return;
|
||||
}
|
||||
|
||||
// Expand first log row using the expand menu to trigger highlighting
|
||||
await pm.logsPage.clickFirstExpandMenu();
|
||||
await page.waitForTimeout(1000);
|
||||
testLogger.info('Expanded first log row');
|
||||
|
||||
// Close the dialog/detail panel that opens (press Escape or click outside)
|
||||
await pm.logsPage.pressEscapeToCloseDialog();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Click on the last visible log row to select/highlight it
|
||||
const lastRow = pm.logsPage.getLastRow();
|
||||
await lastRow.scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Get the expand menu for last row and click it
|
||||
await pm.logsPage.clickLastExpandMenu();
|
||||
await page.waitForTimeout(1000);
|
||||
testLogger.info('Expanded last log row');
|
||||
|
||||
// Check if the log detail panel shows for last row
|
||||
const isPanelVisible = await pm.logsPage.isLogDetailPanelVisible();
|
||||
testLogger.info(`Log detail panel visible: ${isPanelVisible}`);
|
||||
|
||||
// Close the detail panel
|
||||
await pm.logsPage.pressEscapeToCloseDialog();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Capture the last row's visual state AFTER closing (closed-but-still-highlighted state)
|
||||
// This is the persistent highlight state we expect to survive when expanding a different row
|
||||
const lastRowClasses = await lastRow.getAttribute('class') || '';
|
||||
testLogger.info(`Last row baseline state (closed but highlighted) - classes: ${lastRowClasses}`);
|
||||
|
||||
// Now expand first row again - this is when the bug would cause last row to lose highlighting
|
||||
const firstRowExpandMenu = pm.logsPage.getFirstRowExpandMenu();
|
||||
await firstRowExpandMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
testLogger.info('Expanded first log row again');
|
||||
|
||||
// PRIMARY ASSERTION 1: Last row should still be visible
|
||||
await pm.logsPage.expectVisible(lastRow);
|
||||
testLogger.info('✓ Last row is still visible after expanding another log');
|
||||
|
||||
// Check last row's visual state after expanding a different row
|
||||
const lastRowFinalClasses = await lastRow.getAttribute('class') || '';
|
||||
testLogger.info(`Last row final state - classes: ${lastRowFinalClasses}`);
|
||||
|
||||
// PRIMARY ASSERTION 2: Last row should retain its original styling classes
|
||||
// Compare that the classes from before expansion are still present
|
||||
expect(lastRowFinalClasses).toBeTruthy();
|
||||
// Filter out generic/transient table classes that don't indicate highlighting state
|
||||
const ignoredClasses = new Set(['table-row', 'q-tr', 'cursor-pointer']);
|
||||
const originalClassSet = new Set(lastRowClasses.split(/\s+/).filter(c => c && !ignoredClasses.has(c)));
|
||||
const finalClassSet = new Set(lastRowFinalClasses.split(/\s+/).filter(c => c && !ignoredClasses.has(c)));
|
||||
|
||||
// Log class differences for debugging
|
||||
const missingClasses = [...originalClassSet].filter(cls => !finalClassSet.has(cls));
|
||||
const addedClasses = [...finalClassSet].filter(cls => !originalClassSet.has(cls));
|
||||
testLogger.info(`Class comparison: original=${originalClassSet.size}, final=${finalClassSet.size}`);
|
||||
if (missingClasses.length > 0) testLogger.info(`Missing classes: ${missingClasses.join(', ')}`);
|
||||
if (addedClasses.length > 0) testLogger.info(`Added classes: ${addedClasses.join(', ')}`);
|
||||
|
||||
// Look for specific highlight/selection-related classes (known patterns in the app)
|
||||
const highlightPatterns = ['selected', 'highlight', 'active', 'bg-'];
|
||||
const originalHighlightClasses = [...originalClassSet].filter(cls =>
|
||||
highlightPatterns.some(pattern => cls.toLowerCase().includes(pattern))
|
||||
);
|
||||
|
||||
// Guard: Ensure we have meaningful classes to verify (prevents vacuous pass)
|
||||
expect(originalClassSet.size, 'Row should have selection/styling classes after being clicked and closed').toBeGreaterThan(0);
|
||||
|
||||
// If the row has specific highlight classes, verify they're preserved
|
||||
if (originalHighlightClasses.length > 0) {
|
||||
const preservedHighlightClasses = originalHighlightClasses.filter(cls => finalClassSet.has(cls));
|
||||
expect(preservedHighlightClasses.length,
|
||||
`Row lost highlight classes after expanding different row. Lost: ${originalHighlightClasses.filter(c => !finalClassSet.has(c)).join(', ')}`
|
||||
).toBe(originalHighlightClasses.length);
|
||||
testLogger.info(`✓ Row preserved ${preservedHighlightClasses.length} highlight classes: ${preservedHighlightClasses.join(', ')}`);
|
||||
} else {
|
||||
// No specific highlight classes, but row has other styling - verify those are preserved
|
||||
const classesPreserved = [...originalClassSet].every(cls => finalClassSet.has(cls));
|
||||
expect(classesPreserved, `Last row lost classes after expanding different row. Missing: ${missingClasses.join(', ')}`).toBe(true);
|
||||
testLogger.info(`✓ Last row preserved all ${originalClassSet.size} styling classes`);
|
||||
}
|
||||
|
||||
// Close the expanded row
|
||||
await pm.logsPage.pressEscapeToCloseDialog();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// PRIMARY ASSERTION 3: Table should still have rows (streaming may add/remove, but table intact)
|
||||
const finalRowCount = await pm.logsPage.getLogRowCount();
|
||||
expect(finalRowCount).toBeGreaterThan(0);
|
||||
testLogger.info(`✓ Table remains intact with ${finalRowCount} rows`);
|
||||
|
||||
testLogger.info('✓ PRIMARY CHECK PASSED: Last log maintains highlighting on expand');
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
testLogger.info('Logs regression test completed');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user