mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
678 lines
32 KiB
JavaScript
678 lines
32 KiB
JavaScript
const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
|
|
const testLogger = require('../utils/test-logger.js');
|
|
const PageManager = require('../../pages/page-manager.js');
|
|
const { ingestTestData, waitForStreamData } = require('../utils/data-ingestion.js');
|
|
|
|
// Cross-link settings are one whole-object PUT per stream, so each parallel worker owns its own streams and concurrent tests can never clobber one another.
|
|
const WORKER_SLOT = process.env.TEST_PARALLEL_INDEX || '0';
|
|
const STREAM_A = `e2e_crosslink_ms_w${WORKER_SLOT}_a`;
|
|
const STREAM_B = `e2e_crosslink_ms_w${WORKER_SLOT}_b`;
|
|
|
|
test.describe("Cross-Linking Multi-Stream testcases", () => {
|
|
test.describe.configure({ mode: 'serial' });
|
|
let pm;
|
|
let dataIngested = false;
|
|
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
testLogger.testStart(testInfo.title, testInfo.file);
|
|
await navigateToBase(page);
|
|
pm = new PageManager(page);
|
|
await page.waitForTimeout(1000);
|
|
|
|
if (!dataIngested) {
|
|
await ingestTestData(page, STREAM_A);
|
|
await ingestTestData(page, STREAM_B);
|
|
// These streams are created only here, so nothing else masks the lag before they are queryable and listed.
|
|
for (const streamName of [STREAM_A, STREAM_B]) {
|
|
expect(await waitForStreamData(page, streamName, 1, 30000),
|
|
`stream ${streamName} never became queryable within 30s of ingestion`).toBe(true);
|
|
}
|
|
dataIngested = true;
|
|
testLogger.info('Test data ingested into both streams');
|
|
}
|
|
|
|
testLogger.info('Test setup completed');
|
|
});
|
|
|
|
test.afterEach(async ({}, testInfo) => {
|
|
if (testInfo.status) {
|
|
testLogger.testEnd(testInfo.title, testInfo.status, testInfo.duration);
|
|
}
|
|
});
|
|
|
|
test.afterAll(async ({ browser }) => {
|
|
testLogger.info('Cleaning up cross-links on both streams after test suite');
|
|
const context = await browser.newContext({
|
|
storageState: 'playwright-tests/utils/auth/user.json'
|
|
});
|
|
const page = await context.newPage();
|
|
try {
|
|
const orgId = process.env["ORGNAME"] || 'default';
|
|
await page.goto(`${process.env["ZO_BASE_URL"] || 'http://localhost:5080'}/web/streams?org_identifier=${orgId}`);
|
|
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
|
const cleanupPm = new PageManager(page);
|
|
await page.waitForTimeout(1000);
|
|
|
|
for (const streamName of [STREAM_A, STREAM_B]) {
|
|
try {
|
|
await cleanupPm.crossLinkPage.navigateToStreams();
|
|
await cleanupPm.crossLinkPage.searchStream(streamName);
|
|
await cleanupPm.crossLinkPage.openStreamDetail();
|
|
|
|
const isTabVisible = await cleanupPm.crossLinkPage.isCrossLinkingTabVisible();
|
|
if (isTabVisible) {
|
|
await cleanupPm.crossLinkPage.clickCrossLinkingTab();
|
|
await cleanupPm.crossLinkPage.deleteAllCrossLinks();
|
|
await cleanupPm.crossLinkPage.clickUpdateSettings();
|
|
await page.waitForTimeout(2000);
|
|
testLogger.info(`Cross-links cleaned up for ${streamName}`);
|
|
}
|
|
} catch (e) {
|
|
testLogger.warn(`Cleanup failed for ${streamName}`, { error: e.message });
|
|
}
|
|
}
|
|
} catch (e) {
|
|
testLogger.warn('Cleanup failed', { error: e.message });
|
|
} finally {
|
|
await page.close();
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Helper: Set up a cross-link on a stream via the UI.
|
|
* Navigates to the stream schema, opens the cross-linking tab,
|
|
* cleans existing links, adds the new one, and persists.
|
|
*/
|
|
async function setupStreamCrossLink(page, pm, streamName, crossLink) {
|
|
await pm.crossLinkPage.navigateToStreams();
|
|
await pm.crossLinkPage.searchStream(streamName);
|
|
await pm.crossLinkPage.openStreamDetail();
|
|
|
|
const isTabVisible = await pm.crossLinkPage.isCrossLinkingTabVisible();
|
|
if (!isTabVisible) {
|
|
return false;
|
|
}
|
|
|
|
await pm.crossLinkPage.clickCrossLinkingTab();
|
|
await pm.crossLinkPage.deleteAllCrossLinks();
|
|
await pm.crossLinkPage.addCrossLink(crossLink);
|
|
|
|
// Wait for the PUT settings API to confirm the save succeeded
|
|
const settingsResponse = page.waitForResponse(
|
|
(resp) => resp.url().includes('/streams/') && resp.url().includes('/settings') && resp.request().method() === 'PUT',
|
|
{ timeout: 15000 }
|
|
);
|
|
await pm.crossLinkPage.clickUpdateSettings();
|
|
const resp = await settingsResponse;
|
|
testLogger.info(`Cross-link "${crossLink.name}" saved on ${streamName}`, { status: resp.status() });
|
|
return true;
|
|
}
|
|
|
|
// P0: Multi-stream non-SQL mode — cross-links from both streams appear
|
|
test("should show cross-links from both streams when multiple streams are selected in non-SQL mode", {
|
|
tag: ['@crossLinking', '@multiStream', '@smoke', '@P0', '@all']
|
|
}, async ({ page }) => {
|
|
testLogger.info('Testing multi-stream cross-links in non-SQL mode');
|
|
|
|
const crossLinkNameA = `CL-StreamA-${Date.now()}`;
|
|
const crossLinkNameB = `CL-StreamB-${Date.now()}`;
|
|
|
|
// Step 1: Set up cross-link on Stream A
|
|
const featureEnabledA = await setupStreamCrossLink(page, pm, STREAM_A, {
|
|
name: crossLinkNameA,
|
|
url: 'https://stream-a.example.com/${field.__value}',
|
|
fields: ['kubernetes_container_name']
|
|
});
|
|
if (!featureEnabledA) {
|
|
test.skip(true, 'Cross-linking feature not enabled');
|
|
}
|
|
|
|
// Step 2: Set up cross-link on Stream B
|
|
const featureEnabledB = await setupStreamCrossLink(page, pm, STREAM_B, {
|
|
name: crossLinkNameB,
|
|
url: 'https://stream-b.example.com/${field.__value}',
|
|
fields: ['kubernetes_container_name']
|
|
});
|
|
if (!featureEnabledB) {
|
|
test.skip(true, 'Cross-linking feature not enabled on second stream');
|
|
}
|
|
|
|
testLogger.info('Both stream cross-links configured', { crossLinkNameA, crossLinkNameB });
|
|
|
|
// Step 3: Navigate to logs and select both streams
|
|
await pm.logsPage.navigateToLogs();
|
|
await pm.logsPage.selectIndexAndStreamJoinUnion(STREAM_A, STREAM_B);
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Disable quick mode for full field visibility
|
|
await pm.logsPage.ensureQuickModeState(false);
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Arm the result_schema waiter BEFORE pressing Run so we don't miss a
|
|
// fast response. The cross-linking data is loaded by the parallel
|
|
// result_schema (cross_linking=true) calls fired alongside the search.
|
|
const crossLinkCapture = pm.crossLinkPage.captureCrossLinkResponses();
|
|
|
|
// Run query + retry if responses come back empty. The backend's
|
|
// result_schema cross-link merge is eventually-consistent — a query
|
|
// fired immediately after the schema-settings PUT can race the
|
|
// cross-link materialisation and return `stream_links: []` for both
|
|
// streams. Re-firing the query forces fresh result_schema calls, which
|
|
// pick up the now-persisted links. We cap at 3 attempts so a genuinely
|
|
// broken backend still fails fast.
|
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
crossLinkCapture.reset();
|
|
await pm.logsPage.runQueryAndWaitForResults();
|
|
await page.waitForTimeout(3000);
|
|
|
|
const captured = await crossLinkCapture.waitForAtLeast(1, { timeout: 8000 });
|
|
await page.waitForTimeout(500);
|
|
|
|
const nonEmpty = crossLinkCapture.getNonEmptyCount();
|
|
testLogger.info('result_schema cross_linking responses captured', {
|
|
attempt,
|
|
count: captured,
|
|
nonEmpty,
|
|
});
|
|
|
|
if (nonEmpty >= 1) break;
|
|
|
|
if (attempt < 3) {
|
|
testLogger.warn(
|
|
`Attempt ${attempt}: both result_schema responses empty — re-firing query`,
|
|
);
|
|
await page.waitForTimeout(1500);
|
|
}
|
|
}
|
|
|
|
// Step 4: Expand a log row to reveal JSON preview
|
|
await pm.crossLinkPage.expandFirstLogRow();
|
|
|
|
// Step 5 & 6: Open kubernetes_container_name field action dropdown and
|
|
// verify BOTH cross-links appear. expectCrossLinksFromBothStreams polls
|
|
// and re-opens the dropdown until a late-arriving result_schema lands.
|
|
await pm.crossLinkPage.expectCrossLinksFromBothStreams(
|
|
'kubernetes_container_name',
|
|
crossLinkNameA,
|
|
crossLinkNameB,
|
|
);
|
|
|
|
testLogger.info('PASSED: Both streams cross-links visible in non-SQL multi-stream mode');
|
|
});
|
|
|
|
// P0: UNION ALL SQL query — cross-links from both streams via backend merging
|
|
test("should show cross-links from both streams when using UNION ALL SQL query", {
|
|
tag: ['@crossLinking', '@multiStream', '@join', '@smoke', '@P0', '@all']
|
|
}, async ({ page }) => {
|
|
testLogger.info('Testing multi-stream cross-links with UNION ALL SQL query');
|
|
|
|
const crossLinkNameA = `CL-UnionA-${Date.now()}`;
|
|
const crossLinkNameB = `CL-UnionB-${Date.now()}`;
|
|
|
|
// Step 1: Set up cross-links on both streams
|
|
const featureEnabledA = await setupStreamCrossLink(page, pm, STREAM_A, {
|
|
name: crossLinkNameA,
|
|
url: 'https://union-a.example.com/${field.__value}',
|
|
fields: ['kubernetes_container_name']
|
|
});
|
|
if (!featureEnabledA) {
|
|
test.skip(true, 'Cross-linking feature not enabled');
|
|
}
|
|
|
|
const featureEnabledB = await setupStreamCrossLink(page, pm, STREAM_B, {
|
|
name: crossLinkNameB,
|
|
url: 'https://union-b.example.com/${field.__value}',
|
|
fields: ['kubernetes_container_name']
|
|
});
|
|
if (!featureEnabledB) {
|
|
test.skip(true, 'Cross-linking feature not enabled on second stream');
|
|
}
|
|
|
|
testLogger.info('Both stream cross-links configured for UNION test', { crossLinkNameA, crossLinkNameB });
|
|
|
|
// Step 2: Navigate to logs, select both streams, enable SQL mode
|
|
await pm.logsPage.navigateToLogs();
|
|
await pm.logsPage.selectIndexAndStreamJoinUnion(STREAM_A, STREAM_B);
|
|
// selectIndexAndStreamJoinUnion already waits for stream options and selection
|
|
|
|
await pm.logsPage.enableSqlModeIfNeeded();
|
|
|
|
// Step 3: Enter UNION ALL query
|
|
const unionQuery = `SELECT * FROM "${STREAM_A}" UNION ALL BY NAME SELECT * FROM "${STREAM_B}" LIMIT 100`;
|
|
await pm.logsPage.clearAndFillQueryEditor(unionQuery);
|
|
testLogger.info('UNION ALL query entered', { query: unionQuery });
|
|
|
|
// Disable quick mode for full field visibility
|
|
await pm.logsPage.ensureQuickModeState(false);
|
|
// ensureQuickModeState already waits for the toggle data-state to flip
|
|
|
|
// Run query and wait for the result_schema cross-linking response. One call
|
|
// covers every stream in the query: the backend resolves each table in the
|
|
// UNION and merges their cross-links into a single response.
|
|
const schemaPromise = new Promise((resolve) => {
|
|
const handler = (resp) => {
|
|
if (resp.url().includes('result_schema') && resp.url().includes('cross_linking=true') && resp.status() === 200) {
|
|
page.off('response', handler);
|
|
resolve();
|
|
}
|
|
};
|
|
page.on('response', handler);
|
|
setTimeout(() => { page.off('response', handler); resolve(); }, 20000);
|
|
});
|
|
await pm.logsPage.selectRunQuery();
|
|
await schemaPromise;
|
|
|
|
// Step 4: Expand a log row
|
|
// expandFirstLogRow waits up to 30 s for the first row to appear —
|
|
// UNION ALL queries can be slow to render rows in CI.
|
|
await pm.crossLinkPage.expandFirstLogRow();
|
|
|
|
// Step 5 & 6: Poll until BOTH cross-links appear (backend merges from both streams).
|
|
// The result_schema response is eventually-consistent, so use the polling helper
|
|
// that re-opens the dropdown until late-arriving responses land.
|
|
// Check each cross-link separately to isolate failures.
|
|
const hasA = await pm.crossLinkPage.expectLogCrossLinkVisible('kubernetes_container_name', crossLinkNameA);
|
|
expect(hasA, `Cross-link "${crossLinkNameA}" from ${STREAM_A} should be visible`).toBe(true);
|
|
|
|
const hasB = await pm.crossLinkPage.expectLogCrossLinkVisible('kubernetes_container_name', crossLinkNameB);
|
|
expect(hasB, `Cross-link "${crossLinkNameB}" from ${STREAM_B} should be visible`).toBe(true);
|
|
|
|
testLogger.info('PASSED: Both streams cross-links visible in UNION ALL SQL query');
|
|
});
|
|
|
|
// P1: Network verification — one result_schema call covering every selected stream
|
|
test("should fire one result_schema call covering both streams in multi-stream mode", {
|
|
tag: ['@crossLinking', '@multiStream', '@functional', '@P1', '@all']
|
|
}, async ({ page }) => {
|
|
testLogger.info('Testing network calls for multi-stream cross-links');
|
|
|
|
// Step 1: Navigate to logs and select both streams
|
|
await pm.logsPage.navigateToLogs();
|
|
await pm.logsPage.selectIndexAndStreamJoinUnion(STREAM_A, STREAM_B);
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Step 2: Set up network interception for result_schema calls
|
|
const requestCapture = pm.crossLinkPage.captureCrossLinkRequests();
|
|
|
|
// Step 3: Run query (triggers result_schema calls for cross-links)
|
|
await pm.logsPage.runQueryAndWaitForResults();
|
|
await page.waitForTimeout(3000);
|
|
|
|
// Step 4: Verify the cross-linking call fired for the union query
|
|
testLogger.info('result_schema calls captured', {
|
|
count: requestCapture.getCount(),
|
|
urls: requestCapture.getUrls(),
|
|
});
|
|
|
|
// Multi-stream search is one UNION ALL BY NAME query, so cross-linking needs a
|
|
// single result_schema call — the backend resolves every table in the query and
|
|
// merges their links (see resolve_stream_names in the cross_linking branch).
|
|
expect(requestCapture.getCount(),
|
|
'Should fire exactly one result_schema call for the union query'
|
|
).toBe(1);
|
|
|
|
const payload = requestCapture.getPayloads()[0] || '';
|
|
expect(payload, `result_schema payload should query ${STREAM_A}`).toContain(STREAM_A);
|
|
expect(payload, `result_schema payload should query ${STREAM_B}`).toContain(STREAM_B);
|
|
|
|
testLogger.info('PASSED: single result_schema call covers both streams');
|
|
});
|
|
|
|
// P1: Dashboard table panel — single stream cross-link in drilldown menu
|
|
test("should show cross-link in dashboard table panel drilldown menu for single stream", {
|
|
tag: ['@crossLinking', '@multiStream', '@dashboard', '@functional', '@P1', '@all']
|
|
}, async ({ page }) => {
|
|
testLogger.info('Testing cross-link in dashboard table panel (single stream)');
|
|
|
|
const crossLinkName = `CL-Dash-${Date.now()}`;
|
|
|
|
// Step 1: Set up cross-link on Stream A
|
|
const featureEnabled = await setupStreamCrossLink(page, pm, STREAM_A, {
|
|
name: crossLinkName,
|
|
url: 'https://dashboard-single.example.com/?field=${field.__name}&value=${field.__value}&from=${start_time}&to=${end_time}',
|
|
fields: ['kubernetes_container_name']
|
|
});
|
|
if (!featureEnabled) {
|
|
test.skip(true, 'Cross-linking feature not enabled');
|
|
}
|
|
|
|
// Step 2: Create a dashboard with a table panel using Stream A
|
|
const dashboardName = `CL-Multi-Dash-${Date.now()}`;
|
|
const orgId = process.env["ORGNAME"] || 'default';
|
|
await page.goto(`${process.env["ZO_BASE_URL"] || 'http://localhost:5080'}/web/dashboards?org_identifier=${orgId}`);
|
|
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
|
|
await pm.dashboardCreate.createDashboard(dashboardName);
|
|
await pm.dashboardCreate.addPanel();
|
|
await pm.chartTypeSelector.selectChartType("table");
|
|
await pm.chartTypeSelector.selectStream(STREAM_A);
|
|
// Add Panel auto-seeds y_axis_1 = count(_timestamp); remove it to keep
|
|
// the original single-column table shape (x-axis only).
|
|
await pm.chartTypeSelector.removeField("y_axis_1", "y");
|
|
await pm.chartTypeSelector.searchAndAddField("kubernetes_container_name", "x");
|
|
await pm.dashboardPanelActions.addPanelName("CrossLink Single Stream Panel");
|
|
await pm.dashboardPanelActions.applyDashboardBtn();
|
|
await page.waitForTimeout(3000);
|
|
await pm.dashboardPanelActions.savePanel();
|
|
await page.waitForTimeout(3000);
|
|
|
|
testLogger.info('Dashboard with table panel created');
|
|
|
|
// Step 3: Intercept window.open to capture cross-link URL
|
|
await page.evaluate(() => {
|
|
window.__capturedCrossLinkUrl = null;
|
|
window.__originalOpen = window.open;
|
|
window.open = (url) => {
|
|
window.__capturedCrossLinkUrl = url;
|
|
return null;
|
|
};
|
|
});
|
|
|
|
try {
|
|
// Step 4: Click a data cell in the table panel to trigger drilldown menu
|
|
await page.waitForTimeout(2000);
|
|
await pm.crossLinkPage.clickFirstDashboardTableCell();
|
|
await page.waitForTimeout(1500);
|
|
|
|
// Step 5: Verify the drilldown menu appears with the cross-link
|
|
const menuVisible = await pm.crossLinkPage.isDrilldownMenuVisible();
|
|
|
|
expect(menuVisible, 'Drilldown menu should appear after clicking table cell').toBe(true);
|
|
testLogger.info('Drilldown menu is visible');
|
|
|
|
const crossLinkMenuVisible = await pm.crossLinkPage.isDrilldownMenuItemVisible(crossLinkName);
|
|
|
|
expect(crossLinkMenuVisible, `Cross-link "${crossLinkName}" should be visible in drilldown menu`).toBe(true);
|
|
testLogger.info('Cross-link menu item found in dashboard drilldown');
|
|
|
|
testLogger.info('PASSED: Single stream cross-link visible in dashboard table panel drilldown');
|
|
} finally {
|
|
await page.evaluate(() => {
|
|
if (window.__originalOpen) window.open = window.__originalOpen;
|
|
delete window.__capturedCrossLinkUrl;
|
|
delete window.__originalOpen;
|
|
});
|
|
|
|
// Cleanup: delete the test dashboard
|
|
try {
|
|
await pm.dashboardCreate.backToDashboardList();
|
|
await page.waitForTimeout(1000);
|
|
await pm.dashboardCreate.searchDashboard(dashboardName);
|
|
await page.waitForTimeout(1000);
|
|
await pm.dashboardCreate.deleteDashboard();
|
|
await page.waitForTimeout(1000);
|
|
testLogger.info('Test dashboard deleted');
|
|
} catch (e) {
|
|
testLogger.warn('Dashboard cleanup failed', { error: e.message });
|
|
}
|
|
}
|
|
});
|
|
|
|
// P1: Dashboard table panel with UNION ALL SQL + Dynamic Columns — cross-links from both streams
|
|
test("should show cross-links from both streams in dashboard table panel with UNION ALL SQL", {
|
|
tag: ['@crossLinking', '@multiStream', '@dashboard', '@join', '@functional', '@P1', '@all']
|
|
}, async ({ page }) => {
|
|
testLogger.info('Testing multi-stream cross-links in dashboard table panel with UNION ALL SQL');
|
|
|
|
const crossLinkNameA = `CL-DashA-${Date.now()}`;
|
|
const crossLinkNameB = `CL-DashB-${Date.now()}`;
|
|
|
|
// Probe the feature gate before building the dashboard so an unsupported build skips instead of failing deep in the panel setup.
|
|
const featureCheckResult = await (async () => {
|
|
await pm.crossLinkPage.navigateToStreams();
|
|
await pm.crossLinkPage.searchStream(STREAM_A);
|
|
await pm.crossLinkPage.openStreamDetail();
|
|
return pm.crossLinkPage.isCrossLinkingTabVisible();
|
|
})();
|
|
if (!featureCheckResult) {
|
|
test.skip(true, 'Cross-linking feature not enabled');
|
|
}
|
|
|
|
// Step 2: Create a dashboard with a table panel using custom UNION ALL SQL.
|
|
// Cross-links are intentionally NOT set up yet.
|
|
const dashboardName = `CL-Union-Dash-${Date.now()}`;
|
|
const orgId = process.env["ORGNAME"] || 'default';
|
|
await page.goto(`${process.env["ZO_BASE_URL"] || 'http://localhost:5080'}/web/dashboards?org_identifier=${orgId}`);
|
|
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
|
|
await pm.dashboardCreate.createDashboard(dashboardName);
|
|
await pm.dashboardCreate.addPanel();
|
|
await pm.chartTypeSelector.selectChartType("table");
|
|
await pm.dashboardPanelActions.addPanelName("CrossLink UNION Panel");
|
|
|
|
// Open config sidebar and enable "Allow Dynamic Columns"
|
|
await pm.dashboardPanelConfigs.openConfigPanel();
|
|
await pm.dashboardPanelConfigs.selectDynamicColumns();
|
|
await page.waitForTimeout(500);
|
|
testLogger.info('Allow Dynamic Columns enabled');
|
|
|
|
// Switch to Custom SQL mode and enter UNION ALL query — `setCustomSQL`
|
|
// clicks the SQL + Custom Query type buttons and then enters the SQL
|
|
// (mirrors `chartTypeSelector.setCustomSQL` in dashboard-chart.js).
|
|
const unionQuery = `SELECT * FROM "${STREAM_A}" UNION ALL BY NAME SELECT * FROM "${STREAM_B}" LIMIT 100`;
|
|
await pm.chartTypeSelector.setCustomSQL(unionQuery);
|
|
await page.waitForTimeout(500);
|
|
testLogger.info('UNION ALL dashboard query entered', { query: unionQuery });
|
|
|
|
// Apply and save the panel — cross-links are set up AFTER this.
|
|
await pm.dashboardPanelActions.applyDashboardBtn();
|
|
await page.waitForTimeout(3000);
|
|
await pm.dashboardPanelActions.savePanel();
|
|
await page.waitForTimeout(3000);
|
|
|
|
testLogger.info('Dashboard with UNION ALL table panel saved');
|
|
|
|
// Confirm the table panel rendered with data rows so we know the query
|
|
// works. `waitForTablePanelWithData` is the approved PO wrapper around
|
|
// the scoped `waitForFunction` that scans `[data-test="dashboard-panel-table"]`
|
|
// for any populated cell.
|
|
await pm.dashboardPanelActions.waitForTablePanelWithData();
|
|
testLogger.info('Table panel confirmed with data rows');
|
|
|
|
// Capture the dashboard view URL so we can reload it after cross-link setup.
|
|
const dashboardViewUrl = page.url();
|
|
|
|
// Step 3: Set up both cross-links immediately before navigating back so result_schema fires with both present.
|
|
const featureEnabledB = await setupStreamCrossLink(page, pm, STREAM_B, {
|
|
name: crossLinkNameB,
|
|
url: 'https://dash-union-b.example.com/${field.__value}',
|
|
fields: ['kubernetes_container_name']
|
|
});
|
|
if (!featureEnabledB) {
|
|
test.skip(true, 'Cross-linking feature not enabled on second stream');
|
|
}
|
|
|
|
// result_schema can be served without a just-saved cross-link, so re-set A and reload rather than failing on the first miss.
|
|
let crossLinkResponseData = null;
|
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
// (Re-)set up A's cross-link immediately before navigating back.
|
|
const featureEnabledA = await setupStreamCrossLink(page, pm, STREAM_A, {
|
|
name: crossLinkNameA,
|
|
url: 'https://dash-union-a.example.com/${field.__value}',
|
|
fields: ['kubernetes_container_name']
|
|
});
|
|
if (!featureEnabledA) {
|
|
test.skip(true, 'Cross-linking feature not enabled');
|
|
}
|
|
|
|
testLogger.info(`Attempt ${attempt}: cross-links set up, navigating to dashboard`, {
|
|
crossLinkNameA, crossLinkNameB
|
|
});
|
|
|
|
// Navigating back remounts the panel, which drops its per-mount result_schema cache.
|
|
await page.goto(dashboardViewUrl);
|
|
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
|
await pm.dashboardPanelActions.waitForTablePanelWithData(30000);
|
|
|
|
// result_schema is fetched lazily on the first drilldown click (usePanelDrilldown ensureDrilldownSchema), not on render.
|
|
const crossLinkResponsePromise = page
|
|
.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes('result_schema') &&
|
|
resp.url().includes('cross_linking=true') &&
|
|
resp.request().method() === 'POST',
|
|
{ timeout: 20000 }
|
|
)
|
|
.catch(() => null);
|
|
await pm.crossLinkPage.clickFirstDashboardTableCell();
|
|
|
|
const crossLinkResponse = await crossLinkResponsePromise;
|
|
if (!crossLinkResponse) {
|
|
testLogger.warn(`Attempt ${attempt}: result_schema response timed out`);
|
|
continue;
|
|
}
|
|
|
|
// Inspect the response body to decide whether to retry.
|
|
crossLinkResponseData = await crossLinkResponse.json().catch(() => null);
|
|
const streamLinks = crossLinkResponseData?.cross_links?.stream_links ?? [];
|
|
const hasAInResponse = streamLinks.some((l) => l.name === crossLinkNameA);
|
|
const hasBInResponse = streamLinks.some((l) => l.name === crossLinkNameB);
|
|
|
|
testLogger.info(`Attempt ${attempt}: result_schema cross-link check`, {
|
|
hasA: hasAInResponse, hasB: hasBInResponse,
|
|
streamLinkNames: streamLinks.map((l) => l.name)
|
|
});
|
|
|
|
if (hasAInResponse && hasBInResponse) {
|
|
testLogger.info('Both cross-links confirmed in result_schema response');
|
|
break;
|
|
}
|
|
|
|
if (attempt < 3) {
|
|
testLogger.warn(`Attempt ${attempt}: cross-link(s) missing — retrying`, {
|
|
missingA: !hasAInResponse, missingB: !hasBInResponse
|
|
});
|
|
}
|
|
}
|
|
|
|
// Allow Vue's reactive update + nextTick to settle after the response.
|
|
await page.waitForTimeout(1500);
|
|
|
|
// Step 4: Intercept window.open
|
|
await page.evaluate(() => {
|
|
window.__capturedCrossLinkUrl = null;
|
|
window.__originalOpen = window.open;
|
|
window.open = (url) => {
|
|
window.__capturedCrossLinkUrl = url;
|
|
return null;
|
|
};
|
|
});
|
|
|
|
try {
|
|
// Step 5 & 6: Click a table cell and verify BOTH cross-link items appear in the
|
|
// drilldown menu. expect.poll provides automatic retry if the menu opens before
|
|
// Vue has finished wiring the freshly-loaded crossLinksData into drilldownArray.
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
if (await pm.crossLinkPage.isDrilldownMenuVisible()) {
|
|
await page.mouse.move(0, 0);
|
|
await page.waitForTimeout(300);
|
|
}
|
|
|
|
await pm.crossLinkPage.clickFirstDashboardTableCell();
|
|
|
|
await page.waitForTimeout(800);
|
|
|
|
const menuVisible = await pm.crossLinkPage.isDrilldownMenuVisible();
|
|
const hasA = menuVisible
|
|
? await pm.crossLinkPage.isDrilldownMenuItemVisible(crossLinkNameA)
|
|
: false;
|
|
const hasB = menuVisible
|
|
? await pm.crossLinkPage.isDrilldownMenuItemVisible(crossLinkNameB)
|
|
: false;
|
|
|
|
return { menuVisible, hasA, hasB };
|
|
},
|
|
{
|
|
timeout: 15000,
|
|
intervals: [800, 1200, 1500],
|
|
message:
|
|
'Drilldown menu should show cross-links from both streams in UNION ALL panel',
|
|
}
|
|
)
|
|
.toEqual({ menuVisible: true, hasA: true, hasB: true });
|
|
|
|
testLogger.info('Drilldown menu is visible with both cross-links');
|
|
testLogger.info('PASSED: Both streams cross-links visible in dashboard UNION ALL table panel drilldown');
|
|
} finally {
|
|
await page.evaluate(() => {
|
|
if (window.__originalOpen) window.open = window.__originalOpen;
|
|
delete window.__capturedCrossLinkUrl;
|
|
delete window.__originalOpen;
|
|
});
|
|
|
|
// Cleanup: delete the test dashboard
|
|
try {
|
|
await pm.dashboardCreate.backToDashboardList();
|
|
await page.waitForTimeout(1000);
|
|
await pm.dashboardCreate.searchDashboard(dashboardName);
|
|
await page.waitForTimeout(1000);
|
|
await pm.dashboardCreate.deleteDashboard();
|
|
await page.waitForTimeout(1000);
|
|
testLogger.info('Test dashboard deleted');
|
|
} catch (e) {
|
|
testLogger.warn('Dashboard cleanup failed', { error: e.message });
|
|
}
|
|
}
|
|
});
|
|
|
|
// P1: Single-stream regression — cross-links still work with one stream
|
|
test("should still show cross-links correctly with single stream (regression)", {
|
|
tag: ['@crossLinking', '@multiStream', '@regression', '@P1', '@all']
|
|
}, async ({ page }) => {
|
|
testLogger.info('Testing single-stream cross-links regression');
|
|
|
|
const crossLinkName = `CL-Single-${Date.now()}`;
|
|
|
|
// Step 1: Set up cross-link on Stream A only
|
|
const featureEnabled = await setupStreamCrossLink(page, pm, STREAM_A, {
|
|
name: crossLinkName,
|
|
url: 'https://single.example.com/${field.__value}',
|
|
fields: ['kubernetes_container_name']
|
|
});
|
|
if (!featureEnabled) {
|
|
test.skip(true, 'Cross-linking feature not enabled');
|
|
}
|
|
|
|
// Step 2: Navigate to logs, select only Stream A
|
|
await pm.logsPage.navigateToLogs();
|
|
await pm.logsPage.selectStream(STREAM_A);
|
|
// selectStream already waits for the stream option and selection
|
|
|
|
// Disable quick mode
|
|
await pm.logsPage.ensureQuickModeState(false);
|
|
// ensureQuickModeState already waits for the toggle data-state to flip
|
|
|
|
// Run query and wait for search API + result_schema cross-linking response
|
|
const resultSchemaPromise = page.waitForResponse(
|
|
(resp) => resp.url().includes('result_schema') && resp.url().includes('cross_linking=true') && resp.status() === 200,
|
|
{ timeout: 20000 }
|
|
).catch(() => {});
|
|
await pm.logsPage.runQueryAndWaitForResults();
|
|
await resultSchemaPromise;
|
|
|
|
// Step 3: Expand a log row
|
|
await pm.crossLinkPage.expandFirstLogRow();
|
|
|
|
// Step 4 & 5: Open kubernetes_container_name field action dropdown and
|
|
// poll until the cross-link appears. The `result_schema?cross_linking=true`
|
|
// response is eventually-consistent — a one-shot `isVisible()` raced the
|
|
// schema-merge on CI, so use the polling helper that re-opens the dropdown
|
|
// until the late-arriving response lands (mirrors `expectCrossLinksFromBothStreams`).
|
|
const hasCrossLink = await pm.crossLinkPage.expectLogCrossLinkVisible(
|
|
'kubernetes_container_name',
|
|
crossLinkName,
|
|
);
|
|
|
|
testLogger.info('Single-stream cross-link visibility', { visible: hasCrossLink });
|
|
|
|
expect(hasCrossLink, `Cross-link "${crossLinkName}" should be visible for single stream`).toBe(true);
|
|
|
|
testLogger.info('PASSED: Single-stream cross-link regression verified');
|
|
});
|
|
});
|