Files
openobserve/tests/ui-testing/playwright-tests/Streams/multiselect-stream.spec.js
T
f15e27e5fd refactor: design token migration + CI guards (#13173)
# Design System Unification — one visual language, enforced end-to-end

Establishes **one source of truth for every visual decision**, wires the
whole app to it, and **locks it in with CI** so it can't drift back.

### Foundations
- **Design-token system ("one knob per decision").** Every colour, text
size, spacing, corner radius, and shadow is a named token defined in
exactly one place — change the token, every screen updates. Collapsed
hundreds of near-duplicate definitions and removed "fake" duplicates
that pretended to be different but weren't.
- **Shared component library (O2).** Buttons, tables, dialogs, drawers,
form fields, badges, etc. already know the correct colours, spacing, and
dark-mode behaviour; new screens compose them instead of hand-styling.
Added new primitives on this branch (`OContent`, `KpiCard`) and
standardized existing ones.
- **One page structure everywhere** (`PageLayout` / `AppPageHeader` /
`AppPage` / `OContent`) — titles, breadcrumbs, spacing, and content
edges line up identically across every screen.
- **One table implementation** — all tabular data uses the same table
(sorting, density, sticky headers, empty states).
- **Styling consolidated onto Tailwind; old systems removed** —
hand-written CSS driven to the practical minimum, the legacy **Quasar**
framework fully removed, surviving stylesheets required to justify
themselves.
- **Two-tier corner radius** — a tight **4px** for controls and a soft
**12px** for surfaces (dialogs, cards, the app shell), plus a
full-circle option — replacing five radius names that all secretly meant
the same thing.

### Detailed improvements across screens
**Page structure & alignment** — unified page shell rolled across ~40+
screens (IAM, Settings, Alerts, Dashboards, Functions, Pipelines, RUM,
Metrics, Synthetics, Billing, editors, lists); one header height; one
page-edge grid driving headers, tables, rails, and tabs; unified
side-rail widths; inset rail selection/hover pills; every tab strip on
the grid line; tightened title/subtitle spacing.

**Tables (cross-screen)** — action columns consistently centered (incl.
Organizations); unified row-number column with no "#" truncation;
non-standard tables restyled to match; loading skeletons made
geometrically identical to the loaded table; symmetric edge inset; IAM
first-column grid alignment fixed.

**Traces** — flame graph + span-detail correlation panel fixes;
correlated logs/metrics placeholder fit; sticky headers; removed a
double scrollbar + a 1px sliver; aligned table fonts/badges; polished
the service detail drawer; stream-selector + correlation alignment;
aligned loading spinners; floating overlay arrows for the
disabled-services strip.

**Logs** — field-list controls aligned (focus ring no longer clipped);
detail-drawer spacing + metrics-tab overflow fixed; Run Query toolbar
tidied; listing sizing + stray border fixed.

**Metrics** — search added to the "Type" facet rail and aligned with the
other rails; proper empty states for the explorer and facet rail.

**Dashboards** — removed inconsistent card shadows; tightened
borders/resizers/field-list spacing; aligned View Dashboard chrome to
the grid; chart colours via the token helper.

**Home** — redesigned the Usage tab; added reusable KPI stat cards so
summary strips stop drifting.

**Dark mode** — new theme-switch animation; fixed a 1px content shift on
toggle and a white flash on load; fixed AI-chat text and assorted
light/dark mismatches; completed the token-driven dark migration.

**Empty states, controls & small polish** — restored 3 broken
empty-state animations; adopted the standard empty state; added the
sliding indicator to the toggle control; fixed the Quality-page inset +
drawer badge; refreshed Search History / Schedulers; fixed the
alert-history drawer scrollbar + timeline hover; removed a stray
menu-bar border.

### Dead code & cruft removal
- **Deleted the entire legacy `utilities.css`** global stylesheet (well
over a thousand lines) — evacuated in waves, roughly half of it dead
once traced.
- **Removed Quasar entirely**, including dead `.q-*` styles and ~25
phantom `--q-*` variables that were silently breaking bits of the UI.
- Stripped hundreds of **dead CSS classes and tokens that compiled to
nothing** — e.g. 46 uses of a colour class with no backing token, a
no-op shadow class on 51 elements, dead layout/utility classes, a
leftover `card-container` block, a dead table-actions token, and a
mis-registered gradient that rendered nothing.
- Cut **unused keyframe animations (19 → 3)** and removed **unused
components** + a stray build artifact.
- Net effect: a much smaller, leaner styling surface — less to maintain,
fewer dead ends to copy.

## Safety & verification
Large but low-risk: this is overwhelmingly a **styling and structure**
change, not product behaviour or data. Guarded by the
design-token/consistency/type checks above plus the existing test suite,
and reviewed screen-by-screen.

---------

Co-authored-by: Kirtan Thakkar <kirtan@kiara.tech>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: ktx-mihir <mihir.patel@kiara.tech>
2026-07-20 18:06:21 +00:00

485 lines
19 KiB
JavaScript

const { test, expect } = require('../utils/enhanced-baseFixtures.js');
const logData = require("../../fixtures/log.json");
const PageManager = require('../../pages/page-manager.js');
const { getHeaders, getIngestionUrl, sendRequest } = require('../../utils/apiUtils.js');
const testLogger = require('../utils/test-logger.js');
// Constants for consistent timeouts and test data
const MULTISTREAM_CONFIG = {
TIMEOUTS: {
STREAM_FILTER: 2000,
STREAM_SELECTION: 4000,
MULTISTREAM_SETUP: 5000,
QUERY_EXECUTION: 5000,
DATA_INDEXING: 3000,
UI_INTERACTION: 1000,
NAVIGATION: 2000,
TOAST_WAIT: 500
},
TEST_DATA: {
STREAMS: ['e2e_automate', 'e2e_stream1'],
QUERIES: {
INVALID_FIELD: 'random_test=2',
VRL_FUNCTION: '.a=2'
},
PAYLOADS: {
STREAM1: {
level: "debug",
job: "test_job_1",
log: "enhanced test message for stream 1",
e2e: "enhanced1",
stream_id: "e2e_automate"
},
STREAM2: {
level: "warning",
job: "test_job_2",
log: "enhanced test message for stream 2",
e2e: "enhanced2",
stream_id: "e2e_stream1"
}
}
}
};
// Helper functions to reduce code duplication
async function verifyMultistreamSelection(page, streams = MULTISTREAM_CONFIG.TEST_DATA.STREAMS) {
const pageManager = new PageManager(page);
await pageManager.logsPage.expectLogsSearchIndexListContainsText(streams.join(', '));
}
async function setupBasicMultistream(page, streams = MULTISTREAM_CONFIG.TEST_DATA.STREAMS) {
const pageManager = new PageManager(page);
// Add second stream using POM
await pageManager.logsPage.fillStreamFilter(streams[1]);
await page.waitForTimeout(MULTISTREAM_CONFIG.TIMEOUTS.STREAM_FILTER);
await pageManager.logsPage.toggleStreamSelection(streams[1]);
await page.waitForTimeout(MULTISTREAM_CONFIG.TIMEOUTS.STREAM_SELECTION);
// Verify both streams are selected
await verifyMultistreamSelection(page, streams);
}
async function validateCombinedStreamResults(page, payloads) {
const pageManager = new PageManager(page);
const tableContent = await pageManager.logsPage.getLogsTableContent();
// Extract stream identifiers from payloads for validation
const stream1Indicators = [payloads.STREAM1.e2e, payloads.STREAM1.job, payloads.STREAM1.level];
const stream2Indicators = [payloads.STREAM2.e2e, payloads.STREAM2.job, payloads.STREAM2.level];
// Check for data from both streams
const hasStream1Data = stream1Indicators.some(indicator => tableContent.includes(indicator));
const hasStream2Data = stream2Indicators.some(indicator => tableContent.includes(indicator));
if (!hasStream1Data || !hasStream2Data) {
// Alternative check - verify we have multiple rows of data
const rowCount = await pageManager.logsPage.getLogsTableRowCount();
expect(rowCount).toBeGreaterThan(1);
testLogger.info('Multiple data rows found indicating combined results', { rowCount });
} else {
testLogger.info('Data from both streams verified in results');
}
}
test.describe.configure({ mode: "parallel" });
test.describe("Stream multiselect testcases", () => {
let pm;
function removeUTFCharacters(text) {
return text.replace(/[^\x00-\x7F]/g, " ");
}
async function applyQueryButton(page) {
const pm = new PageManager(page);
await pm.logsPage.applyQueryButton(logData.logsUrl);
}
test.beforeEach(async ({ page }) => {
pm = new PageManager(page);
const orgId = process.env["ORGNAME"];
const streamNames = ["e2e_automate", "e2e_stream1"];
const headers = getHeaders();
for (const streamName of streamNames) {
const ingestionUrl = getIngestionUrl(orgId, streamName);
const payload = {
level: "info",
job: "test",
log: "test message for openobserve",
e2e: "1",
};
const response = await sendRequest(page, ingestionUrl, payload, headers);
testLogger.debug('API response received', { streamName, response });
}
await page.goto(`${logData.logsUrl}?org_identifier=${process.env["ORGNAME"]}`);
await pm.logsPage.selectStream("e2e_automate");
await applyQueryButton(page);
// Ensure quick mode is ON (idempotent) so the All Fields toggle is visible
await pm.logsPage.enableQuickModeIfDisabled();
await pm.logsPage.clickAllFieldsButton();
});
async function multistreamselect(page) {
const pageManager = new PageManager(page);
// Add second stream using POM (we're already on logs page from beforeEach with e2e_automate selected and all fields enabled)
await pageManager.logsPage.fillStreamFilter('e2e_stream1');
await page.waitForTimeout(2000);
await pageManager.logsPage.toggleStreamSelection('e2e_stream1');
await page.waitForTimeout(4000);
// Enable function editor using POM (all fields already clicked in beforeEach)
await pageManager.logsPage.toggleQueryModeEditor();
// Extra wait for Firefox to render the Monaco editor
await page.waitForTimeout(3000);
await pageManager.logsPage.clickMonacoEditor();
// Run query to populate results first
await pageManager.logsPage.selectRunQuery();
await page.waitForTimeout(3000);
// Verify common fields group header is present.
// When applyFieldGrouping is active (semantic grouping), the header renders as
// "Common" or "Common (N)"; legacy path renders "Common Group Fields".
const cell = await pageManager.logsPage.getCellByName(/^Common/);
const cellText = await cell.textContent();
expect(cellText).toMatch(/Common/);
// Verify both streams are selected in the index list
await pageManager.logsPage.expectLogsSearchIndexListContainsText('e2e_automate, e2e_stream1');
// Execute query and navigate time picker using POM
await pageManager.logsPage.selectRunQuery();
await pageManager.logsPage.clickDateTimeButton();
await pageManager.logsPage.selectRelative6Hours();
await pageManager.logsPage.clickTimestampColumnMenu();
}
test("should add a function and display it in streams", {
tag: ['@vrl', '@function', '@multistream', '@all']
}, async ({ page }) => {
const pageManager = new PageManager(page);
await multistreamselect(page);
await pageManager.logsPage.fillMonacoEditor('.a=2');
await page.waitForTimeout(1000);
await applyQueryButton(page);
await pageManager.logsPage.clickTableExpandMenuFirst();
await pageManager.logsPage.expectVrlFunctionVisible(".a=2");
await pageManager.logsPage.expectLogsTableVisible();
await pageManager.logsPage.selectRunQuery();
});
// test("should click on live mode on button and select 5 sec, switch off, and then click run query", async ({
// page,
// }) => {
// await multistreamselect(page);
// await page.route("**/logData.ValueQuery", (route) => route.continue());
// await page.locator('[data-test="date-time-btn"]').click({ force: true });
// await page
// .locator('[data-test="date-time-relative-6-w-btn"] button')
// .click({
// force: true,
// });
// await page
// .locator('[data-test="logs-search-bar-refresh-interval-btn-dropdown"]')
// .click({ force: true });
// await page.locator('[data-test="logs-search-bar-refresh-time-5"]').click({
// force: true,
// });
// await page.waitForTimeout(1000);
// await expect(page.locator('[role="alert"]')).toContainText(
// "Live mode is enabled"
// );
// await page.waitForTimeout(5000);
// await page
// .locator('[data-test="logs-search-off-refresh-interval"] button')
// .click({ force: true });
// await page
// .locator(
// '[data-test="logs-search-off-refresh-interval"] button'
// )
// .click({ force: true });
// await applyQueryButton(page);
// });
test("should redirect to logs after clicking on stream explorer via stream page", {
tag: ['@navigation', '@streamExplorer', '@multistream', '@all']
}, async ({ page }) => {
const pageManager = new PageManager(page);
await multistreamselect(page);
await pageManager.logsPage.clickDateTimeButton();
await pageManager.logsPage.navigateToStreams();
await page.waitForTimeout(1000);
await pageManager.logsPage.navigateToStreamsAlternate();
await pageManager.logsPage.searchStreamByPlaceholder("e2e");
await page.waitForTimeout(1000);
await pageManager.logsPage.clickFirstExploreButton();
await page.waitForTimeout(1000);
await expect(page.url()).toContain("logs");
});
test("should click on interesting fields icon and display query in editor", {
tag: ['@interestingFields', '@multistream']
}, async ({ page }) => {
const pageManager = new PageManager(page);
testLogger.info('Testing interesting fields with multistream selection');
await multistreamselect(page);
// The interesting-field (ⓘ) button only renders when quick mode is on
// (FieldExpansion.vue v-if="showQuickMode"), and surfaces on row hover.
await pageManager.logsPage.ensureQuickModeState(true);
// Search for job field using POM
await pageManager.logsPage.searchFieldByName('job');
await page.waitForTimeout(2000);
// Hover the field row so its action buttons render, then click ⓘ
await pageManager.logsPage.hoverAndClickInterestingFieldLast('job');
// Enable SQL mode using POM
await pageManager.logsPage.enableSQLMode();
await page.waitForTimeout(2000);
// Run query using POM
await pageManager.logsPage.clickSearchBarRefreshButton();
// Click on first result
await pageManager.logsPage.clickLogTableColumnSource();
// Verify table is visible
await pageManager.logsPage.expectLogsTableVisible();
testLogger.info('Interesting fields test completed');
});
test("should display results in selected time when multiple stream selected", {
tag: ['@timePicker', '@dateTime', '@multistream', '@all']
}, async ({ page }) => {
const pageManager = new PageManager(page);
await multistreamselect(page);
await pageManager.logsPage.setDateTimeToToday();
await pageManager.logsPage.expectLogsSearchIndexListContainsText('e2e_automate, e2e_stream1');
});
test("should not show error when histogram toggle is on with multiple streams @multi-histogram @multistream @regression", async ({
page,
}) => {
const pageManager = new PageManager(page);
// Navigate to logs page using POM
await pageManager.logsPage.navigateToHome();
await pageManager.logsPage.navigateToLogs();
await page.waitForTimeout(2000);
// Select first stream (e2e_automate) using POM
await pageManager.logsPage.selectStream("e2e_automate");
await pageManager.logsPage.selectRunQuery();
await page.waitForTimeout(2000);
// Select additional stream using POM
await pageManager.logsPage.fillStreamFilter("e2e_stream1");
await page.waitForTimeout(2000);
await pageManager.logsPage.toggleStreamSelection("e2e_stream1");
await page.waitForTimeout(4000);
// Ensure quick mode is ON so the All Fields toggle is visible
// (quickMode default depends on whether localStorage has saved streams from prior navigation)
await pageManager.logsPage.enableQuickModeIfDisabled();
await pageManager.logsPage.clickAllFieldsButton();
await pageManager.logsPage.toggleQueryModeEditor();
await pageManager.logsPage.clickMonacoEditor();
// Enable histogram using POM - ensure it's actually enabled
await pageManager.logsPage.toggleHistogram();
await pageManager.logsPage.toggleHistogram(); // Double click to ensure ON state
await pageManager.logsPage.selectRunQuery();
await page.waitForTimeout(3000);
// Verify no histogram error is displayed
await pageManager.logsPage.expectErrorWhileFetchingNotVisible();
});
// Enhanced multistream tests
test("should verify search filter functionality with multiple streams selected", {
tag: ['@filter', '@invalidfield', '@multistream']
}, async ({ page }) => {
testLogger.info('Testing search filter functionality with multiple streams');
const pageManager = new PageManager(page);
// Setup multistream selection using helper
await setupBasicMultistream(page);
// Add query in query editor and run
await pageManager.logsPage.fillQueryEditorWithRole(MULTISTREAM_CONFIG.TEST_DATA.QUERIES.INVALID_FIELD);
await pageManager.logsPage.clickSearchBarRefreshButton();
await page.waitForTimeout(MULTISTREAM_CONFIG.TIMEOUTS.QUERY_EXECUTION);
testLogger.info('Search filter steps executed successfully with multiple streams');
});
test("should add a function and display it in streams with multiple streams", {
tag: ['@vrl', '@function', '@transformation']
}, async ({ page }) => {
testLogger.info('Testing Monaco editor function with multiple streams');
const pageManager = new PageManager(page);
// Setup multiple streams using existing multistreamselect function
await multistreamselect(page);
// Fill Monaco editor with VRL function using constant
await pageManager.logsPage.fillMonacoEditor(MULTISTREAM_CONFIG.TEST_DATA.QUERIES.VRL_FUNCTION);
await page.waitForTimeout(MULTISTREAM_CONFIG.TIMEOUTS.UI_INTERACTION);
await applyQueryButton(page);
await pageManager.logsPage.clickTableExpandMenuFirst();
// Verify VRL function is visible and table is displayed
await pageManager.logsPage.expectVrlFunctionVisible(MULTISTREAM_CONFIG.TEST_DATA.QUERIES.VRL_FUNCTION);
await pageManager.logsPage.expectLogsTableVisible();
await pageManager.logsPage.selectRunQuery();
testLogger.info('Monaco editor function test with multiple streams completed successfully');
});
test("should display combined results when both streams have data", {
tag: ['@multistreamdata', '@datavalidation', '@multistreams']
}, async ({ page }) => {
testLogger.info('Testing combined results display when both streams have data');
const pageManager = new PageManager(page);
// Ensure both streams have data by ingesting test data
const orgId = process.env["ORGNAME"];
const headers = getHeaders();
const streams = MULTISTREAM_CONFIG.TEST_DATA.STREAMS;
const payloads = MULTISTREAM_CONFIG.TEST_DATA.PAYLOADS;
await sendRequest(page, getIngestionUrl(orgId, streams[0]), payloads.STREAM1, headers);
await sendRequest(page, getIngestionUrl(orgId, streams[1]), payloads.STREAM2, headers);
// Wait for data to be indexed
await page.waitForTimeout(MULTISTREAM_CONFIG.TIMEOUTS.DATA_INDEXING);
// Setup multiple streams
await multistreamselect(page);
// Verify both streams are selected
await verifyMultistreamSelection(page);
// Run query to get combined results
await pageManager.logsPage.selectRunQuery();
await page.waitForTimeout(MULTISTREAM_CONFIG.TIMEOUTS.QUERY_EXECUTION);
// Verify results table is visible and contains data
await pageManager.logsPage.expectLogsTableVisible();
// Verify we have results from both streams
await validateCombinedStreamResults(page, payloads);
// Verify no error messages are present
await pageManager.logsPage.expectNotificationErrorNotVisible();
testLogger.info('Combined results from both streams test completed successfully');
});
test("should create and delete saved view with multiple streams selected", {
tag: ['@savedview', '@viewcrud', '@multiconfig']
}, async ({ page }) => {
testLogger.info('Testing saved view creation and deletion with multiple streams');
const pageManager = new PageManager(page);
const streams = MULTISTREAM_CONFIG.TEST_DATA.STREAMS;
// Setup multiple streams selection using existing function
await multistreamselect(page);
// Verify both streams are selected using helper
await verifyMultistreamSelection(page);
// Generate a random saved view name with multistream prefix
const randomSavedViewName = `multistream_view_${Math.random().toString(36).substring(2, 10)}`;
testLogger.info(`Creating saved view with name: ${randomSavedViewName}`);
// Create saved view with multistream configuration
await pageManager.logsPage.clickSavedViewsExpand();
await pageManager.logsPage.clickSaveViewButton();
await pageManager.logsPage.fillSavedViewName(randomSavedViewName);
await pageManager.logsPage.clickSavedViewDialogSave();
// Wait for success message with timeout constant
try {
await page.locator('[data-test-variant="success"]').waitFor({
state: 'visible',
timeout: MULTISTREAM_CONFIG.TIMEOUTS.DATA_INDEXING
});
testLogger.info('Success toast validated: Multistream view created successfully');
} catch (error) {
testLogger.info('View creation toast may have appeared and disappeared quickly - continuing with test');
}
// Wait for saved view creation to complete using constant
await page.waitForTimeout(MULTISTREAM_CONFIG.TIMEOUTS.NAVIGATION);
// Navigate away and back to verify saved view persistence
await pageManager.logsPage.clickStreamsMenuItem();
await pageManager.logsPage.clickSearchStreamInput();
await pageManager.logsPage.fillSearchStreamInput('e2e');
await page.waitForTimeout(MULTISTREAM_CONFIG.TIMEOUTS.TOAST_WAIT);
await pageManager.logsPage.clickExploreButton();
await page.waitForTimeout(MULTISTREAM_CONFIG.TIMEOUTS.NAVIGATION);
// Verify saved view exists and can be loaded
await pageManager.logsPage.waitForSavedViewsButton();
await pageManager.logsPage.clickSavedViewsExpand();
await pageManager.logsPage.clickSavedViewSearchInput();
await pageManager.logsPage.fillSavedViewSearchInput(randomSavedViewName);
await page.waitForTimeout(MULTISTREAM_CONFIG.TIMEOUTS.UI_INTERACTION);
// Apply the saved view
await pageManager.logsPage.waitForSavedViewText(randomSavedViewName);
await pageManager.logsPage.clickSavedViewByText(randomSavedViewName);
await page.waitForTimeout(MULTISTREAM_CONFIG.TIMEOUTS.NAVIGATION);
// Verify that multistream configuration was saved and restored using helper
await verifyMultistreamSelection(page);
testLogger.info('Verified: Saved view correctly restored multistream selection');
// Verify query results are displayed
await pageManager.logsPage.expectLogsTableVisible();
testLogger.info('Verified: Query results are displayed after loading saved view');
// Clean up: Try to delete the saved view (with error handling)
try {
await pageManager.logsPage.clickSavedViewsExpand();
await pageManager.logsPage.clickSavedViewSearchInput();
await pageManager.logsPage.fillSavedViewSearchInput(randomSavedViewName);
await page.waitForTimeout(MULTISTREAM_CONFIG.TIMEOUTS.UI_INTERACTION);
// Delete the saved view using POM
await pageManager.logsPage.clickDeleteSavedViewButton(randomSavedViewName);
await page.waitForTimeout(MULTISTREAM_CONFIG.TIMEOUTS.TOAST_WAIT);
await pageManager.logsPage.clickConfirmButton();
testLogger.info(`Successfully deleted multistream saved view: ${randomSavedViewName}`);
} catch (cleanupError) {
testLogger.warn(`Cleanup failed - saved view may still exist: ${randomSavedViewName}`, { error: cleanupError.message });
// Test is still successful even if cleanup fails
}
testLogger.info('Multistream saved view creation and deletion test completed successfully');
});
})