Compare commits

...

2 Commits

Author SHA1 Message Date
aks07
bee0924164 test(explorer): add e2e coverage for the list order-by
Covers the two reported bugs, back navigation on traces and a deep link on
logs, and the two paths that unit tests cannot reach: the URL round trip and
the clear between the LLM raw tabs.

The order-by control gets a data-testid. Its four mount points share no other
hook, since the LLM trace view styles it with a CSS module.
2026-10-14 15:44:07 +05:30
aks07
65298c25b0 test(logs-explorer): cover the list query rebuild on an order change
The existing cases assert the default order on first render. None of them
change the order, so nothing caught the effect that rebuilds the request.
2026-10-14 15:43:52 +05:30
6 changed files with 344 additions and 1 deletions

View File

@@ -104,6 +104,7 @@ function ListViewOrderBy({
return (
<Select
data-testid="list-view-order-by"
showSearch
value={value}
onChange={onChange}

View File

@@ -1,3 +1,4 @@
import { useState } from 'react';
import { VirtuosoMockContext } from 'react-virtuoso';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
@@ -8,7 +9,13 @@ import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { fireEvent, render, RenderResult, waitFor } from 'tests/test-utils';
import {
fireEvent,
render,
RenderResult,
screen,
waitFor,
} from 'tests/test-utils';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
@@ -396,5 +403,95 @@ describe('LogsExplorerViews -', () => {
}
});
});
it('rebuilds the List View query when the order changes', async () => {
(useCopyLogLink as jest.Mock).mockReturnValue({ activeLogId: undefined });
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
data: { payload: logsQueryRangeSuccessNewFormatResponse },
});
const stagedQueryOrderedBy = (
columnName: string,
id: string,
): Record<string, unknown> => ({
...mockQueryBuilderContextValue.stagedQuery,
id,
builder: {
...mockQueryBuilderContextValue.stagedQuery.builder,
queryData: mockQueryBuilderContextValue.stagedQuery.builder.queryData.map(
(item: Record<string, unknown>) => ({
...item,
orderBy: [{ columnName, order: 'desc' }],
}),
),
},
});
// `customRender` wraps a captured element, so `rerender` does not reach the
// tree. The order change is driven from state inside it instead.
function Harness(): JSX.Element {
const [ordered, setOrdered] = useState(false);
const stagedQuery = ordered
? stagedQueryOrderedBy('severity_text', 'query-2')
: stagedQueryOrderedBy('timestamp', 'query-1');
return (
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<PreferenceContextProvider>
<QueryBuilderContext.Provider
value={
{
...mockQueryBuilderContextValue,
panelType: PANEL_TYPES.LIST,
stagedQuery,
} as any
}
>
<button
type="button"
data-testid="change-order"
onClick={(): void => setOrdered(true)}
/>
<LogsExplorerViews
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
setWarning={(): void => {}}
showLiveLogs={false}
handleChangeSelectedView={(): void => {}}
/>
</QueryBuilderContext.Provider>
</PreferenceContextProvider>
</VirtuosoMockContext.Provider>
);
}
const latestListOrderBy = (): unknown => {
const calls = (useGetExplorerQueryRange as jest.Mock).mock.calls.filter(
(call) => call[1] === PANEL_TYPES.LIST && call[0],
);
return calls[calls.length - 1]?.[0]?.builder?.queryData?.[0]?.orderBy;
};
render(<Harness />);
await waitFor(() => {
expect(latestListOrderBy()).toStrictEqual([
{ columnName: 'timestamp', order: 'desc' },
{ columnName: 'id', order: 'desc' },
]);
});
fireEvent.click(screen.getByTestId('change-order'));
await waitFor(() => {
expect(latestListOrderBy()).toStrictEqual([
{ columnName: 'severity_text', order: 'desc' },
{ columnName: 'id', order: 'desc' },
]);
});
});
});
});

View File

@@ -0,0 +1,113 @@
import { expect, type Locator, type Page } from '@playwright/test';
import {
dismissQuickFiltersAnnouncement,
LOGS_EXPLORER_PATH,
} from './quick-filters';
export { LOGS_EXPLORER_PATH };
export const TRACES_EXPLORER_PATH = '/traces-explorer';
export const LLM_EXPLORER_PATH = '/ai-observability/explorer';
const RELATIVE_TIME = '6h';
const ORDER_BY_TEST_ID = 'list-view-order-by';
export type ExplorerView = 'list' | 'trace' | 'timeseries' | 'table';
const VIEW_TAB_CLASS: Record<ExplorerView, string> = {
list: 'list-view-tab',
trace: 'trace-view-tab',
timeseries: 'timeseries-view-tab',
table: 'table-view-tab',
};
export async function gotoExplorer(page: Page, path: string): Promise<void> {
await dismissQuickFiltersAnnouncement(page);
await page.goto(`${path}?relativeTime=${RELATIVE_TIME}`);
await expect(orderByControl(page)).toBeVisible();
}
export function orderByControl(page: Page): Locator {
return page.getByTestId(ORDER_BY_TEST_ID);
}
export async function expectOrderBy(page: Page, value: string): Promise<void> {
await expect(
orderByControl(page).locator('.ant-select-selection-item'),
).toHaveText(value);
}
export async function setOrderBy(page: Page, value: string): Promise<void> {
await orderByControl(page).click();
await page
.locator('.ant-select-item-option')
.filter({ hasText: value })
.first()
.click();
await expectOrderBy(page, value);
}
export function viewTab(page: Page, view: ExplorerView): Locator {
return page.locator(`.${VIEW_TAB_CLASS[view]}`);
}
export async function switchView(
page: Page,
view: ExplorerView,
): Promise<void> {
await viewTab(page, view).click();
await expect(viewTab(page, view)).toHaveClass(/active-tab/);
}
export function orderByFromUrl(
page: Page,
): { columnName: string; order: string }[] {
const raw = new URL(page.url()).searchParams.get('compositeQuery');
if (!raw) {
return [];
}
const query = JSON.parse(decodeURIComponent(raw));
return query?.builder?.queryData?.[0]?.orderBy ?? [];
}
export function withOrderByInUrl(
path: string,
orderBy: { columnName: string; order: string },
dataSource: 'logs' | 'traces',
): string {
const compositeQuery = {
queryType: 'builder',
builder: {
queryData: [
{
dataSource,
queryName: 'A',
aggregateOperator: 'noop',
expression: 'A',
disabled: false,
filters: { items: [], op: 'AND' },
groupBy: [],
orderBy: [orderBy],
limit: null,
having: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [],
},
promql: [],
clickhouse_sql: [],
id: 'e2e-order-by',
};
const params = new URLSearchParams({
relativeTime: RELATIVE_TIME,
panelTypes: JSON.stringify('list'),
compositeQuery: encodeURIComponent(JSON.stringify(compositeQuery)),
});
return `${path}?${params.toString()}`;
}

View File

@@ -0,0 +1,37 @@
import { expect, test } from '../../fixtures/auth';
import {
expectOrderBy,
gotoExplorer,
LLM_EXPLORER_PATH,
orderByFromUrl,
setOrderBy,
switchView,
} from '../../helpers/explorer-order-by';
test.describe('LLM Observability Explorer — order by', () => {
test('TC-01 does not carry a trace field into the list view', async ({
authedPage: page,
}) => {
await gotoExplorer(page, LLM_EXPLORER_PATH);
await switchView(page, 'trace');
await expectOrderBy(page, 'last_activity_time (desc)');
await switchView(page, 'list');
await expectOrderBy(page, 'timestamp (desc)');
expect(orderByFromUrl(page)).toStrictEqual([]);
});
test('TC-02 does not carry a span field into the trace view', async ({
authedPage: page,
}) => {
await gotoExplorer(page, LLM_EXPLORER_PATH);
await setOrderBy(page, 'timestamp (asc)');
await switchView(page, 'trace');
await expectOrderBy(page, 'last_activity_time (desc)');
expect(orderByFromUrl(page)).toStrictEqual([]);
});
});

View File

@@ -0,0 +1,38 @@
import { expect, test } from '../../fixtures/auth';
import {
expectOrderBy,
gotoExplorer,
LOGS_EXPLORER_PATH,
orderByFromUrl,
withOrderByInUrl,
} from '../../helpers/explorer-order-by';
import { dismissQuickFiltersAnnouncement } from '../../helpers/quick-filters';
test.describe('Logs Explorer — order by', () => {
test('TC-01 applies the order a deep link carries', async ({
authedPage: page,
}) => {
await dismissQuickFiltersAnnouncement(page);
await page.goto(
withOrderByInUrl(
LOGS_EXPLORER_PATH,
{ columnName: 'timestamp', order: 'asc' },
'logs',
),
);
await expectOrderBy(page, 'timestamp (asc)');
expect(orderByFromUrl(page)).toStrictEqual([
{ columnName: 'timestamp', order: 'asc' },
]);
});
test('TC-02 writes nothing to the URL until the order is changed', async ({
authedPage: page,
}) => {
await gotoExplorer(page, LOGS_EXPLORER_PATH);
await expectOrderBy(page, 'timestamp (desc)');
expect(orderByFromUrl(page)).toStrictEqual([]);
});
});

View File

@@ -0,0 +1,57 @@
import { expect, test } from '../../fixtures/auth';
import {
expectOrderBy,
gotoExplorer,
orderByFromUrl,
setOrderBy,
switchView,
TRACES_EXPLORER_PATH,
} from '../../helpers/explorer-order-by';
test.describe('Traces Explorer — order by', () => {
test('TC-01 keeps the order after opening a trace and going back', async ({
authedPage: page,
}) => {
await gotoExplorer(page, TRACES_EXPLORER_PATH);
await setOrderBy(page, 'duration_nano (desc)');
await page.locator('.ant-table-row').first().click();
await expect(page).toHaveURL(/\/trace\//);
await page.goBack();
await expectOrderBy(page, 'duration_nano (desc)');
expect(orderByFromUrl(page)).toStrictEqual([
{ columnName: 'duration_nano', order: 'desc' },
]);
});
test('TC-02 writes the order to the URL and keeps it across a reload', async ({
authedPage: page,
}) => {
await gotoExplorer(page, TRACES_EXPLORER_PATH);
expect(orderByFromUrl(page)).toStrictEqual([]);
await setOrderBy(page, 'duration_nano (asc)');
expect(orderByFromUrl(page)).toStrictEqual([
{ columnName: 'duration_nano', order: 'asc' },
]);
await page.reload();
await expectOrderBy(page, 'duration_nano (asc)');
});
test('TC-03 clears the order when the Time Series tab hands over to List', async ({
authedPage: page,
}) => {
await gotoExplorer(page, TRACES_EXPLORER_PATH);
await setOrderBy(page, 'duration_nano (desc)');
await switchView(page, 'timeseries');
await switchView(page, 'list');
await expectOrderBy(page, 'timestamp (desc)');
expect(orderByFromUrl(page)).toStrictEqual([]);
});
});