@jielga/tmdatagrid 2.0.0-beta.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -212
- package/dist/index.d.ts +1281 -768
- package/dist/index.js +4607 -3250
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/adding-rows.md +132 -0
- package/docs/anatomy.md +119 -0
- package/docs/card-view.md +108 -0
- package/docs/cell-selection.md +194 -0
- package/docs/column-layout.md +182 -0
- package/docs/column-menu.md +66 -0
- package/docs/columns.md +268 -0
- package/docs/components.md +311 -0
- package/docs/draft-store.md +242 -0
- package/docs/editing.md +303 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +319 -0
- package/docs/filtering.md +362 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/migrating-to-2.md +163 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +114 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +179 -0
- package/docs/quick-search.md +84 -0
- package/docs/row-details.md +115 -0
- package/docs/row-interaction.md +149 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +136 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +112 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +744 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +22 -46
- package/skills/appearance/SKILL.md +72 -19
- package/skills/cell-selection/SKILL.md +46 -47
- package/skills/columns/SKILL.md +90 -34
- package/skills/data/SKILL.md +86 -16
- package/skills/editing/SKILL.md +67 -40
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +22 -19
- package/skills/editing/references/editors-and-validation.md +24 -17
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +17 -15
- package/skills/grouping/SKILL.md +31 -16
- package/skills/options/SKILL.md +7 -7
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +150 -32
- package/skills/testing-components/SKILL.md +230 -0
- package/skills/testing-editing/SKILL.md +240 -0
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +5 -51
- package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +5 -69
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +357 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +11 -48
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
- package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/components/generatedColumns.tsx +187 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/{tmdatagrid/core → core}/editEngine.ts +1107 -460
- package/src/core/export.ts +704 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
- package/src/{tmdatagrid/core → core}/labels.ts +51 -6
- package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
- package/src/core/pageReset.ts +120 -0
- package/src/core/pagination.ts +81 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/{tmdatagrid/core → core}/summary.ts +20 -4
- package/src/{tmdatagrid/index.ts → index.ts} +69 -35
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/capabilities.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
- /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
package/skills/testing/SKILL.md
CHANGED
|
@@ -4,18 +4,21 @@ description: >
|
|
|
4
4
|
Write tests against TMDataGrid from a consuming app - Playwright or React
|
|
5
5
|
Testing Library. Covers the data-dg-part contract, data-row-id/data-column-id
|
|
6
6
|
coordinates, naming a grid with data-testid, the roles and ARIA the grid
|
|
7
|
-
publishes, the cell/gridcell role flip under cell selection,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
publishes, the cell/gridcell role flip under cell selection, the surfaces that
|
|
8
|
+
render in a portal (the menu, the export picker, Select listboxes), reaching
|
|
9
|
+
rows past virtualization with data-dg-row-count, scrollToRow and
|
|
10
|
+
data-dg-scroll-container, waiting on aria-busy, and the DataGrid page object.
|
|
11
|
+
Load when writing or fixing tests that drive a grid, or when a selector for a
|
|
12
|
+
row, cell or control does not resolve.
|
|
11
13
|
metadata:
|
|
12
14
|
type: core
|
|
13
15
|
library: '@jielga/tmdatagrid'
|
|
14
|
-
library_version: '2.0.0
|
|
16
|
+
library_version: '2.0.0'
|
|
15
17
|
sources:
|
|
16
|
-
- 'Jielga/TMDataGrid:
|
|
17
|
-
- 'Jielga/TMDataGrid:
|
|
18
|
-
- 'Jielga/TMDataGrid:
|
|
18
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/testing.md'
|
|
19
|
+
- 'Jielga/TMDataGrid:playwright/support/DataGrid.ts'
|
|
20
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGrid.tsx'
|
|
21
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGridTable.tsx'
|
|
19
22
|
---
|
|
20
23
|
|
|
21
24
|
# TMDataGrid - Testing
|
|
@@ -33,6 +36,11 @@ The grid mints no `data-testid` of its own - that attribute belongs to the app,
|
|
|
33
36
|
and Playwright's `testIdAttribute` is configurable. `@mantine/core` and
|
|
34
37
|
`@tanstack/*` ship none either.
|
|
35
38
|
|
|
39
|
+
Adding, changing and deleting rows, the draft store, and where a new row's
|
|
40
|
+
temporary id goes at commit are in the `testing-editing` skill. Running the grid
|
|
41
|
+
in a real browser through Playwright's `mount` fixture - virtualization, resize,
|
|
42
|
+
drag, pinning, clipboard - is in the `testing-components` skill.
|
|
43
|
+
|
|
36
44
|
## Naming a grid
|
|
37
45
|
|
|
38
46
|
Parts repeat across grids on a page. Name the grid, scope through it:
|
|
@@ -52,70 +60,134 @@ accessible name belongs to the element carrying the `grid` role.
|
|
|
52
60
|
| Element | Role | Key attributes |
|
|
53
61
|
| --- | --- | --- |
|
|
54
62
|
| Grid | `table`, or `grid` under cell selection | `aria-rowcount`, `aria-colcount`, `aria-busy`, `data-dg-row-count` |
|
|
63
|
+
| Scroll container | - | `data-dg-scroll-container` - the element to scroll |
|
|
55
64
|
| Body row | `row` | `data-row-id`, `aria-rowindex`, `data-selected`, `data-highlighted`, `data-grouped`, `data-pinned`, `data-deleted` |
|
|
56
65
|
| Body cell | `cell`, or `gridcell` under cell selection | `data-row-id`, `data-column-id`, `data-editing`, `data-dirty`, `data-invalid`, `data-focused` |
|
|
57
66
|
| Header cell | `columnheader` | `data-column-id`, `aria-sort` |
|
|
58
67
|
|
|
59
68
|
Body cells carry no `data-dg-part` - the coordinate pair already names them.
|
|
69
|
+
The `editor` part of an open cell carries the same pair, so a cell locator is
|
|
70
|
+
`[data-row-id="42"][data-column-id="total"]:not([data-dg-part])`; without the
|
|
71
|
+
exclusion it matches two elements while the cell is being edited.
|
|
72
|
+
|
|
73
|
+
State attributes are present only while they apply: `data-selected`,
|
|
74
|
+
`data-new`, `data-invalid` and the rest are rendered as `"true"` while the
|
|
75
|
+
state holds and omitted otherwise, so `[data-new]` and `[data-new="true"]`
|
|
76
|
+
match the same rows, and the negative is `:not([data-new])` in CSS and
|
|
77
|
+
`not.toHaveAttribute("data-new")` in a test.
|
|
60
78
|
|
|
61
79
|
## Parts
|
|
62
80
|
|
|
63
81
|
**Whole-grid** (unique, no coordinate needed): `toolbar`, `summary-count`,
|
|
64
82
|
`loading`, `search`, `search-clear`, `filter-button`, `filter-panel`,
|
|
65
|
-
`filter-
|
|
66
|
-
`
|
|
67
|
-
`
|
|
68
|
-
`
|
|
69
|
-
`
|
|
70
|
-
`
|
|
83
|
+
`filter-popup`, `filter-sidebar`, `filter-panel-close`, `filter-add`,
|
|
84
|
+
`filter-clear-all`, `filter-pills`, `header-filter-row`,
|
|
85
|
+
`menu-button`, `menu-export`, `menu-export-selected`, `export-picker`,
|
|
86
|
+
`export-picker-search`, `export-picker-hint`, `export-picker-count`,
|
|
87
|
+
`export-column-all`, `export-picker-confirm`, `export-picker-cancel`,
|
|
88
|
+
`columns-panel`, `columns-search`, `columns-toggle-all`,
|
|
89
|
+
`columns-reset`, `footer`, `page-size`, `page-range`, `page-number`,
|
|
90
|
+
`page-prev`, `page-next`, `summary-row`, `pinned-top`, `pinned-bottom`,
|
|
91
|
+
`select-all`, `details-toggle-all`, `save-all`, `discard-all`,
|
|
92
|
+
`editor-confirm`, `editor-cancel`, `editor-input`, `sort-index`, `tab-guard`.
|
|
71
93
|
|
|
72
94
|
**Keyed by `data-row-id`**: `row`, `entry-row`, `details`, `select-row`,
|
|
73
95
|
`details-toggle`, `group-toggle`, `edit-row`, `delete-row`, `save-row`,
|
|
74
96
|
`cancel-row`, `row-state`, `revert-row`, `restore-row`, `confirm-new-row`,
|
|
75
|
-
`discard-new-row`.
|
|
97
|
+
`discard-new-row`, `open-rows-note`.
|
|
76
98
|
|
|
77
99
|
**Keyed by `data-column-id`**: `header`, `header-sort`, `header-menu`,
|
|
78
|
-
`header-
|
|
100
|
+
`header-resize` (the resize handle; present only when the column can resize),
|
|
101
|
+
`header-filter` (absent under `filters.inHeader`), `header-filter-cell`,
|
|
102
|
+
`header-filter-operator`, `filter-row`, `filter-pill`, `columns-toggle`,
|
|
103
|
+
`export-column`.
|
|
79
104
|
|
|
80
105
|
**Keyed by both**: `editor`.
|
|
81
106
|
|
|
82
107
|
Inside a `filter-row` the controls are `filter-column`, `filter-operator` and
|
|
83
|
-
`filter-value` - or `filter-value-from` / `filter-value-to` for `between
|
|
108
|
+
`filter-value` - or `filter-value-from` / `filter-value-to` for `between` - and
|
|
109
|
+
`filter-remove` is its ✕. None of them carries `data-column-id`; scope through
|
|
110
|
+
the row. Inside a `filter-pill`, `filter-pill-remove` is the ✕.
|
|
111
|
+
`TMDataGridFilterPills` renders where you place it; outside the root, scope
|
|
112
|
+
through its own container rather than through the grid.
|
|
84
113
|
|
|
85
114
|
A column declaring `meta.filter.control` or `meta.edit.editor` renders your own
|
|
86
115
|
component in that slot, so `filter-value` and `editor-input` cover the built-ins
|
|
87
116
|
only. `filter-row` and `editor` still hold; scope through them.
|
|
88
117
|
|
|
118
|
+
Sorting: click `header`. `header-sort` is hidden until the header is hovered;
|
|
119
|
+
it shows the state, and `aria-sort` on the header is the assertion.
|
|
120
|
+
|
|
121
|
+
## Portals
|
|
122
|
+
|
|
123
|
+
These surfaces render at the end of `<body>`, outside the grid's root, so a
|
|
124
|
+
locator scoped to the root never finds them:
|
|
125
|
+
|
|
126
|
+
- the `TMDataGrid.Menu` dropdown - `page.getByRole("menu")`, holding
|
|
127
|
+
`columns-toggle`, `columns-toggle-all`, `columns-reset`, `menu-export`,
|
|
128
|
+
`menu-export-selected`
|
|
129
|
+
- a column's menu, opened by `header-menu` (hover the header first) or a right
|
|
130
|
+
click on the header - `page.getByRole("menu")`; its items (sort, filter,
|
|
131
|
+
group, pin, hide) carry no part, so reach one by role and label,
|
|
132
|
+
`menu.getByRole("menuitem", { name: "Group by Location" })` - a translated
|
|
133
|
+
label
|
|
134
|
+
- the `header-filter-operator` menu - `page.getByRole("menu")`
|
|
135
|
+
- the export column picker - `page.getByRole("dialog")`, holding the
|
|
136
|
+
`export-*` parts
|
|
137
|
+
- the listbox of every `Select` or `MultiSelect` the grid renders -
|
|
138
|
+
`page-size`, `filter-column`, `filter-operator`, and the `filter-value` of a
|
|
139
|
+
boolean or select-type filter - the element named by the input's
|
|
140
|
+
`aria-controls`; each option carries its value in `value`
|
|
141
|
+
|
|
142
|
+
One menu or dialog is open at a time, so the page-level locator is unambiguous.
|
|
143
|
+
`filter-popup` and `filter-sidebar` render inside the root.
|
|
144
|
+
|
|
89
145
|
## A page object
|
|
90
146
|
|
|
147
|
+
The full class is on the Testing docs page and in the repository at
|
|
148
|
+
`playwright/support/DataGrid.ts`; it is the one the grid's own suite runs
|
|
149
|
+
against the docs demos. The shape:
|
|
150
|
+
|
|
91
151
|
```ts
|
|
92
152
|
import { type Locator, type Page, expect } from "@playwright/test";
|
|
93
153
|
|
|
94
154
|
type PartKey = { rowId?: string; columnId?: string };
|
|
95
155
|
|
|
96
156
|
export class DataGrid {
|
|
157
|
+
readonly page: Page;
|
|
97
158
|
readonly root: Locator;
|
|
98
159
|
readonly grid: Locator;
|
|
99
160
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.
|
|
161
|
+
/** `root` is the element carrying `data-dg-root`. */
|
|
162
|
+
constructor(root: Locator) {
|
|
163
|
+
this.page = root.page();
|
|
164
|
+
this.root = root;
|
|
165
|
+
this.grid = root.getByRole("table").or(root.getByRole("grid"));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static byTestId(page: Page, testId: string): DataGrid {
|
|
169
|
+
return new DataGrid(page.getByTestId(testId));
|
|
103
170
|
}
|
|
104
171
|
|
|
105
172
|
part(name: string, key: PartKey = {}): Locator {
|
|
106
|
-
return this.root.locator(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
173
|
+
return this.root.locator(partSelector(name, key));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A part inside the open menu dropdown, which renders in a portal. */
|
|
177
|
+
menuPart(name: string, key: PartKey = {}): Locator {
|
|
178
|
+
return this.page.getByRole("menu").locator(partSelector(name, key));
|
|
111
179
|
}
|
|
112
180
|
|
|
113
181
|
cell({ rowId, columnId }: { rowId: string; columnId: string }): Locator {
|
|
114
182
|
return this.root.locator(
|
|
115
|
-
`[data-row-id="${rowId}"][data-column-id="${columnId}"]`,
|
|
183
|
+
`[data-row-id="${rowId}"][data-column-id="${columnId}"]:not([data-dg-part])`,
|
|
116
184
|
);
|
|
117
185
|
}
|
|
118
186
|
|
|
187
|
+
async sortBy(columnId: string): Promise<void> {
|
|
188
|
+
await this.part("header", { columnId }).click();
|
|
189
|
+
}
|
|
190
|
+
|
|
119
191
|
async expectRowCount(count: number): Promise<void> {
|
|
120
192
|
await expect(this.grid).toHaveAttribute("data-dg-row-count", String(count));
|
|
121
193
|
}
|
|
@@ -126,6 +198,31 @@ export class DataGrid {
|
|
|
126
198
|
}
|
|
127
199
|
```
|
|
128
200
|
|
|
201
|
+
The full class adds `search`, `filterBy` (adds a filter row for the column
|
|
202
|
+
when the panel has none; fills the built-in text and number inputs, so a
|
|
203
|
+
select-type filter takes `chooseOption` on its `filter-value` instead),
|
|
204
|
+
`toggleColumn`, `openColumnMenu` (hovers the header, clicks `header-menu`,
|
|
205
|
+
returns the menu), `chooseOption` (an option of a portaled `Select`, by value),
|
|
206
|
+
and the editing methods of the `testing-editing` skill.
|
|
207
|
+
|
|
208
|
+
## Recipes
|
|
209
|
+
|
|
210
|
+
`grid` is a `DataGrid`; the parts are the steps, the attributes are the proof.
|
|
211
|
+
|
|
212
|
+
| Interaction | Steps | Assertion |
|
|
213
|
+
| --- | --- | --- |
|
|
214
|
+
| Quick search | `grid.search("Cecilia")`; `search-clear` | `grid.expectRowCount(10)`, then the full count |
|
|
215
|
+
| Sort | `grid.sortBy("lastName")` once, then again | `aria-sort` on `header`: `ascending`, then `descending` |
|
|
216
|
+
| Filter | `grid.filterBy({ columnId, value })`; `filter-clear-all` | `grid.expectRowCount(n)`, then the full count |
|
|
217
|
+
| Remove a filter | `filter-remove` in its `filter-row`, or `filter-pill-remove` in its `filter-pill` | the `filter-pill` has count 0; the row count |
|
|
218
|
+
| Hide a column | `grid.toggleColumn("location")`; `grid.menuPart("columns-reset")` | the `header` has count 0, then is visible |
|
|
219
|
+
| Page | `page-next`; `grid.chooseOption({ select: grid.part("page-size"), value: "50" })` | `page-range` text changes, first `row` has a new `data-row-id`; `grid.expectRowCount(50)` |
|
|
220
|
+
| Select rows | `select-row` of a row; `select-all` | `data-selected="true"` on the `row`; `[data-dg-part="row"]:not([data-selected])` has count 0 |
|
|
221
|
+
| Group | `grid.openColumnMenu("location")`, the "Group by" item; `group-toggle` of a group row | rows carry `data-grouped`; `data-dg-row-count` grows on expand, shrinks on collapse |
|
|
222
|
+
| Load more | scroll `data-dg-scroll-container` to `scrollHeight` | `data-dg-row-count` grows; `grid.expectSettled()` |
|
|
223
|
+
| Export | `menu-button`, then `menu-export` in the menu | `page.waitForEvent("download")`, `download.suggestedFilename()` |
|
|
224
|
+
| Edit a cell | `grid.cell(...).dblclick()`, `grid.fillRow(rowId, { columnId: value })`, Enter | the cell's text; Escape instead of Enter leaves it unchanged |
|
|
225
|
+
|
|
129
226
|
## Virtualization
|
|
130
227
|
|
|
131
228
|
Only the rows in the viewport plus overscan are in the DOM. A row at index 500
|
|
@@ -138,7 +235,10 @@ otherwise. (`aria-rowcount` also counts the header and summary rows.)
|
|
|
138
235
|
**Reach a row by narrowing to it** - filter or search. Faster, more stable, and
|
|
139
236
|
what a user would do. Where the row must be reached in place,
|
|
140
237
|
`grid.scrollToRow({ rowId, align })` moves the virtualizer and answers whether
|
|
141
|
-
the row was reachable; from Playwright that needs the app to expose the api
|
|
238
|
+
the row was reachable; from Playwright that needs the app to expose the api on
|
|
239
|
+
`window`. Scrolling the element carrying `data-dg-scroll-container` moves the
|
|
240
|
+
virtualizer without it, and is also how an infinite-scroll grid is made to
|
|
241
|
+
load its next page.
|
|
142
242
|
|
|
143
243
|
## Waiting
|
|
144
244
|
|
|
@@ -165,14 +265,32 @@ layout, so the count depends on the stubbed element size. Assert
|
|
|
165
265
|
### getByRole("cell") on a grid with cell selection
|
|
166
266
|
|
|
167
267
|
`cellSelection` turns every `cell` into a `gridcell`, and the grid's `table`
|
|
168
|
-
into a `grid
|
|
169
|
-
Tests written on the role break when the feature is switched on. Query cells by
|
|
268
|
+
into a `grid`. Tests written on the role break when the feature is switched on. Query cells by
|
|
170
269
|
`[data-row-id][data-column-id]` instead.
|
|
171
270
|
|
|
271
|
+
### Matching a cell while it is edited
|
|
272
|
+
|
|
273
|
+
`[data-row-id="42"][data-column-id="total"]` matches the cell and the `editor`
|
|
274
|
+
inside it once the cell is open, and strict mode fails on the pair. Add
|
|
275
|
+
`:not([data-dg-part])`.
|
|
276
|
+
|
|
277
|
+
### Clicking header-sort to sort
|
|
278
|
+
|
|
279
|
+
The arrow is `display: none` until the header is hovered, so the click waits
|
|
280
|
+
for visibility and times out. Click `header`; `aria-sort` on it is the result.
|
|
281
|
+
|
|
282
|
+
### Reaching the menu through the root
|
|
283
|
+
|
|
284
|
+
`orders.locator('[data-dg-part="columns-toggle"]')` never resolves: the
|
|
285
|
+
dropdown renders in a portal at the end of `<body>`. Use
|
|
286
|
+
`page.getByRole("menu")` after `menu-button` is clicked. The same goes for the
|
|
287
|
+
export picker (`dialog`) and every `Select` listbox (`aria-controls`).
|
|
288
|
+
|
|
172
289
|
### Expecting a row far down the list to exist
|
|
173
290
|
|
|
174
|
-
`
|
|
175
|
-
with no useful message. Filter or search down to it first
|
|
291
|
+
`[data-row-id="450"]` has no element until it is scrolled to, so the locator times out
|
|
292
|
+
with no useful message. Filter or search down to it first, or scroll
|
|
293
|
+
`data-dg-scroll-container`.
|
|
176
294
|
|
|
177
295
|
### Unscoped parts with two grids on a page
|
|
178
296
|
|
|
@@ -196,4 +314,4 @@ different column than it did. Use `[data-column-id]`.
|
|
|
196
314
|
|
|
197
315
|
`data-dg-part="loading"` only renders where `TMDataGrid.LoadingIndicator` was
|
|
198
316
|
placed, and only while `meta.loading` is true. `aria-busy` on the grid is set
|
|
199
|
-
regardless of whether that component is rendered.
|
|
317
|
+
regardless of whether that component is rendered.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-components
|
|
3
|
+
description: >
|
|
4
|
+
Test a TMDataGrid in a real browser with Playwright component tests: the
|
|
5
|
+
stories-and-gallery model of Playwright 1.62+, the mount fixture, how a story
|
|
6
|
+
wraps the grid (MantineProvider env="test", a fixed-size container, the api on
|
|
7
|
+
window for scrollToRow), and the browser-only behaviour worth testing there -
|
|
8
|
+
virtualization, column resize through header-resize, column reorder by drag,
|
|
9
|
+
sticky pinned columns, the clipboard. Load when setting up or writing
|
|
10
|
+
Playwright component tests for a grid, or when a jsdom test cannot observe
|
|
11
|
+
layout, scrolling or drag.
|
|
12
|
+
metadata:
|
|
13
|
+
type: core
|
|
14
|
+
library: '@jielga/tmdatagrid'
|
|
15
|
+
library_version: '2.0.0'
|
|
16
|
+
sources:
|
|
17
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/testing.md'
|
|
18
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/test/gallery/main.tsx'
|
|
19
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/test/stories/Grid.story.tsx'
|
|
20
|
+
- 'Jielga/TMDataGrid:playwright/components/virtualization.spec.ts'
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# TMDataGrid - Component tests in a browser
|
|
24
|
+
|
|
25
|
+
Builds on the `testing` skill: parts are `[data-dg-part]`, coordinates are
|
|
26
|
+
`[data-row-id]` / `[data-column-id]`, and `DataGrid` is the page object.
|
|
27
|
+
|
|
28
|
+
jsdom has no layout. The virtualizer mounts a handful of rows whatever the data
|
|
29
|
+
says, a drag never moves anything, and jsdom does not apply `position: sticky`.
|
|
30
|
+
The behaviour below needs a browser:
|
|
31
|
+
|
|
32
|
+
- virtualization - rows mount and unmount as the body scrolls; `scrollToRow`
|
|
33
|
+
- column resize - a drag on `header-resize`, and double-click to fit
|
|
34
|
+
- column reorder - a header dragged onto another header (native HTML5 drag)
|
|
35
|
+
- pinned columns - sticky offsets while the body scrolls horizontally
|
|
36
|
+
- sticky header and summary row while the body scrolls vertically
|
|
37
|
+
- the clipboard - Ctrl+C on a cell selection
|
|
38
|
+
|
|
39
|
+
## The model
|
|
40
|
+
|
|
41
|
+
Playwright 1.62 replaced `@playwright/experimental-ct-react` with three pieces
|
|
42
|
+
that live in plain `@playwright/test`:
|
|
43
|
+
|
|
44
|
+
- **a story** - a `*.story.tsx` file; each named export is one scenario, a
|
|
45
|
+
component with hard-coded data, options and providers
|
|
46
|
+
- **a gallery** - one page served by your own dev server that discovers the
|
|
47
|
+
story files, exposes `window.mount({ story, props })` and `window.unmount()`,
|
|
48
|
+
and renders into `#root` through one reused React root
|
|
49
|
+
- **`mount`** - a built-in fixture: `await mount("Orders/Virtualized")`
|
|
50
|
+
navigates to the gallery (`baseURL`), mounts the story and returns a
|
|
51
|
+
`Locator` for the gallery root
|
|
52
|
+
|
|
53
|
+
The story id is the file path under the stories folder without `.story.tsx`,
|
|
54
|
+
then `/` and the export name. `mount` accepts any string, so a renamed story
|
|
55
|
+
fails at run time, not under `tsc`; registering the ids in Playwright's
|
|
56
|
+
`Stories` interface gives completion and prop checking:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
declare module "@playwright/test" {
|
|
60
|
+
interface Stories {
|
|
61
|
+
"Orders/Virtualized": typeof Virtualized;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
What the gallery page must expose is in Playwright's docs
|
|
67
|
+
(https://playwright.dev/docs/test-components); the grid's own gallery is a few
|
|
68
|
+
dozen lines over `import.meta.glob`.
|
|
69
|
+
|
|
70
|
+
Project config, next to the page project:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
{
|
|
74
|
+
name: "components",
|
|
75
|
+
testDir: "playwright/components",
|
|
76
|
+
use: {
|
|
77
|
+
baseURL: "http://localhost:5274/",
|
|
78
|
+
serviceWorkers: "block",
|
|
79
|
+
permissions: ["clipboard-read", "clipboard-write"],
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
with a `webServer` entry such as
|
|
85
|
+
`{ command: "npm run gallery", url: "http://localhost:5274/" }`. Every
|
|
86
|
+
`webServer` entry starts on every run; Playwright does not scope them to a
|
|
87
|
+
project.
|
|
88
|
+
|
|
89
|
+
## Writing a story for the grid
|
|
90
|
+
|
|
91
|
+
The story owns everything the grid needs:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
// Orders.story.tsx
|
|
95
|
+
declare global {
|
|
96
|
+
interface Window {
|
|
97
|
+
__grid?: TMDataGridApi<Order>; // for scrollToRow from the test
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const Virtualized = () => {
|
|
102
|
+
const grid = useTMDataGrid<Order>({
|
|
103
|
+
data: orders, // 5000 rows
|
|
104
|
+
columns,
|
|
105
|
+
getRowId: (row) => row.id,
|
|
106
|
+
});
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
window.__grid = grid;
|
|
109
|
+
}, [grid]);
|
|
110
|
+
return (
|
|
111
|
+
<div style={{ display: "flex", flexDirection: "column", height: 400 }}>
|
|
112
|
+
<TMDataGrid {...grid} style={{ flex: 1, minHeight: 0 }} data-testid="orders">
|
|
113
|
+
<TMDataGrid.Table<Order> />
|
|
114
|
+
</TMDataGrid>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- **`<MantineProvider env="test">`** around every story, in the gallery or in
|
|
121
|
+
the story. It turns Mantine's transitions off, so a panel is open the moment
|
|
122
|
+
the click lands.
|
|
123
|
+
- **A flex column of fixed height,** with `flex: 1` and `minHeight: 0` on the
|
|
124
|
+
grid. The grid root is a shrinkable flex item; in a plain block it grows to
|
|
125
|
+
fit every row, nothing scrolls, and the virtualization test proves nothing.
|
|
126
|
+
Give the pinned story a width smaller than its columns (`minSize` sets a
|
|
127
|
+
fluid column's floor; `size` alone does not widen it), so the body overflows
|
|
128
|
+
horizontally.
|
|
129
|
+
- **The api on `window`** only where a test calls `scrollToRow`. Everything
|
|
130
|
+
else is reachable through the DOM contract. Declare the global in the story
|
|
131
|
+
and again in the spec, which is typechecked apart from it.
|
|
132
|
+
- **No props unless needed.** One export per scenario, and `mount(id)` needs
|
|
133
|
+
no generic.
|
|
134
|
+
|
|
135
|
+
## Writing the test
|
|
136
|
+
|
|
137
|
+
`mount` returns the gallery root; the page object takes the grid root inside it:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
declare global {
|
|
141
|
+
interface Window {
|
|
142
|
+
__grid?: { scrollToRow(target: { rowId: string; align?: "start" | "center" | "end" }): boolean };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
test("reaches a row past the viewport", async ({ mount, page }) => {
|
|
147
|
+
const component = await mount("Orders/Virtualized");
|
|
148
|
+
const grid = new DataGrid(component.locator("[data-dg-root]"));
|
|
149
|
+
await grid.expectRowCount(5000);
|
|
150
|
+
await expect(grid.part("row", { rowId: "4500" })).toHaveCount(0);
|
|
151
|
+
|
|
152
|
+
const found = await page.evaluate(() => {
|
|
153
|
+
return window.__grid!.scrollToRow({ rowId: "4500", align: "center" });
|
|
154
|
+
});
|
|
155
|
+
expect(found).toBe(true);
|
|
156
|
+
await expect(grid.part("row", { rowId: "4500" })).toBeVisible();
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Recipes
|
|
161
|
+
|
|
162
|
+
**Virtualization.** `data-dg-row-count` is the data; the number of
|
|
163
|
+
`[data-dg-part="row"]` elements is the window. Asserting that the second is
|
|
164
|
+
far below the first is the one place counting row elements is right. Scroll
|
|
165
|
+
the element carrying `data-dg-scroll-container` to its `scrollHeight` and the
|
|
166
|
+
last row appears.
|
|
167
|
+
|
|
168
|
+
**Resize.** `header-resize` is the handle, keyed by `data-column-id`:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const handle = grid.part("header-resize", { columnId: "name" });
|
|
172
|
+
const box = (await handle.boundingBox())!;
|
|
173
|
+
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
174
|
+
await page.mouse.down();
|
|
175
|
+
await page.mouse.move(box.x + 80, box.y, { steps: 8 });
|
|
176
|
+
await page.mouse.up();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Then compare the header's `boundingBox().width` before and after. A
|
|
180
|
+
`dblclick()` on the handle fits the column to its content.
|
|
181
|
+
|
|
182
|
+
**Reorder.** `await grid.part("header", { columnId: "city" }).dragTo(grid.part("header", { columnId: "name" }), { targetPosition: { x: 10, y: 10 } })`
|
|
183
|
+
drops `city` before `name`; read the new order from `[data-dg-part="header"]`
|
|
184
|
+
`data-column-id`s. A header dragged across a pinned lane does not move.
|
|
185
|
+
|
|
186
|
+
**Pinning.** Scroll the scroll container horizontally (`scrollLeft`) and compare
|
|
187
|
+
`boundingBox().x` of a pinned header before and after: unchanged, while an
|
|
188
|
+
unpinned header moved. A pinned header and a body cell of the same column share
|
|
189
|
+
the same `x`.
|
|
190
|
+
|
|
191
|
+
**Sticky header and summary row.** Scroll vertically; `boundingBox().y` of the
|
|
192
|
+
header row (`[data-dg-header-row]`) and of `summary-row` do not change, and
|
|
193
|
+
both stay inside the scroll container's box.
|
|
194
|
+
|
|
195
|
+
**Clipboard.** In a story with `cellSelection` on: click a cell, shift-click
|
|
196
|
+
another, `page.keyboard.press("Control+C")`, then
|
|
197
|
+
`page.evaluate(() => navigator.clipboard.readText())`. Cells are separated by
|
|
198
|
+
a tab and rows by `\r\n`. Needs the `permissions` above; Chromium only.
|
|
199
|
+
|
|
200
|
+
## Common mistakes
|
|
201
|
+
|
|
202
|
+
### Mounting without a provider
|
|
203
|
+
|
|
204
|
+
A story rendered outside `MantineProvider` throws on the first Mantine
|
|
205
|
+
component. Put the provider in the gallery's `window.mount`, so no story can
|
|
206
|
+
forget it.
|
|
207
|
+
|
|
208
|
+
### A story with no height
|
|
209
|
+
|
|
210
|
+
The grid fills its container. With `height: auto` the container grows with the
|
|
211
|
+
rows, nothing scrolls, and the virtualizer mounts everything; the test passes
|
|
212
|
+
for the wrong reason or hangs on 5000 rows.
|
|
213
|
+
|
|
214
|
+
### Building JSX in the test
|
|
215
|
+
|
|
216
|
+
The test runs in Node and the component in the browser; there is no JSX across
|
|
217
|
+
that boundary. A scenario is a story export. A test that needs a different
|
|
218
|
+
composition gets another export.
|
|
219
|
+
|
|
220
|
+
### Passing a callback as a prop
|
|
221
|
+
|
|
222
|
+
Callbacks do not cross to the browser either. The story owns the state and the
|
|
223
|
+
callbacks, and writes what a test must see into the DOM - a hidden input, or
|
|
224
|
+
the grid's own `data-*` attributes, which already carry most of it.
|
|
225
|
+
|
|
226
|
+
### Counting row elements everywhere
|
|
227
|
+
|
|
228
|
+
Outside the virtualization claim itself, assert `data-dg-row-count`. The
|
|
229
|
+
window size depends on the viewport, overscan and row height, and changes with
|
|
230
|
+
`devices` presets.
|