@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.
Files changed (154) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1281 -768
  3. package/dist/index.js +4607 -3250
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/adding-rows.md +132 -0
  7. package/docs/anatomy.md +119 -0
  8. package/docs/card-view.md +108 -0
  9. package/docs/cell-selection.md +194 -0
  10. package/docs/column-layout.md +182 -0
  11. package/docs/column-menu.md +66 -0
  12. package/docs/columns.md +268 -0
  13. package/docs/components.md +311 -0
  14. package/docs/draft-store.md +242 -0
  15. package/docs/editing.md +303 -0
  16. package/docs/editors.md +250 -0
  17. package/docs/export.md +319 -0
  18. package/docs/filtering.md +362 -0
  19. package/docs/getting-started.md +123 -0
  20. package/docs/grouping.md +165 -0
  21. package/docs/loading-and-empty.md +92 -0
  22. package/docs/localization.md +79 -0
  23. package/docs/menu.md +143 -0
  24. package/docs/migrating-to-2.md +163 -0
  25. package/docs/pagination.md +144 -0
  26. package/docs/persistence.md +114 -0
  27. package/docs/portfolio-rebalancer.md +94 -0
  28. package/docs/query-builder.md +179 -0
  29. package/docs/quick-search.md +84 -0
  30. package/docs/row-details.md +115 -0
  31. package/docs/row-interaction.md +149 -0
  32. package/docs/row-pinning.md +132 -0
  33. package/docs/row-selection.md +136 -0
  34. package/docs/row-styling.md +133 -0
  35. package/docs/scrolling.md +112 -0
  36. package/docs/server-query.md +246 -0
  37. package/docs/server-side.md +206 -0
  38. package/docs/sorting.md +101 -0
  39. package/docs/styling.md +126 -0
  40. package/docs/summary-row.md +76 -0
  41. package/docs/testing.md +744 -0
  42. package/docs/toolbar.md +161 -0
  43. package/docs/use-tm-data-grid.md +361 -0
  44. package/package.json +22 -46
  45. package/skills/appearance/SKILL.md +72 -19
  46. package/skills/cell-selection/SKILL.md +46 -47
  47. package/skills/columns/SKILL.md +90 -34
  48. package/skills/data/SKILL.md +86 -16
  49. package/skills/editing/SKILL.md +67 -40
  50. package/skills/editing/references/common-mistakes.md +77 -69
  51. package/skills/editing/references/editing-api.md +22 -19
  52. package/skills/editing/references/editors-and-validation.md +24 -17
  53. package/skills/filtering/SKILL.md +148 -40
  54. package/skills/getting-started/SKILL.md +17 -15
  55. package/skills/grouping/SKILL.md +31 -16
  56. package/skills/options/SKILL.md +7 -7
  57. package/skills/rows/SKILL.md +22 -18
  58. package/skills/server-side/SKILL.md +170 -17
  59. package/skills/testing/SKILL.md +150 -32
  60. package/skills/testing-components/SKILL.md +230 -0
  61. package/skills/testing-editing/SKILL.md +240 -0
  62. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
  63. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
  64. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  65. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  66. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +5 -51
  67. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  68. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
  69. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
  70. package/src/components/TMDataGridExportPicker.module.css +77 -0
  71. package/src/components/TMDataGridExportPicker.tsx +234 -0
  72. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  73. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  74. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  75. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  76. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  77. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  78. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +5 -69
  79. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  80. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  81. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  82. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  83. package/src/components/TMDataGridMenu.tsx +357 -0
  84. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +11 -48
  85. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
  86. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
  87. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  88. package/src/components/TMDataGridToolbar.tsx +181 -0
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  91. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  96. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  97. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  98. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  99. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  100. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  101. package/src/components/filters/controlLayout.ts +32 -0
  102. package/src/components/filters/filterControlFor.ts +65 -0
  103. package/src/components/generatedColumns.tsx +187 -0
  104. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  105. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  106. package/src/components/useHideableColumns.ts +52 -0
  107. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  108. package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
  109. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  110. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  111. package/src/core/controlledStateSync.ts +108 -0
  112. package/src/core/deletedRows.ts +34 -0
  113. package/src/core/dom.ts +74 -0
  114. package/src/{tmdatagrid/core → core}/editEngine.ts +1107 -460
  115. package/src/core/export.ts +704 -0
  116. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  117. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  118. package/src/core/filterSurface.ts +99 -0
  119. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  120. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  121. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  122. package/src/core/pageReset.ts +120 -0
  123. package/src/core/pagination.ts +81 -0
  124. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  125. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  126. package/src/{tmdatagrid/index.ts → index.ts} +69 -35
  127. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
  128. package/src/useTMDataGridExport.ts +78 -0
  129. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  130. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  131. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
  134. /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/capabilities.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -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, reaching rows
8
- past virtualization with data-dg-row-count and scrollToRow, and waiting on
9
- aria-busy. Load when writing or fixing tests that drive a grid, or when a
10
- selector for a row, cell or control does not resolve.
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-beta.9'
16
+ library_version: '2.0.0'
15
17
  sources:
16
- - 'Jielga/TMDataGrid:src/docs/testing.md'
17
- - 'Jielga/TMDataGrid:src/tmdatagrid/components/TMDataGrid.tsx'
18
- - 'Jielga/TMDataGrid:src/tmdatagrid/components/TMDataGridTable.tsx'
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-panel-close`, `filter-add`, `filter-clear-all`, `filter-pills`,
66
- `columns-button`, `columns-panel`, `columns-search`, `columns-toggle-all`,
67
- `columns-reset`, `footer`, `page-size`, `page-range`, `page-prev`, `page-next`,
68
- `summary-row`, `pinned-top`, `pinned-bottom`, `select-all`,
69
- `details-toggle-all`, `save-all`, `discard-all`, `editor-confirm`,
70
- `editor-cancel`, `editor-input`, `sort-index`.
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-filter`, `filter-row`, `filter-pill`, `columns-toggle`.
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
- constructor(page: Page, testId: string) {
101
- this.root = page.getByTestId(testId);
102
- this.grid = this.root.getByRole("table");
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
- `[data-dg-part="${name}"]` +
108
- (key.rowId === undefined ? "" : `[data-row-id="${key.rowId}"]`) +
109
- (key.columnId === undefined ? "" : `[data-column-id="${key.columnId}"]`),
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`, because a widget with a keyboard cursor is not a static table.
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
- `dg-row-450` has no element until it is scrolled to, so the locator times out
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.