void;
- virtualIndex?: number;
}) => {
const openSidebar = useSidebarOpener();
return (
+ // Deliberately not measured by the virtualizer: the enclosing `tbody` is
+ // the element registered for this row's virtual index.
|
diff --git a/packages/app/tests/e2e/components/TableComponent.ts b/packages/app/tests/e2e/components/TableComponent.ts
index c3e9e4cd13..f854e4516e 100644
--- a/packages/app/tests/e2e/components/TableComponent.ts
+++ b/packages/app/tests/e2e/components/TableComponent.ts
@@ -78,6 +78,53 @@ export class TableComponent {
await this.firstExpandedRow.waitFor({ state: 'visible', timeout: 10_000 });
}
+ /**
+ * Collapse a row expanded by {@link expandRow}, via the same chevron.
+ */
+ async collapseRow(index: number) {
+ await this.getRow(index)
+ .getByRole('button', { name: 'Collapse log details' })
+ .click();
+ }
+
+ /**
+ * Scroll the virtualized container to an absolute offset.
+ */
+ async scrollTo(top: number) {
+ await this.tableContainer.evaluate(
+ (el, offset) => el.scrollTo({ top: offset }),
+ top,
+ );
+ }
+
+ /**
+ * Layout readings for the virtualized scroll container.
+ *
+ * `visibleGapPx` is the empty space between the bottom of the last rendered
+ * row and the bottom of the viewport. While rows remain below, a healthy
+ * table overfills the viewport and this is negative; it turns positive when
+ * the virtualizer holds stale row heights and stops rendering enough rows to
+ * cover the screen.
+ */
+ async getVirtualizationMetrics() {
+ return this.tableContainer.evaluate(el => {
+ const rows = el.querySelectorAll('[data-testid^="table-row-"]');
+ const last = rows[rows.length - 1];
+ return {
+ renderedRows: rows.length,
+ scrollHeight: el.scrollHeight,
+ clientHeight: el.clientHeight,
+ visibleGapPx: Math.round(
+ el.getBoundingClientRect().bottom -
+ (last ? last.getBoundingClientRect().bottom : 0),
+ ),
+ remainingBelowPx: Math.round(
+ el.scrollHeight - el.scrollTop - el.clientHeight,
+ ),
+ };
+ });
+ }
+
/**
* All inline expanded rows currently rendered. Each carries
* `data-testid="expanded-row-"`, where rowWhere is a SQL fragment,
diff --git a/packages/app/tests/e2e/features/search/virtualized-row-heights.spec.ts b/packages/app/tests/e2e/features/search/virtualized-row-heights.spec.ts
new file mode 100644
index 0000000000..4008e2240d
--- /dev/null
+++ b/packages/app/tests/e2e/features/search/virtualized-row-heights.spec.ts
@@ -0,0 +1,62 @@
+import { SearchPage } from '../../page-objects/SearchPage';
+import { expect, test } from '../../utils/base-test';
+
+test.use({ viewport: { width: 1280, height: 900 } });
+
+test.describe('Virtualized row heights', { tag: '@search' }, () => {
+ let searchPage: SearchPage;
+
+ test.beforeEach(async ({ page }) => {
+ searchPage = new SearchPage(page);
+ await searchPage.goto();
+ await searchPage.submitEmptySearch();
+ await expect(searchPage.table.firstRow).toBeVisible();
+ });
+
+ // An expanded row used to take over its row's slot in the virtualizer's
+ // measurement cache and leave its height behind when it collapsed. Every
+ // expand/collapse shrank the render window until the viewport went blank.
+ test('collapsing an expanded row restores the table height', async () => {
+ const before = await searchPage.table.getVirtualizationMetrics();
+ expect(before.renderedRows).toBeGreaterThan(20);
+
+ for (const index of [0, 1, 2]) {
+ await searchPage.table.expandRow(index);
+ await expect(searchPage.table.firstExpandedRow).toBeVisible();
+
+ await searchPage.table.collapseRow(index);
+ await expect(searchPage.table.expandedRows).toHaveCount(0);
+
+ await expect
+ .poll(async () => {
+ const m = await searchPage.table.getVirtualizationMetrics();
+ return m.renderedRows;
+ })
+ .toBe(before.renderedRows);
+ }
+
+ const after = await searchPage.table.getVirtualizationMetrics();
+ expect(after.scrollHeight).toBeLessThanOrEqual(before.scrollHeight + 20);
+ });
+
+ test('scrolling after expanding keeps the viewport filled', async () => {
+ for (const index of [0, 1, 2]) {
+ await searchPage.table.expandRow(index);
+ await expect(searchPage.table.firstExpandedRow).toBeVisible();
+ await searchPage.table.collapseRow(index);
+ await expect(searchPage.table.expandedRows).toHaveCount(0);
+ }
+
+ for (const top of [300, 700, 1200]) {
+ await searchPage.table.scrollTo(top);
+
+ await expect
+ .poll(async () => {
+ const m = await searchPage.table.getVirtualizationMetrics();
+ // Rows still below, so the rendered window must cover the viewport.
+ return m.remainingBelowPx > 0 ? m.visibleGapPx : 0;
+ })
+ .toBeLessThanOrEqual(0);
+ }
+ });
+});
|