@oneuptime/common 12.0.31 → 12.0.32

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 (188) hide show
  1. package/Models/DatabaseModels/AlertState.ts +0 -2
  2. package/Models/DatabaseModels/Domain.ts +1 -1
  3. package/Models/DatabaseModels/Incident.ts +1 -1
  4. package/Models/DatabaseModels/IncidentTemplate.ts +1 -1
  5. package/Models/DatabaseModels/Index.ts +2 -0
  6. package/Models/DatabaseModels/MetricType.ts +0 -2
  7. package/Models/DatabaseModels/MonitorStatusTimeline.ts +0 -2
  8. package/Models/DatabaseModels/MonitorTest.ts +0 -2
  9. package/Models/DatabaseModels/NetworkDevice.ts +121 -2
  10. package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +3 -1
  11. package/Models/DatabaseModels/NetworkDeviceRole.ts +648 -0
  12. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -1
  13. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -1
  14. package/Models/DatabaseModels/StatusPageAnnouncement.ts +0 -2
  15. package/Models/DatabaseModels/StatusPageAnnouncementTemplate.ts +0 -2
  16. package/Models/DatabaseModels/StatusPagePrivateUser.ts +0 -2
  17. package/Models/DatabaseModels/StatusPageResource.ts +0 -2
  18. package/Models/DatabaseModels/StatusPageSubscriber.ts +0 -2
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.ts +72 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  21. package/Server/Services/DatabaseService.ts +39 -9
  22. package/Server/Services/Index.ts +2 -0
  23. package/Server/Services/NetworkDeviceRoleService.ts +90 -0
  24. package/Server/Services/ProjectService.ts +85 -0
  25. package/Server/Services/UserNotificationSettingService.ts +0 -6
  26. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +31 -1
  27. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +35 -8
  28. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +107 -3
  29. package/Tests/App/Dashboard/DashboardWideChartZoom.test.tsx +401 -0
  30. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +318 -0
  31. package/Tests/App/Dashboard/NetworkDeviceBulkDelete.test.tsx +357 -0
  32. package/Tests/App/Dashboard/NetworkSideMenu.test.tsx +590 -0
  33. package/Tests/App/Dashboard/SideMenuHarness.tsx +11 -3
  34. package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +115 -41
  35. package/Tests/Server/Infrastructure/Postgres/NetworkDeviceRoleTableMigration.test.ts +476 -0
  36. package/Tests/Server/Services/DatabaseServiceGenerateSlug.test.ts +215 -0
  37. package/Tests/Server/Services/NetworkDeviceRoleModel.test.ts +672 -0
  38. package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +1 -1
  39. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +67 -4
  40. package/Tests/Server/Services/ScheduledMaintenanceSubscriberEndTime.test.ts +369 -0
  41. package/Tests/Server/Services/UserNotificationSettingMonitorDefaults.test.ts +248 -0
  42. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +43 -2
  43. package/Tests/Server/Utils/BasicCron.test.ts +283 -0
  44. package/Tests/Server/Utils/IncomingCallPhoneNumber.test.ts +174 -0
  45. package/Tests/Types/NetworkDevice/DefaultNetworkDeviceRole.test.ts +417 -0
  46. package/Tests/UI/Components/Charts/ChartClickDoubleClickDisambiguation.test.tsx +221 -0
  47. package/Tests/UI/Components/Charts/ChartDoubleClickReset.test.tsx +220 -0
  48. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +751 -0
  49. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +475 -43
  50. package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +89 -0
  51. package/Tests/UI/Components/NavBarCommandKGating.test.tsx +103 -8
  52. package/Tests/UI/Components/SideMenuItem.test.tsx +30 -0
  53. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +30 -0
  54. package/Tests/UI/Utils/UseDashboardTimeRangeZoom.test.tsx +187 -0
  55. package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +302 -0
  56. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +242 -0
  57. package/Tests/Utils/Monitor/NetworkDeviceRoleCatalog.test.ts +981 -0
  58. package/Tests/Utils/Monitor/NetworkTopologyRoleCatalog.test.ts +865 -0
  59. package/Tests/Utils/NetworkDevice/DeviceRoleKeyUtil.test.ts +538 -0
  60. package/Tests/Utils/Slo/SloEvaluation.test.ts +171 -0
  61. package/Tests/Utils/Slug.test.ts +61 -1
  62. package/Tests/jest.setup.ts +19 -0
  63. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +61 -0
  64. package/Types/NetworkDevice/DefaultNetworkDeviceRole.ts +145 -0
  65. package/Types/Permission.ts +45 -0
  66. package/UI/Components/Charts/Area/AreaChart.tsx +7 -0
  67. package/UI/Components/Charts/Bar/BarChart.tsx +6 -0
  68. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +9 -1
  69. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +97 -35
  70. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +14 -0
  71. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +97 -35
  72. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +13 -0
  73. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  74. package/UI/Components/ModelTable/BaseModelTable.tsx +50 -15
  75. package/UI/Components/Navbar/NavBar.tsx +62 -13
  76. package/UI/Components/Navbar/NavBarMenuModal.tsx +7 -2
  77. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +2 -0
  78. package/UI/Components/SideMenu/SideMenu.tsx +12 -2
  79. package/UI/Components/SideMenu/SideMenuItem.tsx +4 -1
  80. package/UI/Components/SideMenu/SideMenuSection.tsx +14 -2
  81. package/UI/Utils/UseDashboardTimeRangeZoom.ts +77 -0
  82. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +118 -0
  83. package/Utils/Monitor/NetworkDeviceRoleCatalog.ts +232 -0
  84. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +2 -1
  85. package/Utils/Monitor/NetworkTopologyUtil.ts +116 -33
  86. package/Utils/NetworkDevice/DeviceRoleKeyUtil.ts +137 -0
  87. package/Utils/Rules/RulePatternMatchUtil.ts +1 -1
  88. package/Utils/Slug.ts +34 -1
  89. package/build/dist/Models/DatabaseModels/AlertState.js +0 -2
  90. package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
  91. package/build/dist/Models/DatabaseModels/Domain.js +1 -1
  92. package/build/dist/Models/DatabaseModels/Domain.js.map +1 -1
  93. package/build/dist/Models/DatabaseModels/Incident.js +1 -1
  94. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +1 -1
  96. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  98. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/MetricType.js +0 -2
  100. package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +0 -2
  102. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  103. package/build/dist/Models/DatabaseModels/MonitorTest.js +0 -2
  104. package/build/dist/Models/DatabaseModels/MonitorTest.js.map +1 -1
  105. package/build/dist/Models/DatabaseModels/NetworkDevice.js +124 -3
  106. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  107. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +3 -1
  108. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -1
  109. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js +677 -0
  110. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js.map +1 -0
  111. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -1
  112. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -1
  114. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +0 -2
  116. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js +0 -2
  118. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +0 -2
  120. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPageResource.js +0 -2
  122. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  123. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +0 -2
  124. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js +39 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js.map +1 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  129. package/build/dist/Server/Services/DatabaseService.js +31 -5
  130. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  131. package/build/dist/Server/Services/Index.js +2 -0
  132. package/build/dist/Server/Services/Index.js.map +1 -1
  133. package/build/dist/Server/Services/NetworkDeviceRoleService.js +70 -0
  134. package/build/dist/Server/Services/NetworkDeviceRoleService.js.map +1 -0
  135. package/build/dist/Server/Services/ProjectService.js +65 -0
  136. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  137. package/build/dist/Server/Services/UserNotificationSettingService.js +0 -1
  138. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  139. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js +105 -0
  140. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js.map +1 -0
  141. package/build/dist/Types/Permission.js +40 -0
  142. package/build/dist/Types/Permission.js.map +1 -1
  143. package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
  144. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  145. package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
  146. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  147. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -1
  148. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  149. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +79 -31
  150. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  151. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +7 -1
  152. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  153. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +79 -31
  154. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  155. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +14 -0
  156. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -0
  157. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  158. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  159. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +39 -13
  160. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  161. package/build/dist/UI/Components/Navbar/NavBar.js +44 -9
  162. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  163. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  164. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  165. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +1 -1
  166. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  167. package/build/dist/UI/Components/SideMenu/SideMenu.js +9 -6
  168. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  169. package/build/dist/UI/Components/SideMenu/SideMenuItem.js +1 -1
  170. package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
  171. package/build/dist/UI/Components/SideMenu/SideMenuSection.js +7 -2
  172. package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
  173. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js +42 -0
  174. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js.map +1 -0
  175. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +74 -0
  176. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -0
  177. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js +126 -0
  178. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js.map +1 -0
  179. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +2 -1
  180. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  181. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +67 -32
  182. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  183. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js +114 -0
  184. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js.map +1 -0
  185. package/build/dist/Utils/Rules/RulePatternMatchUtil.js +1 -1
  186. package/build/dist/Utils/Slug.js +31 -1
  187. package/build/dist/Utils/Slug.js.map +1 -1
  188. package/package.json +1 -1
@@ -0,0 +1,590 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
2
+ import "@testing-library/jest-dom";
3
+ import { act, cleanup, fireEvent, RenderResult } from "@testing-library/react";
4
+ import * as React from "react";
5
+ import { FunctionComponent } from "react";
6
+ import NetworkSideMenu from "../../../../App/FeatureSet/Dashboard/src/Components/Network/NetworkSideMenu";
7
+ import NetworkDeviceSideMenu from "../../../../App/FeatureSet/Dashboard/src/Pages/NetworkDevice/SideMenu";
8
+ import { getNetworkDeviceBreadcrumbs } from "../../../../App/FeatureSet/Dashboard/src/Pages/NetworkDevice/Utils/Breadcrumbs";
9
+ import NetworkSiteSideMenu from "../../../../App/FeatureSet/Dashboard/src/Pages/NetworkSite/SideMenu";
10
+ import { getNetworkSiteBreadcrumbs } from "../../../../App/FeatureSet/Dashboard/src/Pages/NetworkSite/Utils/Breadcrumbs";
11
+ import PageMap from "../../../../App/FeatureSet/Dashboard/src/Utils/PageMap";
12
+ import { RouteUtil } from "../../../../App/FeatureSet/Dashboard/src/Utils/RouteMap";
13
+ import Link from "../../../Types/Link";
14
+ import {
15
+ allLinks,
16
+ DESKTOP_WIDTH,
17
+ hrefsInMenu,
18
+ iconCountIn,
19
+ isExpanded,
20
+ linksIn,
21
+ MenuLink,
22
+ MOBILE_WIDTH,
23
+ mobileSummaryText,
24
+ PROJECT_ID,
25
+ goTo,
26
+ renderMenu,
27
+ routeFor,
28
+ sectionBody,
29
+ sectionTitlesInOrder,
30
+ sectionToggle,
31
+ setViewportWidth,
32
+ titlesInMenu,
33
+ } from "./SideMenuHarness";
34
+
35
+ /*
36
+ * Network Devices and Network Sites are two route families, but one product.
37
+ * The shared menu is the map of that product, so an organisational change has
38
+ * three independent failure modes worth pinning together:
39
+ *
40
+ * - a page can move to the wrong section, disappear, or be duplicated;
41
+ * - the two route-family wrappers can quietly stop rendering the same menu;
42
+ * - breadcrumbs and the responsive summary can retain the old section name.
43
+ *
44
+ * These tests render the real menu against the real RouteMap. The complete
45
+ * expected taxonomy is written here rather than inferred from the component,
46
+ * so changing the production array alone cannot make the regression pass.
47
+ */
48
+
49
+ type BreadcrumbGetter = (path: string) => Array<Link> | undefined;
50
+
51
+ interface ExpectedMenuEntry {
52
+ title: string;
53
+ pageMapKey: string;
54
+ getBreadcrumbs: BreadcrumbGetter;
55
+ breadcrumbTitles: Array<string>;
56
+ resetsMapDrill?: boolean;
57
+ }
58
+
59
+ interface ExpectedMenuSection {
60
+ title: string;
61
+ defaultCollapsed: boolean;
62
+ entries: Array<ExpectedMenuEntry>;
63
+ }
64
+
65
+ interface SideMenuWrapper {
66
+ name: string;
67
+ Component: FunctionComponent;
68
+ }
69
+
70
+ const EXPECTED_SECTIONS: Array<ExpectedMenuSection> = [
71
+ {
72
+ title: "Network",
73
+ defaultCollapsed: false,
74
+ entries: [
75
+ {
76
+ title: "Overview",
77
+ pageMapKey: PageMap.NETWORK_OVERVIEW,
78
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
79
+ breadcrumbTitles: ["Project", "Network", "Overview"],
80
+ },
81
+ {
82
+ title: "Devices",
83
+ pageMapKey: PageMap.NETWORK_DEVICES,
84
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
85
+ breadcrumbTitles: ["Project", "Network", "Devices"],
86
+ },
87
+ {
88
+ title: "Sites",
89
+ pageMapKey: PageMap.NETWORK_SITES,
90
+ getBreadcrumbs: getNetworkSiteBreadcrumbs,
91
+ breadcrumbTitles: ["Project", "Network", "Sites"],
92
+ },
93
+ {
94
+ title: "Endpoints",
95
+ pageMapKey: PageMap.NETWORK_DEVICE_ENDPOINTS,
96
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
97
+ breadcrumbTitles: ["Project", "Network", "Endpoints"],
98
+ },
99
+ {
100
+ title: "Discovery Scans",
101
+ pageMapKey: PageMap.NETWORK_DEVICE_DISCOVERY,
102
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
103
+ breadcrumbTitles: ["Project", "Network", "Discovery Scans"],
104
+ },
105
+ {
106
+ title: "Archived Devices",
107
+ pageMapKey: PageMap.NETWORK_DEVICE_ARCHIVED,
108
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
109
+ breadcrumbTitles: ["Project", "Network", "Archived Devices"],
110
+ },
111
+ ],
112
+ },
113
+ {
114
+ title: "Topology",
115
+ defaultCollapsed: false,
116
+ entries: [
117
+ {
118
+ title: "Network Map",
119
+ pageMapKey: PageMap.NETWORK_SITE_MAP,
120
+ getBreadcrumbs: getNetworkSiteBreadcrumbs,
121
+ breadcrumbTitles: ["Project", "Network", "Topology", "Network Map"],
122
+ resetsMapDrill: true,
123
+ },
124
+ {
125
+ title: "Device Topology",
126
+ pageMapKey: PageMap.NETWORK_DEVICE_TOPOLOGY,
127
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
128
+ breadcrumbTitles: ["Project", "Network", "Topology", "Device Topology"],
129
+ },
130
+ {
131
+ title: "Latency Matrix",
132
+ pageMapKey: PageMap.NETWORK_DEVICE_LATENCY_MATRIX,
133
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
134
+ breadcrumbTitles: ["Project", "Network", "Topology", "Latency Matrix"],
135
+ },
136
+ {
137
+ title: "Site Links",
138
+ pageMapKey: PageMap.NETWORK_SITE_LINKS,
139
+ getBreadcrumbs: getNetworkSiteBreadcrumbs,
140
+ breadcrumbTitles: ["Project", "Network", "Topology", "Site Links"],
141
+ },
142
+ {
143
+ title: "Device Links",
144
+ pageMapKey: PageMap.NETWORK_DEVICE_LINKS,
145
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
146
+ breadcrumbTitles: ["Project", "Network", "Topology", "Device Links"],
147
+ },
148
+ ],
149
+ },
150
+ {
151
+ title: "Rules",
152
+ defaultCollapsed: true,
153
+ entries: [
154
+ {
155
+ title: "Auto Import Rules",
156
+ pageMapKey: PageMap.NETWORK_DEVICE_SETTINGS_AUTO_IMPORT_RULES,
157
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
158
+ breadcrumbTitles: ["Project", "Network", "Rules", "Auto Import Rules"],
159
+ },
160
+ {
161
+ title: "Site Assignment Rules",
162
+ pageMapKey: PageMap.NETWORK_SITE_ASSIGNMENT_RULES,
163
+ getBreadcrumbs: getNetworkSiteBreadcrumbs,
164
+ breadcrumbTitles: [
165
+ "Project",
166
+ "Network",
167
+ "Rules",
168
+ "Site Assignment Rules",
169
+ ],
170
+ },
171
+ {
172
+ title: "Owner Rules",
173
+ pageMapKey: PageMap.NETWORK_DEVICE_SETTINGS_OWNER_RULES,
174
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
175
+ breadcrumbTitles: ["Project", "Network", "Rules", "Owner Rules"],
176
+ },
177
+ {
178
+ title: "Label Rules",
179
+ pageMapKey: PageMap.NETWORK_DEVICE_SETTINGS_LABEL_RULES,
180
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
181
+ breadcrumbTitles: ["Project", "Network", "Rules", "Label Rules"],
182
+ },
183
+ {
184
+ title: "Link Rules",
185
+ pageMapKey: PageMap.NETWORK_DEVICE_SETTINGS_LINK_RULES,
186
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
187
+ breadcrumbTitles: ["Project", "Network", "Rules", "Link Rules"],
188
+ },
189
+ ],
190
+ },
191
+ {
192
+ title: "Settings",
193
+ defaultCollapsed: true,
194
+ entries: [
195
+ {
196
+ title: "Device Roles",
197
+ pageMapKey: PageMap.NETWORK_DEVICE_SETTINGS_DEVICE_ROLES,
198
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
199
+ breadcrumbTitles: ["Project", "Network", "Settings", "Device Roles"],
200
+ },
201
+ {
202
+ title: "OID Collection Templates",
203
+ pageMapKey: PageMap.NETWORK_DEVICE_SETTINGS_OID_TEMPLATES,
204
+ getBreadcrumbs: getNetworkDeviceBreadcrumbs,
205
+ breadcrumbTitles: [
206
+ "Project",
207
+ "Network",
208
+ "Settings",
209
+ "OID Collection Templates",
210
+ ],
211
+ },
212
+ {
213
+ title: "Site Types",
214
+ pageMapKey: PageMap.NETWORK_SITE_SETTINGS_SITE_TYPES,
215
+ getBreadcrumbs: getNetworkSiteBreadcrumbs,
216
+ breadcrumbTitles: ["Project", "Network", "Settings", "Site Types"],
217
+ },
218
+ ],
219
+ },
220
+ ];
221
+
222
+ const EXPECTED_ENTRIES: Array<ExpectedMenuEntry> = EXPECTED_SECTIONS.flatMap(
223
+ (section: ExpectedMenuSection): Array<ExpectedMenuEntry> => {
224
+ return section.entries;
225
+ },
226
+ );
227
+
228
+ function expectedHref(entry: ExpectedMenuEntry): string {
229
+ const href: string = routeFor(entry.pageMapKey);
230
+ return entry.resetsMapDrill ? `${href}?site=` : href;
231
+ }
232
+
233
+ function expectedLinks(section: ExpectedMenuSection): Array<MenuLink> {
234
+ return section.entries.map((entry: ExpectedMenuEntry): MenuLink => {
235
+ return {
236
+ title: entry.title,
237
+ href: expectedHref(entry),
238
+ };
239
+ });
240
+ }
241
+
242
+ function findAnchor(title: string): HTMLAnchorElement {
243
+ const anchor: HTMLAnchorElement | undefined = Array.from(
244
+ document.querySelectorAll("a"),
245
+ ).find((candidate: Element): boolean => {
246
+ return (
247
+ candidate.querySelector("span.truncate")?.textContent?.trim() === title
248
+ );
249
+ }) as HTMLAnchorElement | undefined;
250
+
251
+ if (!anchor) {
252
+ throw new Error(`No side-menu link titled "${title}" was rendered.`);
253
+ }
254
+
255
+ return anchor;
256
+ }
257
+
258
+ function expectActiveLink(title: string): void {
259
+ const anchor: HTMLAnchorElement = findAnchor(title);
260
+
261
+ expect(anchor).toHaveClass("text-indigo-700");
262
+ expect(anchor).not.toHaveClass("text-gray-600");
263
+ expect(anchor.querySelector(".h-5.bg-indigo-600")).not.toBeNull();
264
+ }
265
+
266
+ function expectCompleteMenu(): void {
267
+ expect(sectionTitlesInOrder()).toEqual(
268
+ EXPECTED_SECTIONS.map((section: ExpectedMenuSection): string => {
269
+ return section.title;
270
+ }),
271
+ );
272
+
273
+ EXPECTED_SECTIONS.forEach((section: ExpectedMenuSection): void => {
274
+ expect(linksIn(section.title)).toEqual(expectedLinks(section));
275
+ });
276
+ }
277
+
278
+ function renderNetworkMenu(): Promise<RenderResult> {
279
+ return renderMenu(<NetworkSideMenu />);
280
+ }
281
+
282
+ describe("Network side menu", () => {
283
+ beforeEach(() => {
284
+ setViewportWidth(DESKTOP_WIDTH);
285
+ goTo(`/dashboard/${PROJECT_ID}/network-devices/overview`);
286
+ });
287
+
288
+ afterEach(() => {
289
+ cleanup();
290
+ });
291
+
292
+ describe("taxonomy", () => {
293
+ test("renders Network, Topology, Rules and Settings in order", async () => {
294
+ await renderNetworkMenu();
295
+
296
+ expect(sectionTitlesInOrder()).toEqual([
297
+ "Network",
298
+ "Topology",
299
+ "Rules",
300
+ "Settings",
301
+ ]);
302
+ });
303
+
304
+ test("puts every page in its intended section and order", async () => {
305
+ await renderNetworkMenu();
306
+
307
+ expectCompleteMenu();
308
+ });
309
+
310
+ test("keeps day-to-day navigation open and configuration collapsed", async () => {
311
+ await renderNetworkMenu();
312
+
313
+ EXPECTED_SECTIONS.forEach((section: ExpectedMenuSection): void => {
314
+ expect(isExpanded(section.title)).toBe(!section.defaultCollapsed);
315
+ });
316
+ });
317
+ });
318
+
319
+ describe("collapsed sections", () => {
320
+ test("Rules expands and collapses without unmounting its links", async () => {
321
+ await renderNetworkMenu();
322
+
323
+ expect(isExpanded("Rules")).toBe(false);
324
+ expect(sectionBody("Rules")).toHaveClass("max-h-0", "opacity-0");
325
+ expect(linksIn("Rules")).toHaveLength(5);
326
+
327
+ fireEvent.click(sectionToggle("Rules"));
328
+
329
+ expect(isExpanded("Rules")).toBe(true);
330
+ expect(sectionBody("Rules")).not.toHaveClass("max-h-0", "opacity-0");
331
+ expect(sectionBody("Rules")).toHaveClass("opacity-100");
332
+
333
+ fireEvent.click(sectionToggle("Rules"));
334
+
335
+ expect(isExpanded("Rules")).toBe(false);
336
+ expect(sectionBody("Rules")).toHaveClass("max-h-0", "opacity-0");
337
+ expect(linksIn("Rules")).toEqual(expectedLinks(EXPECTED_SECTIONS[2]!));
338
+ });
339
+
340
+ test("Settings expands and collapses without unmounting its links", async () => {
341
+ await renderNetworkMenu();
342
+
343
+ expect(isExpanded("Settings")).toBe(false);
344
+ expect(linksIn("Settings")).toHaveLength(3);
345
+
346
+ fireEvent.click(sectionToggle("Settings"));
347
+ expect(isExpanded("Settings")).toBe(true);
348
+
349
+ fireEvent.click(sectionToggle("Settings"));
350
+ expect(isExpanded("Settings")).toBe(false);
351
+ expect(linksIn("Settings")).toEqual(expectedLinks(EXPECTED_SECTIONS[3]!));
352
+ });
353
+ });
354
+
355
+ describe("coverage", () => {
356
+ test("keeps all nineteen destinations reachable exactly once", async () => {
357
+ await renderNetworkMenu();
358
+
359
+ expect(EXPECTED_ENTRIES).toHaveLength(19);
360
+ expect(allLinks()).toHaveLength(19);
361
+ expect(hrefsInMenu().sort()).toEqual(
362
+ EXPECTED_ENTRIES.map(expectedHref).sort(),
363
+ );
364
+ });
365
+
366
+ test("does not duplicate a route, page key or title", async () => {
367
+ await renderNetworkMenu();
368
+
369
+ const hrefs: Array<string> = hrefsInMenu();
370
+ const titles: Array<string> = titlesInMenu();
371
+ const pageMapKeys: Array<string> = EXPECTED_ENTRIES.map(
372
+ (entry: ExpectedMenuEntry): string => {
373
+ return entry.pageMapKey;
374
+ },
375
+ );
376
+
377
+ expect(hrefs).toEqual(Array.from(new Set(hrefs)));
378
+ expect(titles).toEqual(Array.from(new Set(titles)));
379
+ expect(pageMapKeys).toEqual(Array.from(new Set(pageMapKeys)));
380
+ });
381
+
382
+ test("fully populates every project route", async () => {
383
+ await renderNetworkMenu();
384
+
385
+ allLinks().forEach((link: MenuLink): void => {
386
+ expect(link.href.startsWith(`/dashboard/${PROJECT_ID}/`)).toBe(true);
387
+ expect(link.href).not.toContain(":projectId");
388
+ expect(link.href).not.toContain(":modelId");
389
+ expect(link.title).not.toBe("");
390
+ });
391
+ });
392
+
393
+ test("gives every destination an icon", async () => {
394
+ await renderNetworkMenu();
395
+
396
+ EXPECTED_SECTIONS.forEach((section: ExpectedMenuSection): void => {
397
+ expect(iconCountIn(section.title)).toBe(section.entries.length);
398
+ });
399
+ });
400
+ });
401
+
402
+ describe("shared route-family wrappers", () => {
403
+ const WRAPPERS: Array<SideMenuWrapper> = [
404
+ { name: "Network Devices", Component: NetworkDeviceSideMenu },
405
+ { name: "Network Sites", Component: NetworkSiteSideMenu },
406
+ ];
407
+
408
+ test.each(WRAPPERS)(
409
+ "$name renders the complete shared product menu",
410
+ async ({ Component }: SideMenuWrapper): Promise<void> => {
411
+ await renderMenu(<Component />);
412
+
413
+ expectCompleteMenu();
414
+ },
415
+ );
416
+ });
417
+
418
+ describe("active routes", () => {
419
+ test("an active Rules deep link expands Rules and highlights its item", async () => {
420
+ goTo(routeFor(PageMap.NETWORK_DEVICE_SETTINGS_LINK_RULES));
421
+
422
+ await renderNetworkMenu();
423
+
424
+ expect(isExpanded("Rules")).toBe(true);
425
+ expect(sectionBody("Rules")).toHaveClass("opacity-100");
426
+ expect(isExpanded("Settings")).toBe(false);
427
+ expectActiveLink("Link Rules");
428
+ });
429
+
430
+ test("an active Settings deep link expands Settings and highlights its item", async () => {
431
+ goTo(routeFor(PageMap.NETWORK_SITE_SETTINGS_SITE_TYPES));
432
+
433
+ await renderNetworkMenu();
434
+
435
+ expect(isExpanded("Settings")).toBe(true);
436
+ expect(sectionBody("Settings")).toHaveClass("opacity-100");
437
+ expect(isExpanded("Rules")).toBe(false);
438
+ expectActiveLink("Site Types");
439
+ });
440
+
441
+ test("opens Rules when navigation moves to a Rules route", async () => {
442
+ const rendered: RenderResult = await renderNetworkMenu();
443
+
444
+ expect(isExpanded("Rules")).toBe(false);
445
+
446
+ goTo(routeFor(PageMap.NETWORK_DEVICE_SETTINGS_LINK_RULES));
447
+ await act(async () => {
448
+ rendered.rerender(<NetworkSideMenu />);
449
+ });
450
+
451
+ expect(isExpanded("Rules")).toBe(true);
452
+ expect(sectionBody("Rules")).toHaveClass("opacity-100");
453
+ expectActiveLink("Link Rules");
454
+ });
455
+
456
+ test("opens Settings after navigation moves from Rules to Settings", async () => {
457
+ goTo(routeFor(PageMap.NETWORK_DEVICE_SETTINGS_LINK_RULES));
458
+ const rendered: RenderResult = await renderNetworkMenu();
459
+
460
+ expect(isExpanded("Rules")).toBe(true);
461
+ expect(isExpanded("Settings")).toBe(false);
462
+
463
+ goTo(routeFor(PageMap.NETWORK_SITE_SETTINGS_SITE_TYPES));
464
+ await act(async () => {
465
+ rendered.rerender(<NetworkSideMenu />);
466
+ });
467
+
468
+ expect(isExpanded("Rules")).toBe(true);
469
+ expect(isExpanded("Settings")).toBe(true);
470
+ expect(sectionBody("Settings")).toHaveClass("opacity-100");
471
+ expectActiveLink("Site Types");
472
+ });
473
+ });
474
+
475
+ describe("Network Map", () => {
476
+ test("links to an explicit root reset instead of the bare map route", async () => {
477
+ await renderNetworkMenu();
478
+
479
+ const mapLink: MenuLink = linksIn("Topology")[0]!;
480
+ const bareMapRoute: string = routeFor(PageMap.NETWORK_SITE_MAP);
481
+
482
+ expect(mapLink).toEqual({
483
+ title: "Network Map",
484
+ href: `${bareMapRoute}?site=`,
485
+ });
486
+ expect(mapLink.href).not.toBe(bareMapRoute);
487
+ });
488
+
489
+ test("is active on the map page even though its navigation target carries the reset query", async () => {
490
+ goTo(routeFor(PageMap.NETWORK_SITE_MAP));
491
+
492
+ await renderNetworkMenu();
493
+
494
+ expect(isExpanded("Topology")).toBe(true);
495
+ expectActiveLink("Network Map");
496
+ expect(findAnchor("Network Map")).toHaveAttribute(
497
+ "href",
498
+ `${routeFor(PageMap.NETWORK_SITE_MAP)}?site=`,
499
+ );
500
+ });
501
+
502
+ test("names the active map page in the mobile summary", async () => {
503
+ setViewportWidth(MOBILE_WIDTH);
504
+ goTo(routeFor(PageMap.NETWORK_SITE_MAP));
505
+
506
+ await renderNetworkMenu();
507
+
508
+ expect(mobileSummaryText()).toContain("Topology / Network Map");
509
+ });
510
+ });
511
+
512
+ describe("mobile summaries", () => {
513
+ test.each([
514
+ [PageMap.NETWORK_DEVICE_ENDPOINTS, "Network / Endpoints"],
515
+ [PageMap.NETWORK_DEVICE_TOPOLOGY, "Topology / Device Topology"],
516
+ [PageMap.NETWORK_DEVICE_SETTINGS_OWNER_RULES, "Rules / Owner Rules"],
517
+ [PageMap.NETWORK_SITE_SETTINGS_SITE_TYPES, "Settings / Site Types"],
518
+ ])(
519
+ "%s reports its section and page",
520
+ async (pageMapKey: string, expectedSummary: string): Promise<void> => {
521
+ setViewportWidth(MOBILE_WIDTH);
522
+ goTo(routeFor(pageMapKey));
523
+
524
+ await renderNetworkMenu();
525
+
526
+ expect(mobileSummaryText()).toContain(expectedSummary);
527
+ },
528
+ );
529
+ });
530
+
531
+ describe("breadcrumbs", () => {
532
+ test("every rendered entry has a matching breadcrumb leaf and section", async () => {
533
+ await renderNetworkMenu();
534
+
535
+ const renderedLinks: Array<MenuLink> = allLinks();
536
+
537
+ for (const section of EXPECTED_SECTIONS) {
538
+ for (const entry of section.entries) {
539
+ const renderedLink: MenuLink | undefined = renderedLinks.find(
540
+ (link: MenuLink): boolean => {
541
+ return link.href === expectedHref(entry);
542
+ },
543
+ );
544
+
545
+ expect(renderedLink).toBeDefined();
546
+ expect(renderedLink?.title).toBe(entry.title);
547
+
548
+ goTo(routeFor(entry.pageMapKey));
549
+
550
+ const trail: Array<Link> | undefined = entry.getBreadcrumbs(
551
+ RouteUtil.getRouteString(entry.pageMapKey),
552
+ );
553
+ const trailTitles: Array<string> | undefined = trail?.map(
554
+ (link: Link): string => {
555
+ return link.title;
556
+ },
557
+ );
558
+
559
+ expect(trailTitles).toEqual(entry.breadcrumbTitles);
560
+ expect(trailTitles?.[trailTitles.length - 1]).toBe(
561
+ renderedLink?.title,
562
+ );
563
+
564
+ if (section.title === "Network") {
565
+ expect(trailTitles?.[1]).toBe(section.title);
566
+ } else {
567
+ expect(trailTitles?.[trailTitles.length - 2]).toBe(section.title);
568
+ }
569
+ }
570
+ }
571
+ });
572
+
573
+ test("every breadcrumb destination is a concrete dashboard route", () => {
574
+ for (const entry of EXPECTED_ENTRIES) {
575
+ goTo(routeFor(entry.pageMapKey));
576
+
577
+ const trail: Array<Link> | undefined = entry.getBreadcrumbs(
578
+ RouteUtil.getRouteString(entry.pageMapKey),
579
+ );
580
+
581
+ expect(trail).toBeDefined();
582
+ trail?.forEach((link: Link): void => {
583
+ expect(link.to.toString().startsWith("/dashboard/")).toBe(true);
584
+ expect(link.to.toString()).not.toContain(":projectId");
585
+ expect(link.to.toString()).not.toContain(":modelId");
586
+ });
587
+ }
588
+ });
589
+ });
590
+ });
@@ -1,4 +1,4 @@
1
- import { act, render } from "@testing-library/react";
1
+ import { act, render, RenderResult } from "@testing-library/react";
2
2
  import { ReactElement } from "react";
3
3
  import { Location } from "react-router-dom";
4
4
  import Route from "../../../Types/API/Route";
@@ -180,8 +180,16 @@ export function mobileSummaryText(): string {
180
180
  * sets state when its (stubbed) count resolves, and an unawaited render
181
181
  * leaves that update outside the test.
182
182
  */
183
- export async function renderMenu(menu: ReactElement): Promise<void> {
183
+ export async function renderMenu(menu: ReactElement): Promise<RenderResult> {
184
+ let rendered: RenderResult | undefined;
185
+
184
186
  await act(async () => {
185
- render(menu);
187
+ rendered = render(menu);
186
188
  });
189
+
190
+ if (!rendered) {
191
+ throw new Error("The side menu did not render.");
192
+ }
193
+
194
+ return rendered;
187
195
  }