Skip to content
Merged
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
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) |
| `EuiPaginationObject` | [src/components/pagination/README.md](src/components/pagination/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) |
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 { EuiPaginationObject } from './playwright/components/pagination/object';
export { EuiBasicTableObject } from './playwright/components/basic_table/object';
export { EuiColorPickerObject } from './playwright/components/color_picker/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 @@ -21,6 +21,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 @@ -47,6 +48,7 @@ export const selectors = {
range: EuiRangeSelectors,
selectable: EuiSelectableSelectors,
superSelect: EuiSuperSelectSelectors,
table: EuiTableSelectors,
globalToastList: EuiGlobalToastListSelectors,
toolTip: EuiToolTipSelectors,
treeView: EuiTreeViewSelectors,
Expand Down