@oneuptime/common 14.0.3 → 14.0.6

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 (663) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/DatabaseServer.ts +1650 -0
  3. package/Models/DatabaseModels/DatabaseServerEndpoint.ts +540 -0
  4. package/Models/DatabaseModels/DatabaseServerFeed.ts +625 -0
  5. package/Models/DatabaseModels/DatabaseServerLabelRule.ts +514 -0
  6. package/Models/DatabaseModels/DatabaseServerOwnerRule.ts +596 -0
  7. package/Models/DatabaseModels/DatabaseServerOwnerTeam.ts +491 -0
  8. package/Models/DatabaseModels/DatabaseServerOwnerUser.ts +490 -0
  9. package/Models/DatabaseModels/Incident.ts +55 -0
  10. package/Models/DatabaseModels/Index.ts +16 -0
  11. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +26 -0
  12. package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
  13. package/Models/DatabaseModels/TelemetryException.ts +2 -0
  14. package/Models/DatabaseModels/UserProjectSsoConsent.ts +146 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.ts +673 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.ts +51 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  18. package/Server/Middleware/MultipartFormData.ts +1 -1
  19. package/Server/Middleware/TelemetryIngest.ts +15 -8
  20. package/Server/Services/AlertReminderRuleService.ts +100 -24
  21. package/Server/Services/DatabaseServerEndpointService.ts +895 -0
  22. package/Server/Services/DatabaseServerFeedService.ts +128 -0
  23. package/Server/Services/DatabaseServerLabelRuleEngineService.ts +340 -0
  24. package/Server/Services/DatabaseServerLabelRuleService.ts +14 -0
  25. package/Server/Services/DatabaseServerOwnerRuleEngineService.ts +425 -0
  26. package/Server/Services/DatabaseServerOwnerRuleService.ts +14 -0
  27. package/Server/Services/DatabaseServerOwnerTeamService.ts +180 -0
  28. package/Server/Services/DatabaseServerOwnerUserService.ts +173 -0
  29. package/Server/Services/DatabaseServerService.ts +4546 -0
  30. package/Server/Services/DatabaseService.ts +14 -8
  31. package/Server/Services/DockerResourceService.ts +43 -2
  32. package/Server/Services/IncidentReminderRuleService.ts +103 -23
  33. package/Server/Services/Index.ts +16 -0
  34. package/Server/Services/InventoryItemService.ts +113 -5
  35. package/Server/Services/IoTDeviceService.ts +145 -37
  36. package/Server/Services/MetricService.ts +1 -1
  37. package/Server/Services/OpenTelemetryIngestService.ts +19 -0
  38. package/Server/Services/PodmanResourceService.ts +43 -2
  39. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +107 -23
  40. package/Server/Services/ServiceService.ts +1 -1
  41. package/Server/Services/TelemetryUsageBillingService.ts +29 -2
  42. package/Server/Services/UserProjectSsoConsentService.ts +74 -0
  43. package/Server/Services/UserService.ts +14 -1
  44. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +18 -0
  45. package/Server/Types/Database/QueryHelper.ts +84 -2
  46. package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
  47. package/Server/Utils/AI/Toolbox/ResourceTools.ts +81 -2
  48. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
  49. package/Server/Utils/Database/AffectedResourceRelations.ts +6 -0
  50. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
  51. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +192 -9
  52. package/Server/Utils/Monitor/MonitorAlert.ts +28 -1
  53. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +2264 -755
  54. package/Server/Utils/Monitor/MonitorIncident.ts +28 -1
  55. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +268 -6
  56. package/Server/Utils/Monitor/MonitorResource.ts +47 -11
  57. package/Server/Utils/Monitor/MonitorResourceContext.ts +1 -0
  58. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +239 -21
  59. package/Server/Utils/Monitor/PerSeriesResolutionRootCause.ts +43 -0
  60. package/Server/Utils/Monitor/SeriesResourceLabels.ts +291 -0
  61. package/Server/Utils/Monitor/SeriesResourceLinker.ts +111 -12
  62. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
  63. package/Server/Utils/StartServer.ts +26 -6
  64. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
  65. package/Server/Utils/Telemetry/DatabaseEndpointDiscovery.ts +688 -0
  66. package/Server/Utils/Telemetry/EntityRegistry.ts +5 -1
  67. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +18 -1
  68. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +175 -12
  69. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +12 -0
  70. package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +606 -36
  71. package/Server/Utils/Telemetry/Telemetry.ts +16 -0
  72. package/Server/Utils/Telemetry/TelemetryEntity.ts +22 -22
  73. package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +16 -10
  74. package/Tests/App/Accounts/SignedInRedirect.test.tsx +241 -0
  75. package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +221 -4
  76. package/Tests/App/Dashboard/AIInsightsResourcePageContext.test.ts +42 -0
  77. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +100 -0
  78. package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +86 -0
  79. package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +2 -0
  80. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +19 -0
  81. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +14 -0
  82. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +26 -1
  83. package/Tests/App/Dashboard/DashboardAppSsoError.test.tsx +501 -0
  84. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +64 -1
  85. package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +178 -18
  86. package/Tests/App/Dashboard/DashboardMasterPageSsoRedirect.test.tsx +444 -0
  87. package/Tests/App/Dashboard/DashboardNavBarForeignHiddenRule.test.tsx +573 -0
  88. package/Tests/App/Dashboard/DatabaseChartAxes.test.tsx +431 -0
  89. package/Tests/App/Dashboard/DatabaseContainerDetailBadge.test.tsx +249 -0
  90. package/Tests/App/Dashboard/DatabaseDashboardE2EFixes.test.ts +681 -0
  91. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +1801 -0
  92. package/Tests/App/Dashboard/DatabaseFleetSummaryRefresh.test.tsx +280 -0
  93. package/Tests/App/Dashboard/DatabaseFormHelpers.test.tsx +321 -0
  94. package/Tests/App/Dashboard/DatabaseMetricMonitorLink.test.tsx +795 -0
  95. package/Tests/App/Dashboard/DatabaseOverviewHeader.test.tsx +580 -0
  96. package/Tests/App/Dashboard/DatabaseRecommendationsEngineMetrics.test.ts +270 -0
  97. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +1486 -0
  98. package/Tests/App/Dashboard/DatabaseServerEngineMetricsFilter.test.ts +223 -0
  99. package/Tests/App/Dashboard/DatabaseServerPresentation.test.ts +435 -0
  100. package/Tests/App/Dashboard/DatabaseServerSummary.test.ts +186 -0
  101. package/Tests/App/Dashboard/DatabaseServerTelemetryQueries.test.ts +1553 -0
  102. package/Tests/App/Dashboard/DatabaseTableTruncation.test.tsx +326 -0
  103. package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +512 -0
  104. package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +774 -0
  105. package/Tests/App/Dashboard/DatabaseViewLayout.test.tsx +233 -0
  106. package/Tests/App/Dashboard/DatabaseWorkloadBadge.test.tsx +972 -0
  107. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +5 -0
  108. package/Tests/App/Dashboard/DiscoveryReviewUnnamedHostHint.test.tsx +1061 -0
  109. package/Tests/App/Dashboard/EventOverlayScope.test.ts +147 -0
  110. package/Tests/App/Dashboard/EventOverlayScopeHook.test.tsx +1 -0
  111. package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +1 -0
  112. package/Tests/App/Dashboard/MetricsViewerDrillDown.test.tsx +310 -0
  113. package/Tests/App/Dashboard/MetricsViewerEntityScopedRows.test.tsx +495 -0
  114. package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +132 -1
  115. package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +5 -0
  116. package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
  117. package/Tests/App/Dashboard/ProductOrientation.test.tsx +4 -3
  118. package/Tests/App/Dashboard/RecommendationsLoadedContext.test.tsx +349 -0
  119. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +62 -0
  120. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
  121. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +14 -0
  122. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
  123. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
  124. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +2 -0
  125. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +1085 -0
  126. package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
  127. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
  128. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +10 -0
  129. package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +2 -1
  130. package/Tests/App/StatusPage/StatusPageHeaderForeignHiddenRule.test.tsx +284 -0
  131. package/Tests/ClassStrings.ts +178 -0
  132. package/Tests/Fixtures/MobilePasskeyUser.json +1 -2
  133. package/Tests/ForeignHiddenRuleGuard.ts +2152 -0
  134. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +2250 -0
  135. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +24 -5
  136. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  137. package/Tests/RenderedMarkup.ts +518 -0
  138. package/Tests/ResponsiveVisibility.test.ts +490 -4
  139. package/Tests/ResponsiveVisibility.ts +505 -52
  140. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  141. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +541 -0
  142. package/Tests/Server/Infrastructure/Postgres/AddUserProjectSsoConsentMigration.test.ts +234 -0
  143. package/Tests/Server/Services/ContainerSnapshotUpsertPostgres.test.ts +319 -0
  144. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +13 -0
  145. package/Tests/Server/Services/DatabaseServerEndpointService.test.ts +2033 -0
  146. package/Tests/Server/Services/DatabaseServerFeedAndOwnerServices.test.ts +529 -0
  147. package/Tests/Server/Services/DatabaseServerLifecycle.test.ts +3302 -0
  148. package/Tests/Server/Services/DatabaseServerRegistries.test.ts +603 -0
  149. package/Tests/Server/Services/DatabaseServerRuleEngines.test.ts +754 -0
  150. package/Tests/Server/Services/DatabaseServerService.test.ts +3558 -0
  151. package/Tests/Server/Services/DatabaseServerSqlPostgres.test.ts +2045 -0
  152. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedPostgres.test.ts +362 -0
  153. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedReply.test.ts +210 -0
  154. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +18 -0
  155. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +2 -0
  156. package/Tests/Server/Services/IncomingCallPolicyEscalationRuleTarget.test.ts +225 -0
  157. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +280 -2
  158. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +8 -2
  159. package/Tests/Server/Services/IoTDeviceLatestMetricsPostgres.test.ts +689 -0
  160. package/Tests/Server/Services/IoTDeviceService.test.ts +379 -0
  161. package/Tests/Server/Services/LatestMetricsValuesShape.test.ts +360 -0
  162. package/Tests/Server/Services/LlmModelPriceService.test.ts +402 -0
  163. package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
  164. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
  165. package/Tests/Server/Services/OwnerRowUniqueness.test.ts +61 -9
  166. package/Tests/Server/Services/ReminderRuleLookupPostgres.test.ts +2250 -0
  167. package/Tests/Server/Services/ReminderRuleRefreshOpenSubjectsPaging.test.ts +653 -0
  168. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +68 -20
  169. package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
  170. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
  171. package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
  172. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
  173. package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +9 -0
  174. package/Tests/Server/Services/UserProjectSsoConsentPostgres.test.ts +153 -0
  175. package/Tests/Server/Services/UserProjectSsoConsentService.test.ts +192 -0
  176. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +71 -0
  177. package/Tests/Server/Services/UserSignupIdTakeoverPostgres.test.ts +238 -0
  178. package/Tests/Server/Services/WorkspaceNotificationSummaryHelpers.test.ts +592 -0
  179. package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +3 -0
  180. package/Tests/Server/Types/Database/EventOverlayScopePostgres.test.ts +1 -0
  181. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +11 -0
  182. package/Tests/Server/Types/Database/Permissions/InfrastructureOwnershipScope.test.ts +8 -0
  183. package/Tests/Server/Types/Database/QueryHelperBooleanForCriteriaBackedRule.test.ts +1094 -0
  184. package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
  185. package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
  186. package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
  187. package/Tests/Server/Utils/AI/ResourceTools.test.ts +134 -0
  188. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
  189. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +41 -0
  190. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +279 -9
  191. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaDisplayContext.test.ts +933 -0
  192. package/Tests/Server/Utils/Monitor/DatabaseMaintenanceSuppression.test.ts +582 -0
  193. package/Tests/Server/Utils/Monitor/DatabaseServerMonitorLinking.test.ts +753 -0
  194. package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
  195. package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +1 -0
  196. package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +1 -0
  197. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +119 -0
  198. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +60 -25
  199. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +1815 -0
  200. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesAffectedResources.test.ts +1447 -0
  201. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesRatioEmail.test.ts +352 -0
  202. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformMetricUnits.test.ts +717 -0
  203. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformRawScan.test.ts +2125 -0
  204. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformSeriesAffectedResources.test.ts +1288 -0
  205. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
  206. package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +1 -0
  207. package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +1 -0
  208. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +88 -0
  209. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +205 -0
  210. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +62 -0
  211. package/Tests/Server/Utils/Monitor/MonitorResourceDatabaseMaintenance.test.ts +803 -0
  212. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +133 -3
  213. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
  214. package/Tests/Server/Utils/Monitor/PerSeriesRecoveryResolution.test.ts +472 -1
  215. package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
  216. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +236 -2
  217. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +70 -0
  218. package/Tests/Server/Utils/StartServerProtobufContentType.test.ts +54 -0
  219. package/Tests/Server/Utils/Telemetry/DatabaseEndpointDiscovery.test.ts +1895 -0
  220. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +7 -0
  221. package/Tests/Server/Utils/Telemetry/LogExceptionExtractorSeverityGate.test.ts +90 -0
  222. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +570 -0
  223. package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +97 -2
  224. package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +1337 -0
  225. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +295 -0
  226. package/Tests/Types/AI/AIResourceContext.test.ts +30 -0
  227. package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
  228. package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
  229. package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
  230. package/Tests/Types/DatabaseServer/DatabaseContainerClassifier.test.ts +3644 -0
  231. package/Tests/Types/DatabaseServer/DatabaseContainerCommand.test.ts +340 -0
  232. package/Tests/Types/DatabaseServer/DatabaseContainerCommandCorpus.ts +826 -0
  233. package/Tests/Types/DatabaseServer/DatabaseContainerCommandPostgres.test.ts +150 -0
  234. package/Tests/Types/DatabaseServer/DatabaseEndpoint.test.ts +2657 -0
  235. package/Tests/Types/DatabaseServer/DatabaseServerDiscoverySource.test.ts +64 -0
  236. package/Tests/Types/DatabaseServer/DatabaseServerEntityKeys.test.ts +952 -0
  237. package/Tests/Types/DatabaseServer/DatabaseServerMetricCatalog.test.ts +1161 -0
  238. package/Tests/Types/DatabaseServer/DatabaseSystem.test.ts +1514 -0
  239. package/Tests/Types/DatabaseServer/DatabaseTelemetryResolver.test.ts +1558 -0
  240. package/Tests/Types/Monitor/CephAlertTemplates.test.ts +255 -6
  241. package/Tests/Types/Monitor/DatabaseAlertTemplates.test.ts +2254 -0
  242. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +292 -0
  243. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +272 -0
  244. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +253 -9
  245. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +13 -5
  246. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +4 -2
  247. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +111 -2
  248. package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +61 -0
  249. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +114 -0
  250. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +202 -0
  251. package/Tests/Types/NetworkDevice/DiscoveredHostNamingStatus.test.ts +401 -0
  252. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +4 -1
  253. package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +24 -2
  254. package/Tests/UI/Components/Card.test.tsx +93 -2
  255. package/Tests/UI/Components/FeedOptions.test.ts +6 -0
  256. package/Tests/UI/Components/FeedOptionsButton.test.tsx +8 -1
  257. package/Tests/UI/Components/ForeignHiddenRuleResilience.test.tsx +1147 -0
  258. package/Tests/UI/Components/HeaderRightRail.test.tsx +41 -1
  259. package/Tests/UI/Components/LogsEntityNames.test.ts +16 -1
  260. package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +7 -3
  261. package/Tests/UI/Components/ResourceFacetConfigs.test.ts +2 -1
  262. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  263. package/Tests/UI/Components/SrOnlyForeignRule.test.tsx +994 -0
  264. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +34 -1
  265. package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +4 -1
  266. package/Tests/UI/ForeignHiddenRuleGuard.test.ts +1951 -0
  267. package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
  268. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +19 -1
  269. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
  270. package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
  271. package/Tests/UI/Rum/ReplayTabSwitcher.test.tsx +19 -1
  272. package/Tests/UI/TailwindPlayCdnForeignHiddenRule.test.ts +1005 -0
  273. package/Tests/UI/ThemeGroupHoverReveal.test.ts +847 -0
  274. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +70 -0
  275. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +21 -0
  276. package/Tests/UI/Utils/ModelAPISsoRedirect.test.ts +127 -0
  277. package/Tests/UI/Utils/Navigation.test.ts +102 -0
  278. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +23 -0
  279. package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
  280. package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
  281. package/Tests/Utils/MetricUnitUtil.test.ts +227 -2
  282. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +288 -2
  283. package/Tests/Utils/Monitor/PlatformResourceIdentityAndUnits.test.ts +1559 -0
  284. package/Tests/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.test.ts +2546 -0
  285. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +315 -0
  286. package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +336 -1
  287. package/Tests/Utils/NetworkDiscovery/NetbiosNameUtil.test.ts +244 -0
  288. package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
  289. package/Tests/Utils/ValueFormatter.test.ts +50 -0
  290. package/Tests/tsconfig.json +16 -0
  291. package/Types/AI/AIResourceContext.ts +7 -0
  292. package/Types/BaseDatabase/AggregationType.ts +7 -0
  293. package/Types/Dashboard/DashboardTemplates.ts +22 -8
  294. package/Types/DatabaseServer/DatabaseConnectionSpan.ts +77 -0
  295. package/Types/DatabaseServer/DatabaseContainerClassifier.ts +2068 -0
  296. package/Types/DatabaseServer/DatabaseContainerCommand.ts +801 -0
  297. package/Types/DatabaseServer/DatabaseEndpoint.ts +1943 -0
  298. package/Types/DatabaseServer/DatabaseServerDiscoverySource.ts +68 -0
  299. package/Types/DatabaseServer/DatabaseServerMetricCatalog.ts +966 -0
  300. package/Types/DatabaseServer/DatabaseSystem.ts +2124 -0
  301. package/Types/DatabaseServer/DatabaseTelemetryResolver.ts +555 -0
  302. package/Types/Email/EmailTemplateType.ts +1 -0
  303. package/Types/Monitor/CephAlertTemplates.ts +18 -0
  304. package/Types/Monitor/DatabaseAlertTemplates.ts +2020 -0
  305. package/Types/Monitor/KubernetesAlertTemplates.ts +64 -24
  306. package/Types/Monitor/KubernetesMetricCatalog.ts +118 -19
  307. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +14 -0
  308. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +93 -10
  309. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +109 -8
  310. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +36 -7
  311. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +43 -2
  312. package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +41 -6
  313. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +26 -0
  314. package/Types/NetworkDevice/DiscoveredHostNamingStatus.ts +147 -0
  315. package/Types/Permission.ts +267 -0
  316. package/Types/Probe/ProbeApiIngestResponse.ts +8 -0
  317. package/Types/Rules/RuleCriteriaFieldRegistry.ts +10 -0
  318. package/Types/Rules/RuleRun.ts +12 -0
  319. package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
  320. package/Types/Rum/WebVitals.ts +13 -0
  321. package/Types/Telemetry/EntityType.ts +23 -0
  322. package/Types/Telemetry/ResourceFacetCatalog.ts +14 -0
  323. package/Types/Telemetry/ServiceType.ts +8 -0
  324. package/UI/Components/Banner/Banner.tsx +1 -1
  325. package/UI/Components/Card/Card.tsx +1 -1
  326. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +2 -2
  327. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  328. package/UI/Components/CommandPalette/CommandPalette.tsx +1 -1
  329. package/UI/Components/Date/StartAndEndDate.tsx +5 -5
  330. package/UI/Components/Diagram/HorizontalStepChain.tsx +1 -1
  331. package/UI/Components/Feed/FeedOptionsButton.tsx +1 -1
  332. package/UI/Components/Forms/BasicForm.tsx +1 -1
  333. package/UI/Components/Header/Header.tsx +1 -1
  334. package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -1
  335. package/UI/Components/HeaderAlert/HeaderAlert.tsx +1 -1
  336. package/UI/Components/Input/Input.tsx +11 -0
  337. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +1 -1
  338. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +1 -1
  339. package/UI/Components/Modal/ModalFooter.tsx +1 -1
  340. package/UI/Components/ModelDetail/CardModelDetail.tsx +2 -2
  341. package/UI/Components/ModelTable/BaseModelTable.tsx +4 -4
  342. package/UI/Components/Navbar/NavBar.tsx +9 -2
  343. package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -2
  344. package/UI/Components/Page/Page.tsx +1 -1
  345. package/UI/Components/Pagination/Pagination.tsx +3 -3
  346. package/UI/Components/ProgressButtons/ProgressButtonItem.tsx +1 -1
  347. package/UI/Components/SideMenu/SideMenu.tsx +4 -1
  348. package/UI/Components/StatusPage/ResourceGroupNavigator.tsx +4 -2
  349. package/UI/Components/StatusPage/ResourceList.tsx +6 -6
  350. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +1 -1
  351. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +1 -1
  352. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +1 -1
  353. package/UI/Components/Workflow/ComponentsModal.tsx +2 -2
  354. package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
  355. package/UI/Styles/Theme.css +8 -1
  356. package/UI/Utils/Navigation.ts +89 -42
  357. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +7 -0
  358. package/UI/Utils/TelemetryService.ts +4 -3
  359. package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
  360. package/Utils/LabelRuleImportExport.ts +2 -0
  361. package/Utils/MetricUnitUtil.ts +103 -11
  362. package/Utils/Monitor/MetricValueFormatter.ts +66 -13
  363. package/Utils/Monitor/PlatformMetricUnitUtil.ts +215 -0
  364. package/Utils/Monitor/PlatformResourceIdentity.ts +514 -0
  365. package/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.ts +425 -0
  366. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +32 -0
  367. package/Utils/NetworkDiscovery/NetbiosNameUtil.ts +73 -0
  368. package/Utils/Telemetry/DatabaseServerEntityKeys.ts +275 -0
  369. package/Utils/Telemetry/EntityKey.ts +176 -0
  370. package/Utils/ValueFormatter.ts +16 -0
  371. package/build/dist/Models/DatabaseModels/Alert.js +54 -0
  372. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  373. package/build/dist/Models/DatabaseModels/DatabaseServer.js +1683 -0
  374. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -0
  375. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js +559 -0
  376. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js.map +1 -0
  377. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js +648 -0
  378. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js.map +1 -0
  379. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js +522 -0
  380. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js.map +1 -0
  381. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js +603 -0
  382. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js.map +1 -0
  383. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js +506 -0
  384. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js.map +1 -0
  385. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js +505 -0
  386. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js.map +1 -0
  387. package/build/dist/Models/DatabaseModels/Incident.js +54 -0
  388. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  389. package/build/dist/Models/DatabaseModels/Index.js +16 -0
  390. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  391. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  392. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
  393. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  394. package/build/dist/Models/DatabaseModels/TelemetryException.js +2 -0
  395. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  396. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js +162 -0
  397. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js.map +1 -0
  398. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js +240 -0
  399. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js.map +1 -0
  400. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js +26 -0
  401. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js.map +1 -0
  402. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  403. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  404. package/build/dist/Server/Middleware/MultipartFormData.js +1 -1
  405. package/build/dist/Server/Middleware/TelemetryIngest.js +15 -8
  406. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  407. package/build/dist/Server/Services/AlertReminderRuleService.js +72 -23
  408. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
  409. package/build/dist/Server/Services/DatabaseServerEndpointService.js +694 -0
  410. package/build/dist/Server/Services/DatabaseServerEndpointService.js.map +1 -0
  411. package/build/dist/Server/Services/DatabaseServerFeedService.js +110 -0
  412. package/build/dist/Server/Services/DatabaseServerFeedService.js.map +1 -0
  413. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js +272 -0
  414. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js.map +1 -0
  415. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js +13 -0
  416. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js.map +1 -0
  417. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js +340 -0
  418. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js.map +1 -0
  419. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js +13 -0
  420. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js.map +1 -0
  421. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js +160 -0
  422. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js.map +1 -0
  423. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js +153 -0
  424. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js.map +1 -0
  425. package/build/dist/Server/Services/DatabaseServerService.js +3272 -0
  426. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -0
  427. package/build/dist/Server/Services/DatabaseService.js +8 -4
  428. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  429. package/build/dist/Server/Services/DockerResourceService.js +30 -3
  430. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  431. package/build/dist/Server/Services/IncidentReminderRuleService.js +73 -23
  432. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
  433. package/build/dist/Server/Services/Index.js +16 -0
  434. package/build/dist/Server/Services/Index.js.map +1 -1
  435. package/build/dist/Server/Services/InventoryItemService.js +88 -5
  436. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  437. package/build/dist/Server/Services/IoTDeviceService.js +111 -24
  438. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  439. package/build/dist/Server/Services/MetricService.js +1 -1
  440. package/build/dist/Server/Services/OpenTelemetryIngestService.js +24 -9
  441. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  442. package/build/dist/Server/Services/PodmanResourceService.js +30 -3
  443. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  444. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +75 -23
  445. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
  446. package/build/dist/Server/Services/ServiceService.js +1 -1
  447. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  448. package/build/dist/Server/Services/TelemetryUsageBillingService.js +23 -2
  449. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  450. package/build/dist/Server/Services/UserProjectSsoConsentService.js +79 -0
  451. package/build/dist/Server/Services/UserProjectSsoConsentService.js.map +1 -0
  452. package/build/dist/Server/Services/UserService.js +13 -1
  453. package/build/dist/Server/Services/UserService.js.map +1 -1
  454. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +18 -0
  455. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  456. package/build/dist/Server/Types/Database/QueryHelper.js +73 -2
  457. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  458. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
  459. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
  460. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +58 -1
  461. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  462. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
  463. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +6 -0
  464. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -1
  465. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
  466. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  467. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +135 -14
  468. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  469. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -6
  470. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  471. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1489 -524
  472. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  473. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
  474. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  475. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +196 -6
  476. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  477. package/build/dist/Server/Utils/Monitor/MonitorResource.js +36 -9
  478. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  479. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +1 -0
  480. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
  481. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +175 -17
  482. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
  483. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js +29 -0
  484. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js.map +1 -0
  485. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +211 -0
  486. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  487. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +82 -10
  488. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  489. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
  490. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  491. package/build/dist/Server/Utils/StartServer.js +20 -4
  492. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  493. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
  494. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
  495. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js +442 -0
  496. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js.map +1 -0
  497. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -1
  498. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  499. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +15 -1
  500. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
  501. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +121 -5
  502. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  503. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +12 -0
  504. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  505. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +438 -36
  506. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -1
  507. package/build/dist/Server/Utils/Telemetry/Telemetry.js +12 -0
  508. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  509. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +17 -18
  510. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  511. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +16 -10
  512. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -1
  513. package/build/dist/Types/AI/AIResourceContext.js +7 -0
  514. package/build/dist/Types/AI/AIResourceContext.js.map +1 -1
  515. package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
  516. package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
  517. package/build/dist/Types/Dashboard/DashboardTemplates.js +22 -8
  518. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  519. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js +71 -0
  520. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js.map +1 -0
  521. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js +1462 -0
  522. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js.map +1 -0
  523. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js +691 -0
  524. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js.map +1 -0
  525. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js +1407 -0
  526. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js.map +1 -0
  527. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js +60 -0
  528. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js.map +1 -0
  529. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js +804 -0
  530. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js.map +1 -0
  531. package/build/dist/Types/DatabaseServer/DatabaseSystem.js +1860 -0
  532. package/build/dist/Types/DatabaseServer/DatabaseSystem.js.map +1 -0
  533. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js +406 -0
  534. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js.map +1 -0
  535. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  536. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  537. package/build/dist/Types/Monitor/CephAlertTemplates.js +18 -0
  538. package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
  539. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js +1518 -0
  540. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js.map +1 -0
  541. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +57 -24
  542. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  543. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +104 -18
  544. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
  545. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +80 -3
  546. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  547. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +10 -3
  548. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  549. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +29 -0
  550. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  551. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +21 -6
  552. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -1
  553. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +25 -0
  554. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
  555. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js +136 -0
  556. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js.map +1 -0
  557. package/build/dist/Types/Permission.js +234 -0
  558. package/build/dist/Types/Permission.js.map +1 -1
  559. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +10 -0
  560. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  561. package/build/dist/Types/Rules/RuleRun.js +4 -0
  562. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  563. package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
  564. package/build/dist/Types/Rum/WebVitals.js +12 -0
  565. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  566. package/build/dist/Types/Telemetry/EntityType.js +23 -0
  567. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  568. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +14 -0
  569. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -1
  570. package/build/dist/Types/Telemetry/ServiceType.js +8 -0
  571. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  572. package/build/dist/UI/Components/Banner/Banner.js +1 -1
  573. package/build/dist/UI/Components/Banner/Banner.js.map +1 -1
  574. package/build/dist/UI/Components/Card/Card.js +1 -1
  575. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  576. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  577. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  578. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  579. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  580. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +1 -1
  581. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -1
  582. package/build/dist/UI/Components/Date/StartAndEndDate.js +5 -5
  583. package/build/dist/UI/Components/Date/StartAndEndDate.js.map +1 -1
  584. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js +1 -1
  585. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js.map +1 -1
  586. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +1 -1
  587. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -1
  588. package/build/dist/UI/Components/Forms/BasicForm.js +1 -1
  589. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  590. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
  591. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  592. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js +1 -1
  593. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js.map +1 -1
  594. package/build/dist/UI/Components/Input/Input.js +1 -1
  595. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  596. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +1 -1
  597. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
  598. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
  599. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  600. package/build/dist/UI/Components/Modal/ModalFooter.js +1 -1
  601. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  602. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +2 -2
  603. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  604. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +4 -4
  605. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  606. package/build/dist/UI/Components/Navbar/NavBar.js +9 -2
  607. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  608. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  609. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  610. package/build/dist/UI/Components/Page/Page.js +1 -1
  611. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  612. package/build/dist/UI/Components/Pagination/Pagination.js +3 -3
  613. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  614. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js +1 -1
  615. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js.map +1 -1
  616. package/build/dist/UI/Components/SideMenu/SideMenu.js +4 -1
  617. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  618. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js +3 -1
  619. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js.map +1 -1
  620. package/build/dist/UI/Components/StatusPage/ResourceList.js +6 -6
  621. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  622. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
  623. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  624. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +1 -1
  625. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
  626. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +1 -1
  627. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -1
  628. package/build/dist/UI/Components/Workflow/ComponentsModal.js +2 -2
  629. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  630. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
  631. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
  632. package/build/dist/UI/Utils/Navigation.js +66 -33
  633. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  634. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +7 -0
  635. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -1
  636. package/build/dist/UI/Utils/TelemetryService.js +3 -2
  637. package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
  638. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
  639. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
  640. package/build/dist/Utils/LabelRuleImportExport.js +2 -0
  641. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  642. package/build/dist/Utils/MetricUnitUtil.js +77 -13
  643. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  644. package/build/dist/Utils/Monitor/MetricValueFormatter.js +58 -13
  645. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -1
  646. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js +161 -0
  647. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js.map +1 -0
  648. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js +323 -0
  649. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js.map +1 -0
  650. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js +270 -0
  651. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js.map +1 -0
  652. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +23 -0
  653. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
  654. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js +61 -0
  655. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js.map +1 -1
  656. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js +178 -0
  657. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js.map +1 -0
  658. package/build/dist/Utils/Telemetry/EntityKey.js +133 -0
  659. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  660. package/build/dist/Utils/ValueFormatter.js +15 -0
  661. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  662. package/package.json +1 -1
  663. package/tsconfig.tests.json +7 -5
@@ -0,0 +1,1951 @@
1
+ import {
2
+ CONDITIONAL_PARTNER_ADVICE,
3
+ ClassAttribute,
4
+ DISAGREEING_PIECES_ADVICE,
5
+ DISCLOSURE_EXAMPLE_FILE,
6
+ EjsTag,
7
+ ForeignHiddenRuleFinding,
8
+ ForeignHiddenRuleScan,
9
+ GROUP_HOVER_EXAMPLE_FILE,
10
+ GROUP_HOVER_REWRITE_ADVICE,
11
+ GROUP_REWRITE_CAVEAT,
12
+ MIXED_BREAKPOINT_ADVICE,
13
+ NAMED_GROUP_EXAMPLE_FILE,
14
+ SR_ONLY_CONDITIONAL_PARTNER_ADVICE,
15
+ SR_ONLY_MEDIA_ADVICE,
16
+ SR_ONLY_MIXED_BREAKPOINT_ADVICE,
17
+ SR_ONLY_WHY_PARAGRAPH,
18
+ STATE_VARIANT_ADVICE,
19
+ THEME_STYLESHEET_FILE,
20
+ VariantHiddenSite,
21
+ WHY_PARAGRAPH,
22
+ extractClassAttributes,
23
+ findEjsTags,
24
+ findForeignHiddenRuleHazards,
25
+ formatFindings,
26
+ isMarkupFile,
27
+ listScanRoots,
28
+ listSourceFiles,
29
+ listViewFiles,
30
+ listViewRoots,
31
+ scanFiles,
32
+ toRelativePath,
33
+ } from "../ForeignHiddenRuleGuard";
34
+ import {
35
+ PHONE_WIDTH_IN_PX,
36
+ ScreenReaderOnlyOptions,
37
+ TAILWIND_BREAKPOINTS_IN_PX,
38
+ VisibilityOptions,
39
+ WIDE_DESKTOP_WIDTH_IN_PX,
40
+ isVisuallyCollapsed,
41
+ resolveDisplay,
42
+ splitVariants,
43
+ } from "../ResponsiveVisibility";
44
+ import { afterAll, beforeAll, describe, expect, test } from "@jest/globals";
45
+ import fs from "fs";
46
+ import os from "os";
47
+ import path from "path";
48
+
49
+ /*
50
+ * A customer's dashboard rendered with no navigation bar - no Home, no
51
+ * Products menu, so no way into any product. Their browser carried a rule the
52
+ * app does not ship, `.hidden{display:none !important}` (Bootstrap 3 and HTML5
53
+ * Boilerplate define exactly that; browser extensions and user stylesheets
54
+ * inject it), and the bar was written `hidden md:flex`. The foreign rule
55
+ * matches the bare class and outranks `md:flex`, so the bar was gone at every
56
+ * width. The fix rewrote every such string to `max-md:hidden md:flex`, which
57
+ * displays identically and never carries the class the rule targets.
58
+ *
59
+ * Nothing about the old idiom shows up in a type check, in jsdom, or on a
60
+ * developer's clean browser: it only breaks on a machine that carries the
61
+ * foreign rule. This guard is what stops it coming back. It reads every
62
+ * browser module (packages/App/FeatureSet/<name>/src, Common/UI and, when the
63
+ * checkout has it, ee/Dashboard and ee/AdminDashboard) through the TypeScript
64
+ * AST, and every server-rendered view that loads the same Play CDN (the Docs,
65
+ * the API Reference, the website in packages/Home/Views, Common's server
66
+ * views and the frontends' index.ejs shells), and names the file, the line,
67
+ * the string and what to write instead. It also holds the `sr-only` analogue:
68
+ * the same stylesheets ship `.sr-only`, which collapses a
69
+ * `sr-only sm:not-sr-only` label to 1px at every width.
70
+ *
71
+ * The detector lives in Common/Tests/ForeignHiddenRuleGuard.ts. This file
72
+ * pins it on inline snippets first - every shape it must catch and every
73
+ * shape it must leave alone - and only then runs it over the real tree, with
74
+ * checks that the scan really read the tree so a broken walk cannot make the
75
+ * main assertion pass over nothing.
76
+ */
77
+
78
+ // packages/Common/Tests/UI -> the repository root.
79
+ const REPOSITORY_ROOT: string = path.resolve(__dirname, "..", "..", "..", "..");
80
+
81
+ const FIXTURE_FILE: string = "Fixture.tsx";
82
+ const VIEW_FIXTURE_FILE: string = "Fixture.ejs";
83
+
84
+ // The Common NavBar's desktop row before and after the fix.
85
+ const OLD_NAVBAR_CLASS: string =
86
+ "bg-white flex text-center items-center lg:py-2 hidden md:flex";
87
+ const NEW_NAVBAR_CLASS: string =
88
+ "bg-white flex text-center items-center lg:py-2 max-md:hidden md:flex";
89
+
90
+ // FacetValueRow's exclude button before and after it was converted by hand.
91
+ const OLD_FACET_EXCLUDE_CLASS: string =
92
+ "hidden h-5 w-5 items-center justify-center rounded text-[10px] text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500 group-hover:flex";
93
+ const NEW_FACET_EXCLUDE_CLASS: string =
94
+ "flex h-5 w-5 items-center justify-center rounded text-[10px] text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500 group-[:not(:hover)]:hidden";
95
+
96
+ // ExecutionView's "Hide output" label before and after.
97
+ const OLD_DISCLOSURE_CLASS: string = "hidden group-open:inline";
98
+ const NEW_DISCLOSURE_CLASS: string =
99
+ "inline [details:not([open])>summary>&]:hidden";
100
+
101
+ // The Docs desktop sidebar (every page's navigation) before and after.
102
+ const OLD_DOCS_SIDEBAR_CLASS: string =
103
+ "docs-layout-sidebar hidden lg:relative lg:block lg:flex-none";
104
+ const NEW_DOCS_SIDEBAR_CLASS: string =
105
+ "docs-layout-sidebar max-lg:hidden lg:relative lg:block lg:flex-none";
106
+
107
+ // The website's primary nav, after the fix.
108
+ const HOME_PRIMARY_NAV_CLASS: string = "max-md:hidden space-x-10 md:flex";
109
+
110
+ // The visible labels that were `sr-only <bp>:not-sr-only`, after the fix.
111
+ const NEW_SM_LABEL_CLASS: string = "max-sm:sr-only sm:not-sr-only";
112
+ const NEW_XL_LABEL_CLASS: string = "max-xl:sr-only xl:not-sr-only";
113
+
114
+ // The skip-link idiom the sr-only check must leave alone.
115
+ const SKIP_LINK_CLASS_PREFIX: string = "sr-only focus:not-sr-only";
116
+
117
+ // A JavaScript-chosen `hidden` in a tag, with no un-hider beside it: must stay clean.
118
+ const CODE_TABS_PANEL_CLASS: string =
119
+ "code-panel relative <%= index === 0 ? '' : 'hidden' %>";
120
+
121
+ const NAVBAR_FILE: string = "packages/Common/UI/Components/Navbar/NavBar.tsx";
122
+ const TELEMETRY_FACET_FILE: string =
123
+ "packages/Common/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx";
124
+ const ENTERPRISE_AUDIT_LOG_FILE: string =
125
+ "ee/Dashboard/AuditLogs/AuditLogChangesModal.tsx";
126
+
127
+ const DOCS_INDEX_VIEW: string = "packages/App/FeatureSet/Docs/Views/Index.ejs";
128
+ const DOCS_HEADER_VIEW: string =
129
+ "packages/App/FeatureSet/Docs/Views/Partials/Header.ejs";
130
+ const API_REFERENCE_NAV_VIEW: string =
131
+ "packages/App/FeatureSet/APIReference/views/partials/nav.ejs";
132
+ const API_REFERENCE_PAGE_VIEW: string =
133
+ "packages/App/FeatureSet/APIReference/views/pages/index.ejs";
134
+ const API_REFERENCE_CODE_TABS_VIEW: string =
135
+ "packages/App/FeatureSet/APIReference/views/partials/code-tabs.ejs";
136
+ const HOME_NAV_VIEW: string = "packages/Home/Views/nav.ejs";
137
+ const DASHBOARD_OFFLINE_PAGE: string =
138
+ "packages/App/FeatureSet/Dashboard/public/offline.html";
139
+
140
+ const SR_ONLY_LABEL_SITES: Array<[string, string]> = [
141
+ [
142
+ "packages/App/FeatureSet/Dashboard/src/Components/SessionReplay/ReplayScreenshotActions.tsx",
143
+ NEW_SM_LABEL_CLASS,
144
+ ],
145
+ [
146
+ "packages/App/FeatureSet/Dashboard/src/Components/SessionReplay/ReplayUi.tsx",
147
+ NEW_SM_LABEL_CLASS,
148
+ ],
149
+ [
150
+ "packages/App/FeatureSet/Dashboard/src/Components/Workspace/SendTestNotificationButton.tsx",
151
+ NEW_SM_LABEL_CLASS,
152
+ ],
153
+ [
154
+ "packages/Common/UI/Components/Feed/FeedOptionsButton.tsx",
155
+ NEW_XL_LABEL_CLASS,
156
+ ],
157
+ ];
158
+
159
+ const SKIP_LINK_FILES: Array<string> = [
160
+ "packages/Common/UI/Components/MasterPage/MasterPage.tsx",
161
+ "packages/App/FeatureSet/StatusPage/src/Components/MasterPage/MasterPage.tsx",
162
+ API_REFERENCE_PAGE_VIEW,
163
+ HOME_NAV_VIEW,
164
+ ];
165
+
166
+ const FOREIGN_RULE: VisibilityOptions = { withForeignHiddenRule: true };
167
+ const FOREIGN_SR_ONLY_RULE: ScreenReaderOnlyOptions = {
168
+ withForeignSrOnlyRule: true,
169
+ };
170
+
171
+ // Both sides of every breakpoint, plus the phone and the customer's screen.
172
+ const WIDTHS_IN_PX: Array<number> = [
173
+ 320,
174
+ PHONE_WIDTH_IN_PX,
175
+ ...Object.values(TAILWIND_BREAKPOINTS_IN_PX).flatMap(
176
+ (breakpoint: number): Array<number> => {
177
+ return [breakpoint - 1, breakpoint];
178
+ },
179
+ ),
180
+ WIDE_DESKTOP_WIDTH_IN_PX,
181
+ ];
182
+
183
+ // `max-md:hidden` -> `hidden`, and the same for `max-md:sr-only`: the pre-fix idioms.
184
+ const MAX_WIDTH_HIDDEN: RegExp =
185
+ /(^|[\s"'`])max-(?:sm|md|lg|xl|2xl):hidden(?=[\s"'`]|$)/gm;
186
+ const MAX_WIDTH_SR_ONLY: RegExp =
187
+ /(^|[\s"'`])max-(?:sm|md|lg|xl|2xl):sr-only(?=[\s"'`]|$)/gm;
188
+
189
+ function lines(...sourceLines: Array<string>): string {
190
+ return sourceLines.join("\n");
191
+ }
192
+
193
+ function findingsIn(source: string): Array<ForeignHiddenRuleFinding> {
194
+ return findForeignHiddenRuleHazards(FIXTURE_FILE, source);
195
+ }
196
+
197
+ function viewFindingsIn(source: string): Array<ForeignHiddenRuleFinding> {
198
+ return findForeignHiddenRuleHazards(VIEW_FIXTURE_FILE, source);
199
+ }
200
+
201
+ function onlyFinding(source: string): ForeignHiddenRuleFinding {
202
+ const findings: Array<ForeignHiddenRuleFinding> = findingsIn(source);
203
+
204
+ expect(findings).toHaveLength(1);
205
+
206
+ return findings[0]!;
207
+ }
208
+
209
+ function onlyViewFinding(source: string): ForeignHiddenRuleFinding {
210
+ const findings: Array<ForeignHiddenRuleFinding> = viewFindingsIn(source);
211
+
212
+ expect(findings).toHaveLength(1);
213
+
214
+ return findings[0]!;
215
+ }
216
+
217
+ function jsxWithClass(classAttribute: string): string {
218
+ return `const element = <div className="${classAttribute}" />;`;
219
+ }
220
+
221
+ function readRepositoryFile(relative: string): string {
222
+ return fs.readFileSync(
223
+ path.join(REPOSITORY_ROOT, ...relative.split("/")),
224
+ "utf8",
225
+ );
226
+ }
227
+
228
+ function lineContaining(source: string, text: string): number {
229
+ return (
230
+ source.split("\n").findIndex((sourceLine: string): boolean => {
231
+ return sourceLine.includes(text);
232
+ }) + 1
233
+ );
234
+ }
235
+
236
+ // One line per finding, for a failure message that reads as a list.
237
+ function describeFindings(findings: Array<ForeignHiddenRuleFinding>): string {
238
+ return findings
239
+ .map((finding: ForeignHiddenRuleFinding): string => {
240
+ return `${finding.line} ${finding.kind} ${finding.hidingClass}: ${finding.text} -> ${finding.replacement}`;
241
+ })
242
+ .join("\n");
243
+ }
244
+
245
+ // A class attribute as `line: static | text`, one `|` where each tag was.
246
+ function readAttributes(source: string): Array<string> {
247
+ return extractClassAttributes(source).map(
248
+ (attribute: ClassAttribute): string => {
249
+ return `${attribute.line}: ${attribute.parts.join("|")}`;
250
+ },
251
+ );
252
+ }
253
+
254
+ describe("ForeignHiddenRuleGuard detector", () => {
255
+ test("flags the class string that took the customer's navigation bar away", () => {
256
+ const finding: ForeignHiddenRuleFinding = onlyFinding(
257
+ lines(
258
+ "const className: string =",
259
+ " props.className ||",
260
+ ` "${OLD_NAVBAR_CLASS}";`,
261
+ ),
262
+ );
263
+
264
+ expect(finding).toEqual({
265
+ file: FIXTURE_FILE,
266
+ line: 3,
267
+ kind: "same-string",
268
+ hidingClass: "hidden",
269
+ text: OLD_NAVBAR_CLASS,
270
+ partnerText: null,
271
+ unhidingTokens: ["md:flex"],
272
+ replacement: NEW_NAVBAR_CLASS,
273
+ advice: "replace the bare `hidden` with `max-md:hidden`",
274
+ });
275
+
276
+ // The string the fix shipped is the one the guard recommends, and passes.
277
+ expect(readRepositoryFile(NAVBAR_FILE)).toContain(`"${NEW_NAVBAR_CLASS}"`);
278
+ expect(
279
+ findingsIn(`const className: string = "${NEW_NAVBAR_CLASS}";`),
280
+ ).toEqual([]);
281
+ });
282
+
283
+ test("reverting the NavBar fix in memory makes the real file fail the guard", () => {
284
+ /*
285
+ * Negative control on the real file: the guard over NavBar.tsx as it was
286
+ * before the fix. Only the class string is reverted - the comment above
287
+ * it, which quotes `hidden md:flex` on purpose, stays and is not read.
288
+ */
289
+ const current: string = readRepositoryFile(NAVBAR_FILE);
290
+ const reverted: string = current.replace(
291
+ `"${NEW_NAVBAR_CLASS}"`,
292
+ `"${OLD_NAVBAR_CLASS}"`,
293
+ );
294
+
295
+ expect(reverted).not.toBe(current);
296
+ expect(current).toContain("never `hidden md:flex`");
297
+ expect(findForeignHiddenRuleHazards(NAVBAR_FILE, current)).toEqual([]);
298
+
299
+ const findings: Array<ForeignHiddenRuleFinding> =
300
+ findForeignHiddenRuleHazards(NAVBAR_FILE, reverted);
301
+
302
+ expect(
303
+ findings.map((finding: ForeignHiddenRuleFinding): string => {
304
+ return `${finding.file}:${finding.line} ${finding.replacement}`;
305
+ }),
306
+ ).toEqual([
307
+ `${NAVBAR_FILE}:${lineContaining(reverted, OLD_NAVBAR_CLASS)} ${NEW_NAVBAR_CLASS}`,
308
+ ]);
309
+ });
310
+
311
+ test("a breakpoint un-hider gets the exact max-<bp>:hidden rewrite, display-equivalent at every width", () => {
312
+ /*
313
+ * The rewrite is only worth printing if pasting it is safe. Each pair is
314
+ * resolved with the same cascade model the header suites use: the
315
+ * replacement must display exactly like the original at every width,
316
+ * stay that way with the foreign rule on the page - while the original
317
+ * vanishes at every width under it, which is the bug.
318
+ */
319
+ const cases: Array<[string, string]> = [
320
+ ["hidden md:flex", "max-md:hidden md:flex"],
321
+ ["hidden lg:flex items-center", "max-lg:hidden lg:flex items-center"],
322
+ ["hidden sm:flex sm:flex-wrap", "max-sm:hidden sm:flex sm:flex-wrap"],
323
+ ["mr-3 hidden md:inline-flex", "mr-3 max-md:hidden md:inline-flex"],
324
+ ["hidden w-32 xl:block", "max-xl:hidden w-32 xl:block"],
325
+ [
326
+ "text-gray-400 hidden 2xl:inline",
327
+ "text-gray-400 max-2xl:hidden 2xl:inline",
328
+ ],
329
+ ["hidden sm:grid sm:grid-cols-2", "max-sm:hidden sm:grid sm:grid-cols-2"],
330
+ // Several screens: the narrowest one decides.
331
+ ["hidden lg:flex md:block", "max-md:hidden lg:flex md:block"],
332
+ // A max-width un-hider is mirrored: hide from the widest screen up.
333
+ ["hidden max-md:flex", "md:hidden max-md:flex"],
334
+ ["hidden max-lg:block max-md:flex", "lg:hidden max-lg:block max-md:flex"],
335
+ ];
336
+
337
+ for (const [original, replacement] of cases) {
338
+ const finding: ForeignHiddenRuleFinding = onlyFinding(
339
+ jsxWithClass(original),
340
+ );
341
+
342
+ expect(finding.text).toBe(original);
343
+ expect(finding.replacement).toBe(replacement);
344
+ expect(findingsIn(jsxWithClass(replacement))).toEqual([]);
345
+
346
+ for (const width of WIDTHS_IN_PX) {
347
+ expect(
348
+ `${replacement} @ ${width}px: ${resolveDisplay(replacement, width)}`,
349
+ ).toBe(
350
+ `${replacement} @ ${width}px: ${resolveDisplay(original, width)}`,
351
+ );
352
+ expect(
353
+ `${replacement} @ ${width}px, foreign rule: ${resolveDisplay(replacement, width, FOREIGN_RULE)}`,
354
+ ).toBe(
355
+ `${replacement} @ ${width}px, foreign rule: ${resolveDisplay(original, width)}`,
356
+ );
357
+ expect(resolveDisplay(original, width, FOREIGN_RULE)).toBe("hidden");
358
+ }
359
+ }
360
+ });
361
+
362
+ test("the important modifier does not hide a display utility", () => {
363
+ expect(onlyFinding(jsxWithClass("hidden md:!flex")).replacement).toBe(
364
+ "max-md:hidden md:!flex",
365
+ );
366
+ });
367
+
368
+ test("state and arbitrary variants are flagged too, and pointed at the inverted pattern", () => {
369
+ /*
370
+ * The foreign rule beats `group-hover:flex` exactly as it beats `md:flex`,
371
+ * so a hover affordance written that way never appears. There is no
372
+ * breakpoint to move the `hidden` behind; the fix is to show the element
373
+ * by default and hide it under the negated state.
374
+ */
375
+ const facet: ForeignHiddenRuleFinding = onlyFinding(
376
+ jsxWithClass(OLD_FACET_EXCLUDE_CLASS),
377
+ );
378
+
379
+ expect(facet.unhidingTokens).toEqual(["group-hover:flex"]);
380
+ // The mechanical inversion is exactly what FacetValueRow.tsx now ships.
381
+ expect(facet.replacement).toBe(NEW_FACET_EXCLUDE_CLASS);
382
+ expect(facet.advice).toBe(GROUP_HOVER_REWRITE_ADVICE);
383
+ expect(readRepositoryFile(GROUP_HOVER_EXAMPLE_FILE)).toContain(
384
+ `"${NEW_FACET_EXCLUDE_CLASS}"`,
385
+ );
386
+
387
+ const stateVariants: Array<string> = [
388
+ OLD_DISCLOSURE_CLASS,
389
+ "hidden peer-checked:flex",
390
+ "hidden [&.active]:flex",
391
+ "hidden data-[state=open]:block",
392
+ "hidden aria-expanded:grid",
393
+ "hidden dark:block",
394
+ "hidden print:block",
395
+ "hidden lg:group-hover:flex",
396
+ "hidden group-hover/nav-row:flex",
397
+ "hidden group-[.is-open]:inline-flex",
398
+ "hidden min-[900px]:flex",
399
+ // A second unprefixed display would compete with the one moved forward.
400
+ "flex hidden group-hover:block",
401
+ ];
402
+
403
+ for (const classAttribute of stateVariants) {
404
+ const finding: ForeignHiddenRuleFinding = onlyFinding(
405
+ jsxWithClass(classAttribute),
406
+ );
407
+
408
+ expect(`${classAttribute}: ${finding.replacement}`).toBe(
409
+ `${classAttribute}: null`,
410
+ );
411
+ expect(finding.advice).toBe(STATE_VARIANT_ADVICE);
412
+ }
413
+
414
+ expect(STATE_VARIANT_ADVICE).toContain(DISCLOSURE_EXAMPLE_FILE);
415
+ expect(STATE_VARIANT_ADVICE).toContain(NEW_DISCLOSURE_CLASS);
416
+ expect(STATE_VARIANT_ADVICE).toContain("group-[:not(:hover)]:hidden");
417
+ });
418
+
419
+ test("the group rewrite advice says when the inversion is exact, how to name an outer group, and what dark mode does", () => {
420
+ /*
421
+ * `group-hover:flex` shows while ANY enclosing `.group` is hovered;
422
+ * `group-[:not(:hover)]:hidden` hides while any is NOT. With one group
423
+ * they agree. With nested groups the rewrite follows the innermost one,
424
+ * so the advice has to say so and name the escape hatch - a named group -
425
+ * and the dark theme brightens these reveals by class substring, so it
426
+ * has to say which substrings Theme.css already knows.
427
+ */
428
+ for (const advice of [STATE_VARIANT_ADVICE, GROUP_HOVER_REWRITE_ADVICE]) {
429
+ expect(advice).toContain(GROUP_REWRITE_CAVEAT);
430
+ }
431
+
432
+ expect(GROUP_REWRITE_CAVEAT).toContain("innermost `.group`");
433
+ expect(GROUP_REWRITE_CAVEAT).toContain("`group/card` on the trigger");
434
+ expect(GROUP_REWRITE_CAVEAT).toContain(
435
+ "`group-[:not(:hover)]/card:hidden` on the element",
436
+ );
437
+ expect(GROUP_REWRITE_CAVEAT).toContain(NAMED_GROUP_EXAMPLE_FILE);
438
+ expect(GROUP_REWRITE_CAVEAT).toContain(THEME_STYLESHEET_FILE);
439
+ expect(GROUP_REWRITE_CAVEAT).toContain(
440
+ "already recognises `group-[:not(:hover)` beside `group-hover`",
441
+ );
442
+
443
+ // The named form the advice recommends is itself one the guard accepts.
444
+ expect(
445
+ findingsIn(jsxWithClass("flex h-5 w-5 group-[:not(:hover)]/card:hidden")),
446
+ ).toEqual([]);
447
+ expect(splitVariants("group-[:not(:hover)]/card:hidden")).toEqual([
448
+ "group-[:not(:hover)]/card",
449
+ "hidden",
450
+ ]);
451
+ });
452
+
453
+ test("min-width and max-width un-hiders together get advice, not a wrong rewrite", () => {
454
+ const finding: ForeignHiddenRuleFinding = onlyFinding(
455
+ jsxWithClass("hidden max-sm:block lg:flex"),
456
+ );
457
+
458
+ expect(finding.replacement).toBe(null);
459
+ expect(finding.advice).toBe(MIXED_BREAKPOINT_ADVICE);
460
+ });
461
+
462
+ test("template literals: the static text is one string, and each substitution is read", () => {
463
+ const withTrailingSubstitution: ForeignHiddenRuleFinding = onlyFinding(
464
+ "const element = <div className={`hidden md:flex ${props.className}`} />;",
465
+ );
466
+
467
+ expect(withTrailingSubstitution.text).toBe(
468
+ "hidden md:flex ${props.className}",
469
+ );
470
+ expect(withTrailingSubstitution.replacement).toBe(
471
+ "max-md:hidden md:flex ${props.className}",
472
+ );
473
+
474
+ expect(
475
+ onlyFinding(
476
+ "const element = <div className={`${base} hidden md:flex`} />;",
477
+ ).replacement,
478
+ ).toBe("${base} max-md:hidden md:flex");
479
+
480
+ // The `hidden` and the un-hider on either side of a substitution still meet.
481
+ expect(
482
+ onlyFinding(
483
+ "const element = <div className={`hidden ${gap} md:flex`} />;",
484
+ ).replacement,
485
+ ).toBe("max-md:hidden ${gap} md:flex");
486
+
487
+ // A string inside a substitution is a string of its own (Card.tsx's shape).
488
+ const nested: ForeignHiddenRuleFinding = onlyFinding(
489
+ 'const element = <div className={`flex mb-5${props.hideOnMobile ? " hidden md:flex" : ""}`} />;',
490
+ );
491
+
492
+ expect(nested.kind).toBe("same-string");
493
+ expect(nested.text).toBe(" hidden md:flex");
494
+ expect(nested.replacement).toBe(" max-md:hidden md:flex");
495
+ });
496
+
497
+ test("multi-line strings are read whole, and reported at the line they start on", () => {
498
+ const template: ForeignHiddenRuleFinding = onlyFinding(
499
+ lines(
500
+ "const element = (",
501
+ " <div",
502
+ " className={`",
503
+ " hidden",
504
+ " items-center",
505
+ " md:flex",
506
+ " `}",
507
+ " />",
508
+ ");",
509
+ ),
510
+ );
511
+
512
+ expect(template.line).toBe(3);
513
+ expect(template.text).toBe("hidden items-center md:flex");
514
+ expect(template.replacement).toBe("max-md:hidden items-center md:flex");
515
+
516
+ // A string literal continued with a backslash.
517
+ const continued: ForeignHiddenRuleFinding = onlyFinding(
518
+ lines('const menuClassName: string = "hidden \\', 'lg:flex";'),
519
+ );
520
+
521
+ expect(continued.line).toBe(1);
522
+ expect(continued.replacement).toBe("max-lg:hidden lg:flex");
523
+ });
524
+
525
+ test("a hidden and its un-hider in different pieces of one class expression are flagged together", () => {
526
+ const ternaryInTemplate: ForeignHiddenRuleFinding = onlyFinding(
527
+ 'const element = <div className={`${isOpen ? "flex" : "hidden"} md:flex`} />;',
528
+ );
529
+
530
+ expect(ternaryInTemplate).toEqual({
531
+ file: FIXTURE_FILE,
532
+ line: 1,
533
+ kind: "cross-piece",
534
+ hidingClass: "hidden",
535
+ text: "hidden",
536
+ partnerText: '${isOpen ? "flex" : "hidden"} md:flex',
537
+ unhidingTokens: ["md:flex"],
538
+ replacement: "max-md:hidden",
539
+ advice: "replace the bare `hidden` with `max-md:hidden`",
540
+ });
541
+
542
+ // One finding per bare `hidden`, each at its own line.
543
+ expect(
544
+ findingsIn(
545
+ lines(
546
+ "const element = (",
547
+ " <aside",
548
+ " className={`${",
549
+ ' isPaneOpenOnMobile ? "hidden" : "block"',
550
+ " } rounded-b-xl bg-gray-50 px-5 py-5 lg:block md:px-6`}",
551
+ " />",
552
+ ");",
553
+ "const panel = (",
554
+ " <section",
555
+ " className={`${",
556
+ ' isPaneOpenOnMobile ? "block" : "hidden"',
557
+ " } px-5 py-5 lg:block lg:min-w-0 md:px-6`}",
558
+ " />",
559
+ ");",
560
+ ),
561
+ ).map((finding: ForeignHiddenRuleFinding): string => {
562
+ return `${finding.line} ${finding.kind} ${finding.replacement}`;
563
+ }),
564
+ ).toEqual(["4 cross-piece max-lg:hidden", "11 cross-piece max-lg:hidden"]);
565
+
566
+ const crossPieceSources: Array<string> = [
567
+ 'const element = <div className={clsx("hidden", "md:flex")} />;',
568
+ 'const element = <div className={cn("hidden", isWide && "lg:flex")} />;',
569
+ 'const merged: string = twMerge("hidden", "sm:block");',
570
+ 'const names: string = classNames("hidden", { "md:flex": isWide });',
571
+ 'const names: string = cx({ hidden: !isOpen }, "lg:block");',
572
+ 'const navClassName: string = "hidden " + "md:flex";',
573
+ 'const props = { menuClassName: ["hidden", "md:flex"].join(" ") };',
574
+ 'element.className = "hidden " + "sm:block";',
575
+ 'function Nav({ className = "hidden" + " md:flex" }: Props): void {}',
576
+ 'const link = <NavLink className={({ isActive }) => { return (isActive ? "font-bold" : "hidden") + " lg:flex"; }} />;',
577
+ lines(
578
+ "const containerClassName: string =",
579
+ ' "hidden " +',
580
+ ' "md:flex";',
581
+ ),
582
+ ];
583
+
584
+ for (const source of crossPieceSources) {
585
+ const findings: Array<ForeignHiddenRuleFinding> = findingsIn(source);
586
+
587
+ expect(
588
+ `${source}\n=> ${findings
589
+ .map((finding: ForeignHiddenRuleFinding): string => {
590
+ return finding.kind;
591
+ })
592
+ .join(", ")}`,
593
+ ).toBe(`${source}\n=> cross-piece`);
594
+ }
595
+ });
596
+
597
+ test("cross-piece state variants and disagreeing branches get advice, not a wrong rewrite", () => {
598
+ // ResourceGroupNavigator's shape: a named group un-hides a ternary's hidden.
599
+ const namedGroup: ForeignHiddenRuleFinding = onlyFinding(
600
+ 'const actions = <div className={`absolute group-hover/nav-row:flex ${isSelected ? "flex bg-indigo-50" : "hidden bg-gray-100"}`} />;',
601
+ );
602
+
603
+ expect(namedGroup.kind).toBe("cross-piece");
604
+ expect(namedGroup.text).toBe("hidden bg-gray-100");
605
+ expect(namedGroup.replacement).toBe(null);
606
+ expect(namedGroup.advice).toBe(STATE_VARIANT_ADVICE);
607
+
608
+ /*
609
+ * The two un-hiders are alternatives that never meet, and they un-hide
610
+ * at different screens: `max-md:hidden` would leave the lg branch
611
+ * visible between 768px and 1024px, so no one rewrite is offered.
612
+ */
613
+ const disagreeing: ForeignHiddenRuleFinding = onlyFinding(
614
+ 'const element = <div className={`${isWide ? "md:flex" : "lg:block"} ${isCollapsed ? "hidden" : ""}`} />;',
615
+ );
616
+
617
+ expect(disagreeing.replacement).toBe(null);
618
+ expect(disagreeing.advice).toBe(DISAGREEING_PIECES_ADVICE);
619
+ });
620
+
621
+ test("an un-hider that is only there under its own condition gets advice, not a rewrite that shows the element when it is off", () => {
622
+ /*
623
+ * `cn("hidden", isWide && "lg:flex")` hides the element at every width
624
+ * while isWide is false. Rewriting the `hidden` alone to `max-lg:hidden`
625
+ * would show it from 1024px up in exactly that case - the cascade model
626
+ * says so below - so no rewrite is printed.
627
+ */
628
+ expect(resolveDisplay("hidden", 1024)).toBe("hidden");
629
+ expect(resolveDisplay("max-lg:hidden", 1024)).toBe(null);
630
+
631
+ const conditionalSources: Array<string> = [
632
+ 'const element = <div className={cn("hidden", isWide && "lg:flex")} />;',
633
+ 'const names: string = classNames("hidden", { "lg:flex": isWide });',
634
+ 'const element = <div className={`hidden ${isWide ? "lg:flex" : ""}`} />;',
635
+ 'const element = <div className={`hidden ${label || "lg:flex"}`} />;',
636
+ ];
637
+
638
+ for (const source of conditionalSources) {
639
+ const finding: ForeignHiddenRuleFinding = onlyFinding(source);
640
+
641
+ expect(`${source}\n=> ${finding.kind} ${finding.replacement}`).toBe(
642
+ `${source}\n=> cross-piece null`,
643
+ );
644
+ expect(finding.advice).toBe(CONDITIONAL_PARTNER_ADVICE);
645
+ }
646
+
647
+ expect(CONDITIONAL_PARTNER_ADVICE).toContain(
648
+ '`condition ? "max-<bp>:hidden <bp>:<display>" : "hidden"`',
649
+ );
650
+
651
+ // Only the partner's own condition matters: a conditional `hidden` beside an unconditional un-hider is exact.
652
+ expect(
653
+ onlyFinding('const names: string = cx({ hidden: !isOpen }, "lg:block");')
654
+ .replacement,
655
+ ).toBe("max-lg:hidden");
656
+ expect(
657
+ onlyFinding(
658
+ 'const element = <div className={cn(isWide && "hidden", isWide && "lg:flex")} />;',
659
+ ).advice,
660
+ ).toBe(CONDITIONAL_PARTNER_ADVICE);
661
+ });
662
+
663
+ test("the sr-only analogue: a breakpoint not-sr-only gets the exact max-<bp>:sr-only rewrite, identical at every width", () => {
664
+ /*
665
+ * Bootstrap 3 and HTML5 Boilerplate also ship `.sr-only`. Appended after
666
+ * Tailwind's <style> it ties with `.sm\:not-sr-only` (a media query adds
667
+ * no specificity) and wins on source order, so a label meant to show
668
+ * from `sm` up stays a 1px box at every width - the session replay
669
+ * actions, test-notification states and the feed options label. The
670
+ * sr-only resolver in ResponsiveVisibility.ts - the same one
671
+ * SrOnlyForeignRule.test.tsx sweeps the tree with - checks each rewrite.
672
+ */
673
+ const cases: Array<[string, string]> = [
674
+ ["sr-only sm:not-sr-only", NEW_SM_LABEL_CLASS],
675
+ ["sr-only xl:not-sr-only", NEW_XL_LABEL_CLASS],
676
+ [
677
+ "text-xs sr-only md:not-sr-only md:ml-1",
678
+ "text-xs max-md:sr-only md:not-sr-only md:ml-1",
679
+ ],
680
+ // Several screens: the narrowest one decides.
681
+ [
682
+ "sr-only lg:not-sr-only md:not-sr-only",
683
+ "max-md:sr-only lg:not-sr-only md:not-sr-only",
684
+ ],
685
+ // A max-width un-hider is mirrored.
686
+ ["sr-only max-md:not-sr-only", "md:sr-only max-md:not-sr-only"],
687
+ ];
688
+
689
+ for (const [original, replacement] of cases) {
690
+ const finding: ForeignHiddenRuleFinding = onlyFinding(
691
+ jsxWithClass(original),
692
+ );
693
+
694
+ expect(finding.hidingClass).toBe("sr-only");
695
+ expect(finding.text).toBe(original);
696
+ expect(finding.replacement).toBe(replacement);
697
+ expect(findingsIn(jsxWithClass(replacement))).toEqual([]);
698
+
699
+ for (const width of WIDTHS_IN_PX) {
700
+ expect(
701
+ `${replacement} @ ${width}px collapsed: ${isVisuallyCollapsed(replacement, width)}`,
702
+ ).toBe(
703
+ `${replacement} @ ${width}px collapsed: ${isVisuallyCollapsed(original, width)}`,
704
+ );
705
+ expect(
706
+ `${replacement} @ ${width}px, foreign rule, collapsed: ${isVisuallyCollapsed(replacement, width, FOREIGN_SR_ONLY_RULE)}`,
707
+ ).toBe(
708
+ `${replacement} @ ${width}px, foreign rule, collapsed: ${isVisuallyCollapsed(original, width)}`,
709
+ );
710
+ expect(isVisuallyCollapsed(original, width, FOREIGN_SR_ONLY_RULE)).toBe(
711
+ true,
712
+ );
713
+ }
714
+ }
715
+
716
+ // The four label files ship the recommended strings.
717
+ for (const [file, shipped] of SR_ONLY_LABEL_SITES) {
718
+ expect(readRepositoryFile(file)).toContain(`"${shipped}"`);
719
+ }
720
+ });
721
+
722
+ test("a state variant out-specifies a foreign .sr-only, so a not-sr-only behind one is left alone", () => {
723
+ /*
724
+ * The `.sr-only` those stylesheets ship is not !important, so the
725
+ * selector with more specificity wins wherever the sheet sits. Every
726
+ * state or pseudo variant adds to the selector: `.focus\:not-sr-only:focus`
727
+ * keeps the skip links working, and hover, group-*, peer-*, aria-*,
728
+ * data-*, dark: (darkMode "class") and arbitrary [&...] variants beat
729
+ * the lone foreign class the same way - even stacked under a screen.
730
+ * Flagging them would ask for rewrites that fix nothing. The real skip
731
+ * links must stay clean.
732
+ */
733
+ const stateRevealed: Array<string> = [
734
+ `${SKIP_LINK_CLASS_PREFIX} focus:absolute focus:top-4 focus:left-4`,
735
+ "sr-only focus-visible:not-sr-only",
736
+ "sr-only focus-within:not-sr-only",
737
+ "sr-only group-focus:not-sr-only",
738
+ "sr-only group-focus-within/menu:not-sr-only",
739
+ "sr-only peer-focus-visible:not-sr-only",
740
+ "sr-only [&:focus]:not-sr-only",
741
+ "sr-only sm:focus:not-sr-only",
742
+ "sr-only hover:not-sr-only",
743
+ "sr-only group-hover:not-sr-only",
744
+ "sr-only group-hover/card:not-sr-only",
745
+ "sr-only peer-checked:not-sr-only",
746
+ "sr-only aria-expanded:not-sr-only",
747
+ "sr-only data-[state=open]:not-sr-only",
748
+ "sr-only dark:not-sr-only",
749
+ "sr-only [&.is-open]:not-sr-only",
750
+ "sr-only group-[.is-open]:not-sr-only",
751
+ "sr-only lg:group-hover:not-sr-only",
752
+ "sr-only md:hover:not-sr-only",
753
+ ];
754
+
755
+ for (const classAttribute of stateRevealed) {
756
+ expect(
757
+ `${classAttribute} => ${formatFindings(findingsIn(jsxWithClass(classAttribute)))}`,
758
+ ).toBe(`${classAttribute} => `);
759
+ }
760
+
761
+ // Split across pieces, a state partner is left alone just the same.
762
+ expect(
763
+ findingsIn(
764
+ 'const label = <span className={clsx("sr-only", "group-hover:not-sr-only")} />;',
765
+ ),
766
+ ).toEqual([]);
767
+
768
+ for (const file of SKIP_LINK_FILES) {
769
+ const source: string = readRepositoryFile(file);
770
+
771
+ expect(source).toContain(SKIP_LINK_CLASS_PREFIX);
772
+ expect(
773
+ `${file}: ${formatFindings(findForeignHiddenRuleHazards(file, source))}`,
774
+ ).toBe(`${file}: `);
775
+ }
776
+ });
777
+
778
+ test("every media-query not-sr-only is flagged, named screen or not; the others get advice", () => {
779
+ /*
780
+ * An @media wrapper adds no specificity, so a foreign `.sr-only` ties
781
+ * with any of these and wins on source order. Only the named screens
782
+ * have the mechanical max-<bp> rewrite (see the test above); the rest
783
+ * get the advice.
784
+ */
785
+ const mediaRevealed: Array<string> = [
786
+ "sr-only print:not-sr-only",
787
+ "sr-only min-[900px]:not-sr-only",
788
+ "sr-only max-[900px]:not-sr-only",
789
+ "sr-only motion-safe:not-sr-only",
790
+ "sr-only motion-reduce:not-sr-only",
791
+ "sr-only contrast-more:not-sr-only",
792
+ "sr-only contrast-less:not-sr-only",
793
+ "sr-only portrait:not-sr-only",
794
+ "sr-only landscape:not-sr-only",
795
+ "sr-only forced-colors:not-sr-only",
796
+ // A stack of media queries is still one @media block.
797
+ "sr-only md:print:not-sr-only",
798
+ ];
799
+
800
+ for (const classAttribute of mediaRevealed) {
801
+ const finding: ForeignHiddenRuleFinding = onlyFinding(
802
+ jsxWithClass(classAttribute),
803
+ );
804
+
805
+ expect(
806
+ `${classAttribute}: ${finding.hidingClass} ${finding.replacement}`,
807
+ ).toBe(`${classAttribute}: sr-only null`);
808
+ expect(finding.advice).toBe(SR_ONLY_MEDIA_ADVICE);
809
+ }
810
+
811
+ const mixed: ForeignHiddenRuleFinding = onlyFinding(
812
+ jsxWithClass("sr-only max-sm:not-sr-only lg:not-sr-only"),
813
+ );
814
+
815
+ expect(mixed.replacement).toBe(null);
816
+ expect(mixed.advice).toBe(SR_ONLY_MIXED_BREAKPOINT_ADVICE);
817
+
818
+ // Split across pieces, the pair is read the same way as a split `hidden`.
819
+ const crossPiece: ForeignHiddenRuleFinding = onlyFinding(
820
+ 'const label = <span className={clsx("sr-only", "sm:not-sr-only")} />;',
821
+ );
822
+
823
+ expect(crossPiece.kind).toBe("cross-piece");
824
+ expect(crossPiece.hidingClass).toBe("sr-only");
825
+ expect(crossPiece.replacement).toBe("max-sm:sr-only");
826
+ expect(
827
+ onlyFinding(
828
+ 'const label = <span className={cn("sr-only", isWide && "sm:not-sr-only")} />;',
829
+ ).advice,
830
+ ).toBe(SR_ONLY_CONDITIONAL_PARTNER_ADVICE);
831
+
832
+ // What is not the pair: the shipped form, a lone sr-only, `!sr-only`.
833
+ for (const classAttribute of [
834
+ NEW_SM_LABEL_CLASS,
835
+ "sr-only",
836
+ "not-sr-only",
837
+ "md:sr-only",
838
+ "!sr-only md:not-sr-only",
839
+ ]) {
840
+ expect(findingsIn(jsxWithClass(classAttribute))).toEqual([]);
841
+ }
842
+ });
843
+
844
+ test("reverting the sr-only labels in memory fails each file, and the guard recommends what shipped", () => {
845
+ for (const [file, shipped] of SR_ONLY_LABEL_SITES) {
846
+ const current: string = readRepositoryFile(file);
847
+ const reverted: string = current.replace(MAX_WIDTH_SR_ONLY, "$1sr-only");
848
+
849
+ expect(reverted).not.toBe(current);
850
+ expect(findForeignHiddenRuleHazards(file, current)).toEqual([]);
851
+
852
+ const findings: Array<ForeignHiddenRuleFinding> =
853
+ findForeignHiddenRuleHazards(file, reverted);
854
+
855
+ expect(findings.length).toBeGreaterThan(0);
856
+ expect(
857
+ findings.map((finding: ForeignHiddenRuleFinding): string => {
858
+ return `${file}:${finding.line} ${finding.hidingClass} ${finding.replacement}`;
859
+ }),
860
+ ).toEqual(
861
+ findings.map((finding: ForeignHiddenRuleFinding): string => {
862
+ return `${file}:${finding.line} sr-only ${shipped}`;
863
+ }),
864
+ );
865
+ }
866
+ });
867
+
868
+ test("leaves alone every shape the foreign rule cannot break, and anything that is not a string", () => {
869
+ const safeSources: Array<string> = [
870
+ jsxWithClass("max-md:hidden md:flex"),
871
+ jsxWithClass(NEW_NAVBAR_CLASS),
872
+ jsxWithClass("flex lg:hidden"),
873
+ jsxWithClass("block md:hidden lg:block"),
874
+ // Toggled from JavaScript: the foreign rule can only hide more.
875
+ jsxWithClass("hidden"),
876
+ 'const element = <div className={isOpen ? "block" : "hidden"} />;',
877
+ // The two branches of a ternary never meet in one class list.
878
+ 'const element = <div className={isOpen ? "hidden" : "md:flex"} />;',
879
+ 'const element = <div className={label || "hidden"} />;',
880
+ jsxWithClass("md:hidden"),
881
+ jsxWithClass("overflow-hidden md:flex"),
882
+ // The shipped sr-only form, and the skip-link idiom.
883
+ jsxWithClass(NEW_SM_LABEL_CLASS),
884
+ jsxWithClass(`${SKIP_LINK_CLASS_PREFIX} focus:absolute`),
885
+ // `.\!hidden` is not the class the foreign rule names.
886
+ jsxWithClass("!hidden md:flex"),
887
+ // Variants that do not set a display.
888
+ jsxWithClass(
889
+ "hidden md:flex-col md:opacity-100 group-hover:text-gray-900",
890
+ ),
891
+ // The converted forms themselves.
892
+ jsxWithClass(NEW_FACET_EXCLUDE_CLASS),
893
+ jsxWithClass(NEW_DISCLOSURE_CLASS),
894
+ // Comments and JSX text are not strings.
895
+ '// <div className="hidden md:flex" />',
896
+ "/* hidden lg:flex */ const value: number = 1;",
897
+ "/** Use `hidden md:flex` for desktop-only rows. */ const doc: number = 1;",
898
+ 'const element = <div>{/* className="hidden md:flex" */}</div>;',
899
+ "const element = <p>hidden md:flex hides it on phones</p>;",
900
+ // A ternary's condition is not class text.
901
+ 'const element = <div className={mode === "hidden" ? "flex" : "md:flex"} />;',
902
+ // An object literal holds classes for different elements.
903
+ 'const classes = { nav: "hidden", panel: "md:flex" };',
904
+ // Tokens glued to a substitution are only part of a class name.
905
+ "const element = <div className={`peer-${state}:hidden md:flex`} />;",
906
+ // Strings in no class expression never meet.
907
+ 'const first: string = "hidden"; const second: string = "md:flex";',
908
+ ];
909
+
910
+ for (const source of safeSources) {
911
+ expect(`${source}\n=> ${formatFindings(findingsIn(source))}`).toBe(
912
+ `${source}\n=> `,
913
+ );
914
+ }
915
+ });
916
+
917
+ test("the failure message names file:line, the string, the exact replacement and why", () => {
918
+ const findings: Array<ForeignHiddenRuleFinding> = findingsIn(
919
+ lines(
920
+ 'const nav = <nav className="hidden md:flex" />;',
921
+ 'const hint = <span className="hidden group-open:inline" />;',
922
+ 'const pane = <div className={`${isOpen ? "block" : "hidden"} lg:block`} />;',
923
+ 'const row = <div className="hidden text-gray-400 group-hover:flex" />;',
924
+ ),
925
+ );
926
+
927
+ const message: string = formatFindings(findings);
928
+
929
+ expect(message).toContain("4 class string(s)");
930
+ expect(message).toContain("max-<bp>:hidden <bp>:<display>");
931
+
932
+ expect(message).toContain(`${FIXTURE_FILE}:1\n`);
933
+ expect(message).toContain('offending string: "hidden md:flex"');
934
+ expect(message).toContain('replace with: "max-md:hidden md:flex"');
935
+
936
+ expect(message).toContain(`${FIXTURE_FILE}:2\n`);
937
+ expect(message).toContain(`offending string: "${OLD_DISCLOSURE_CLASS}"`);
938
+ expect(message).toContain(DISCLOSURE_EXAMPLE_FILE);
939
+ expect(message).toContain(GROUP_HOVER_EXAMPLE_FILE);
940
+
941
+ expect(message).toContain(
942
+ `${FIXTURE_FILE}:3 (the bare \`hidden\` and the utility that shows the element again are in different pieces of one class expression)`,
943
+ );
944
+ expect(message).toContain('replace with: "max-lg:hidden"');
945
+
946
+ // Both state-variant answers carry the nested-group and dark-mode caveat.
947
+ expect(message).toContain(`${FIXTURE_FILE}:4\n`);
948
+ expect(message).toContain(
949
+ 'replace with: "flex text-gray-400 group-[:not(:hover)]:hidden"',
950
+ );
951
+ expect(message).toContain(`fix: ${STATE_VARIANT_ADVICE}`);
952
+ expect(message).toContain(
953
+ `fix: ${GROUP_HOVER_REWRITE_ADVICE}`,
954
+ );
955
+ expect(message).toContain("group-[:not(:hover)]/card:hidden");
956
+ expect(message).toContain(THEME_STYLESHEET_FILE);
957
+
958
+ expect(message.endsWith(WHY_PARAGRAPH)).toBe(true);
959
+ expect(message).not.toContain(SR_ONLY_WHY_PARAGRAPH);
960
+ expect(WHY_PARAGRAPH).toContain(".hidden{display:none !important}");
961
+ expect(WHY_PARAGRAPH).toContain("Bootstrap 3");
962
+ expect(WHY_PARAGRAPH).toContain("navigation bar");
963
+
964
+ expect(formatFindings([])).toBe("");
965
+ });
966
+
967
+ test("the failure message counts sr-only findings apart and explains them", () => {
968
+ const srOnlyMessage: string = formatFindings(
969
+ findingsIn('const label = <span className="sr-only sm:not-sr-only" />;'),
970
+ );
971
+
972
+ expect(srOnlyMessage).toContain(
973
+ "1 class string(s) visually hide an element with the bare `sr-only` class",
974
+ );
975
+ expect(srOnlyMessage).toContain(
976
+ "Use `max-<bp>:sr-only <bp>:not-sr-only` instead of `sr-only <bp>:not-sr-only`",
977
+ );
978
+ expect(srOnlyMessage).toContain(
979
+ `replace with: "${NEW_SM_LABEL_CLASS}"`,
980
+ );
981
+ expect(srOnlyMessage).not.toContain("bare `hidden` class");
982
+ expect(srOnlyMessage.endsWith(SR_ONLY_WHY_PARAGRAPH)).toBe(true);
983
+ expect(SR_ONLY_WHY_PARAGRAPH).toContain("`.sr-only`");
984
+ expect(SR_ONLY_WHY_PARAGRAPH).toContain("skip link");
985
+ expect(SR_ONLY_WHY_PARAGRAPH).toContain("group-hover:not-sr-only");
986
+
987
+ const bothMessage: string = formatFindings(
988
+ findingsIn(
989
+ lines(
990
+ 'const label = <span className="sr-only sm:not-sr-only" />;',
991
+ 'const nav = <nav className="hidden md:flex" />;',
992
+ ),
993
+ ),
994
+ );
995
+
996
+ expect(bothMessage).toContain(
997
+ "1 class string(s) hide an element with the bare `hidden` class",
998
+ );
999
+ expect(bothMessage).toContain(
1000
+ "1 class string(s) visually hide an element with the bare `sr-only` class",
1001
+ );
1002
+ expect(bothMessage).toContain(SR_ONLY_WHY_PARAGRAPH);
1003
+ expect(bothMessage.endsWith(WHY_PARAGRAPH)).toBe(true);
1004
+ });
1005
+
1006
+ test("the files the advice points at still show the pattern it describes", () => {
1007
+ /*
1008
+ * A failure message that sends a developer to an example that no longer
1009
+ * exists is worse than none.
1010
+ */
1011
+ expect(readRepositoryFile(GROUP_HOVER_EXAMPLE_FILE)).toContain(
1012
+ "group-[:not(:hover)]:hidden",
1013
+ );
1014
+ expect(readRepositoryFile(DISCLOSURE_EXAMPLE_FILE)).toContain(
1015
+ `"${NEW_DISCLOSURE_CLASS}"`,
1016
+ );
1017
+
1018
+ const namedGroupSource: string = readRepositoryFile(
1019
+ NAMED_GROUP_EXAMPLE_FILE,
1020
+ );
1021
+
1022
+ expect(namedGroupSource).toContain("group/nav-row ");
1023
+ expect(namedGroupSource).toMatch(
1024
+ /group-\[:not\(:hover\)[^\s"]*\/nav-row:hidden/,
1025
+ );
1026
+
1027
+ const themeStylesheet: string = readRepositoryFile(THEME_STYLESHEET_FILE);
1028
+
1029
+ expect(themeStylesheet).toContain(
1030
+ 'html.dark .group:hover [class*="group-hover"][class*="text-gray-"]',
1031
+ );
1032
+ expect(themeStylesheet).toContain(
1033
+ 'html.dark .group:hover [class*="group-[:not(:hover)"][class*="text-gray-"]',
1034
+ );
1035
+ });
1036
+ });
1037
+
1038
+ describe("ForeignHiddenRuleGuard view extractor", () => {
1039
+ test("reads the static text of a class attribute, with each <% %> tag dropped as a token boundary", () => {
1040
+ const source: string = lines(
1041
+ "<a href=\"/docs/<%= lang %>/<%= link %>\" class=\"docs-nav__link <%= isActive ? 'is-active' : '' %> px-2\">",
1042
+ " <%= title %>",
1043
+ "</a>",
1044
+ );
1045
+
1046
+ const attributes: Array<ClassAttribute> = extractClassAttributes(source);
1047
+
1048
+ // The href's tags are not in the class attribute.
1049
+ expect(attributes).toHaveLength(1);
1050
+ expect(attributes[0]!.line).toBe(1);
1051
+ expect(attributes[0]!.parts).toEqual(["docs-nav__link ", " px-2"]);
1052
+ expect(
1053
+ attributes[0]!.tags.map((tag: EjsTag): [string, string, boolean] => {
1054
+ return [tag.text, tag.code, tag.outputs];
1055
+ }),
1056
+ ).toEqual([
1057
+ [
1058
+ "<%= isActive ? 'is-active' : '' %>",
1059
+ " isActive ? 'is-active' : '' ",
1060
+ true,
1061
+ ],
1062
+ ]);
1063
+ });
1064
+
1065
+ test("reads double-, single- and un-quoted values, in any case, over as many lines as they take", () => {
1066
+ expect(
1067
+ readAttributes(
1068
+ lines(
1069
+ "<span class='tooltip hidden md:inline'>",
1070
+ '<div CLASS="hidden lg:flex">',
1071
+ "<p class=hidden>",
1072
+ '<b class = "a b">',
1073
+ ),
1074
+ ),
1075
+ ).toEqual([
1076
+ "1: tooltip hidden md:inline",
1077
+ "2: hidden lg:flex",
1078
+ "3: hidden",
1079
+ "4: a b",
1080
+ ]);
1081
+
1082
+ // Reported at the line of the attribute's name, not of the tag.
1083
+ const multiLine: Array<ClassAttribute> = extractClassAttributes(
1084
+ lines(
1085
+ "<div",
1086
+ ' id="toc-panel"',
1087
+ ' class="hidden',
1088
+ " items-center",
1089
+ ' md:flex"',
1090
+ ">",
1091
+ ),
1092
+ );
1093
+
1094
+ expect(multiLine).toHaveLength(1);
1095
+ expect(multiLine[0]!.line).toBe(3);
1096
+ expect(multiLine[0]!.parts).toEqual([
1097
+ "hidden\n items-center\n md:flex",
1098
+ ]);
1099
+ });
1100
+
1101
+ test("a quote inside a tag, or a > inside another attribute, does not end the markup early", () => {
1102
+ expect(
1103
+ readAttributes(
1104
+ lines(
1105
+ '<div class="<%= wide ? "w-full" : "w-1/2" %> hidden md:flex">',
1106
+ '<button onclick="items.forEach((item) => item.close())" class="hidden sm:flex">',
1107
+ '<<%= headingTag %> class="hidden lg:block">',
1108
+ ),
1109
+ ),
1110
+ ).toEqual([
1111
+ "1: | hidden md:flex",
1112
+ "2: hidden sm:flex",
1113
+ "3: hidden lg:block",
1114
+ ]);
1115
+ });
1116
+
1117
+ test("other attributes, comments, text and text-only elements are not class attributes", () => {
1118
+ const source: string = lines(
1119
+ '<div data-class="hidden md:flex"></div>',
1120
+ '<div :class="{ hidden: !open }" x-bind:class="md:flex"></div>',
1121
+ '<div className="hidden md:flex"></div>',
1122
+ "<div title=\"class='hidden md:flex'\"></div>",
1123
+ '<p>Write class="hidden md:flex" and it vanishes.</p>',
1124
+ '<pre><code>&lt;div class="hidden md:flex"&gt;</code></pre>',
1125
+ '<!-- <div class="hidden md:flex"></div> -->',
1126
+ "<style>.tip::after { content: '<i class=\"hidden md:flex\">'; }</style>",
1127
+ '<textarea><div class="hidden md:flex"></div></textarea>',
1128
+ '<title>class="hidden md:flex"</title>',
1129
+ );
1130
+
1131
+ expect(readAttributes(source)).toEqual([]);
1132
+ expect(viewFindingsIn(source)).toEqual([]);
1133
+ });
1134
+
1135
+ test("markup inside a <script>'s strings is read as an attribute; className and classList are not", () => {
1136
+ /*
1137
+ * The decision for script text: `el.innerHTML = '<span class="...">'`
1138
+ * becomes a real class attribute the moment it is inserted, so the
1139
+ * foreign rule hits it just the same, and it is read. A class a script
1140
+ * sets with `className =` or `classList` is not an attribute and is not
1141
+ * read here: toggling a lone `hidden` from JavaScript is the one use the
1142
+ * foreign rule cannot break. A JSON <script> that carries markup with
1143
+ * escaped quotes is read too.
1144
+ */
1145
+ const source: string = lines(
1146
+ "<script>",
1147
+ " button.innerHTML = '<span class=\"hidden sm:inline\">Copy</span>';",
1148
+ ' panel.className = "hidden md:flex";',
1149
+ ' if (a < b) { list.innerHTML = "<li class=\'hidden lg:block\'>" + label + "</li>"; }',
1150
+ "</script>",
1151
+ '<script type="application/json">{"html": "<b class=\\"hidden md:flex\\">"}</script>',
1152
+ '<div class="after-script">',
1153
+ );
1154
+
1155
+ expect(readAttributes(source)).toEqual([
1156
+ "2: hidden sm:inline",
1157
+ "4: hidden lg:block",
1158
+ "6: hidden md:flex",
1159
+ "7: after-script",
1160
+ ]);
1161
+
1162
+ expect(
1163
+ viewFindingsIn(source).map(
1164
+ (finding: ForeignHiddenRuleFinding): string => {
1165
+ return `${finding.line} ${finding.replacement}`;
1166
+ },
1167
+ ),
1168
+ ).toEqual([
1169
+ "2 max-sm:hidden sm:inline",
1170
+ "4 max-lg:hidden lg:block",
1171
+ "6 max-md:hidden md:flex",
1172
+ ]);
1173
+ });
1174
+
1175
+ test("tells output tags from scriptlets and comments, and reads <%% as text", () => {
1176
+ const tags: Array<EjsTag> = findEjsTags(
1177
+ lines(
1178
+ "<%# note %><%- html -%><%_ trim _%>",
1179
+ "<%% literal %%><% if (a) { %>",
1180
+ ),
1181
+ );
1182
+
1183
+ expect(
1184
+ tags.map((tag: EjsTag): string => {
1185
+ return `${tag.codeLine} ${tag.text} [${tag.code}] outputs=${tag.outputs} comment=${tag.isComment}`;
1186
+ }),
1187
+ ).toEqual([
1188
+ "1 <%# note %> [ note ] outputs=false comment=true",
1189
+ "1 <%- html -%> [ html ] outputs=true comment=false",
1190
+ "1 <%_ trim _%> [ trim ] outputs=false comment=false",
1191
+ "2 <% if (a) { %> [ if (a) { ] outputs=false comment=false",
1192
+ ]);
1193
+ });
1194
+
1195
+ test("views and HTML pages are read as markup; modules as code", () => {
1196
+ expect(isMarkupFile("packages/Home/Views/nav.ejs")).toBe(true);
1197
+ expect(isMarkupFile(DASHBOARD_OFFLINE_PAGE)).toBe(true);
1198
+ expect(isMarkupFile(NAVBAR_FILE)).toBe(false);
1199
+
1200
+ // In a module this line is a comment; in a page it is markup after some text.
1201
+ const text: string = '// <div class="hidden md:flex"></div>';
1202
+
1203
+ expect(findForeignHiddenRuleHazards("offline.html", text)).toHaveLength(1);
1204
+ expect(findForeignHiddenRuleHazards("Nav.ts", text)).toEqual([]);
1205
+ });
1206
+ });
1207
+
1208
+ describe("ForeignHiddenRuleGuard over views", () => {
1209
+ test("flags the Docs sidebar that took every docs page's navigation away, and recommends what shipped", () => {
1210
+ const finding: ForeignHiddenRuleFinding = onlyViewFinding(
1211
+ lines(
1212
+ '<div class="relative z-0 mx-auto flex w-full">',
1213
+ " <!-- Desktop sidebar (left): hidden lg:block -->",
1214
+ ` <div class="${OLD_DOCS_SIDEBAR_CLASS}">`,
1215
+ ),
1216
+ );
1217
+
1218
+ expect(finding).toEqual({
1219
+ file: VIEW_FIXTURE_FILE,
1220
+ line: 3,
1221
+ kind: "same-string",
1222
+ hidingClass: "hidden",
1223
+ text: OLD_DOCS_SIDEBAR_CLASS,
1224
+ partnerText: null,
1225
+ // `lg:relative` is not a display utility; `lg:block` is what shows it.
1226
+ unhidingTokens: ["lg:block"],
1227
+ replacement: NEW_DOCS_SIDEBAR_CLASS,
1228
+ advice: "replace the bare `hidden` with `max-lg:hidden`",
1229
+ });
1230
+
1231
+ for (const width of WIDTHS_IN_PX) {
1232
+ expect(
1233
+ `${width}px: ${resolveDisplay(NEW_DOCS_SIDEBAR_CLASS, width, FOREIGN_RULE)}`,
1234
+ ).toBe(`${width}px: ${resolveDisplay(OLD_DOCS_SIDEBAR_CLASS, width)}`);
1235
+ expect(resolveDisplay(OLD_DOCS_SIDEBAR_CLASS, width, FOREIGN_RULE)).toBe(
1236
+ "hidden",
1237
+ );
1238
+ }
1239
+
1240
+ expect(readRepositoryFile(DOCS_INDEX_VIEW)).toContain(
1241
+ `class="${NEW_DOCS_SIDEBAR_CLASS}"`,
1242
+ );
1243
+ });
1244
+
1245
+ test("reverting Index.ejs in memory makes the real view fail the guard", () => {
1246
+ const current: string = readRepositoryFile(DOCS_INDEX_VIEW);
1247
+ const reverted: string = current.replace(
1248
+ `class="${NEW_DOCS_SIDEBAR_CLASS}"`,
1249
+ `class="${OLD_DOCS_SIDEBAR_CLASS}"`,
1250
+ );
1251
+
1252
+ expect(reverted).not.toBe(current);
1253
+ expect(findForeignHiddenRuleHazards(DOCS_INDEX_VIEW, current)).toEqual([]);
1254
+ expect(
1255
+ findForeignHiddenRuleHazards(DOCS_INDEX_VIEW, reverted).map(
1256
+ (finding: ForeignHiddenRuleFinding): string => {
1257
+ return `${finding.file}:${finding.line} ${finding.replacement}`;
1258
+ },
1259
+ ),
1260
+ ).toEqual([
1261
+ `${DOCS_INDEX_VIEW}:${lineContaining(reverted, OLD_DOCS_SIDEBAR_CLASS)} ${NEW_DOCS_SIDEBAR_CLASS}`,
1262
+ ]);
1263
+ });
1264
+
1265
+ test("a tag is a token boundary: the text around it is one string, and a token glued to it is not read", () => {
1266
+ const aroundTag: ForeignHiddenRuleFinding = onlyViewFinding(
1267
+ '<a href="/" class="docs-brand <%= extra %> hidden sm:flex items-center">',
1268
+ );
1269
+
1270
+ expect(aroundTag.text).toBe(
1271
+ "docs-brand <%= extra %> hidden sm:flex items-center",
1272
+ );
1273
+ expect(aroundTag.replacement).toBe(
1274
+ "docs-brand <%= extra %> max-sm:hidden sm:flex items-center",
1275
+ );
1276
+
1277
+ // The `hidden` and its un-hider on either side of a tag still meet.
1278
+ expect(
1279
+ onlyViewFinding('<div class="hidden <%= gap %> md:flex">').replacement,
1280
+ ).toBe("max-md:hidden <%= gap %> md:flex");
1281
+
1282
+ // A multi-line value is reported at the attribute and printed on one line.
1283
+ const multiLine: ForeignHiddenRuleFinding = onlyViewFinding(
1284
+ lines("<nav", ' class="hidden', " <%= navClass %>", ' lg:flex">'),
1285
+ );
1286
+
1287
+ expect(multiLine.line).toBe(2);
1288
+ expect(multiLine.text).toBe("hidden <%= navClass %> lg:flex");
1289
+
1290
+ for (const glued of [
1291
+ '<div class="hidden<%= suffix %> md:flex">',
1292
+ '<div class="hidden md:<%= display %>">',
1293
+ '<div class="<%= size %>hidden lg:block">',
1294
+ ]) {
1295
+ expect(`${glued} => ${formatFindings(viewFindingsIn(glued))}`).toBe(
1296
+ `${glued} => `,
1297
+ );
1298
+ }
1299
+ });
1300
+
1301
+ test("a string an output tag prints meets the attribute's static text", () => {
1302
+ const printed: ForeignHiddenRuleFinding = onlyViewFinding(
1303
+ lines(
1304
+ "<% tabs.forEach(function (tab, index) { %>",
1305
+ " <div class=\"code-panel md:block <%= index === 0 ? '' : 'hidden' %>\">",
1306
+ ),
1307
+ );
1308
+
1309
+ expect(printed).toEqual({
1310
+ file: VIEW_FIXTURE_FILE,
1311
+ line: 2,
1312
+ kind: "cross-piece",
1313
+ hidingClass: "hidden",
1314
+ text: "hidden",
1315
+ partnerText: "code-panel md:block <%= index === 0 ? '' : 'hidden' %>",
1316
+ unhidingTokens: ["md:block"],
1317
+ replacement: "max-md:hidden",
1318
+ advice: "replace the bare `hidden` with `max-md:hidden`",
1319
+ });
1320
+
1321
+ const printedShapes: Array<string> = [
1322
+ "<div class=\"lg:flex <%- collapsed ? 'hidden' : '' %>\">",
1323
+ "<div class=\"<%= collapsed ? 'hidden' : '' %> <%= tone %> lg:flex\">",
1324
+ '<div class=\'lg:flex <%= collapsed ? "hidden" : "" %>\'>',
1325
+ "<div class=\"lg:flex <%= collapsed && 'hidden px-2' %>\">",
1326
+ ];
1327
+
1328
+ for (const source of printedShapes) {
1329
+ const finding: ForeignHiddenRuleFinding = onlyViewFinding(source);
1330
+
1331
+ expect(`${source} => ${finding.kind} ${finding.replacement}`).toMatch(
1332
+ /[=]> cross-piece max-lg:hidden( px-2)?$/,
1333
+ );
1334
+ }
1335
+
1336
+ // The un-hider only printed under a condition: no rewrite of the static `hidden` is exact.
1337
+ const conditional: ForeignHiddenRuleFinding = onlyViewFinding(
1338
+ "<div class=\"hidden <%= wide ? 'md:flex' : '' %>\">",
1339
+ );
1340
+
1341
+ expect(conditional.kind).toBe("cross-piece");
1342
+ expect(conditional.text).toBe("hidden <%= wide ? 'md:flex' : '' %>");
1343
+ expect(conditional.replacement).toBe(null);
1344
+ expect(conditional.advice).toBe(CONDITIONAL_PARTNER_ADVICE);
1345
+ });
1346
+
1347
+ test("only a tag's printed value is class text: conditions, scriptlets and comments are not", () => {
1348
+ const safeViews: Array<string> = [
1349
+ // The API Reference code tabs: a lone, JavaScript-chosen `hidden`.
1350
+ `<div class="${CODE_TABS_PANEL_CLASS}">`,
1351
+ "<div class=\"<%= open ? 'block' : 'hidden' %>\">",
1352
+ // The branches of one ternary never meet.
1353
+ "<div class=\"<%= open ? 'hidden' : 'md:flex' %>\">",
1354
+ // A condition is not class text.
1355
+ "<div class=\"md:flex <%= mode === 'hidden' ? 'px-2' : 'px-4' %>\">",
1356
+ // A scriptlet prints nothing, and a comment is not code.
1357
+ "<div class=\"md:flex <% if (mode === 'hidden') { %> px-2 <% } %>\">",
1358
+ "<div class=\"md:flex <%# 'hidden' is set by the script %>\">",
1359
+ // An object prints [object Object], not its keys.
1360
+ '<div class="md:flex <%= { hidden: true } %>">',
1361
+ // The converted markup and the skip link.
1362
+ `<div class="${NEW_DOCS_SIDEBAR_CLASS}">`,
1363
+ `<a href="#main" class="${SKIP_LINK_CLASS_PREFIX} focus:absolute">Skip</a>`,
1364
+ ];
1365
+
1366
+ for (const source of safeViews) {
1367
+ expect(`${source} => ${formatFindings(viewFindingsIn(source))}`).toBe(
1368
+ `${source} => `,
1369
+ );
1370
+ }
1371
+
1372
+ // The real code tabs file carries that shape, and stays clean.
1373
+ const codeTabs: string = readRepositoryFile(API_REFERENCE_CODE_TABS_VIEW);
1374
+
1375
+ expect(codeTabs).toContain(`class="${CODE_TABS_PANEL_CLASS}"`);
1376
+ expect(
1377
+ findForeignHiddenRuleHazards(API_REFERENCE_CODE_TABS_VIEW, codeTabs),
1378
+ ).toEqual([]);
1379
+ });
1380
+
1381
+ test("the code in every tag is read like a module, at the line it is on, and reported once", () => {
1382
+ // A string literal in a class attribute's tag, holding the whole pair.
1383
+ const inTag: Array<ForeignHiddenRuleFinding> = viewFindingsIn(
1384
+ "<div class=\"px-2 <%= wide ? 'hidden md:flex' : 'block' %>\">",
1385
+ );
1386
+
1387
+ expect(describeFindings(inTag)).toBe(
1388
+ "1 same-string hidden: hidden md:flex -> max-md:hidden md:flex",
1389
+ );
1390
+
1391
+ // A class built in a scriptlet, far from any attribute.
1392
+ const scriptlets: Array<ForeignHiddenRuleFinding> = viewFindingsIn(
1393
+ lines(
1394
+ "<header>",
1395
+ "<% const navClass = 'hidden lg:flex'; %>",
1396
+ "<%",
1397
+ " const menuClassName =",
1398
+ " 'hidden ' +",
1399
+ " 'md:flex';",
1400
+ "%>",
1401
+ '<nav class="<%= navClass %>"></nav>',
1402
+ "</header>",
1403
+ ),
1404
+ );
1405
+
1406
+ expect(describeFindings(scriptlets)).toBe(
1407
+ lines(
1408
+ "2 same-string hidden: hidden lg:flex -> max-lg:hidden lg:flex",
1409
+ "5 cross-piece hidden: hidden -> max-md:hidden ",
1410
+ ),
1411
+ );
1412
+
1413
+ // A template literal the tag prints: its own reading reports the `hidden`, the attribute's does not repeat it.
1414
+ expect(
1415
+ describeFindings(
1416
+ viewFindingsIn(
1417
+ "<div class=\"px-2 <%= `${open ? 'hidden' : ''} md:flex` %>\">",
1418
+ ),
1419
+ ),
1420
+ ).toBe("1 cross-piece hidden: hidden -> max-md:hidden");
1421
+ });
1422
+
1423
+ test("the sr-only analogue in a view, and the website's skip link left alone", () => {
1424
+ const label: ForeignHiddenRuleFinding = onlyViewFinding(
1425
+ '<button><svg></svg><span class="sr-only sm:not-sr-only">Copy</span></button>',
1426
+ );
1427
+
1428
+ expect(label.hidingClass).toBe("sr-only");
1429
+ expect(label.replacement).toBe(NEW_SM_LABEL_CLASS);
1430
+
1431
+ const homeNav: string = readRepositoryFile(HOME_NAV_VIEW);
1432
+
1433
+ expect(homeNav).toContain(`class="${SKIP_LINK_CLASS_PREFIX}`);
1434
+ expect(findForeignHiddenRuleHazards(HOME_NAV_VIEW, homeNav)).toEqual([]);
1435
+ });
1436
+
1437
+ test("a view's cross-piece finding says the pieces are the attribute's static text and a tag", () => {
1438
+ const message: string = formatFindings(
1439
+ viewFindingsIn(
1440
+ "<div class=\"code-panel md:block <%= index === 0 ? '' : 'hidden' %>\">",
1441
+ ),
1442
+ );
1443
+
1444
+ expect(message).toContain(
1445
+ `${VIEW_FIXTURE_FILE}:1 (the bare \`hidden\` and the utility that shows the element again are in different pieces of one class attribute (its static text and a <% %> tag))`,
1446
+ );
1447
+ expect(message).toContain('replace with: "max-md:hidden"');
1448
+ });
1449
+ });
1450
+
1451
+ describe("ForeignHiddenRuleGuard file walk", () => {
1452
+ const temporaryRoots: Array<string> = [];
1453
+
1454
+ afterAll(() => {
1455
+ for (const root of temporaryRoots) {
1456
+ fs.rmSync(root, { recursive: true, force: true });
1457
+ }
1458
+ });
1459
+
1460
+ function makeRoot(): string {
1461
+ const root: string = fs.realpathSync(
1462
+ fs.mkdtempSync(path.join(os.tmpdir(), "foreign-hidden-guard-")),
1463
+ );
1464
+ temporaryRoots.push(root);
1465
+ return root;
1466
+ }
1467
+
1468
+ function writeFixture(
1469
+ root: string,
1470
+ relative: string,
1471
+ contents: string,
1472
+ ): void {
1473
+ const filePath: string = path.join(root, ...relative.split("/"));
1474
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1475
+ fs.writeFileSync(filePath, contents);
1476
+ }
1477
+
1478
+ function relativeTo(root: string): (directory: string) => string {
1479
+ return (directory: string): string => {
1480
+ return toRelativePath(root, directory);
1481
+ };
1482
+ }
1483
+
1484
+ test("a checkout without ee/ scans the core roots only, and skips what is not source", () => {
1485
+ /*
1486
+ * The Community Edition checkout - and the Common Test job, which deletes
1487
+ * ee/ before it runs - has no ee/ at all. The walk must not fail there,
1488
+ * and must not read build output, dependencies or declaration files.
1489
+ * The Docs feature set has no browser source: its server-side TypeScript
1490
+ * is not read, but its views are (see the next test).
1491
+ */
1492
+ const root: string = makeRoot();
1493
+ const offending: string = jsxWithClass("hidden md:flex");
1494
+
1495
+ writeFixture(
1496
+ root,
1497
+ "packages/App/FeatureSet/Dashboard/src/Nav.tsx",
1498
+ offending,
1499
+ );
1500
+ writeFixture(root, "packages/App/FeatureSet/Docs/Index.ts", offending);
1501
+ writeFixture(
1502
+ root,
1503
+ "packages/App/FeatureSet/Docs/Views/Index.ejs",
1504
+ '<div class="hidden lg:block"></div>',
1505
+ );
1506
+ writeFixture(root, "packages/Common/UI/Card.tsx", jsxWithClass("hidden"));
1507
+ writeFixture(
1508
+ root,
1509
+ "packages/Common/UI/node_modules/x/Index.tsx",
1510
+ offending,
1511
+ );
1512
+ writeFixture(root, "packages/Common/UI/build/Nav.tsx", offending);
1513
+ writeFixture(root, "packages/Common/UI/dist/Nav.tsx", offending);
1514
+ writeFixture(root, "packages/Common/UI/Types.d.ts", offending);
1515
+
1516
+ const roots: Array<string> = listScanRoots(root);
1517
+
1518
+ expect(roots.map(relativeTo(root))).toEqual([
1519
+ "packages/App/FeatureSet/Dashboard/src",
1520
+ "packages/Common/UI",
1521
+ ]);
1522
+
1523
+ const scan: ForeignHiddenRuleScan = scanFiles(
1524
+ [
1525
+ ...roots.flatMap(listSourceFiles),
1526
+ ...listViewRoots(root).flatMap(listViewFiles),
1527
+ ],
1528
+ root,
1529
+ );
1530
+
1531
+ expect(scan.files.sort()).toEqual([
1532
+ "packages/App/FeatureSet/Dashboard/src/Nav.tsx",
1533
+ "packages/App/FeatureSet/Docs/Views/Index.ejs",
1534
+ "packages/Common/UI/Card.tsx",
1535
+ ]);
1536
+ expect(
1537
+ scan.findings
1538
+ .map((finding: ForeignHiddenRuleFinding): string => {
1539
+ return `${finding.file}:${finding.line}`;
1540
+ })
1541
+ .sort(),
1542
+ ).toEqual([
1543
+ "packages/App/FeatureSet/Dashboard/src/Nav.tsx:1",
1544
+ "packages/App/FeatureSet/Docs/Views/Index.ejs:1",
1545
+ ]);
1546
+
1547
+ // With ee/ present, both Enterprise screens are read as well.
1548
+ writeFixture(root, "ee/Dashboard/AuditLogs/Modal.tsx", offending);
1549
+ writeFixture(root, "ee/AdminDashboard/Index.tsx", "export {};");
1550
+
1551
+ expect(listScanRoots(root).map(relativeTo(root))).toEqual([
1552
+ "packages/App/FeatureSet/Dashboard/src",
1553
+ "packages/Common/UI",
1554
+ "ee/Dashboard",
1555
+ "ee/AdminDashboard",
1556
+ ]);
1557
+ });
1558
+
1559
+ test("the views are found by listing, whatever the case of their directory, and each is read once", () => {
1560
+ /*
1561
+ * git tracks Docs/Views but APIReference/views. A guessed spelling would
1562
+ * miss one on a case-sensitive filesystem and read both spellings of the
1563
+ * same directory on a case-insensitive one, so the directories are read
1564
+ * from the listing and deduped by real path - here through a symlink,
1565
+ * which behaves the same on every filesystem.
1566
+ */
1567
+ const root: string = makeRoot();
1568
+ const offendingView: string = '<nav class="hidden md:flex"></nav>';
1569
+
1570
+ writeFixture(
1571
+ root,
1572
+ "packages/App/FeatureSet/Docs/Views/Partials/Header.ejs",
1573
+ offendingView,
1574
+ );
1575
+ writeFixture(
1576
+ root,
1577
+ "packages/App/FeatureSet/APIReference/views/partials/nav.ejs",
1578
+ offendingView,
1579
+ );
1580
+ writeFixture(
1581
+ root,
1582
+ "packages/App/FeatureSet/Dashboard/views/index.ejs",
1583
+ '<div id="root" class="h-full"></div>',
1584
+ );
1585
+ writeFixture(
1586
+ root,
1587
+ "packages/App/FeatureSet/Dashboard/public/offline.html",
1588
+ offendingView,
1589
+ );
1590
+ writeFixture(
1591
+ root,
1592
+ "packages/App/FeatureSet/Dashboard/public/assets/logo.svg",
1593
+ '<svg class="hidden md:block"></svg>',
1594
+ );
1595
+ writeFixture(
1596
+ root,
1597
+ "packages/Common/Server/Views/ViewMessage.ejs",
1598
+ '<p class="text-sm"><%= message %></p>',
1599
+ );
1600
+ writeFixture(root, "packages/Home/Views/nav.ejs", offendingView);
1601
+ writeFixture(
1602
+ root,
1603
+ "packages/Home/Views/node_modules/x/index.ejs",
1604
+ offendingView,
1605
+ );
1606
+ writeFixture(root, "packages/Home/Static/page.html", offendingView);
1607
+
1608
+ fs.mkdirSync(path.join(root, "packages", "App", "FeatureSet", "Legacy"));
1609
+ fs.symlinkSync(
1610
+ path.join(root, "packages", "App", "FeatureSet", "Docs", "Views"),
1611
+ path.join(root, "packages", "App", "FeatureSet", "Legacy", "views"),
1612
+ "dir",
1613
+ );
1614
+
1615
+ const viewRoots: Array<string> = listViewRoots(root);
1616
+
1617
+ expect(viewRoots.map(relativeTo(root))).toEqual([
1618
+ "packages/App/FeatureSet/APIReference/views",
1619
+ "packages/App/FeatureSet/Dashboard/views",
1620
+ "packages/App/FeatureSet/Dashboard/public",
1621
+ "packages/App/FeatureSet/Docs/Views",
1622
+ "packages/Common/Server/Views",
1623
+ "packages/Home/Views",
1624
+ ]);
1625
+
1626
+ const scan: ForeignHiddenRuleScan = scanFiles(
1627
+ viewRoots.flatMap(listViewFiles),
1628
+ root,
1629
+ );
1630
+
1631
+ expect(scan.files.sort()).toEqual([
1632
+ "packages/App/FeatureSet/APIReference/views/partials/nav.ejs",
1633
+ "packages/App/FeatureSet/Dashboard/public/offline.html",
1634
+ "packages/App/FeatureSet/Dashboard/views/index.ejs",
1635
+ "packages/App/FeatureSet/Docs/Views/Partials/Header.ejs",
1636
+ "packages/Common/Server/Views/ViewMessage.ejs",
1637
+ "packages/Home/Views/nav.ejs",
1638
+ ]);
1639
+ expect(
1640
+ scan.findings
1641
+ .map((finding: ForeignHiddenRuleFinding): string => {
1642
+ return `${finding.file}:${finding.line} ${finding.replacement}`;
1643
+ })
1644
+ .sort(),
1645
+ ).toEqual([
1646
+ "packages/App/FeatureSet/APIReference/views/partials/nav.ejs:1 max-md:hidden md:flex",
1647
+ "packages/App/FeatureSet/Dashboard/public/offline.html:1 max-md:hidden md:flex",
1648
+ "packages/App/FeatureSet/Docs/Views/Partials/Header.ejs:1 max-md:hidden md:flex",
1649
+ "packages/Home/Views/nav.ejs:1 max-md:hidden md:flex",
1650
+ ]);
1651
+ });
1652
+ });
1653
+
1654
+ describe("ForeignHiddenRuleGuard over the real source tree", () => {
1655
+ let roots: Array<string> = [];
1656
+ let viewRoots: Array<string> = [];
1657
+ let files: Array<string> = [];
1658
+ let viewFiles: Array<string> = [];
1659
+ let scan: ForeignHiddenRuleScan = {
1660
+ files: [],
1661
+ findings: [],
1662
+ variantHiddenSites: [],
1663
+ };
1664
+
1665
+ /*
1666
+ * Parsing ~3,600 files takes seconds. Doing it in beforeAll rather than in
1667
+ * the describe body keeps it under a timeout and reports a failure as a
1668
+ * failed test instead of a suite that could not even be collected.
1669
+ */
1670
+ beforeAll(() => {
1671
+ roots = listScanRoots(REPOSITORY_ROOT);
1672
+ viewRoots = listViewRoots(REPOSITORY_ROOT);
1673
+ files = roots.flatMap(listSourceFiles);
1674
+ viewFiles = viewRoots.flatMap(listViewFiles);
1675
+ scan = scanFiles([...files, ...viewFiles], REPOSITORY_ROOT);
1676
+ }, 180000);
1677
+
1678
+ const enterprisePresent: boolean = fs.existsSync(
1679
+ path.join(REPOSITORY_ROOT, "ee", "Dashboard"),
1680
+ );
1681
+
1682
+ function sitesIn(relative: string): Array<string> {
1683
+ return scan.variantHiddenSites
1684
+ .filter((site: VariantHiddenSite): boolean => {
1685
+ return site.file === relative;
1686
+ })
1687
+ .map((site: VariantHiddenSite): string => {
1688
+ return site.text;
1689
+ });
1690
+ }
1691
+
1692
+ test("the scan read every frontend, Common/UI, the server-rendered views and, when the checkout has it, ee/", () => {
1693
+ /*
1694
+ * Guards the guard: a moved directory or a broken walk would otherwise
1695
+ * leave the zero-findings assertion below passing over nothing.
1696
+ */
1697
+ const relativeRoots: Array<string> = roots.map((root: string): string => {
1698
+ return toRelativePath(REPOSITORY_ROOT, root);
1699
+ });
1700
+
1701
+ expect(relativeRoots).toEqual(
1702
+ expect.arrayContaining([
1703
+ "packages/App/FeatureSet/Accounts/src",
1704
+ "packages/App/FeatureSet/AdminDashboard/src",
1705
+ "packages/App/FeatureSet/Dashboard/src",
1706
+ "packages/App/FeatureSet/PublicDashboard/src",
1707
+ "packages/App/FeatureSet/StatusPage/src",
1708
+ "packages/Common/UI",
1709
+ ]),
1710
+ );
1711
+
1712
+ // The Community Edition checkout has no ee/, and must still pass.
1713
+ expect(relativeRoots.includes("ee/Dashboard")).toBe(enterprisePresent);
1714
+ expect(relativeRoots.includes("ee/AdminDashboard")).toBe(
1715
+ fs.existsSync(path.join(REPOSITORY_ROOT, "ee", "AdminDashboard")),
1716
+ );
1717
+
1718
+ const relativeViewRoots: Array<string> = viewRoots.map(
1719
+ (root: string): string => {
1720
+ return toRelativePath(REPOSITORY_ROOT, root);
1721
+ },
1722
+ );
1723
+
1724
+ expect(relativeViewRoots).toEqual(
1725
+ expect.arrayContaining([
1726
+ "packages/App/FeatureSet/Docs/Views",
1727
+ "packages/App/FeatureSet/APIReference/views",
1728
+ "packages/App/FeatureSet/Dashboard/views",
1729
+ "packages/App/FeatureSet/Dashboard/public",
1730
+ "packages/App/FeatureSet/Identity/Views",
1731
+ "packages/Common/Server/Views",
1732
+ "packages/Home/Views",
1733
+ ]),
1734
+ );
1735
+
1736
+ // No directory read twice, whatever the filesystem's case rules.
1737
+ expect(
1738
+ new Set<string>(
1739
+ viewRoots.map((root: string): string => {
1740
+ return fs.realpathSync(root);
1741
+ }),
1742
+ ).size,
1743
+ ).toBe(viewRoots.length);
1744
+
1745
+ expect(scan.files.length).toBeGreaterThan(2750);
1746
+ expect(viewFiles.length).toBeGreaterThan(250);
1747
+ expect(scan.files).toContain(NAVBAR_FILE);
1748
+ expect(scan.files).toContain(
1749
+ "packages/App/FeatureSet/Dashboard/src/Components/Header/Header.tsx",
1750
+ );
1751
+
1752
+ for (const view of [
1753
+ DOCS_INDEX_VIEW,
1754
+ DOCS_HEADER_VIEW,
1755
+ API_REFERENCE_NAV_VIEW,
1756
+ API_REFERENCE_PAGE_VIEW,
1757
+ HOME_NAV_VIEW,
1758
+ DASHBOARD_OFFLINE_PAGE,
1759
+ "packages/Common/Server/Views/ViewMessage.ejs",
1760
+ ]) {
1761
+ expect(scan.files).toContain(view);
1762
+ }
1763
+
1764
+ /*
1765
+ * Listing a view is not reading it: an extractor that found no class
1766
+ * attributes would pass the zero-findings test over every view. The Docs
1767
+ * sidebar's converted attribute has to come back out of Index.ejs.
1768
+ */
1769
+ expect(sitesIn(DOCS_INDEX_VIEW)).toContain(NEW_DOCS_SIDEBAR_CLASS);
1770
+
1771
+ const skippedDirectory: RegExp = /(^|\/)(node_modules|dist|build)\//;
1772
+
1773
+ expect(
1774
+ scan.files.filter((file: string): boolean => {
1775
+ return skippedDirectory.test(file) || file.endsWith(".d.ts");
1776
+ }),
1777
+ ).toEqual([]);
1778
+
1779
+ if (enterprisePresent) {
1780
+ expect(scan.files).toContain(ENTERPRISE_AUDIT_LOG_FILE);
1781
+ }
1782
+ });
1783
+
1784
+ test("the scan read the class strings the fix converted", () => {
1785
+ /*
1786
+ * Every converted site carries a variant-prefixed `hidden`. Finding them
1787
+ * by file proves the parser reads real class strings in these modules -
1788
+ * including bracketed arbitrary variants - rather than skipping them.
1789
+ */
1790
+ expect(sitesIn(NAVBAR_FILE)).toContain(NEW_NAVBAR_CLASS);
1791
+ expect(
1792
+ sitesIn(
1793
+ "packages/App/FeatureSet/Dashboard/src/Components/Header/Header.tsx",
1794
+ ),
1795
+ ).toContain("max-lg:hidden items-center gap-2 lg:flex");
1796
+ expect(
1797
+ sitesIn("packages/Common/UI/Components/SideMenu/SideMenu.tsx").some(
1798
+ (text: string): boolean => {
1799
+ return text.startsWith("max-md:hidden md:block");
1800
+ },
1801
+ ),
1802
+ ).toBe(true);
1803
+ expect(sitesIn(GROUP_HOVER_EXAMPLE_FILE)).toContain(
1804
+ NEW_FACET_EXCLUDE_CLASS,
1805
+ );
1806
+ expect(sitesIn(TELEMETRY_FACET_FILE)).toContain(NEW_FACET_EXCLUDE_CLASS);
1807
+ expect(sitesIn(DISCLOSURE_EXAMPLE_FILE)).toContain(NEW_DISCLOSURE_CLASS);
1808
+
1809
+ // And the class attributes of the views the EJS codemod converted.
1810
+ expect(sitesIn(HOME_NAV_VIEW)).toContain(HOME_PRIMARY_NAV_CLASS);
1811
+ expect(sitesIn(API_REFERENCE_NAV_VIEW).length).toBeGreaterThan(0);
1812
+
1813
+ expect(scan.variantHiddenSites.length).toBeGreaterThan(400);
1814
+
1815
+ if (enterprisePresent) {
1816
+ expect(
1817
+ sitesIn(ENTERPRISE_AUDIT_LOG_FILE).some((text: string): boolean => {
1818
+ return text.startsWith("max-md:hidden md:flex");
1819
+ }),
1820
+ ).toBe(true);
1821
+ }
1822
+ });
1823
+
1824
+ test("reverting the fix in memory fails the guard in every converted module and view, with the shipped string as the fix", () => {
1825
+ /*
1826
+ * The tree as it was before the fix, rebuilt in memory: every
1827
+ * `max-<bp>:hidden` goes back to the bare `hidden` - in the modules and
1828
+ * in the Docs, API Reference and website views alike - every
1829
+ * `max-<bp>:sr-only` back to `sr-only`, and the three state variants
1830
+ * converted by hand go back to their old strings. Each of those files
1831
+ * must then fail. And wherever the guard prints a replacement, it must
1832
+ * be exactly the string the fix shipped on that line: the codemod and
1833
+ * the guard agree, site by site.
1834
+ */
1835
+ const shippedSites: Set<string> = new Set<string>(
1836
+ scan.variantHiddenSites.map((site: VariantHiddenSite): string => {
1837
+ return `${site.file}:${site.line} ${site.text}`;
1838
+ }),
1839
+ );
1840
+ const failingFiles: Set<string> = new Set<string>();
1841
+ const srOnlyFiles: Set<string> = new Set<string>();
1842
+ const disagreements: Array<string> = [];
1843
+
1844
+ for (const file of [...files, ...viewFiles]) {
1845
+ const source: string = fs.readFileSync(file, "utf8");
1846
+ const reverted: string = source
1847
+ .replace(MAX_WIDTH_HIDDEN, "$1hidden")
1848
+ .replace(MAX_WIDTH_SR_ONLY, "$1sr-only")
1849
+ .split(`"${NEW_FACET_EXCLUDE_CLASS}"`)
1850
+ .join(`"${OLD_FACET_EXCLUDE_CLASS}"`)
1851
+ .split(`"${NEW_DISCLOSURE_CLASS}"`)
1852
+ .join(`"${OLD_DISCLOSURE_CLASS}"`);
1853
+
1854
+ if (reverted === source) {
1855
+ continue;
1856
+ }
1857
+
1858
+ const relative: string = toRelativePath(REPOSITORY_ROOT, file);
1859
+ const findings: Array<ForeignHiddenRuleFinding> =
1860
+ findForeignHiddenRuleHazards(relative, reverted);
1861
+
1862
+ if (findings.length > 0) {
1863
+ failingFiles.add(relative);
1864
+ }
1865
+
1866
+ for (const finding of findings) {
1867
+ if (finding.hidingClass === "sr-only") {
1868
+ srOnlyFiles.add(relative);
1869
+ continue;
1870
+ }
1871
+
1872
+ if (
1873
+ finding.replacement !== null &&
1874
+ !shippedSites.has(
1875
+ `${relative}:${finding.line} ${finding.replacement}`,
1876
+ )
1877
+ ) {
1878
+ disagreements.push(
1879
+ `${relative}:${finding.line} guard: ${finding.replacement}`,
1880
+ );
1881
+ }
1882
+ }
1883
+ }
1884
+
1885
+ for (const relative of [
1886
+ NAVBAR_FILE,
1887
+ "packages/App/FeatureSet/Dashboard/src/Components/Header/Header.tsx",
1888
+ "packages/App/FeatureSet/AdminDashboard/src/Components/Header/Header.tsx",
1889
+ "packages/App/FeatureSet/StatusPage/src/Components/NavBar/NavBar.tsx",
1890
+ "packages/Common/UI/Components/SideMenu/SideMenu.tsx",
1891
+ "packages/Common/UI/Components/Card/Card.tsx",
1892
+ "packages/Common/UI/Components/Pagination/Pagination.tsx",
1893
+ GROUP_HOVER_EXAMPLE_FILE,
1894
+ TELEMETRY_FACET_FILE,
1895
+ DISCLOSURE_EXAMPLE_FILE,
1896
+ DOCS_INDEX_VIEW,
1897
+ DOCS_HEADER_VIEW,
1898
+ API_REFERENCE_NAV_VIEW,
1899
+ HOME_NAV_VIEW,
1900
+ ]) {
1901
+ expect(`${relative}: ${failingFiles.has(relative)}`).toBe(
1902
+ `${relative}: true`,
1903
+ );
1904
+ }
1905
+
1906
+ expect([...srOnlyFiles].sort()).toEqual(
1907
+ SR_ONLY_LABEL_SITES.map((site: [string, string]): string => {
1908
+ return site[0];
1909
+ }).sort(),
1910
+ );
1911
+
1912
+ const failingViews: Array<string> = [...failingFiles].filter(isMarkupFile);
1913
+
1914
+ for (const viewRoot of [
1915
+ "packages/App/FeatureSet/Docs/Views/",
1916
+ "packages/App/FeatureSet/APIReference/views/",
1917
+ "packages/Home/Views/",
1918
+ ]) {
1919
+ expect(
1920
+ `${viewRoot}: ${failingViews.some((view: string): boolean => {
1921
+ return view.startsWith(viewRoot);
1922
+ })}`,
1923
+ ).toBe(`${viewRoot}: true`);
1924
+ }
1925
+
1926
+ expect(failingViews.length).toBeGreaterThan(85);
1927
+ expect(failingFiles.size - failingViews.length).toBeGreaterThan(60);
1928
+ expect(disagreements).toEqual([]);
1929
+
1930
+ const facetSource: string = readRepositoryFile(GROUP_HOVER_EXAMPLE_FILE);
1931
+ const facetFindings: Array<ForeignHiddenRuleFinding> =
1932
+ findForeignHiddenRuleHazards(
1933
+ GROUP_HOVER_EXAMPLE_FILE,
1934
+ facetSource.replace(NEW_FACET_EXCLUDE_CLASS, OLD_FACET_EXCLUDE_CLASS),
1935
+ );
1936
+
1937
+ expect(
1938
+ facetFindings.map((finding: ForeignHiddenRuleFinding): string | null => {
1939
+ return finding.replacement;
1940
+ }),
1941
+ ).toEqual([NEW_FACET_EXCLUDE_CLASS]);
1942
+ }, 120000);
1943
+
1944
+ test("no class string relies on a variant to undo a bare `hidden` (or `sr-only`)", () => {
1945
+ /*
1946
+ * The guard itself. The message lists each offender with its file:line,
1947
+ * the string, the exact replacement where one exists, and why.
1948
+ */
1949
+ expect(formatFindings(scan.findings)).toBe("");
1950
+ });
1951
+ });