Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
import { SearchPage } from '../../page-objects/SearchPage';
import { expect, test } from '../../utils/base-test';
import { DEFAULT_TRACES_SOURCE_NAME } from '../../utils/constants';

/**
* Regression coverage for HDX-4147 (PR #2189): the dashed drag-select
* rectangle on the Event Deltas heatmap collapses to a 2x2 px residue
* after mouseup unless u.select is mirrored from the URL state. These
* tests assert the visible rectangle survives the round-trip through
* the URL (drag, reload, click-to-clear).
*
* nuqs setFields and uPlot's ready hook both flush asynchronously, so
* URL and bounding-box reads use expect.poll rather than one-shot
* reads.
*/
test.describe('Event Deltas heatmap drag-select', { tag: '@search' }, () => {
let searchPage: SearchPage;

test.beforeEach(async ({ page }) => {
searchPage = new SearchPage(page);
await searchPage.goto();
await searchPage.selectSource(DEFAULT_TRACES_SOURCE_NAME);
await searchPage.openEventDeltasMode();
});

test('drag-select draws a persistent rectangle and writes URL state', async () => {
await searchPage.dragHeatmapSelection();

// URL gains the four selection params nuqs writes via setFields.
for (const param of ['xMin=', 'xMax=', 'yMin=', 'yMax=']) {
await expect
.poll(() => searchPage.page.url(), {
message: `URL should carry ${param.slice(0, -1)}`,
})
.toContain(param);
}

// The dashed rectangle stays visible after mouseup. The bug shipped
// a 2x2 px collapse pinned to (0, 0); a healthy selection has both
// dimensions well above that residue.
await expect

Check failure on line 41 in packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts

View workflow job for this annotation

GitHub Actions/ e2e-tests / E2E Tests - Shard 3

[chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:26:7 › Event Deltas heatmap drag-select › drag-select draws a persistent rectangle and writes URL state @search

1) [chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:26:7 › Event Deltas heatmap drag-select › drag-select draws a persistent rectangle and writes URL state @search Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: selection width should reflect the drag expect(received).toBeGreaterThan(expected) Expected: > 20 Received: 2 Call Log: - Timeout 5000ms exceeded while waiting on the predicate 39 | // a 2x2 px collapse pinned to (0, 0); a healthy selection has both 40 | // dimensions well above that residue. > 41 | await expect | ^ 42 | .poll( 43 | async () => 44 | (await searchPage.getHeatmapSelectionRect().boundingBox())?.width ?? at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts:41:5

Check failure on line 41 in packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts

View workflow job for this annotation

GitHub Actions/ e2e-tests / E2E Tests - Shard 3

[chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:26:7 › Event Deltas heatmap drag-select › drag-select draws a persistent rectangle and writes URL state @search

1) [chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:26:7 › Event Deltas heatmap drag-select › drag-select draws a persistent rectangle and writes URL state @search Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: selection width should reflect the drag expect(received).toBeGreaterThan(expected) Expected: > 20 Received: 2 Call Log: - Timeout 5000ms exceeded while waiting on the predicate 39 | // a 2x2 px collapse pinned to (0, 0); a healthy selection has both 40 | // dimensions well above that residue. > 41 | await expect | ^ 42 | .poll( 43 | async () => 44 | (await searchPage.getHeatmapSelectionRect().boundingBox())?.width ?? at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts:41:5

Check failure on line 41 in packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts

View workflow job for this annotation

GitHub Actions/ e2e-tests / E2E Tests - Shard 3

[chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:26:7 › Event Deltas heatmap drag-select › drag-select draws a persistent rectangle and writes URL state @search

1) [chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:26:7 › Event Deltas heatmap drag-select › drag-select draws a persistent rectangle and writes URL state @search Error: selection width should reflect the drag expect(received).toBeGreaterThan(expected) Expected: > 20 Received: 2 Call Log: - Timeout 5000ms exceeded while waiting on the predicate 39 | // a 2x2 px collapse pinned to (0, 0); a healthy selection has both 40 | // dimensions well above that residue. > 41 | await expect | ^ 42 | .poll( 43 | async () => 44 | (await searchPage.getHeatmapSelectionRect().boundingBox())?.width ?? at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts:41:5
.poll(
async () =>
(await searchPage.getHeatmapSelectionRect().boundingBox())?.width ??
0,
{ message: 'selection width should reflect the drag' },
)
.toBeGreaterThan(20);
const rect = await searchPage.getHeatmapSelectionRect().boundingBox();
expect(rect, 'selection element should be in the DOM').not.toBeNull();
expect(
rect!.height,
'selection height should reflect the drag',
).toBeGreaterThan(20);
});

test('reloading the page restores the rectangle from URL state', async () => {
await searchPage.dragHeatmapSelection();

// Wait for the rectangle to settle, then capture it for cross-check.
await expect

Check failure on line 61 in packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts

View workflow job for this annotation

GitHub Actions/ e2e-tests / E2E Tests - Shard 3

[chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:57:7 › Event Deltas heatmap drag-select › reloading the page restores the rectangle from URL state @search

2) [chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:57:7 › Event Deltas heatmap drag-select › reloading the page restores the rectangle from URL state @search Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toBeGreaterThan(expected) Expected: > 20 Received: 2 Call Log: - Timeout 5000ms exceeded while waiting on the predicate 59 | 60 | // Wait for the rectangle to settle, then capture it for cross-check. > 61 | await expect | ^ 62 | .poll( 63 | async () => 64 | (await searchPage.getHeatmapSelectionRect().boundingBox())?.width ?? at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts:61:5

Check failure on line 61 in packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts

View workflow job for this annotation

GitHub Actions/ e2e-tests / E2E Tests - Shard 3

[chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:57:7 › Event Deltas heatmap drag-select › reloading the page restores the rectangle from URL state @search

2) [chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:57:7 › Event Deltas heatmap drag-select › reloading the page restores the rectangle from URL state @search Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toBeGreaterThan(expected) Expected: > 20 Received: 2 Call Log: - Timeout 5000ms exceeded while waiting on the predicate 59 | 60 | // Wait for the rectangle to settle, then capture it for cross-check. > 61 | await expect | ^ 62 | .poll( 63 | async () => 64 | (await searchPage.getHeatmapSelectionRect().boundingBox())?.width ?? at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts:61:5

Check failure on line 61 in packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts

View workflow job for this annotation

GitHub Actions/ e2e-tests / E2E Tests - Shard 3

[chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:57:7 › Event Deltas heatmap drag-select › reloading the page restores the rectangle from URL state @search

2) [chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:57:7 › Event Deltas heatmap drag-select › reloading the page restores the rectangle from URL state @search Error: expect(received).toBeGreaterThan(expected) Expected: > 20 Received: 2 Call Log: - Timeout 5000ms exceeded while waiting on the predicate 59 | 60 | // Wait for the rectangle to settle, then capture it for cross-check. > 61 | await expect | ^ 62 | .poll( 63 | async () => 64 | (await searchPage.getHeatmapSelectionRect().boundingBox())?.width ?? at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts:61:5
.poll(
async () =>
(await searchPage.getHeatmapSelectionRect().boundingBox())?.width ??
0,
)
.toBeGreaterThan(20);
const beforeReload = await searchPage
.getHeatmapSelectionRect()
.boundingBox();
expect(beforeReload).not.toBeNull();

// Round-trip through the URL: a fresh page load goes through the
// uPlot ready hook path, which is what the on-create path got wrong
// before the fix (scales aren't populated for mode-2 facet data
// until the first draw).
await searchPage.page.reload();
await searchPage.getHeatmap().waitFor({ state: 'visible' });

// The ready hook fires after the first draw, which is async relative
// to the canvas being visible; poll until the rectangle re-renders.
await expect
.poll(
async () =>
(await searchPage.getHeatmapSelectionRect().boundingBox())?.width ??
0,
{ message: 'rectangle width should be restored from URL on reload' },
)
.toBeGreaterThan(20);
const afterReload = await searchPage
.getHeatmapSelectionRect()
.boundingBox();
expect(
afterReload,
'selection element should be in the DOM after reload',
).not.toBeNull();
expect(
afterReload!.height,
'rectangle height should be restored from URL on reload',
).toBeGreaterThan(20);

// Coordinates round-trip with sub-pixel accuracy (small rounding
// from log-space conversion is acceptable).
expect(afterReload!.width).toBeCloseTo(beforeReload!.width, 0);
expect(afterReload!.height).toBeCloseTo(beforeReload!.height, 0);
});

test('clicking off the rectangle clears both URL state and the rectangle', async () => {
await searchPage.dragHeatmapSelection();

// Sanity: the drag set the URL state. nuqs flushes async, so poll.
await expect.poll(() => searchPage.page.url()).toContain('xMin=');

// Click somewhere on the chart canvas that isn't inside the
// selection. Top edge of the canvas is far enough from the mid-band
// selection drawn by dragHeatmapSelection() defaults.
const heatmapBox = await searchPage.getHeatmap().boundingBox();
if (!heatmapBox) {
throw new Error('Heatmap not found');

Check failure on line 119 in packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts

View workflow job for this annotation

GitHub Actions/ e2e-tests / E2E Tests - Shard 3

[chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:108:7 › Event Deltas heatmap drag-select › clicking off the rectangle clears both URL state and the rectangle @search

3) [chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:108:7 › Event Deltas heatmap drag-select › clicking off the rectangle clears both URL state and the rectangle @search Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: Heatmap not found 117 | const heatmapBox = await searchPage.getHeatmap().boundingBox(); 118 | if (!heatmapBox) { > 119 | throw new Error('Heatmap not found'); | ^ 120 | } 121 | await searchPage.page.mouse.click( 122 | heatmapBox.x + heatmapBox.width * 0.9, at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts:119:13

Check failure on line 119 in packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts

View workflow job for this annotation

GitHub Actions/ e2e-tests / E2E Tests - Shard 3

[chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:108:7 › Event Deltas heatmap drag-select › clicking off the rectangle clears both URL state and the rectangle @search

3) [chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:108:7 › Event Deltas heatmap drag-select › clicking off the rectangle clears both URL state and the rectangle @search Error: Heatmap not found 117 | const heatmapBox = await searchPage.getHeatmap().boundingBox(); 118 | if (!heatmapBox) { > 119 | throw new Error('Heatmap not found'); | ^ 120 | } 121 | await searchPage.page.mouse.click( 122 | heatmapBox.x + heatmapBox.width * 0.9, at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts:119:13
}
await searchPage.page.mouse.click(
heatmapBox.x + heatmapBox.width * 0.9,
heatmapBox.y + heatmapBox.height * 0.05,
);

// URL params drop out (nuqs serializes a null value to no key).
await expect

Check failure on line 127 in packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts

View workflow job for this annotation

GitHub Actions/ e2e-tests / E2E Tests - Shard 3

[chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:108:7 › Event Deltas heatmap drag-select › clicking off the rectangle clears both URL state and the rectangle @search

3) [chromium] › tests/e2e/features/search/event-deltas-heatmap.spec.ts:108:7 › Event Deltas heatmap drag-select › clicking off the rectangle clears both URL state and the rectangle @search Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: xMin should be cleared from URL expect(received).not.toContain(expected) // indexOf Expected substring: not "xMin=" Received string: "http://localhost:21300/search?source=69fb7999f343b186ddf5e2ab&where=&select=&whereLanguage=lucene&filters=%5B%5D&orderBy=(toDateTime(Timestamp)%2C%20Timestamp)%20DESC&isLive=false&mode=delta&xMin=1778087666.8716576&xMax=1778088106.0427809&yMin=0.14461744023400372&yMax=48.62600236136342" Call Log: - Timeout 5000ms exceeded while waiting on the predicate 125 | 126 | // URL params drop out (nuqs serializes a null value to no key). > 127 | await expect | ^ 128 | .poll(() => searchPage.page.url(), { 129 | message: 'xMin should be cleared from URL', 130 | }) at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/event-deltas-heatmap.spec.ts:127:5
.poll(() => searchPage.page.url(), {
message: 'xMin should be cleared from URL',
})
.not.toContain('xMin=');

// uPlot resets u.select to width=0/height=0; the element collapses
// to its 1 px border on each side (2x2 with the border included).
// The collapse goes through React state, so poll.
await expect
.poll(
async () =>
(await searchPage.getHeatmapSelectionRect().boundingBox())?.width ??
0,
{ message: 'rectangle width should collapse on clear' },
)
.toBeLessThan(5);
const afterClear = await searchPage.getHeatmapSelectionRect().boundingBox();
expect(
afterClear,
'selection element should still be in the DOM',
).not.toBeNull();
expect(
afterClear!.height,
'rectangle height should collapse on clear',
).toBeLessThan(5);
});
});
62 changes: 62 additions & 0 deletions packages/app/tests/e2e/page-objects/SearchPage.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -295,6 +295,68 @@ export class SearchPage {
await this.page.mouse.up();
}

/**
* Switch the search page into Event Deltas mode and wait for the
* heatmap chart to render. Event Deltas is the analysis-mode tab
* that puts the latency-vs-time heatmap on screen.
*/
async openEventDeltasMode() {
const tab = this.page.getByRole('tab', { name: 'Event Deltas' });
await tab.click();
await this.getHeatmap().waitFor({
state: 'visible',
timeout: this.defaultTimeout * 2,
});
}

/**
* Get the uPlot heatmap canvas wrapper. The Event Deltas page renders
* a single uPlot chart; first match disambiguates from any other charts
* that might appear on the page.
*/
getHeatmap() {
return this.page.locator('.uplot').first();
}

/**
* Get the dashed selection rectangle inside the heatmap. uPlot writes
* `left`/`top`/`width`/`height` into its inline style; tests assert
* against the bounding box.
*/
getHeatmapSelectionRect() {
return this.getHeatmap().locator('.u-select');
}

/**
* Drag a region on the heatmap canvas. Coordinates are percentages of
* the canvas (0-1). The drag spans both axes so the resulting selection
* has non-zero width AND height (zero on either axis is treated as a
* single-click by uPlot and produces no selection).
*/
async dragHeatmapSelection(
startXPercent: number = 0.25,
startYPercent: number = 0.3,
endXPercent: number = 0.7,
endYPercent: number = 0.7,
) {
const heatmap = this.getHeatmap();
const box = await heatmap.boundingBox();

if (!box) {
throw new Error('Heatmap not found');
}

const startX = box.x + box.width * startXPercent;
const startY = box.y + box.height * startYPercent;
const endX = box.x + box.width * endXPercent;
const endY = box.y + box.height * endYPercent;

await this.page.mouse.move(startX, startY);
await this.page.mouse.down();
await this.page.mouse.move(endX, endY, { steps: 10 });
await this.page.mouse.up();
}

// Getters for assertions in spec files

get form() {
Expand Down
Loading