@xh/hoist 59.2.0 → 59.3.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 (104) hide show
  1. package/CHANGELOG.md +63 -14
  2. package/admin/AppComponent.ts +1 -1
  3. package/admin/tabs/activity/ActivityTab.ts +1 -1
  4. package/admin/tabs/general/GeneralTab.ts +2 -2
  5. package/admin/tabs/general/config/ConfigPanel.ts +1 -0
  6. package/admin/tabs/monitor/MonitorTab.ts +1 -1
  7. package/admin/tabs/server/ServerTab.ts +1 -1
  8. package/admin/tabs/userData/UserDataTab.ts +1 -1
  9. package/cmp/ag-grid/AgGrid.scss +51 -25
  10. package/cmp/ag-grid/AgGrid.ts +8 -2
  11. package/cmp/badge/Badge.ts +18 -5
  12. package/cmp/chart/Chart.ts +13 -11
  13. package/cmp/clock/Clock.ts +6 -5
  14. package/cmp/dataview/DataView.ts +5 -3
  15. package/cmp/form/Form.ts +25 -6
  16. package/cmp/grid/Grid.ts +41 -25
  17. package/cmp/grid/GridModel.ts +2 -2
  18. package/cmp/grid/Types.ts +1 -1
  19. package/cmp/grid/columns/Column.ts +45 -2
  20. package/cmp/grid/impl/GridHScrollbar.ts +140 -0
  21. package/cmp/grid/renderers/MultiFieldRenderer.ts +1 -1
  22. package/cmp/input/HoistInputModel.ts +4 -4
  23. package/cmp/input/HoistInputProps.ts +3 -1
  24. package/cmp/layout/Box.ts +4 -2
  25. package/cmp/relativetimestamp/RelativeTimestamp.ts +106 -40
  26. package/cmp/store/StoreFilterField.ts +2 -2
  27. package/cmp/tab/TabContainer.ts +1 -1
  28. package/cmp/zoneGrid/Types.ts +47 -0
  29. package/cmp/zoneGrid/ZoneGrid.ts +62 -0
  30. package/cmp/zoneGrid/ZoneGridModel.ts +666 -0
  31. package/cmp/zoneGrid/impl/ZoneGridPersistenceModel.ts +143 -0
  32. package/cmp/zoneGrid/impl/ZoneMapperModel.ts +335 -0
  33. package/cmp/zoneGrid/index.ts +3 -0
  34. package/core/HoistComponent.ts +23 -10
  35. package/core/HoistProps.ts +25 -6
  36. package/core/XH.ts +49 -27
  37. package/core/elem.ts +11 -3
  38. package/core/impl/InstanceManager.ts +24 -1
  39. package/core/model/HoistModel.ts +4 -4
  40. package/data/RecordAction.ts +7 -4
  41. package/data/StoreRecord.ts +8 -1
  42. package/desktop/appcontainer/AppContainer.ts +2 -0
  43. package/desktop/cmp/appbar/AppBar.ts +8 -6
  44. package/desktop/cmp/button/Button.ts +14 -3
  45. package/desktop/cmp/button/ButtonGroup.ts +14 -3
  46. package/desktop/cmp/button/ZoneMapperButton.ts +82 -0
  47. package/desktop/cmp/button/index.ts +1 -0
  48. package/desktop/cmp/dash/canvas/DashCanvas.ts +14 -4
  49. package/desktop/cmp/dash/container/DashContainer.ts +11 -4
  50. package/desktop/cmp/error/ErrorMessage.ts +9 -8
  51. package/desktop/cmp/form/FormField.ts +34 -10
  52. package/desktop/cmp/grid/columns/Actions.ts +2 -1
  53. package/desktop/cmp/grid/impl/colchooser/ColChooser.ts +3 -2
  54. package/desktop/cmp/grouping/GroupingChooser.ts +29 -29
  55. package/desktop/cmp/input/ButtonGroupInput.ts +1 -1
  56. package/desktop/cmp/input/Checkbox.ts +3 -3
  57. package/desktop/cmp/input/CodeInput.ts +2 -1
  58. package/desktop/cmp/input/DateInput.ts +128 -123
  59. package/desktop/cmp/input/JsonInput.ts +1 -1
  60. package/desktop/cmp/input/NumberInput.ts +3 -2
  61. package/desktop/cmp/input/RadioInput.ts +3 -1
  62. package/desktop/cmp/input/Select.ts +31 -4
  63. package/desktop/cmp/input/SwitchInput.ts +2 -1
  64. package/desktop/cmp/input/TextArea.ts +3 -3
  65. package/desktop/cmp/input/TextInput.ts +51 -47
  66. package/desktop/cmp/panel/Panel.ts +21 -19
  67. package/desktop/cmp/panel/impl/ResizeContainer.ts +3 -2
  68. package/desktop/cmp/pinpad/impl/PinPad.ts +4 -3
  69. package/desktop/cmp/record/RecordActionBar.ts +12 -3
  70. package/desktop/cmp/record/impl/RecordActionButton.ts +1 -0
  71. package/desktop/cmp/rest/Actions.ts +10 -5
  72. package/desktop/cmp/rest/RestGrid.ts +20 -6
  73. package/desktop/cmp/rest/impl/RestForm.ts +5 -4
  74. package/desktop/cmp/rest/impl/RestGridToolbar.ts +3 -2
  75. package/desktop/cmp/tab/TabSwitcher.ts +8 -3
  76. package/desktop/cmp/tab/impl/Tab.ts +2 -1
  77. package/desktop/cmp/tab/impl/TabContainer.ts +18 -15
  78. package/desktop/cmp/toolbar/Toolbar.ts +3 -1
  79. package/desktop/cmp/treemap/SplitTreeMap.ts +2 -1
  80. package/desktop/cmp/treemap/TreeMap.ts +5 -3
  81. package/desktop/cmp/zoneGrid/impl/ZoneMapper.scss +71 -0
  82. package/desktop/cmp/zoneGrid/impl/ZoneMapper.ts +232 -0
  83. package/desktop/cmp/zoneGrid/impl/ZoneMapperDialog.ts +35 -0
  84. package/dynamics/desktop.ts +2 -0
  85. package/dynamics/mobile.ts +2 -0
  86. package/inspector/instances/InstancesModel.ts +2 -2
  87. package/mobile/appcontainer/AppContainer.ts +2 -0
  88. package/mobile/cmp/button/ZoneMapperButton.ts +41 -0
  89. package/mobile/cmp/button/index.ts +1 -0
  90. package/mobile/cmp/error/ErrorMessage.ts +4 -4
  91. package/mobile/cmp/input/Select.scss +1 -0
  92. package/mobile/cmp/input/Select.ts +7 -0
  93. package/mobile/cmp/input/TextInput.ts +1 -0
  94. package/mobile/cmp/panel/DialogPanel.scss +18 -6
  95. package/mobile/cmp/panel/DialogPanel.ts +3 -1
  96. package/mobile/cmp/zoneGrid/impl/ZoneMapper.scss +67 -0
  97. package/mobile/cmp/zoneGrid/impl/ZoneMapper.ts +236 -0
  98. package/package.json +4 -3
  99. package/styles/vars.scss +3 -3
  100. package/svc/InspectorService.ts +1 -1
  101. package/utils/js/DomUtils.ts +10 -0
  102. package/utils/js/LangUtils.ts +10 -0
  103. package/utils/js/TestUtils.ts +9 -0
  104. package/utils/js/index.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -1,29 +1,77 @@
1
1
  # Changelog
2
2
 
3
+ ## 59.3.0 - 2023-11-09
4
+
5
+ ### 🎁 New Features
6
+
7
+ * Improved Hoist support for automated testing via Playwright, Cypress, and similar tools:
8
+ * Core Hoist components now accept an optional `testId` prop, to be rendered at an appropriate
9
+ level of the DOM (within a `data-testid` HTML attribute). This can minimize the need to select
10
+ components using criteria such as CSS classes or labels that are more likely to change and
11
+ break tests.
12
+ * When given a `testId`, certain composite components will generate and set "sub-testIds" on
13
+ selected internal components. For example, a `TabContainer` will set a testId on each switcher
14
+ button (derived from its tabId), and a `Form` will set testIds on nested `FormField`
15
+ and `HoistInput` components (derived from their bound field names).
16
+ * This release represents a first step in ongoing work to facilitate automated end-to-end
17
+ testing of Hoist applications. Additional Hoist-specific utilities for writing tests in
18
+ libraries such as Cypress and Playwright are coming soon.
19
+ * Added new `ZoneGrid` component, a highly specialized `Grid` that always displays its data with
20
+ multi-line, full-width rows. Each row is broken into four zones (top/bottom and left/right),
21
+ each of which can mapped by the user to render data from one or more fields.
22
+ * Primarily intended for mobile, where horizontal scrolling can present usability issues, but
23
+ also available on desktop, where it can serve as an easily user-configurable `DataView`.
24
+ * Added `Column.sortToBottom` to force specified values to sort the bottom, regardless of sort
25
+ direction. Intended primarily to force null values to sort below all others.
26
+ * Upgraded the `RelativeTimestamp` component with a new `localDateMode` option to customize how
27
+ near-term date/time differences are rendered with regards to calendar days.
28
+
29
+ ### 🐞 Bug Fixes
30
+
31
+ * Fixed bug where interacting with a `Select` within a `Popover` can inadvertently cause the
32
+ popover to close. If your app already has special handling in place to prevent this, you should
33
+ be able to unwind it after upgrading.
34
+ * Improved the behavior of the clear button in `TextInput`. Clearing a field no longer drops focus,
35
+ allowing the user to immediately begin typing in a new value.
36
+ * Fixed arguments passed to `ErrorMessageProps.actionFn` and `ErrorMessageProps.detailsFn`.
37
+ * Improved default error text in `ErrorMessage`.
38
+
39
+ ### ⚙️ Technical
40
+
41
+ * Improved core `HoistComponent` performance by preventing unnecessary re-renderings triggered by
42
+ spurious model lookup changes.
43
+ * New flag `GridModel.experimental.enableFullWidthScroll` enables scrollbars to span pinned columns.
44
+ * Early test release behind the flag, expected to made the default behavior in next release.
45
+ * Renamed `XH.getActiveModels()` to `XH.getModels()` for clarity / consistency.
46
+ * API change, but not expected to impact applications.
47
+ * Added `XH.getModel()` convenience method to return the first matching model.
48
+
3
49
  ## 59.2.0 - 2023-10-16
4
50
 
5
51
  ### 🎁 New Features
6
52
 
7
- * New `DockViewConfig.onClose` hook invoked when a user attempts to remove a `DockContainer` view
8
- * Add `GridModel` APIs to lookup and show / hide entire column groups
9
- * Left / right borders are now rendered along `Grid` `ColumnGroup` edges by default. Control
10
- with new boolean property `ColumnGroupSpec.borders`
11
- * The Cube package has been enhanced to support `Query` specific post-processing functions. See
12
- new properties `Query.omitFn`, `Query.bucketSpecFn` and `Query.lockFn`. These properties default
13
- to their respective properties on `Cube`.
53
+ * New `DockViewConfig.onClose` hook invoked when a user attempts to remove a `DockContainer` view.
54
+ * Added `GridModel` APIs to lookup and show / hide entire column groups.
55
+ * Left / right borders are now rendered along `Grid` `ColumnGroup` edges by default, controllable
56
+ with new `ColumnGroupSpec.borders` config.
57
+ * Enhanced the `CubeQuery` to support per-query post-processing functions
58
+ with `Query.omitFn`, `Query.bucketSpecFn` and `Query.lockFn`. These properties default to their
59
+ respective properties on `Cube`.
14
60
 
15
61
  ### 🐞 Bug Fixes
16
62
 
17
63
  * `DashContainerModel` fixes:
18
- * Fix bug where `addView` would throw when adding a view to a row or column
19
- * Fix bug where `allowRemove` flag was dropped from state for containers
20
- * Fix bug in `DockContainer` where adding / removing views would cause other views to be remounted
21
- * Fix erroneous `GridModel` warning when using a tree column within a column group
22
- * Fix regression to alert banners. Resume allowing elements as messages
64
+ * Fix bug where `addView` would throw when adding a view to a row or column
65
+ * Fix bug where `allowRemove` flag was dropped from state for containers
66
+ * Fix bug in `DockContainer` where adding / removing views would cause other views to be
67
+ remounted
68
+ * Fixed erroneous `GridModel` warning when using a tree column within a column group
69
+ * Fixed regression to alert banners. Resume allowing elements as messages.
70
+ * Fix `Grid` cell border styling inconsistencies.
23
71
 
24
72
  ### ⚙️ Typescript API Adjustments
25
73
 
26
- * Add type for `ActionFnData.record`
74
+ * Added type for `ActionFnData.record`.
27
75
 
28
76
  ## 59.1.0 - 2023-09-20
29
77
 
@@ -49,7 +97,8 @@ to their respective properties on `Cube`.
49
97
  * Improved styling for disabled `checkbox` inputs.
50
98
 
51
99
  ### ⚙️ Technical
52
- * `XH.showException` has been deprecated. Use similar methods on `XH.exceptionHandler` instead.
100
+
101
+ * `XH.showException` has been deprecated. Use similar methods on `XH.exceptionHandler` instead.
53
102
 
54
103
  ### 📚 Libraries
55
104
 
@@ -30,7 +30,7 @@ export const AppComponent = hoistCmp({
30
30
  const tbar = hoistCmp.factory<AppModel>(({model}) =>
31
31
  appBar({
32
32
  icon: Icon.gears({size: '2x', prefix: 'fal'}),
33
- leftItems: [tabSwitcher({enableOverflow: true})],
33
+ leftItems: [tabSwitcher({testId: 'tab-switcher', enableOverflow: true})],
34
34
  rightItems: [
35
35
  button({
36
36
  icon: Icon.openExternal(),
@@ -16,7 +16,7 @@ export const activityTab = hoistCmp.factory(() =>
16
16
  tabContainer({
17
17
  modelConfig: {
18
18
  route: 'default.activity',
19
- switcher: {orientation: 'left'},
19
+ switcher: {orientation: 'left', testId: 'activity-tab-switcher'},
20
20
  tabs: [
21
21
  {id: 'tracking', icon: Icon.analytics(), content: activityTrackingPanel},
22
22
  {id: 'clientErrors', icon: Icon.warning(), content: clientErrorsPanel},
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {configPanel} from '@xh/hoist/admin/tabs/general/config/ConfigPanel';
8
8
  import {tabContainer} from '@xh/hoist/cmp/tab';
9
- import {XH, hoistCmp} from '@xh/hoist/core';
9
+ import {hoistCmp, XH} from '@xh/hoist/core';
10
10
  import {Icon} from '@xh/hoist/icon';
11
11
  import {aboutPanel} from './about/AboutPanel';
12
12
  import {alertBannerPanel} from './alertBanner/AlertBannerPanel';
@@ -16,7 +16,7 @@ export const generalTab = hoistCmp.factory(() =>
16
16
  tabContainer({
17
17
  modelConfig: {
18
18
  route: 'default.general',
19
- switcher: {orientation: 'left'},
19
+ switcher: {orientation: 'left', testId: 'general-tab-switcher'},
20
20
  tabs: [
21
21
  {id: 'about', icon: Icon.info(), content: aboutPanel},
22
22
  {id: 'config', icon: Icon.settings(), content: configPanel},
@@ -19,6 +19,7 @@ export const configPanel = hoistCmp.factory({
19
19
  render({model}) {
20
20
  return fragment(
21
21
  restGrid({
22
+ testId: 'config',
22
23
  extraToolbarItems: () => {
23
24
  return button({
24
25
  icon: Icon.diff(),
@@ -17,7 +17,7 @@ export const monitorTab = hoistCmp.factory(() => {
17
17
  ? tabContainer({
18
18
  modelConfig: {
19
19
  route: 'default.monitor',
20
- switcher: {orientation: 'left'},
20
+ switcher: {orientation: 'left', testId: 'monitor-tab-switcher'},
21
21
  tabs: [
22
22
  {id: 'status', icon: Icon.shieldCheck(), content: monitorResultsPanel},
23
23
  {id: 'config', icon: Icon.settings(), content: monitorEditorPanel}
@@ -20,7 +20,7 @@ export const serverTab = hoistCmp.factory(() =>
20
20
  tabContainer({
21
21
  modelConfig: {
22
22
  route: 'default.server',
23
- switcher: {orientation: 'left'},
23
+ switcher: {orientation: 'left', testId: 'server-tab-switcher'},
24
24
  tabs: [
25
25
  {id: 'logViewer', icon: Icon.fileText(), content: logViewer},
26
26
  {id: 'logLevels', icon: Icon.settings(), content: logLevelPanel},
@@ -15,7 +15,7 @@ export const userDataTab = hoistCmp.factory(() =>
15
15
  tabContainer({
16
16
  modelConfig: {
17
17
  route: 'default.userData',
18
- switcher: {orientation: 'left'},
18
+ switcher: {orientation: 'left', testId: 'user-data-tab-switcher'},
19
19
  tabs: [
20
20
  {
21
21
  id: 'prefs',
@@ -9,8 +9,8 @@
9
9
  border-#{$side}: var(--xh-border-solid);
10
10
  }
11
11
 
12
- @mixin pinned-border {
13
- border-right: 1px solid var(--xh-grid-pinned-column-border-color);
12
+ @mixin pinned-border($side) {
13
+ border-#{$side}: 1px solid var(--xh-grid-pinned-column-border-color);
14
14
  }
15
15
 
16
16
  // Ag-grid installs an outer div around itself.
@@ -128,16 +128,17 @@
128
128
  }
129
129
 
130
130
  &--no-row-borders {
131
- .ag-row {
131
+ .ag-row,
132
+ .ag-cell {
132
133
  border-bottom: none;
133
134
  border-top: none;
135
+ }
134
136
 
135
- // Deliberately keep border on full-width grouped rows even when we aren't adding row borders.
136
- // Without this collapsed groups (which don't stripe) blend together in a solid block.
137
- &.ag-row-group.ag-full-width-row {
138
- border-bottom: 1px solid var(--xh-grid-group-border-color);
139
- border-top: 1px solid var(--xh-grid-group-border-color);
140
- }
137
+ // Deliberately keep border on full-width grouped rows even when we aren't adding row borders.
138
+ // Without this collapsed groups (which don't stripe) blend together in a solid block.
139
+ .ag-row.ag-row-group.ag-full-width-row {
140
+ border-bottom: 1px solid var(--xh-grid-group-border-color);
141
+ border-top: 1px solid var(--xh-grid-group-border-color);
141
142
  }
142
143
  }
143
144
 
@@ -197,8 +198,14 @@
197
198
  }
198
199
  }
199
200
 
200
- .ag-cell.ag-cell-last-left-pinned:not(.ag-cell-focus) {
201
- @include pinned-border;
201
+ .ag-cell {
202
+ &.ag-cell-last-left-pinned:not(.ag-cell-focus) {
203
+ @include pinned-border(right);
204
+ }
205
+
206
+ &.ag-cell-first-right-pinned:not(.ag-cell-focus) {
207
+ @include pinned-border(left);
208
+ }
202
209
  }
203
210
 
204
211
  // We use flexbox to consistently vertically center cell contents across
@@ -237,33 +244,52 @@
237
244
  &--no-cell-borders {
238
245
  .ag-cell,
239
246
  .ag-context-menu-open .ag-cell-focus:not(.ag-cell-range-selected) {
240
- border: none;
247
+ // Preserve left and right borders to avoid jumpiness upon cell focus
248
+ border-top: none;
249
+ border-bottom: none;
250
+ border-left-color: transparent;
251
+ border-right-color: transparent;
241
252
  }
242
253
  }
243
254
 
244
255
  // Cell focus
245
256
  &--no-cell-focus {
257
+ // Preserve cell borders when enabled upon "invisible" cell focus
258
+ &.xh-ag-grid--cell-borders .ag-has-focus .ag-cell-focus:not(.ag-cell-range-selected) {
259
+ border-right-color: var(--xh-grid-border-color);
260
+ }
261
+
246
262
  .ag-has-focus {
247
263
  .ag-cell-focus:not(.ag-cell-range-selected) {
248
- border: none;
249
-
250
- &.ag-cell.ag-cell-last-left-pinned {
251
- @include pinned-border;
252
- }
253
-
254
- &.ag-cell.xh-cell--group-border-left {
255
- @include group-border(left);
256
- }
257
-
258
- &.ag-cell.xh-cell--group-border-right {
259
- @include group-border(right);
264
+ // Preserve left and right borders to avoid jumpiness upon cell focus
265
+ border-top: none;
266
+ border-bottom: none;
267
+ border-left-color: transparent;
268
+ border-right-color: transparent;
269
+
270
+ &.ag-cell {
271
+ &.ag-cell-last-left-pinned {
272
+ @include pinned-border(right);
273
+ }
274
+
275
+ &.ag-cell-first-right-pinned {
276
+ @include pinned-border(left);
277
+ }
278
+
279
+ &.xh-cell--group-border-left {
280
+ @include group-border(left);
281
+ }
282
+
283
+ &.xh-cell--group-border-right {
284
+ @include group-border(right);
285
+ }
260
286
  }
261
287
  }
262
288
  }
263
289
  }
264
290
 
265
291
  &--show-cell-focus {
266
- .ag-cell-focus {
292
+ .ag-cell-focus:focus-within {
267
293
  border-color: var(--xh-grid-cell-focus-border-color) !important;
268
294
  }
269
295
  }
@@ -12,6 +12,7 @@ import {
12
12
  HoistProps,
13
13
  LayoutProps,
14
14
  lookup,
15
+ TestSupportProps,
15
16
  useLocalModel,
16
17
  uses,
17
18
  XH
@@ -23,7 +24,11 @@ import {isNil} from 'lodash';
23
24
  import './AgGrid.scss';
24
25
  import {AgGridModel} from './AgGridModel';
25
26
 
26
- export interface AgGridProps extends HoistProps<AgGridModel>, GridOptions, LayoutProps {}
27
+ export interface AgGridProps
28
+ extends HoistProps<AgGridModel>,
29
+ GridOptions,
30
+ LayoutProps,
31
+ TestSupportProps {}
27
32
 
28
33
  /**
29
34
  * Minimal wrapper for AgGridReact, supporting direct use of the ag-Grid component with limited
@@ -53,7 +58,7 @@ export const [AgGrid, agGrid] = hoistCmp.withFactory<AgGridProps>({
53
58
  className: 'xh-ag-grid',
54
59
  model: uses(AgGridModel),
55
60
 
56
- render({model, className, ...props}, ref) {
61
+ render({model, className, testId, ...props}, ref) {
57
62
  if (!AgGridReact) {
58
63
  console.error(
59
64
  'ag-Grid has not been imported in to this application. Please import and ' +
@@ -90,6 +95,7 @@ export const [AgGrid, agGrid] = hoistCmp.withFactory<AgGridProps>({
90
95
  hideHeaders ? 'xh-ag-grid--hide-headers' : null
91
96
  ),
92
97
  ...layoutProps,
98
+ testId,
93
99
  item: createElement(AgGridReact, {
94
100
  ...AgGrid['DEFAULT_PROPS'],
95
101
  // Default some ag-grid props, but allow overriding.
@@ -4,9 +4,12 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {div} from '@xh/hoist/cmp/layout';
7
8
  import {BoxProps, hoistCmp, HoistProps, Intent} from '@xh/hoist/core';
9
+ import {TEST_ID} from '@xh/hoist/utils/js';
10
+ import {splitLayoutProps} from '@xh/hoist/utils/react';
8
11
  import classNames from 'classnames';
9
- import {div} from '@xh/hoist/cmp/layout';
12
+ import {merge} from 'lodash';
10
13
  import './Badge.scss';
11
14
 
12
15
  export interface BadgeProps extends HoistProps, BoxProps {
@@ -26,8 +29,10 @@ export const [Badge, badge] = hoistCmp.withFactory<BadgeProps>({
26
29
 
27
30
  className: 'xh-badge',
28
31
 
29
- render({className, intent, compact = false, ...props}) {
30
- const classes = [];
32
+ render(props, ref) {
33
+ const classes = [],
34
+ [layoutProps, {className, intent, compact, children, testId, ...restProps}] =
35
+ splitLayoutProps(props);
31
36
 
32
37
  if (intent) {
33
38
  classes.push(`xh-badge--intent-${intent}`);
@@ -37,9 +42,17 @@ export const [Badge, badge] = hoistCmp.withFactory<BadgeProps>({
37
42
  classes.push('xh-badge--compact');
38
43
  }
39
44
 
45
+ const divProps = merge(
46
+ {className: classNames(className, classes)},
47
+ {style: layoutProps},
48
+ {[TEST_ID]: testId},
49
+ restProps
50
+ );
51
+
40
52
  return div({
41
- className: classNames(className, classes),
42
- ...props
53
+ ref,
54
+ ...divProps,
55
+ items: children
43
56
  });
44
57
  }
45
58
  });
@@ -6,19 +6,20 @@
6
6
  */
7
7
  import composeRefs from '@seznam/compose-react-refs';
8
8
  import {box, div} from '@xh/hoist/cmp/layout';
9
- import {placeholder} from '../layout';
10
9
  import {
11
- lookup,
12
10
  hoistCmp,
13
11
  HoistModel,
12
+ HoistProps,
13
+ LayoutProps,
14
+ lookup,
15
+ PlainObject,
16
+ TestSupportProps,
14
17
  useLocalModel,
15
18
  uses,
16
- XH,
17
- BoxProps,
18
- HoistProps,
19
- PlainObject
19
+ XH
20
20
  } from '@xh/hoist/core';
21
21
  import {useContextMenu} from '@xh/hoist/dynamics/desktop';
22
+ import {Icon} from '@xh/hoist/icon';
22
23
  import {Highcharts} from '@xh/hoist/kit/highcharts';
23
24
  import {runInAction} from '@xh/hoist/mobx';
24
25
  import {
@@ -28,18 +29,18 @@ import {
28
29
  useOnVisibleChange
29
30
  } from '@xh/hoist/utils/react';
30
31
  import {assign, castArray, cloneDeep, forOwn, isEqual, isPlainObject, merge, omit} from 'lodash';
31
- import {Icon} from '@xh/hoist/icon';
32
+ import {placeholder} from '../layout';
33
+ import './Chart.scss';
32
34
  import {ChartModel} from './ChartModel';
33
- import {installZoomoutGesture} from './impl/zoomout';
34
35
  import {installCopyToClipboard} from './impl/copyToClipboard';
36
+ import {installZoomoutGesture} from './impl/zoomout';
35
37
  import {DarkTheme} from './theme/Dark';
36
38
  import {LightTheme} from './theme/Light';
37
- import './Chart.scss';
38
39
 
39
40
  installZoomoutGesture(Highcharts);
40
41
  installCopyToClipboard(Highcharts);
41
42
 
42
- export interface ChartProps extends HoistProps<ChartModel>, BoxProps {
43
+ export interface ChartProps extends HoistProps<ChartModel>, LayoutProps, TestSupportProps {
43
44
  /**
44
45
  * Ratio of width-to-height of displayed chart. If defined and greater than 0, the chart will
45
46
  * respect this ratio within the available space. Otherwise, the chart will stretch on both
@@ -58,7 +59,7 @@ export const [Chart, chart] = hoistCmp.withFactory<ChartProps>({
58
59
  model: uses(ChartModel),
59
60
  className: 'xh-chart',
60
61
 
61
- render({model, className, aspectRatio, ...props}, ref) {
62
+ render({model, className, aspectRatio, testId, ...props}, ref) {
62
63
  if (!Highcharts) {
63
64
  console.error(
64
65
  'Highcharts has not been imported in to this application. Please import and ' +
@@ -85,6 +86,7 @@ export const [Chart, chart] = hoistCmp.withFactory<ChartProps>({
85
86
  const coreContents = box({
86
87
  ...layoutProps,
87
88
  className,
89
+ testId,
88
90
  ref,
89
91
  item: div({
90
92
  style: {margin: 'auto'},
@@ -6,16 +6,16 @@
6
6
  */
7
7
  import {box, span} from '@xh/hoist/cmp/layout';
8
8
  import {
9
+ BoxProps,
9
10
  hoistCmp,
10
11
  HoistModel,
12
+ HoistProps,
11
13
  managed,
12
- BoxProps,
13
14
  useLocalModel,
14
- XH,
15
- HoistProps
15
+ XH
16
16
  } from '@xh/hoist/core';
17
17
  import {fmtDate, TIME_FMT} from '@xh/hoist/format';
18
- import {action, observable, makeObservable} from '@xh/hoist/mobx';
18
+ import {action, makeObservable, observable} from '@xh/hoist/mobx';
19
19
  import {Timer} from '@xh/hoist/utils/async';
20
20
  import {MINUTES, ONE_SECOND} from '@xh/hoist/utils/datetime';
21
21
  import {isNumber} from 'lodash';
@@ -54,11 +54,12 @@ export const [Clock, clock] = hoistCmp.withFactory<ClockProps>({
54
54
  displayName: 'Clock',
55
55
  className: 'xh-clock',
56
56
 
57
- render({className, ...props}, ref) {
57
+ render({className, testId, ...props}, ref) {
58
58
  const impl = useLocalModel(ClockLocalModel);
59
59
  return box({
60
60
  className,
61
61
  ...getLayoutProps(props),
62
+ testId,
62
63
  ref,
63
64
  item: span(impl.display)
64
65
  });
@@ -7,12 +7,13 @@
7
7
  import {AgGrid} from '@xh/hoist/cmp/ag-grid';
8
8
  import {grid} from '@xh/hoist/cmp/grid';
9
9
  import {
10
- BoxProps,
11
10
  hoistCmp,
12
11
  HoistModel,
13
12
  HoistProps,
13
+ LayoutProps,
14
14
  lookup,
15
15
  PlainObject,
16
+ TestSupportProps,
16
17
  useLocalModel,
17
18
  uses
18
19
  } from '@xh/hoist/core';
@@ -22,7 +23,7 @@ import {isFunction, merge} from 'lodash';
22
23
  import './DataView.scss';
23
24
  import {DataViewModel} from './DataViewModel';
24
25
 
25
- export interface DataViewProps extends HoistProps<DataViewModel>, BoxProps {
26
+ export interface DataViewProps extends HoistProps<DataViewModel>, LayoutProps, TestSupportProps {
26
27
  /**
27
28
  * Options for ag-Grid's API.
28
29
  *
@@ -44,13 +45,14 @@ export const [DataView, dataView] = hoistCmp.withFactory<DataViewProps>({
44
45
  model: uses(DataViewModel),
45
46
  className: 'xh-data-view',
46
47
 
47
- render({model, className, ...props}, ref) {
48
+ render({model, className, testId, ...props}, ref) {
48
49
  const [layoutProps] = splitLayoutProps(props),
49
50
  impl = useLocalModel(DataViewLocalModel);
50
51
 
51
52
  return grid({
52
53
  ...layoutProps,
53
54
  className,
55
+ testId,
54
56
  ref,
55
57
  model: model.gridModel,
56
58
  agOptions: impl.agOptions
package/cmp/form/Form.ts CHANGED
@@ -4,12 +4,19 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {DefaultHoistProps, elementFactory, hoistCmp, HoistProps, uses} from '@xh/hoist/core';
7
+ import {
8
+ DefaultHoistProps,
9
+ elementFactory,
10
+ hoistCmp,
11
+ HoistProps,
12
+ TestSupportProps,
13
+ uses
14
+ } from '@xh/hoist/core';
15
+ import {useCached} from '@xh/hoist/utils/react';
8
16
  import equal from 'fast-deep-equal';
9
17
  import {createContext, useContext} from 'react';
10
- import {useCached} from '@xh/hoist/utils/react';
11
- import {FormModel} from './FormModel';
12
18
  import {BaseFormFieldProps} from './BaseFormFieldProps';
19
+ import {FormModel} from './FormModel';
13
20
 
14
21
  /** @internal */
15
22
  export interface FormContextType {
@@ -18,13 +25,21 @@ export interface FormContextType {
18
25
 
19
26
  /** Reference to associated FormModel. */
20
27
  model?: FormModel;
28
+
29
+ /**
30
+ * Not rendered into the DOM directly - `Form` is a context provider and not a concrete
31
+ * component - but will auto-generate and apply a testId of `${formTestId}-${fieldName}`
32
+ * for every child {@link FormField} component, providing a centralized way to wire up
33
+ * a form and all of its fields for testing.
34
+ */
35
+ testId?: string;
21
36
  }
22
37
 
23
38
  /** @internal */
24
39
  export const FormContext = createContext<FormContextType>({});
25
40
  const formContextProvider = elementFactory(FormContext.Provider);
26
41
 
27
- export interface FormProps extends HoistProps<FormModel> {
42
+ export interface FormProps extends HoistProps<FormModel>, TestSupportProps {
28
43
  /**
29
44
  * Defaults for certain props on child/nested FormFields.
30
45
  * @see FormField (note there are both desktop and mobile implementations).
@@ -50,14 +65,18 @@ export const [Form, form] = hoistCmp.withFactory<FormProps>({
50
65
  displayName: 'Form',
51
66
  model: uses(FormModel, {publishMode: 'none'}),
52
67
 
53
- render({model, fieldDefaults = {}, children}) {
68
+ render({model, fieldDefaults = {}, testId, children}) {
54
69
  // gather own and inherited field defaults...
55
70
  const parentDefaults = useContext(FormContext).fieldDefaults;
56
71
  if (parentDefaults) fieldDefaults = {...parentDefaults, ...fieldDefaults};
57
72
 
58
73
  // ...and deliver as a cached context to avoid spurious re-renders
59
74
  const formContext = useCached(
60
- {model, fieldDefaults},
75
+ {
76
+ model,
77
+ fieldDefaults,
78
+ testId
79
+ },
61
80
  (a, b) => a.model === b.model && equal(a.fieldDefaults, b.fieldDefaults)
62
81
  );
63
82
  return formContextProvider({value: formContext, items: children});