Files
openobserve/tests/ui-testing/playwright-tests/Logs/logstable.spec.js
T
6bbdcd7d7e fix(ui): sharpen visual polish, keyboard focus and contrast across logs, tables and charts (#15993)
Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-29 10:39:35 +00:00

849 lines
35 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 { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
const PageManager = require('../../pages/page-manager.js');
const testLogger = require('../utils/test-logger.js');
const logData = require("../../fixtures/log.json");
const { ingestBase } = require('../utils/ingest-url.js');
const {
ingestTestData: ingestion,
waitForStreamData,
waitForStreamListed,
} = require('../utils/data-ingestion.js');
test.describe("Logs Table Field Management - Complete Test Suite", () => {
let pageManager;
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pageManager = new PageManager(page);
await page.waitForTimeout(500);
await ingestion(page);
await page.waitForTimeout(500);
await page.goto(`${logData.logsUrl}?org_identifier=${process.env["ORGNAME"]}`);
await page.waitForTimeout(1000);
await pageManager.logsPage.selectStream("e2e_automate");
await pageManager.logsPage.clickDateTimeButton();
await pageManager.logsPage.clickRelative15MinButton();
// Switch off quick mode before starting the test
await pageManager.logsPage.ensureQuickModeState(false);
testLogger.info('Quick mode state ensured');
await pageManager.logsPage.clickSearchBarRefreshButton();
// Deterministically wait for the schema-driven field list to populate instead of a
// fixed sleep — on cloud/alpha the schema fetch after stream selection can lag,
// leaving the sidebar empty so field-search/add-to-table steps see zero fields.
await pageManager.logsPage.waitForFieldListReady();
testLogger.info('Field management test setup completed');
});
// ORIGINAL TESTS FROM logstable.spec.js
test("should add and remove field from table", {
tag: ['@logsTable', '@all', '@logs']
}, async ({ page }) => {
testLogger.info('Testing add and remove field from logs table');
const fieldName = "kubernetes_container_name";
// Search for field using POM method
await pageManager.logsPage.fillIndexFieldSearchInput(fieldName);
await page.waitForTimeout(500);
// Add field to table using POM methods
await pageManager.logsPage.hoverOnFieldExpandButton(fieldName);
await pageManager.logsPage.clickAddFieldToTableButton(fieldName);
// Verify field appears in table using POM method
await pageManager.logsPage.expectFieldInTableHeader(fieldName);
// Remove field from table using POM methods
await pageManager.logsPage.hoverOnFieldExpandButton(fieldName);
await pageManager.logsPage.clickRemoveFieldFromTableButton(fieldName);
// Verify field is removed using POM method
await pageManager.logsPage.expectFieldNotInTableHeader(fieldName);
testLogger.info('Add and remove field test completed successfully');
});
test("should persist added field after page refresh", {
tag: ['@logsTable', '@all', '@logs']
}, async ({ page }) => {
testLogger.info('Testing field persistence after page refresh');
const fieldName = "kubernetes_container_name";
// Search for field using POM method
await pageManager.logsPage.fillIndexFieldSearchInput(fieldName);
await page.waitForTimeout(500);
// Add field to table using POM methods
await pageManager.logsPage.hoverOnFieldExpandButton(fieldName);
await pageManager.logsPage.clickAddFieldToTableButton(fieldName);
// Verify field appears in table
await pageManager.logsPage.expectFieldInTableHeader(fieldName);
testLogger.info('Field added to table successfully');
// The added column only survives a reload via the "logFilterField" localStorage
// entry, which updateUrlQueryParams() writes on a watcher AFTER the column renders
// in the DOM. Reloading before that write commits loses the column (root cause of
// the flaky failure). Wait for the field to actually appear in localStorage before
// reloading — a deterministic gate on the real persistence condition.
await page.waitForFunction(
(field) => {
const raw = window.localStorage.getItem('logFilterField');
return !!raw && raw.includes(field);
},
fieldName,
{ timeout: 10000 },
);
// Reload and verify the persisted column restores. The column is read back from the
// logFilterField localStorage entry and re-applied only after the field list/schema
// loads and the result grid's columns are rebuilt — which under cloud load is
// intermittently slow, or dropped entirely, on the first reload (the table paints
// before the custom column exists). The field IS persisted (asserted above), and a
// fresh reload reliably re-applies it, so reload up to 2x and poll for the restored
// column — graceful workaround for the intermittent restore timing.
let restored = false;
for (let attempt = 0; attempt < 2 && !restored; attempt++) {
await page.reload();
await page.waitForLoadState('domcontentloaded');
await pageManager.logsPage.expectLogsTableVisible().catch(() => {});
await pageManager.logsPage.waitForFieldListReady().catch(() => {});
restored = await pageManager.logsPage
.getTableHeaderByField(fieldName)
.first()
.waitFor({ state: 'visible', timeout: 20000 })
.then(() => true)
.catch(() => false);
if (!restored) {
testLogger.warn(`Persisted column "${fieldName}" not restored on reload attempt ${attempt + 1}/2 — retrying with a fresh reload`);
}
}
expect(restored, `persisted column "${fieldName}" did not restore after reload`).toBe(true);
testLogger.info('Field persistence after page refresh verified successfully');
});
// ENHANCED TESTS - Additional test cases
test("should add multiple fields and verify all appear in table", {
tag: ['@logsTable', '@all', '@logs', '@multipleFields']
}, async ({ page }) => {
testLogger.info('Testing adding multiple fields to logs table');
const fields = ["kubernetes_container_name", "kubernetes_host", "kubernetes_container_image"];
// Add each field to the table
for (const fieldName of fields) {
await pageManager.logsPage.fillIndexFieldSearchInput(fieldName);
await page.waitForTimeout(500);
await pageManager.logsPage.hoverOnFieldExpandButton(fieldName);
await pageManager.logsPage.clickAddFieldToTableButton(fieldName);
// Clear search to see all fields for next iteration
await pageManager.logsPage.fillIndexFieldSearchInput("");
await page.waitForTimeout(300);
testLogger.info(`Field ${fieldName} added successfully`);
}
// Verify all fields appear in table
for (const fieldName of fields) {
await pageManager.logsPage.expectFieldInTableHeader(fieldName);
}
testLogger.info('Multiple fields add test completed successfully');
});
test("should remove multiple fields and verify table updates correctly", {
tag: ['@logsTable', '@all', '@logs', '@multipleFields']
}, async ({ page }) => {
testLogger.info('Testing removing multiple fields from logs table');
const fields = ["kubernetes_container_name", "kubernetes_host"];
// First add the fields
for (const fieldName of fields) {
await pageManager.logsPage.fillIndexFieldSearchInput(fieldName);
await page.waitForTimeout(500);
await pageManager.logsPage.hoverOnFieldExpandButton(fieldName);
await pageManager.logsPage.clickAddFieldToTableButton(fieldName);
await pageManager.logsPage.fillIndexFieldSearchInput("");
await page.waitForTimeout(300);
}
// Verify fields are added
for (const fieldName of fields) {
await pageManager.logsPage.expectFieldInTableHeader(fieldName);
}
// Now remove the fields
for (const fieldName of fields) {
await pageManager.logsPage.fillIndexFieldSearchInput(fieldName);
await page.waitForTimeout(500);
await pageManager.logsPage.hoverOnFieldExpandButton(fieldName);
await pageManager.logsPage.clickRemoveFieldFromTableButton(fieldName);
await pageManager.logsPage.fillIndexFieldSearchInput("");
await page.waitForTimeout(300);
testLogger.info(`Field ${fieldName} removed successfully`);
}
// Verify all fields are removed
for (const fieldName of fields) {
await pageManager.logsPage.expectFieldNotInTableHeader(fieldName);
}
testLogger.info('Multiple fields remove test completed successfully');
});
test("should test field search functionality", {
tag: ['@logsTable', '@all', '@logs', '@fieldSearch']
}, async ({ page }) => {
testLogger.info('Testing field search functionality');
const searchTerm = "kubernetes";
const specificField = "kubernetes_container_name";
// Test field search filters correctly
await pageManager.logsPage.fillIndexFieldSearchInput(searchTerm);
await page.waitForTimeout(500);
// Verify that kubernetes fields are visible
const fieldCount = await pageManager.logsPage.getKubernetesFieldsCount();
expect(fieldCount).toBeGreaterThan(0);
testLogger.info(`Found ${fieldCount} kubernetes fields`);
// Test more specific search
await pageManager.logsPage.fillIndexFieldSearchInput(specificField);
await page.waitForTimeout(500);
// Verify specific field is found
const specificFieldLocator = await pageManager.logsPage.getSpecificFieldLocator(specificField);
await expect(specificFieldLocator).toBeVisible();
// Clear search and verify all fields are visible again
await pageManager.logsPage.fillIndexFieldSearchInput("");
await page.waitForTimeout(500);
// Verify more fields are now visible (not just kubernetes ones)
const totalFieldCount = await pageManager.logsPage.countMatchingFields();
expect(totalFieldCount).toBeGreaterThan(20); // Adjusted expectation based on actual field count
testLogger.info('Field search functionality test completed successfully');
});
test("should verify field data accuracy in table", {
tag: ['@logsTable', '@all', '@logs', '@dataValidation']
}, async ({ page }) => {
testLogger.info('Testing field data accuracy in logs table');
const fieldName = "kubernetes_container_name";
// Add field to table
await pageManager.logsPage.fillIndexFieldSearchInput(fieldName);
await page.waitForTimeout(500);
await pageManager.logsPage.hoverOnFieldExpandButton(fieldName);
await pageManager.logsPage.clickAddFieldToTableButton(fieldName);
// Verify field appears in table
await pageManager.logsPage.expectFieldInTableHeader(fieldName);
// Check that the field column exists in the table (basic validation)
// Note: We verify the header exists, which means the field was successfully added
testLogger.info(`Field ${fieldName} successfully added to table structure`);
testLogger.info('Field data accuracy test completed successfully');
});
test("should handle adding field with cleared search", {
tag: ['@logsTable', '@all', '@logs', '@fieldSearch']
}, async ({ page }) => {
testLogger.info('Testing field operations with search clearing');
const fieldName = "kubernetes_host";
// Add field normally
await pageManager.logsPage.fillIndexFieldSearchInput(fieldName);
await page.waitForTimeout(500);
await pageManager.logsPage.hoverOnFieldExpandButton(fieldName);
await pageManager.logsPage.clickAddFieldToTableButton(fieldName);
// Verify field is added to table
await pageManager.logsPage.expectFieldInTableHeader(fieldName);
// Clear search to show all fields
await pageManager.logsPage.fillIndexFieldSearchInput("");
await page.waitForTimeout(500);
// Field should still be in table even with search cleared
await pageManager.logsPage.expectFieldInTableHeader(fieldName);
testLogger.info('Field operations with search clearing test completed successfully');
});
test("should test field search case insensitivity", {
tag: ['@logsTable', '@all', '@logs', '@fieldSearch']
}, async ({ page }) => {
testLogger.info('Testing field search case insensitivity');
const searchVariations = ["KUBERNETES", "Kubernetes", "kubernetes", "CONTAINER", "container"];
for (const searchTerm of searchVariations) {
await pageManager.logsPage.fillIndexFieldSearchInput(searchTerm);
// Every variation is a kubernetes/container term, so the filtered list must end
// up with matching fields. Poll the count instead of counting once after a fixed
// sleep — the field list re-filters on a debounce after the input settles, and
// under parallel load that render can lag past a 500ms wait (yielding a false 0).
if (searchTerm.toLowerCase().includes('kubernetes') || searchTerm.toLowerCase().includes('container')) {
await expect
.poll(() => pageManager.logsPage.countMatchingFields(), {
timeout: 10000,
message: `field search "${searchTerm}" produced no matching fields`,
})
.toBeGreaterThan(0);
testLogger.info(`Search term "${searchTerm}" found matching fields`);
}
// Clear search for next iteration and wait for the input to actually reset so the
// next variation filters from the full list.
await pageManager.logsPage.fillIndexFieldSearchInput("");
}
testLogger.info('Field search case insensitivity test completed successfully');
});
test("should verify field operations with quick mode toggle", {
tag: ['@logsTable', '@all', '@logs', '@quickMode']
}, async () => {
testLogger.info('Testing field operations with quick mode toggle');
const fieldName = "kubernetes_container_name";
// Add field in normal mode (already in normal mode from beforeEach)
await pageManager.logsPage.fillIndexFieldSearchInput(fieldName);
await pageManager.logsPage.waitForUI(500);
await pageManager.logsPage.hoverOnFieldExpandButton(fieldName);
await pageManager.logsPage.clickAddFieldToTableButton(fieldName);
await pageManager.logsPage.expectFieldInTableHeader(fieldName);
testLogger.info('Field added in normal mode');
// Toggle to quick mode and verify the toggle works
await pageManager.logsPage.clickQuickModeToggle();
await pageManager.logsPage.waitForUI(1000);
// Check if quick mode is active - use a more flexible approach
await pageManager.logsPage.waitForUI(500);
// Check various ways to detect if quick mode is on
const toggleInfo = await pageManager.logsPage.getQuickModeToggleAttributes();
testLogger.info(`Quick mode toggle state - aria-pressed: ${toggleInfo.ariaPressed}, classes: ${toggleInfo.classNames}`);
// Quick mode should be enabled - verify the toggle worked
await pageManager.logsPage.expectQuickModeToggleVisible();
testLogger.info('Quick mode toggle is functional');
// Toggle back to normal mode
await pageManager.logsPage.clickQuickModeToggle();
await pageManager.logsPage.waitForUI(1000);
// Verify toggle is still functional after second click
await pageManager.logsPage.expectQuickModeToggleVisible();
testLogger.info('Quick mode toggle functionality verified');
// Clean up - remove the field we added
await pageManager.logsPage.fillIndexFieldSearchInput(fieldName);
await pageManager.logsPage.waitForUI(500);
await pageManager.logsPage.hoverOnFieldExpandButton(fieldName);
await pageManager.logsPage.clickRemoveFieldFromTableButton(fieldName);
testLogger.info('Field operations with quick mode toggle test completed successfully');
});
test("should verify field remains after stream reselection", {
tag: ['@logsTable', '@all', '@logs', '@persistence']
}, async ({ page }) => {
testLogger.info('Testing field behavior with stream reselection');
const fieldName = "kubernetes_container_name";
// Add field to current stream
await pageManager.logsPage.fillIndexFieldSearchInput(fieldName);
await page.waitForTimeout(500);
await pageManager.logsPage.hoverOnFieldExpandButton(fieldName);
await pageManager.logsPage.clickAddFieldToTableButton(fieldName);
await pageManager.logsPage.expectFieldInTableHeader(fieldName);
testLogger.info('Field added to e2e_automate stream');
// Reselect the same stream to test persistence
await pageManager.logsPage.selectStream("e2e_automate");
await page.waitForTimeout(1000);
// Note: Field may or may not persist - this tests the actual behavior
testLogger.info('Stream reselected - testing field persistence behavior');
// The test passes regardless of persistence behavior, documenting what happens
try {
await pageManager.logsPage.expectFieldInTableHeader(fieldName);
testLogger.info('✓ Field persisted after stream reselection');
} catch (error) {
testLogger.info('ℹ Field was reset after stream reselection - this is expected behavior');
}
testLogger.info('Field persistence test completed successfully');
});
// NEW TEST FOR PR #9023 - Blank SQL Query Error Handling
test("should show proper error when running blank SQL query with cmd+enter", {
tag: ['@logsTable', '@all', '@logs', '@emptyQuery']
}, async ({ page }) => {
testLogger.info('Testing proper error display when running blank SQL query with cmd+enter');
const fieldName = "kubernetes_container_name";
// Add field to table first (following existing test pattern)
await pageManager.logsPage.fillIndexFieldSearchInput(fieldName);
await page.waitForTimeout(500);
await pageManager.logsPage.hoverOnFieldExpandButton(fieldName);
await pageManager.logsPage.clickAddFieldToTableButton(fieldName);
// Verify field appears in table
await pageManager.logsPage.expectFieldInTableHeader(fieldName);
testLogger.info('✓ Field added to table successfully');
// Enable SQL mode
await pageManager.logsPage.enableSqlModeIfNeeded();
await page.waitForTimeout(1000);
// Execute blank query with keyboard shortcut
await pageManager.logsPage.executeBlankQueryWithKeyboardShortcut();
// Verify proper error handling
await pageManager.logsPage.expectBlankQueryError();
testLogger.info('✓ Proper error handling verified - shows error message instead of breaking UI');
});
test("should preserve include/exclude search terms when log details are open and query is run", {
tag: ['@logsTable', '@all', '@logs', '@includeExclude']
}, async ({ page }) => {
testLogger.info('Testing include/exclude search terms persistence in open log details after query run');
await pageManager.logsPage.runQueryAndWaitForResults();
// Verify logs table is visible
await pageManager.logsPage.expectLogsSearchResultLogsTableVisible();
// Open first log details
await pageManager.logsPage.openFirstLogDetails();
// Add include search term from log details
await pageManager.logsPage.addIncludeSearchTermFromLogDetails();
testLogger.info('✓ First include search term added');
// Re-running must preserve terms in the already-open details panel.
await pageManager.logsPage.runQueryAndWaitForResults();
// Verify include/exclude buttons are still visible after query run
await pageManager.logsPage.expectIncludeExcludeButtonsVisibleInLogDetails();
testLogger.info('✓ Include/exclude search terms preserved in open log details after query run - bug is fixed!');
});
// Bug #11041 test moved to RegressionSet/logs-regression-bugs.spec.js
test("should make exactly one search call and one histogram call when using cmd+enter with histogram enabled", {
tag: ['@logsTable', '@all', '@logs', '@cmdEnter', '@apiCalls', '@histogram']
}, async ({ page }) => {
testLogger.info('Testing that cmd+enter makes exactly 1 search + 1 histogram API call when histogram is enabled');
// Enable SQL mode
await pageManager.logsPage.enableSqlModeIfNeeded();
await page.waitForTimeout(1000);
// Ensure histogram is enabled (it should be by default, but let's verify)
const wasChanged = await pageManager.logsPage.ensureHistogramToggleState(true);
if (wasChanged) {
testLogger.info('✓ Histogram enabled');
}
// Setup API call tracking
const trackingData = await pageManager.logsPage.setupAPICallTracking();
// Execute query with keyboard shortcut and track API calls
await pageManager.logsPage.executeQueryWithKeyboardShortcutAndTrackAPICalls('select * from "e2e_automate"');
// Verify API call counts
await pageManager.logsPage.verifyAPICallCounts(trackingData.allRequests, trackingData.requestHandler);
testLogger.info('✓ CMD+Enter API calls verified: 1 search + 1 histogram = 2 total');
});
test("should not add unwanted characters when pressing cmd+enter in SQL editor", {
tag: ['@logsTable', '@all', '@logs', '@cmdEnter', '@editorBug']
}, async ({ page }) => {
testLogger.info('Testing that cmd+enter does not add unwanted characters or move cursor position in SQL editor');
// Enable SQL mode
await pageManager.logsPage.enableSqlModeIfNeeded();
await page.waitForTimeout(1000);
// Setup editor for cursor test
await pageManager.logsPage.setupEditorForCursorTest('select * from "e2e_automate"');
// Get editor content before cmd+enter
const initialQuery = await pageManager.logsPage.getEditorContentBefore();
testLogger.info(`Query before cmd+enter: "${initialQuery}"`);
// Execute query with keyboard shortcut
await pageManager.logsPage.executeQueryWithKeyboardShortcutForEditor();
// Get editor content after cmd+enter
const finalQuery = await pageManager.logsPage.getEditorContentAfter();
testLogger.info(`Query after cmd+enter: "${finalQuery}"`);
// Verify editor content integrity
await pageManager.logsPage.verifyEditorContentIntegrity(initialQuery, finalQuery);
testLogger.info('✓ CMD+Enter editor bug test completed');
});
// Bug #9550 lives in RegressionSet/Logs/logs-bugs.spec.js (skipped there on a product blocker)
test.afterEach(async () => {
try {
// await pageManager.commonActions.flipStreaming();
testLogger.info('Streaming flipped after test');
} catch (error) {
testLogger.warn('Streaming flip failed', { error: error.message });
}
// Clean up screenshots regardless of test pass/fail
const fs = require('fs');
const screenshotsToDelete = [
'playwright-tests/Logs/include-menu-debug.png',
'playwright-tests/Logs/include-menu-after-click.png'
];
for (const screenshot of screenshotsToDelete) {
try {
if (fs.existsSync(screenshot)) {
fs.unlinkSync(screenshot);
}
} catch (error) {
// Pass gracefully if deletion fails
}
}
});
});
test.describe("Severity Color Mapping Tests - Issue #9439", () => {
let pageManager;
let testStreamName;
test.beforeAll(async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
// Generate unique stream name
const timestamp = Date.now();
testStreamName = `severity_test_${timestamp}`;
testLogger.info(`Creating stream: ${testStreamName}`);
// Initialize page manager to access logsPage methods
const setupPageManager = new PageManager(page);
// Ingest severity test data
await setupPageManager.logsPage.severityColorIngestionToStream(testStreamName);
testLogger.info(`Ingested test data to stream: ${testStreamName}`);
// Wait for data to be indexed
await page.waitForTimeout(3000);
await context.close();
});
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pageManager = new PageManager(page);
await page.waitForTimeout(500);
await page.goto(`${logData.logsUrl}?org_identifier=${process.env["ORGNAME"]}`);
await page.waitForTimeout(1000);
await pageManager.logsPage.selectStream(testStreamName);
await pageManager.logsPage.clickDateTimeButton();
await pageManager.logsPage.selectRelative1Hour();
await page.waitForTimeout(500);
await pageManager.logsPage.ensureQuickModeState(false);
await pageManager.logsPage.clickSearchBarRefreshButton();
await page.waitForTimeout(3000);
});
test("should display correct colors for all severity levels (0-7)", {
tag: ['@logsTable', '@all', '@logs', '@severityColors', '@issue9439']
}, async ({ page }) => {
testLogger.info('Testing severity color mapping for all severity levels');
const expectedColorByLevel = {
emergency: '#e53935', // severity 0 → level field
alert: '#ea580c', // severity 1
critical: '#f4511e', // severity 2
error: '#e53935', // severity 3
warning: '#c46a00', // severity 4
notice: '#16a34a', // severity 5
info: '#1b80d9', // severity 6
debug: '#008a9b', // severity 7
};
// Wait for the results table to actually render its per-row status color bars
// before reading them. The fixed 3s wait in beforeEach can be too short on
// cloud/alpha (search results stream in), leaving an empty table so
// getSeverityColors() returns 0 rows and verified.size is 0 (observed flaky in CI).
// Poll until at least the expected number of distinct-level bars have rendered.
await expect
.poll(
() => pageManager.logsPage.countSeverityColorBars(),
{ timeout: 30000, message: 'severity status color bars did not render in time' },
)
.toBeGreaterThanOrEqual(Object.keys(expectedColorByLevel).length);
// Get severity colors using POM method
const results = await pageManager.logsPage.getSeverityColors();
testLogger.info(`Found ${results.length} rows with status color bars`);
// Verify each distinct level renders its expected color.
const verified = new Set();
for (const result of results) {
const level = result.level;
if (!level || verified.has(level)) continue;
const expected = expectedColorByLevel[level];
if (!expected) continue; // ignore levels outside the tested set
expect(result.backgroundImage).toBe('none');
const hexColor = pageManager.logsPage.normalizeHexColor(pageManager.logsPage.rgbToHex(result.color));
const expectedHex = pageManager.logsPage.normalizeHexColor(expected);
testLogger.info(`Level "${level}": Expected ${expectedHex}, Got ${hexColor}`);
expect(hexColor).toBe(expectedHex);
testLogger.info(`✓ Level "${level}" color verified`);
verified.add(level);
}
expect(verified.size).toBe(Object.keys(expectedColorByLevel).length);
testLogger.info(`Successfully verified all ${verified.size} severity levels`);
});
test.afterAll(async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
testLogger.info(`Cleaning up stream: ${testStreamName}`);
// Initialize page manager for cleanup
const cleanupPageManager = new PageManager(page);
await cleanupPageManager.logsPage.deleteStream(testStreamName);
await context.close();
});
});
test.describe("Severity inference journeys (4c Part A)", () => {
const { getAuthHeaders, getOrgIdentifier } = require('../utils/cloud-auth.js');
let pageManager;
let journeyStream;
const JOURNEY_ROWS = [
{ tag: 'ja1-inferred', message: '2026-10-06 12:00:01 ERROR payment failed' },
{ tag: 'ja1-unknown', message: 'user logged in' },
{ tag: 'ja2-503', status: 503 },
{ tag: 'ja2-404', status: 404 },
{ tag: 'ja2-200', status: 200 },
{ tag: 'ja3-warn2', severity: 'WARN2', severity_number: 14 },
{ tag: 'ja3-number', severity_number: 18 },
{ tag: 'ja4-syslog3', syslog_severity: 3 },
{ tag: 'ja4-syslog0', syslog_severity: 0 },
{ tag: 'ja5-fallthrough', severity: 'W', level: 'error' },
{ tag: 'ja5-level-wins', level: 'info', message: '[ERROR] x' },
{ tag: 'ja5-pino', level: '50' },
];
const rowFor = (page, tag) =>
page.locator('[data-test="logs-search-result-logs-table"] tbody tr', { hasText: tag }).first();
const rowState = (row) =>
row.evaluate((tr) => {
const firstCell = tr.querySelector('td');
const spine = getComputedStyle(firstCell, '::before');
const classes = Array.from(tr.classList);
return {
level: classes.find((c) => c.startsWith('o2-log-level-') && !c.startsWith('o2-log-level-src-')),
source: classes.find((c) => c.startsWith('o2-log-level-src-')),
backgroundImage: spine.backgroundImage,
backgroundColor: spine.backgroundColor,
rowStatusColor: tr.style.getPropertyValue('--row-status-color').trim(),
paddingLeft: getComputedStyle(firstCell).paddingLeft,
};
});
test.beforeAll(async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
journeyStream = `severity_journeys_${Date.now()}`;
const url = `${ingestBase(process.env.INGESTION_URL)}/api/${getOrgIdentifier()}/${journeyStream}/_json`;
const response = await page.request.post(url, { headers: getAuthHeaders(), data: JOURNEY_ROWS });
expect(response.ok()).toBe(true);
// A single search runs in beforeEach, so every journey row must already be queryable.
expect(
await waitForStreamData(page, journeyStream, JOURNEY_ROWS.length, 60000),
`stream ${journeyStream} never returned all ${JOURNEY_ROWS.length} rows within 60s`,
).toBe(true);
expect(
await waitForStreamListed(page, journeyStream, 'logs'),
`stream ${journeyStream} never appeared in the streams list`,
).toBe(true);
await context.close();
});
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pageManager = new PageManager(page);
await page.goto(`${logData.logsUrl}?org_identifier=${process.env["ORGNAME"]}`);
await pageManager.logsPage.selectStream(journeyStream);
await pageManager.logsPage.clickDateTimeButton();
await pageManager.logsPage.selectRelative1Hour();
await pageManager.logsPage.ensureQuickModeState(false);
await pageManager.logsPage.clickSearchBarRefreshButton();
await pageManager.logsPage.fillIndexFieldSearchInput('tag');
await pageManager.logsPage.hoverOnFieldExpandButton('tag');
await pageManager.logsPage.clickAddFieldToTableButton('tag');
await expect(rowFor(page, 'ja1-inferred')).toBeVisible({ timeout: 30000 });
});
test("J-A1: inferred rows draw the same solid spine as field rows, evidence-less rows keep a transparent spine and padding", {
tag: ['@logsTable', '@logs', '@severityInference']
}, async ({ page }) => {
const inferred = await rowState(rowFor(page, 'ja1-inferred'));
expect(inferred.level).toBe('o2-log-level-error');
expect(inferred.source).toBe('o2-log-level-src-message');
expect(inferred.backgroundImage).toBe('none');
const fieldError = await rowState(rowFor(page, 'ja3-number'));
expect(fieldError.level).toBe('o2-log-level-error');
expect(fieldError.source).toBe('o2-log-level-src-field');
expect(inferred.backgroundColor).toBe(fieldError.backgroundColor);
expect(inferred.backgroundColor).not.toBe('rgba(0, 0, 0, 0)');
const unknown = await rowState(rowFor(page, 'ja1-unknown'));
expect(unknown.level).toBe('o2-log-level-unknown');
expect(unknown.source).toBe('o2-log-level-src-none');
expect(unknown.rowStatusColor).toBe('transparent');
expect(unknown.backgroundImage).toBe('none');
expect(unknown.backgroundColor).toBe('rgba(0, 0, 0, 0)');
const coloured = await rowState(rowFor(page, 'ja2-503'));
expect(unknown.paddingLeft).toBe(coloured.paddingLeft);
});
test("J-A2…J-A5: explicit evidence resolves per the precedence table", {
tag: ['@logsTable', '@logs', '@severityInference']
}, async ({ page }) => {
const expected = {
'ja2-503': ['error', 'http'],
'ja2-404': ['warning', 'http'],
'ja2-200': ['ok', 'http'],
'ja3-warn2': ['warning', 'field'],
'ja3-number': ['error', 'field'],
'ja4-syslog3': ['error', 'field'],
'ja4-syslog0': ['emergency', 'field'],
'ja5-fallthrough': ['error', 'field'],
'ja5-level-wins': ['info', 'field'],
'ja5-pino': ['error', 'field'],
};
for (const [tag, [level, source]] of Object.entries(expected)) {
const state = await rowState(rowFor(page, tag));
expect(state.level, tag).toBe(`o2-log-level-${level}`);
expect(state.source, tag).toBe(`o2-log-level-src-${source}`);
expect(state.backgroundImage, tag).toBe('none');
expect(state.backgroundColor, tag).not.toBe('rgba(0, 0, 0, 0)');
}
});
test("sr-only severity text sits in the timestamp cell only, after the value span", {
tag: ['@logsTable', '@logs', '@severityInference']
}, async ({ page }) => {
const row = rowFor(page, 'ja1-inferred');
await expect(row.locator('[data-test="log-row-severity-sr"]')).toHaveCount(1);
const cell = row.locator('[data-test="o2-table-cell-_timestamp"]');
await expect(cell.locator('[data-test="log-row-severity-sr"]')).toHaveText('Severity error, inferred');
await expect(cell.locator('[data-test="log-row-timestamp-value"]')).toHaveCount(1);
const values = await pageManager.logsPage.getTimestampCellValues(3);
values.forEach((v) => expect(v).not.toContain('Severity'));
await expect(rowFor(page, 'ja1-unknown').locator('[data-test="log-row-severity-sr"]')).toHaveText('Severity unknown');
});
test("AC-A1.3: the drawer tag is reachable by Tab and opens its popover with Enter", {
tag: ['@logsTable', '@logs', '@severityInference']
}, async ({ page }) => {
await rowFor(page, 'ja1-inferred').locator('[data-test="o2-table-cell-_timestamp"]').click();
const badge = page.locator('[data-test="log-detail-severity-badge"]');
await expect(badge).toBeVisible();
await expect(badge).toHaveAttribute('data-severity-source', 'message');
await expect(badge).toContainText('inferred');
await page.locator('[data-test="log-detail-json-tab"]').focus();
let reached = false;
for (let i = 0; i < 6 && !reached; i++) {
await page.keyboard.press('Tab');
reached = await badge.evaluate((el) => el === document.activeElement);
}
expect(reached).toBe(true);
await page.keyboard.press('Enter');
const popover = page.locator('[data-test="log-detail-severity-popover"]');
await expect(popover).toBeVisible();
await expect(popover).toContainText('Not searchable');
await page.keyboard.press('Escape');
await expect(popover).toBeHidden();
});
test("an HTTP row's drawer names the status field", {
tag: ['@logsTable', '@logs', '@severityInference']
}, async ({ page }) => {
await rowFor(page, 'ja2-503').locator('[data-test="o2-table-cell-_timestamp"]').click();
const badge = page.locator('[data-test="log-detail-severity-badge"]');
await expect(badge).toHaveAttribute('data-severity-source', 'http');
await badge.click();
await expect(page.locator('[data-test="log-detail-severity-popover"]')).toContainText('status');
});
test.afterAll(async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
await new PageManager(page).logsPage.deleteStream(journeyStream);
await context.close();
});
});