Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions packages/test-helpers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ their own version at runtime.
| `EuiAccordionObject` | [src/components/accordion/README.md](src/components/accordion/README.md) |
| `EuiContextMenuObject` | [src/components/context_menu/README.md](src/components/context_menu/README.md) |
| `EuiModalObject` | [src/components/modal/README.md](src/components/modal/README.md) |
| `EuiTableObject` | [src/components/table/README.md](src/components/table/README.md) |
| `EuiBasicTableObject` | [src/components/basic_table/README.md](src/components/basic_table/README.md) |
| `EuiColorPickerObject` | [src/components/color_picker/README.md](src/components/color_picker/README.md) |
| `EuiTreeViewObject` | [src/components/tree_view/README.md](src/components/tree_view/README.md) |
Expand Down
1 change: 1 addition & 0 deletions packages/test-helpers/changelogs/upcoming/10213.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added `EuiTableObject` for `EuiTable`, with `rows`, `headerCells`, `column(index)` and `cell(row, column)`. `EuiBasicTableObject` now extends it
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Set `data-test-subj` on the `<EuiBasicTable>`/`<EuiInMemoryTable>` (EUI spreads
|---|---|
| `rows` | `Locator` for the table's data rows, excluding EUI's own "no items found"/error-message row. |
| `cells(field)` | `Locator` for a field-data column's cells across all rows, resolved from EUI's own header `data-test-subj`. |
| `headerCells`, `column(index)`, `cell(row, column)` | Inherited from [EuiTableObject](../table/README.md), positional access when no `field` is available. |

`rows` excludes EUI's empty/error message row by filtering out any row whose cell carries a `colspan` — real data rows don't set one, EUI's own placeholder row always does (it spans every column).

Expand Down
31 changes: 31 additions & 0 deletions packages/test-helpers/src/components/table/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# EuiTableObject

Playwright Component Object for [EuiTable](https://eui.elastic.co/docs/components/tabular-content/tables/), the low level table built from `EuiTableHeader`, `EuiTableRow` and `EuiTableRowCell`. `EuiBasicTableObject` extends it for `EuiBasicTable` and `EuiInMemoryTable`, use that one when the table has field based columns.

## Usage

```ts
import { EuiTableObject } from '@elastic/eui-test-helpers';

const table = new EuiTableObject(page, 'myTable');
await expect(table.rows).toHaveCount(3);
await expect(table.column(1)).toHaveText(['Running', 'Finished', 'Archived']);
await expect(table.cell(0, 1)).toHaveText('Running');
```

Set `data-test-subj` on `<EuiTable>`. EUI spreads it onto the `<table>`, which the component-type guard verifies.

## API

| Member | Description |
|---|---|
| `rows` | `Locator` for the body rows. |
| `headerCells` | `Locator` for the header cells, in column order, including a selection checkbox cell. |
| `column(index)` | `Locator` for one column's cells across all body rows, by 0-based position. |
| `cell(row, column)` | `Locator` for one body cell, by 0-based row and column position. |

Columns are addressed by position because a plain `EuiTable` has no field names. `column` and `cell` match direct children of a row only, both `<td>` and `<th>` since EUI renders a row header cell as `<th scope="row">`.

## Deliberately excluded

Sorting, selection and pagination are not covered. Sorting and selection are consumer wiring on a plain table, and pagination is a separate component.
26 changes: 26 additions & 0 deletions packages/test-helpers/src/components/table/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

/**
* Stable selectors for
* {@link https://eui.elastic.co/docs/components/tabular-content/tables/|EuiTable}.
* `*_SELECTOR` values are CSS.
*/
export const EuiTableSelectors = {
/** The `<table>` element carrying the consumer's `data-test-subj`. */
ROOT_SELECTOR: '.euiTable',

/** A body row. Also matches EUI's own empty/error message row, callers must exclude those. */
ROW_SELECTOR: '.euiTableRow',

/** A body cell. */
ROW_CELL_SELECTOR: '.euiTableRowCell',

/** A header cell, including the selection checkbox cell which has its own class. */
HEADER_CELL_SELECTOR: 'thead > tr > :is(th, td)',
};
1 change: 1 addition & 0 deletions packages/test-helpers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export { EuiFlyoutObject } from './playwright/components/flyout/object';
export { EuiAccordionObject } from './playwright/components/accordion/object';
export { EuiContextMenuObject } from './playwright/components/context_menu/object';
export { EuiModalObject } from './playwright/components/modal/object';
export { EuiTableObject } from './playwright/components/table/object';
export { EuiBasicTableObject } from './playwright/components/basic_table/object';
export { EuiColorPickerObject } from './playwright/components/color_picker/object';
export { EuiTreeViewObject } from './playwright/components/tree_view/object';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ test.describe('EuiBasicTableObject', () => {
});
});

test.describe('headerCells', () => {
test('includes the selection checkbox cell so indexes line up with column()', async () => {
const headerCount = await table.headerCells.count();
const firstRowCellCount = await table.rows.first().locator('> :is(td, th)').count();

expect(headerCount).toBe(firstRowCellCount);
});
});

test.describe('cells', () => {
test('reads a field-data column past the leading selection checkbox', async () => {
const cells = await table.cells('firstName');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

import type { Locator } from '@playwright/test';

import { BaseObject, type ObjectScope } from '../../base_object';
import type { ObjectScope } from '../../base_object';
import { EuiTableObject } from '../table/object';
import { EuiBasicTableSelectors } from '../../../components/basic_table/selectors';

/**
Expand All @@ -19,28 +20,20 @@ import { EuiBasicTableSelectors } from '../../../components/basic_table/selector
*
* `testSubj` must match the `data-test-subj` set by the consumer on the
* `<EuiBasicTable>`/`<EuiInMemoryTable>` (EUI spreads it onto the `.euiBasicTable`
* root). See the package README for what's deliberately excluded from v1.
* root). Extends `EuiTableObject`, so `rows`, `headerCells`, `column` and `cell`
* are available too. See the package README for what's deliberately excluded.
*/
export class EuiBasicTableObject extends BaseObject {
export class EuiBasicTableObject extends EuiTableObject {
constructor(scope: ObjectScope, testSubj: string) {
super(scope, testSubj, EuiBasicTableSelectors.ROOT_SELECTOR);
}

/**
* The table's data rows, as a `Locator` so callers keep Playwright auto-retry
* for count and content assertions (e.g. `expect(table.rows).toHaveCount(3)`).
*
* Excludes EUI's own "no items found" (and error-message) row: when the table
* is empty, EUI still renders a single `.euiTableRow` whose one cell spans
* every column (`colSpan` on `EuiTableRowCell`, the `td[colspan]` HTML
* attribute). Real data rows don't set `colspan` on their cells, so filtering
* it out is what keeps `toHaveCount(0)` correct on an empty table instead of
* reading `1`.
* The data rows. Excludes EUI's own "no items found" row, whose single cell
* spans every column with `colspan`. Basic table data rows never set it.
*/
public get rows(): Locator {
return this.root
.locator(EuiBasicTableSelectors.ROW_SELECTOR)
.filter({ hasNot: this.root.page().locator('td[colspan]') });
public override get rows(): Locator {
return super.rows.filter({ hasNot: this.root.page().locator('td[colspan]') });
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { test, expect } from '@playwright/test';

import { EuiTableObject } from './object';
import { storyUrl } from '../../../storybook';

/**
* Validates `EuiTableObject` against the live component in EUI Storybook.
* The Playground story renders 3 header cells, 20 body rows and a footer row.
* `data-test-subj` is injected via Storybook's `args` URL parameter onto
* the `<table>`.
*/

const TEST_SUBJ = 'testTable';

const PLAYGROUND_URL = storyUrl(
'tabular-content-euitable-euitable--playground',
`data-test-subj:${TEST_SUBJ}`
);

test.describe('EuiTableObject', () => {
let table: EuiTableObject;

test.beforeEach(async ({ page }) => {
await page.goto(PLAYGROUND_URL);
table = new EuiTableObject(page, TEST_SUBJ);
await table.locator.waitFor({ state: 'visible' });
});

test.describe('rows', () => {
test('exposes the body rows only, not the header or footer row', async () => {
await expect(table.rows).toHaveCount(20);
});
});

test.describe('headerCells', () => {
test('exposes the header cells in column order', async () => {
await expect(table.headerCells).toHaveCount(3);
await expect(table.headerCells.first()).toHaveText('Column 1');
});
});

test.describe('column', () => {
test('reads one column across every row', async () => {
const column = table.column(1);

await expect(column).toHaveCount(20);
await expect(column.first()).toHaveText('Row 1, Cell 2');
await expect(column.last()).toHaveText('Row 20, Cell 2');
});
});

test.describe('cell', () => {
test('reads one cell by row and column position', async () => {
await expect(table.cell(2, 0)).toHaveText('Row 3, Cell 1');
});
});
});
57 changes: 57 additions & 0 deletions packages/test-helpers/src/playwright/components/table/object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { Locator } from '@playwright/test';

import { BaseObject, type ObjectScope } from '../../base_object';
import { EuiTableSelectors } from '../../../components/table/selectors';

/**
* Playwright Component Object for {@link
* https://eui.elastic.co/docs/components/tabular-content/tables/ EuiTable},
* the low level table built from `EuiTableHeader`, `EuiTableRow` and
* `EuiTableRowCell`. `EuiBasicTableObject` extends this for the
* `EuiBasicTable` and `EuiInMemoryTable` wrappers.
*
* `testSubj` must be set on `EuiTable` itself, EUI spreads it onto the
* `<table>`. Columns are addressed by position since a plain table has no
* field names.
*/
export class EuiTableObject extends BaseObject {
constructor(
scope: ObjectScope,
testSubj: string,
componentSelector = EuiTableSelectors.ROOT_SELECTOR
) {
super(scope, testSubj, componentSelector);
}

/** The body rows. */
public get rows(): Locator {
return this.root.locator(EuiTableSelectors.ROW_SELECTOR);
Comment thread
steliosmavro marked this conversation as resolved.
}

/** The header cells, in column order. */
public get headerCells(): Locator {
return this.root.locator(EuiTableSelectors.HEADER_CELL_SELECTOR);
}

/**
* The cells of one column across all body rows, by 0-based position.
* Direct children only, so a table nested in a cell is not matched. Matches
* both `<td>` and `<th>`, EUI renders a row header cell as `<th scope="row">`.
*/
public column(index: number): Locator {
return this.rows.locator(`> :is(td, th):nth-child(${index + 1})`);
}

/** One body cell by 0-based row and column position. */
public cell(row: number, column: number): Locator {
return this.rows.nth(row).locator(`> :is(td, th):nth-child(${column + 1})`);
}
}
2 changes: 2 additions & 0 deletions packages/test-helpers/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { EuiPopoverSelectors } from './components/popover/selectors';
import { EuiRangeSelectors } from './components/form/range/selectors';
import { EuiSelectableSelectors } from './components/selectable/selectors';
import { EuiSuperSelectSelectors } from './components/form/super_select/selectors';
import { EuiTableSelectors } from './components/table/selectors';
import { EuiGlobalToastListSelectors } from './components/toast/selectors';
import { EuiToolTipSelectors } from './components/tool_tip/selectors';
import { EuiTreeViewSelectors } from './components/tree_view/selectors';
Expand All @@ -45,6 +46,7 @@ export const selectors = {
range: EuiRangeSelectors,
selectable: EuiSelectableSelectors,
superSelect: EuiSuperSelectSelectors,
table: EuiTableSelectors,
globalToastList: EuiGlobalToastListSelectors,
toolTip: EuiToolTipSelectors,
treeView: EuiTreeViewSelectors,
Expand Down