@oneuptime/common 12.0.7 → 12.0.8

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 (186) hide show
  1. package/Models/DatabaseModels/TelemetryEntity.ts +45 -0
  2. package/Models/DatabaseModels/TelemetryEntityRelationship.ts +26 -0
  3. package/Models/DatabaseModels/WorkflowLog.ts +38 -0
  4. package/Server/API/UserAPI.ts +16 -1
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  8. package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
  9. package/Server/Services/TelemetryEntityRelationshipService.ts +3 -0
  10. package/Server/Services/TelemetryEntityService.ts +114 -1
  11. package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
  12. package/Server/Types/Workflow/TriggerCode.ts +12 -0
  13. package/Server/Types/Workflow/Workflow.ts +5 -0
  14. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
  15. package/Server/Utils/VM/VMAPI.ts +56 -8
  16. package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
  17. package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
  18. package/Tests/Server/Services/TelemetryEntityManualCreate.test.ts +248 -0
  19. package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
  20. package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
  21. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
  22. package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
  23. package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
  24. package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
  25. package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
  26. package/Tests/Types/Workflow/TemplateSyntax.test.ts +379 -0
  27. package/Tests/Types/Workflow/Templates.test.ts +403 -0
  28. package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
  29. package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
  30. package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
  31. package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
  32. package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
  33. package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
  34. package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
  35. package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
  36. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
  37. package/Tests/UI/Components/Workflow/GraphLint.test.ts +821 -0
  38. package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +287 -0
  39. package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +185 -0
  40. package/Tests/UI/Components/Workflow/ModelSchema.test.ts +174 -0
  41. package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +152 -0
  42. package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
  43. package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
  44. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
  45. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +240 -0
  46. package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
  47. package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
  48. package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
  49. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +31 -0
  50. package/Types/SerializableObjectDictionary.ts +133 -39
  51. package/Types/Telemetry/EntitySource.ts +40 -0
  52. package/Types/Telemetry/EntityType.ts +27 -0
  53. package/Types/Telemetry/EntityTypeGroups.ts +65 -0
  54. package/Types/Workflow/Component.ts +29 -0
  55. package/Types/Workflow/Components/API.ts +35 -0
  56. package/Types/Workflow/Components/BaseModel.ts +7 -3
  57. package/Types/Workflow/StepTrace.ts +181 -0
  58. package/Types/Workflow/TemplateSyntax.ts +499 -0
  59. package/Types/Workflow/Templates.ts +316 -0
  60. package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
  61. package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
  62. package/UI/Components/Dictionary/Dictionary.tsx +23 -6
  63. package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
  64. package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
  65. package/UI/Components/Forms/Types/Field.ts +7 -0
  66. package/UI/Components/Forms/Validation.ts +63 -1
  67. package/UI/Components/Input/Input.tsx +31 -3
  68. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
  69. package/UI/Components/Modal/Modal.tsx +37 -4
  70. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
  71. package/UI/Components/Workflow/ArgumentsForm.tsx +194 -7
  72. package/UI/Components/Workflow/Component.tsx +28 -26
  73. package/UI/Components/Workflow/ComponentSettingsModal.tsx +69 -8
  74. package/UI/Components/Workflow/GraphLint.ts +591 -0
  75. package/UI/Components/Workflow/ModelColumnEditor.tsx +396 -0
  76. package/UI/Components/Workflow/ModelSchema.ts +196 -0
  77. package/UI/Components/Workflow/RunForm.tsx +41 -7
  78. package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
  79. package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
  80. package/UI/Components/Workflow/Utils.ts +93 -1
  81. package/UI/Components/Workflow/Workflow.tsx +77 -4
  82. package/UI/Types/LayeredDismissal.ts +31 -0
  83. package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
  84. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
  85. package/Utils/Monitor/NetworkTopologyUtil.ts +45 -7
  86. package/Utils/Telemetry/EntityKey.ts +66 -0
  87. package/build/dist/Models/DatabaseModels/TelemetryEntity.js +47 -0
  88. package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +1 -1
  89. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js +27 -0
  90. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +1 -1
  91. package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
  92. package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
  93. package/build/dist/Server/API/UserAPI.js +16 -1
  94. package/build/dist/Server/API/UserAPI.js.map +1 -1
  95. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
  96. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
  97. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
  98. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js.map +1 -0
  99. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  100. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  101. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -6
  102. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  103. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +3 -0
  104. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  105. package/build/dist/Server/Services/TelemetryEntityService.js +91 -1
  106. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  107. package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
  108. package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
  109. package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
  110. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
  111. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
  112. package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
  113. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  114. package/build/dist/Types/SerializableObjectDictionary.js +133 -39
  115. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  116. package/build/dist/Types/Telemetry/EntitySource.js +39 -0
  117. package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
  118. package/build/dist/Types/Telemetry/EntityType.js +27 -0
  119. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  120. package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
  121. package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
  122. package/build/dist/Types/Workflow/Component.js +21 -0
  123. package/build/dist/Types/Workflow/Component.js.map +1 -1
  124. package/build/dist/Types/Workflow/Components/API.js +35 -0
  125. package/build/dist/Types/Workflow/Components/API.js.map +1 -1
  126. package/build/dist/Types/Workflow/Components/BaseModel.js +7 -3
  127. package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
  128. package/build/dist/Types/Workflow/StepTrace.js +104 -0
  129. package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
  130. package/build/dist/Types/Workflow/TemplateSyntax.js +326 -0
  131. package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
  132. package/build/dist/Types/Workflow/Templates.js +218 -0
  133. package/build/dist/Types/Workflow/Templates.js.map +1 -0
  134. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
  135. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
  136. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
  137. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
  138. package/build/dist/UI/Components/Dictionary/Dictionary.js +22 -6
  139. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  140. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
  141. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
  142. package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
  143. package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
  144. package/build/dist/UI/Components/Forms/Validation.js +44 -0
  145. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  146. package/build/dist/UI/Components/Input/Input.js +12 -4
  147. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  148. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
  149. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  150. package/build/dist/UI/Components/Modal/Modal.js +31 -5
  151. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  152. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
  153. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  154. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +140 -11
  155. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  156. package/build/dist/UI/Components/Workflow/Component.js +20 -19
  157. package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
  158. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +31 -3
  159. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
  160. package/build/dist/UI/Components/Workflow/GraphLint.js +396 -0
  161. package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
  162. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +225 -0
  163. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
  164. package/build/dist/UI/Components/Workflow/ModelSchema.js +118 -0
  165. package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
  166. package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
  167. package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
  168. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
  169. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
  170. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
  171. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
  172. package/build/dist/UI/Components/Workflow/Utils.js +73 -1
  173. package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
  174. package/build/dist/UI/Components/Workflow/Workflow.js +51 -6
  175. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  176. package/build/dist/UI/Types/LayeredDismissal.js +21 -0
  177. package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
  178. package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
  179. package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
  180. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
  181. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
  182. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +35 -6
  183. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  184. package/build/dist/Utils/Telemetry/EntityKey.js +52 -0
  185. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  186. package/package.json +1 -1
@@ -0,0 +1,382 @@
1
+ import BasicForm from "../../../../UI/Components/Forms/BasicForm";
2
+ import ColorPicker from "../../../../UI/Components/Forms/Fields/ColorPicker";
3
+ import IconPicker from "../../../../UI/Components/Forms/Fields/IconPicker";
4
+ import Fields from "../../../../UI/Components/Forms/Types/Fields";
5
+ import FormFieldSchemaType from "../../../../UI/Components/Forms/Types/FormFieldSchemaType";
6
+ import FormValues from "../../../../UI/Components/Forms/Types/FormValues";
7
+ import Modal from "../../../../UI/Components/Modal/Modal";
8
+ import Color from "../../../../Types/Color";
9
+ import IconProp from "../../../../Types/Icon/IconProp";
10
+ import getJestMockFunction, { MockFunction } from "../../../../Tests/MockType";
11
+ import "@testing-library/jest-dom";
12
+ import {
13
+ cleanup,
14
+ fireEvent,
15
+ render,
16
+ screen,
17
+ waitFor,
18
+ within,
19
+ } from "@testing-library/react";
20
+ import React from "react";
21
+ import { afterEach, describe, expect, test } from "@jest/globals";
22
+
23
+ /*
24
+ * The colour and icon fields are triggers dressed as text boxes: a readOnly
25
+ * input whose only job is to open a portalled popup, which it used to do on
26
+ * click and on nothing else. Every key was dead. That left a keyboard user with
27
+ * no way in at all — and on the Create Label form (issue #3143) the colour is
28
+ * required, so no way in meant no way to submit the form either.
29
+ *
30
+ * Both fields drive UseAnchoredFieldPopup, so the behaviour is stated once per
31
+ * field here and the shared parts are pinned in both.
32
+ */
33
+
34
+ const OPENING_KEYS: Array<string> = ["Enter", " ", "ArrowDown", "ArrowUp"];
35
+
36
+ type RenderColorPickerFunction = (
37
+ props?: Partial<{ readOnly: boolean; disabled: boolean }>,
38
+ ) => MockFunction;
39
+
40
+ const renderColorPicker: RenderColorPickerFunction = (
41
+ props: Partial<{ readOnly: boolean; disabled: boolean }> = {},
42
+ ): MockFunction => {
43
+ const onChange: MockFunction = getJestMockFunction();
44
+
45
+ render(
46
+ <ColorPicker
47
+ dataTestId="color-value"
48
+ placeholder="Please select color for this label."
49
+ onChange={onChange}
50
+ readOnly={props.readOnly}
51
+ disabled={props.disabled}
52
+ tabIndex={0}
53
+ />,
54
+ );
55
+
56
+ return onChange;
57
+ };
58
+
59
+ type GetFieldFunction = (testId: string) => HTMLInputElement;
60
+
61
+ const getField: GetFieldFunction = (testId: string): HTMLInputElement => {
62
+ return screen.getByTestId(testId) as HTMLInputElement;
63
+ };
64
+
65
+ describe("Opening an anchored field popup from the keyboard", () => {
66
+ afterEach(() => {
67
+ cleanup();
68
+ });
69
+
70
+ describe("ColorPicker", () => {
71
+ test.each(OPENING_KEYS)(
72
+ "%p opens the popup and moves focus into it",
73
+ (key: string) => {
74
+ renderColorPicker();
75
+
76
+ const field: HTMLInputElement = getField("color-value");
77
+
78
+ field.focus();
79
+ fireEvent.keyDown(field, { key });
80
+
81
+ const popup: HTMLElement = screen.getByTestId("color-picker-popup");
82
+
83
+ expect(popup).toBeInTheDocument();
84
+
85
+ /*
86
+ * Landing on the popup's first control is what makes the field
87
+ * fillable: for a colour that control is ChromePicker's hex box, so
88
+ * "open it and type the brand hex" is a complete keyboard path.
89
+ */
90
+ expect(popup.contains(document.activeElement)).toBe(true);
91
+ },
92
+ );
93
+
94
+ test("a keyboard user can open the picker and set a hex end to end", async () => {
95
+ const onChange: MockFunction = renderColorPicker();
96
+ const field: HTMLInputElement = getField("color-value");
97
+
98
+ field.focus();
99
+ fireEvent.keyDown(field, { key: "Enter" });
100
+
101
+ const focusedControl: HTMLElement = document.activeElement as HTMLElement;
102
+
103
+ expect(focusedControl.tagName).toEqual("INPUT");
104
+
105
+ fireEvent.change(focusedControl, { target: { value: "#32a852" } });
106
+
107
+ await waitFor(() => {
108
+ expect(onChange).toHaveBeenCalledWith(new Color("#32a852"));
109
+ });
110
+
111
+ // Escape puts the user back where they were, with the value kept.
112
+ fireEvent.keyDown(focusedControl, { key: "Escape" });
113
+
114
+ expect(screen.queryByTestId("color-picker-popup")).toBeNull();
115
+ expect(document.activeElement).toBe(field);
116
+ expect(field.value).toEqual("#32a852");
117
+ });
118
+
119
+ test("an opening key is claimed, so nothing downstream acts on it too", () => {
120
+ renderColorPicker();
121
+
122
+ const field: HTMLInputElement = getField("color-value");
123
+
124
+ /*
125
+ * fireEvent returns false when a cancelable event had preventDefault
126
+ * called on it. That is what stops Input's own onEnterPress from also
127
+ * firing, and what would stop a real <form> around the field from
128
+ * submitting on the way past.
129
+ */
130
+ expect(fireEvent.keyDown(field, { key: "Enter" })).toBe(false);
131
+ expect(fireEvent.keyDown(field, { key: " " })).toBe(false);
132
+ });
133
+
134
+ test("Enter on an already open field moves focus in rather than closing it", () => {
135
+ renderColorPicker();
136
+
137
+ const field: HTMLInputElement = getField("color-value");
138
+
139
+ fireEvent.click(field);
140
+
141
+ const popup: HTMLElement = screen.getByTestId("color-picker-popup");
142
+
143
+ // A pointer user's popup opens under their cursor and leaves focus alone.
144
+ expect(popup.contains(document.activeElement)).toBe(false);
145
+
146
+ field.focus();
147
+ fireEvent.keyDown(field, { key: "Enter" });
148
+
149
+ expect(screen.getByTestId("color-picker-popup")).toBeInTheDocument();
150
+ expect(popup.contains(document.activeElement)).toBe(true);
151
+ });
152
+
153
+ test("keys that are not opening keys are left alone", () => {
154
+ renderColorPicker();
155
+
156
+ const field: HTMLInputElement = getField("color-value");
157
+
158
+ expect(fireEvent.keyDown(field, { key: "Tab" })).toBe(true);
159
+ expect(fireEvent.keyDown(field, { key: "a" })).toBe(true);
160
+ expect(screen.queryByTestId("color-picker-popup")).toBeNull();
161
+ });
162
+
163
+ test("a readOnly field opens for neither pointer nor keyboard", () => {
164
+ renderColorPicker({ readOnly: true });
165
+
166
+ const field: HTMLInputElement = getField("color-value");
167
+
168
+ fireEvent.click(field);
169
+ fireEvent.keyDown(field, { key: "Enter" });
170
+ fireEvent.keyDown(field, { key: "ArrowDown" });
171
+
172
+ expect(screen.queryByTestId("color-picker-popup")).toBeNull();
173
+ });
174
+
175
+ test("a disabled field opens for neither pointer nor keyboard", () => {
176
+ renderColorPicker({ disabled: true });
177
+
178
+ const field: HTMLInputElement = getField("color-value");
179
+
180
+ fireEvent.click(field);
181
+ fireEvent.keyDown(field, { key: "Enter" });
182
+ fireEvent.keyDown(field, { key: "ArrowDown" });
183
+
184
+ expect(screen.queryByTestId("color-picker-popup")).toBeNull();
185
+ });
186
+
187
+ test("the field advertises the popup it controls", () => {
188
+ renderColorPicker();
189
+
190
+ const field: HTMLInputElement = getField("color-value");
191
+
192
+ expect(field).toHaveAttribute("aria-haspopup", "dialog");
193
+ expect(field).toHaveAttribute("aria-expanded", "false");
194
+ expect(field).not.toHaveAttribute("aria-controls");
195
+
196
+ fireEvent.click(field);
197
+
198
+ const popup: HTMLElement = screen.getByTestId("color-picker-popup");
199
+
200
+ expect(field).toHaveAttribute("aria-expanded", "true");
201
+ expect(field.getAttribute("aria-controls")).toEqual(popup.id);
202
+ expect(popup.id).toBeTruthy();
203
+ expect(popup).toHaveAttribute("role", "dialog");
204
+ expect(popup).toHaveAttribute("aria-label", "Color picker");
205
+ });
206
+ });
207
+
208
+ describe("IconPicker", () => {
209
+ type RenderIconPickerFunction = () => MockFunction;
210
+
211
+ const renderIconPicker: RenderIconPickerFunction = (): MockFunction => {
212
+ const onChange: MockFunction = getJestMockFunction();
213
+
214
+ render(
215
+ <IconPicker
216
+ dataTestId="icon-value"
217
+ placeholder="No icon"
218
+ onChange={onChange}
219
+ tabIndex={0}
220
+ />,
221
+ );
222
+
223
+ return onChange;
224
+ };
225
+
226
+ test.each(OPENING_KEYS)(
227
+ "%p opens the popup and moves focus into it",
228
+ (key: string) => {
229
+ renderIconPicker();
230
+
231
+ const field: HTMLInputElement = getField("icon-value");
232
+
233
+ field.focus();
234
+ fireEvent.keyDown(field, { key });
235
+
236
+ const popup: HTMLElement = screen.getByTestId("icon-picker-popup");
237
+
238
+ expect(popup).toBeInTheDocument();
239
+ expect(popup.contains(document.activeElement)).toBe(true);
240
+ },
241
+ );
242
+
243
+ test("a keyboard user can search for an icon and choose it", () => {
244
+ const onChange: MockFunction = renderIconPicker();
245
+ const field: HTMLInputElement = getField("icon-value");
246
+
247
+ field.focus();
248
+ fireEvent.keyDown(field, { key: "Enter" });
249
+
250
+ const popup: HTMLElement = screen.getByTestId("icon-picker-popup");
251
+ const searchBox: HTMLElement = document.activeElement as HTMLElement;
252
+
253
+ expect(popup.contains(searchBox)).toBe(true);
254
+
255
+ fireEvent.change(searchBox, { target: { value: IconProp.Alert } });
256
+
257
+ /*
258
+ * The cells used to be plain divs, so Tab skipped straight past the only
259
+ * control that can set this field.
260
+ */
261
+ const cell: HTMLElement = within(popup).getByRole("button", {
262
+ name: IconProp.Alert,
263
+ });
264
+
265
+ cell.focus();
266
+ expect(document.activeElement).toBe(cell);
267
+
268
+ fireEvent.click(cell);
269
+
270
+ expect(onChange).toHaveBeenCalledWith(IconProp.Alert);
271
+ expect(screen.queryByTestId("icon-picker-popup")).toBeNull();
272
+ expect(document.activeElement).toBe(field);
273
+ expect(field.value).toEqual(IconProp.Alert);
274
+ });
275
+
276
+ test("the field advertises the popup it controls", () => {
277
+ renderIconPicker();
278
+
279
+ const field: HTMLInputElement = getField("icon-value");
280
+
281
+ expect(field).toHaveAttribute("aria-haspopup", "dialog");
282
+ expect(field).toHaveAttribute("aria-expanded", "false");
283
+
284
+ fireEvent.click(field);
285
+
286
+ const popup: HTMLElement = screen.getByTestId("icon-picker-popup");
287
+
288
+ expect(field).toHaveAttribute("aria-expanded", "true");
289
+ expect(field.getAttribute("aria-controls")).toEqual(popup.id);
290
+ expect(popup).toHaveAttribute("role", "dialog");
291
+ expect(popup).toHaveAttribute("aria-label", "Icon picker");
292
+ });
293
+ });
294
+
295
+ /*
296
+ * The two surfaces the field actually ships on: inside a form, where Enter is
297
+ * a submit, and inside a modal, where Escape is a dismissal.
298
+ */
299
+ describe("inside the Label form and its modal", () => {
300
+ const LABEL_FIELDS: Fields<FormValues<Record<string, unknown>>> = [
301
+ {
302
+ field: { name: true },
303
+ title: "Name",
304
+ fieldType: FormFieldSchemaType.Text,
305
+ required: true,
306
+ dataTestId: "name",
307
+ },
308
+ {
309
+ field: { color: true },
310
+ title: "Label Color",
311
+ fieldType: FormFieldSchemaType.Color,
312
+ required: true,
313
+ dataTestId: "color-value",
314
+ },
315
+ ];
316
+
317
+ test("Enter on the colour field opens the picker and posts nothing", async () => {
318
+ const onSubmit: MockFunction = getJestMockFunction();
319
+
320
+ render(
321
+ <BasicForm
322
+ id="create-label"
323
+ fields={LABEL_FIELDS}
324
+ onSubmit={onSubmit}
325
+ submitButtonText="Create Label"
326
+ />,
327
+ );
328
+
329
+ fireEvent.change(screen.getByTestId("name"), {
330
+ target: { value: "WB Unit-BB" },
331
+ });
332
+
333
+ const field: HTMLInputElement = getField("color-value");
334
+
335
+ field.focus();
336
+ expect(fireEvent.keyDown(field, { key: "Enter" })).toBe(false);
337
+
338
+ expect(screen.getByTestId("color-picker-popup")).toBeInTheDocument();
339
+
340
+ /*
341
+ * The picker is the only thing that happens: no submit, so no "Label
342
+ * Color is required." for a field being filled in right now.
343
+ */
344
+ await waitFor(() => {
345
+ expect(screen.queryByText("Label Color is required.")).toBeNull();
346
+ });
347
+
348
+ expect(onSubmit).not.toHaveBeenCalled();
349
+ });
350
+
351
+ test("Escape closes the picker without taking the modal with it", () => {
352
+ const onClose: MockFunction = getJestMockFunction();
353
+
354
+ render(
355
+ <Modal title="Create New Label" onClose={onClose}>
356
+ <ColorPicker
357
+ dataTestId="color-value"
358
+ placeholder="Please select color for this label."
359
+ onChange={getJestMockFunction()}
360
+ tabIndex={0}
361
+ />
362
+ </Modal>,
363
+ );
364
+
365
+ const field: HTMLInputElement = getField("color-value");
366
+
367
+ field.focus();
368
+ fireEvent.keyDown(field, { key: "ArrowDown" });
369
+
370
+ expect(screen.getByTestId("color-picker-popup")).toBeInTheDocument();
371
+
372
+ fireEvent.keyDown(document.activeElement as HTMLElement, {
373
+ key: "Escape",
374
+ });
375
+
376
+ expect(screen.queryByTestId("color-picker-popup")).toBeNull();
377
+ expect(onClose).not.toHaveBeenCalled();
378
+ expect(screen.getByTestId("modal")).toBeInTheDocument();
379
+ expect(document.activeElement).toBe(field);
380
+ });
381
+ });
382
+ });