@oneuptime/common 14.0.2 → 14.0.3
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.
- package/Models/DatabaseModels/AlertFeed.ts +2 -0
- package/Models/DatabaseModels/IncidentAlert.ts +540 -0
- package/Models/DatabaseModels/IncidentFeed.ts +2 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/Project.ts +60 -0
- package/Models/DatabaseModels/RumApplication.ts +52 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/IncidentAlertService.ts +1811 -0
- package/Server/Services/IncidentService.ts +210 -10
- package/Server/Services/IncidentStateTimelineService.ts +24 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
- package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
- package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
- package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
- package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
- package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
- package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
- package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
- package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
- package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
- package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
- package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
- package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
- package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
- package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
- package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
- package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
- package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
- package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
- package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
- package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
- package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
- package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
- package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
- package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
- package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
- package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
- package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
- package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
- package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
- package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
- package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
- package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
- package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
- package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
- package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
- package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
- package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
- package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
- package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
- package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
- package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
- package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
- package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
- package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
- package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
- package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
- package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
- package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
- package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
- package/Tests/Types/Rum/WebVitals.test.ts +173 -0
- package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
- package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
- package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
- package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
- package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
- package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
- package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
- package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
- package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
- package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
- package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
- package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
- package/Types/Incident/IncidentAlertLink.ts +38 -0
- package/Types/Permission.ts +48 -0
- package/Types/Rum/SessionReplay.ts +25 -6
- package/Types/Rum/WebVitals.ts +52 -0
- package/UI/Components/Detail/FieldLabel.tsx +8 -0
- package/UI/Components/InfoCard/InfoCard.tsx +47 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +95 -17
- package/UI/Components/ModelTable/Column.ts +5 -0
- package/UI/Components/Table/TableHeader.tsx +47 -2
- package/UI/Components/Table/Types/Column.ts +7 -0
- package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
- package/UI/Utils/PermissionGate.ts +11 -2
- package/Utils/Incident/IncidentFromAlerts.ts +516 -0
- package/Utils/Rum/SessionTraceState.ts +212 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
- package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +62 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +164 -12
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +25 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
- package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +66 -14
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +18 -2
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
- package/build/dist/UI/Utils/PermissionGate.js +4 -2
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
- package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
- package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
- package/package.json +1 -1
|
@@ -372,3 +372,106 @@ describe("SessionReplayGateCache.resolvePolicy names the refusal", (): void => {
|
|
|
372
372
|
expect(resolution.refusal).toBeNull();
|
|
373
373
|
});
|
|
374
374
|
});
|
|
375
|
+
|
|
376
|
+
/*
|
|
377
|
+
* RumApplication.sessionReplaySameOriginTracePropagation: the per-app switch
|
|
378
|
+
* for the recorder's automatic same-origin traceparent + tracestate. The
|
|
379
|
+
* column is NOT NULL DEFAULT true (on for existing applications too), so
|
|
380
|
+
* the gate reads anything but an explicit false as on - the model-default
|
|
381
|
+
* rule of the fallbacks above, not the feature-off rule of the origin list.
|
|
382
|
+
* Only an operator who switched it off gets it off.
|
|
383
|
+
*/
|
|
384
|
+
describe("SessionReplayGateCache same-origin trace propagation switch", (): void => {
|
|
385
|
+
beforeEach((): void => {
|
|
386
|
+
jest.clearAllMocks();
|
|
387
|
+
projectFindOneByMock.mockResolvedValue(projectRow());
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
async function resolveWith(
|
|
391
|
+
overrides?: Record<string, unknown>,
|
|
392
|
+
): Promise<SessionReplayGatePolicy | null> {
|
|
393
|
+
appFindOneByMock.mockResolvedValue(appRow(overrides));
|
|
394
|
+
|
|
395
|
+
return await SessionReplayGateCache.getPolicy({
|
|
396
|
+
projectId: PROJECT_ID,
|
|
397
|
+
appIdentifier: "checkout-web",
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
it("selects the column from RumApplication", async (): Promise<void> => {
|
|
402
|
+
await resolveWith();
|
|
403
|
+
|
|
404
|
+
expect(appFindOneByMock).toHaveBeenCalled();
|
|
405
|
+
|
|
406
|
+
const select: Record<string, unknown> = (
|
|
407
|
+
appFindOneByMock.mock.calls[0]?.[0] as { select: Record<string, unknown> }
|
|
408
|
+
).select;
|
|
409
|
+
|
|
410
|
+
expect(select["sessionReplaySameOriginTracePropagation"]).toBe(true);
|
|
411
|
+
|
|
412
|
+
/* Next to the cross-origin list it complements, which is still read. */
|
|
413
|
+
expect(select["sessionReplayTracePropagationOrigins"]).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("an explicit false turns it off", async (): Promise<void> => {
|
|
417
|
+
const policy: SessionReplayGatePolicy | null = await resolveWith({
|
|
418
|
+
sessionReplaySameOriginTracePropagation: false,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
expect(policy).not.toBeNull();
|
|
422
|
+
expect(policy?.sameOriginTracePropagation).toBe(false);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("an explicit true keeps it on", async (): Promise<void> => {
|
|
426
|
+
const policy: SessionReplayGatePolicy | null = await resolveWith({
|
|
427
|
+
sessionReplaySameOriginTracePropagation: true,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(policy?.sameOriginTracePropagation).toBe(true);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("an absent column reads as the DB default, on", async (): Promise<void> => {
|
|
434
|
+
const policy: SessionReplayGatePolicy | null = await resolveWith();
|
|
435
|
+
|
|
436
|
+
expect(policy).not.toBeNull();
|
|
437
|
+
expect(policy?.sameOriginTracePropagation).toBe(true);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("a null column reads as the DB default, on", async (): Promise<void> => {
|
|
441
|
+
const policy: SessionReplayGatePolicy | null = await resolveWith({
|
|
442
|
+
sessionReplaySameOriginTracePropagation: null,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
expect(policy?.sameOriginTracePropagation).toBe(true);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("is independent of the cross-origin list: empty origins still propagate same-origin", async (): Promise<void> => {
|
|
449
|
+
const policy: SessionReplayGatePolicy | null = await resolveWith({
|
|
450
|
+
sessionReplayTracePropagationOrigins: [],
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
expect(policy?.tracePropagationOrigins).toEqual([]);
|
|
454
|
+
expect(policy?.sameOriginTracePropagation).toBe(true);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("switching it off leaves the cross-origin list alone", async (): Promise<void> => {
|
|
458
|
+
const policy: SessionReplayGatePolicy | null = await resolveWith({
|
|
459
|
+
sessionReplaySameOriginTracePropagation: false,
|
|
460
|
+
sessionReplayTracePropagationOrigins: ["https://api.example.com"],
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
expect(policy?.sameOriginTracePropagation).toBe(false);
|
|
464
|
+
expect(policy?.tracePropagationOrigins).toEqual([
|
|
465
|
+
"https://api.example.com",
|
|
466
|
+
]);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("a refused application yields no policy at all, so nothing can propagate", async (): Promise<void> => {
|
|
470
|
+
const policy: SessionReplayGatePolicy | null = await resolveWith({
|
|
471
|
+
isSessionReplayEnabled: false,
|
|
472
|
+
sessionReplaySameOriginTracePropagation: true,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
expect(policy).toBeNull();
|
|
476
|
+
});
|
|
477
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import {
|
|
3
|
+
describeWebVitalThresholds,
|
|
4
|
+
formatWebVitalThreshold,
|
|
5
|
+
WebVitalDefinition,
|
|
6
|
+
WebVitalDefinitions,
|
|
7
|
+
} from "../../../Types/Rum/WebVitals";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* The web-vital definitions carry the plain-language description the RUM
|
|
11
|
+
* overview shows in each vital's (i) tooltip, and the thresholds its
|
|
12
|
+
* Good / Needs work / Poor chip is judged against. The tooltip's limits are
|
|
13
|
+
* derived from the thresholds rather than typed into the description, so a
|
|
14
|
+
* threshold change cannot leave the tooltip quoting the old number.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function definition(key: string): WebVitalDefinition {
|
|
18
|
+
const found: WebVitalDefinition | undefined = WebVitalDefinitions.find(
|
|
19
|
+
(d: WebVitalDefinition): boolean => {
|
|
20
|
+
return d.key === key;
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (!found) {
|
|
25
|
+
throw new Error(`No web vital "${key}"`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return found;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("WebVitalDefinitions descriptions", () => {
|
|
32
|
+
test("every vital has a description", () => {
|
|
33
|
+
for (const d of WebVitalDefinitions) {
|
|
34
|
+
expect(typeof d.description).toBe("string");
|
|
35
|
+
expect(d.description.trim().length).toBeGreaterThan(20);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test.each(
|
|
40
|
+
WebVitalDefinitions.map(
|
|
41
|
+
(d: WebVitalDefinition): [string, WebVitalDefinition] => {
|
|
42
|
+
return [d.key, d];
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
)(
|
|
46
|
+
"%s: the description is one or two short sentences a tooltip can hold",
|
|
47
|
+
(_: string, d: WebVitalDefinition) => {
|
|
48
|
+
expect(d.description.length).toBeLessThanOrEqual(220);
|
|
49
|
+
expect(d.description).toMatch(/[.]$/);
|
|
50
|
+
expect(d.description).toBe(d.description.trim());
|
|
51
|
+
expect(d.description).not.toMatch(/\s{2,}/);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
test.each(
|
|
56
|
+
WebVitalDefinitions.map(
|
|
57
|
+
(d: WebVitalDefinition): [string, WebVitalDefinition] => {
|
|
58
|
+
return [d.key, d];
|
|
59
|
+
},
|
|
60
|
+
),
|
|
61
|
+
)(
|
|
62
|
+
"%s: the description does not quote a threshold (those are derived)",
|
|
63
|
+
(_: string, d: WebVitalDefinition) => {
|
|
64
|
+
/* No "2.5 s", "200 ms", "0.1" style numbers typed into the prose. */
|
|
65
|
+
expect(d.description).not.toMatch(/\d+(\.\d+)?\s?(ms|s)\b/);
|
|
66
|
+
expect(d.description).not.toMatch(/\b0\.\d+/);
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
test("the descriptions are distinct - no vital borrows another's text", () => {
|
|
71
|
+
const texts: Set<string> = new Set(
|
|
72
|
+
WebVitalDefinitions.map((d: WebVitalDefinition) => {
|
|
73
|
+
return d.description;
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(texts.size).toBe(WebVitalDefinitions.length);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("each description explains its own vital in plain words", () => {
|
|
81
|
+
expect(definition("lcp").description).toMatch(/biggest|largest/i);
|
|
82
|
+
expect(definition("inp").description).toMatch(/click|tap|key/i);
|
|
83
|
+
expect(definition("cls").description).toMatch(/jump|shift|mov/i);
|
|
84
|
+
expect(definition("cls").description).toMatch(/lower is better/i);
|
|
85
|
+
expect(definition("fcp").description).toMatch(/first/i);
|
|
86
|
+
expect(definition("ttfb").description).toMatch(/first byte/i);
|
|
87
|
+
expect(definition("ttfb").description).toMatch(/server/i);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("CLS says it is a score, not a time", () => {
|
|
91
|
+
expect(definition("cls").unit).toBe("score");
|
|
92
|
+
expect(definition("cls").description).toMatch(/score/i);
|
|
93
|
+
expect(definition("cls").description).toMatch(/not a time/i);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("formatWebVitalThreshold", () => {
|
|
98
|
+
test.each([
|
|
99
|
+
[200, "200 ms"],
|
|
100
|
+
[500, "500 ms"],
|
|
101
|
+
[800, "800 ms"],
|
|
102
|
+
[999, "999 ms"],
|
|
103
|
+
[1000, "1 s"],
|
|
104
|
+
[1800, "1.8 s"],
|
|
105
|
+
[2500, "2.5 s"],
|
|
106
|
+
[3000, "3 s"],
|
|
107
|
+
[4000, "4 s"],
|
|
108
|
+
[1234, "1.23 s"],
|
|
109
|
+
[0, "0 ms"],
|
|
110
|
+
])("%d ms reads as %s", (value: number, expected: string) => {
|
|
111
|
+
expect(formatWebVitalThreshold(value, "ms")).toBe(expected);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test.each([
|
|
115
|
+
[0.1, "0.1"],
|
|
116
|
+
[0.25, "0.25"],
|
|
117
|
+
[0, "0"],
|
|
118
|
+
[1, "1"],
|
|
119
|
+
])(
|
|
120
|
+
"a score of %d reads as %s, with no unit",
|
|
121
|
+
(value: number, expected: string) => {
|
|
122
|
+
expect(formatWebVitalThreshold(value, "score")).toBe(expected);
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("describeWebVitalThresholds", () => {
|
|
128
|
+
test("words the published limits for each vital", () => {
|
|
129
|
+
expect(describeWebVitalThresholds(definition("lcp"))).toBe(
|
|
130
|
+
"Good below 2.5 s; poor at 4 s or more.",
|
|
131
|
+
);
|
|
132
|
+
expect(describeWebVitalThresholds(definition("inp"))).toBe(
|
|
133
|
+
"Good below 200 ms; poor at 500 ms or more.",
|
|
134
|
+
);
|
|
135
|
+
expect(describeWebVitalThresholds(definition("cls"))).toBe(
|
|
136
|
+
"Good below 0.1; poor at 0.25 or more.",
|
|
137
|
+
);
|
|
138
|
+
expect(describeWebVitalThresholds(definition("fcp"))).toBe(
|
|
139
|
+
"Good below 1.8 s; poor at 3 s or more.",
|
|
140
|
+
);
|
|
141
|
+
expect(describeWebVitalThresholds(definition("ttfb"))).toBe(
|
|
142
|
+
"Good below 800 ms; poor at 1.8 s or more.",
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("follows the thresholds, so a changed limit changes the words", () => {
|
|
147
|
+
expect(
|
|
148
|
+
describeWebVitalThresholds({
|
|
149
|
+
unit: "ms",
|
|
150
|
+
thresholds: { warn: 1200, danger: 2600 },
|
|
151
|
+
}),
|
|
152
|
+
).toBe("Good below 1.2 s; poor at 2.6 s or more.");
|
|
153
|
+
|
|
154
|
+
expect(
|
|
155
|
+
describeWebVitalThresholds({
|
|
156
|
+
unit: "score",
|
|
157
|
+
thresholds: { warn: 0.05, danger: 0.5 },
|
|
158
|
+
}),
|
|
159
|
+
).toBe("Good below 0.05; poor at 0.5 or more.");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("the wording matches how the overview rates a value (strict below warn is Good, at danger is Poor)", () => {
|
|
163
|
+
/*
|
|
164
|
+
* WebVitalsCard: value < warn -> Good, value < danger -> Needs work,
|
|
165
|
+
* otherwise Poor. "Good below X" and "poor at Y or more" are exactly
|
|
166
|
+
* those two comparisons.
|
|
167
|
+
*/
|
|
168
|
+
const words: string = describeWebVitalThresholds(definition("lcp"));
|
|
169
|
+
|
|
170
|
+
expect(words).toContain("below");
|
|
171
|
+
expect(words).toContain("or more");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
act,
|
|
4
|
+
cleanup,
|
|
5
|
+
fireEvent,
|
|
6
|
+
render,
|
|
7
|
+
screen,
|
|
8
|
+
} from "@testing-library/react";
|
|
9
|
+
import userEvent from "@testing-library/user-event";
|
|
10
|
+
import React from "react";
|
|
11
|
+
import {
|
|
12
|
+
afterEach,
|
|
13
|
+
beforeEach,
|
|
14
|
+
describe,
|
|
15
|
+
expect,
|
|
16
|
+
jest,
|
|
17
|
+
test,
|
|
18
|
+
} from "@jest/globals";
|
|
19
|
+
import FieldLabelElement from "../../../UI/Components/Detail/FieldLabel";
|
|
20
|
+
import InfoCard from "../../../UI/Components/InfoCard/InfoCard";
|
|
21
|
+
import Route from "../../../Types/API/Route";
|
|
22
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* The (i) beside a summary card's title: FieldLabel draws it when given a
|
|
26
|
+
* `tooltip`, and InfoCard - the stat card on every Kubernetes, Proxmox and
|
|
27
|
+
* Ceph detail page - hands its own `tooltip` to FieldLabel.
|
|
28
|
+
*
|
|
29
|
+
* Some InfoCards are clickable (the card is a role="button" that navigates
|
|
30
|
+
* or toggles a filter). Asking what a number means must not activate the
|
|
31
|
+
* card: a click on the (i) is swallowed, and so are Enter and Space.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const READY: string =
|
|
35
|
+
"Pods that passed their readiness check and receive traffic, out of the pods the workload wants.";
|
|
36
|
+
|
|
37
|
+
async function hover(trigger: HTMLElement): Promise<void> {
|
|
38
|
+
fireEvent.mouseEnter(trigger);
|
|
39
|
+
await act(async () => {
|
|
40
|
+
jest.advanceTimersByTime(200);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
jest.useFakeTimers();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
cleanup();
|
|
50
|
+
jest.useRealTimers();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("FieldLabel tooltip", () => {
|
|
54
|
+
test("without a tooltip there is no (i)", () => {
|
|
55
|
+
render(<FieldLabelElement title="Ready" />);
|
|
56
|
+
|
|
57
|
+
expect(screen.getByText("Ready")).toBeInTheDocument();
|
|
58
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test.each([
|
|
62
|
+
["empty", ""],
|
|
63
|
+
["whitespace", " "],
|
|
64
|
+
])("an %s tooltip draws no (i)", (_: string, tooltip: string) => {
|
|
65
|
+
render(<FieldLabelElement title="Ready" tooltip={tooltip} />);
|
|
66
|
+
|
|
67
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("with a tooltip, an (i) named after the title sits beside it", () => {
|
|
71
|
+
render(<FieldLabelElement title="Ready" tooltip={READY} />);
|
|
72
|
+
|
|
73
|
+
const info: HTMLElement = screen.getByRole("button", {
|
|
74
|
+
name: "About Ready",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(info).toBeInTheDocument();
|
|
78
|
+
expect(info.closest("label")).toHaveTextContent("Ready");
|
|
79
|
+
// The label is uppercase and tracked; the (i) resets both.
|
|
80
|
+
expect(info).toHaveClass("normal-case", "tracking-normal");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("the text is not in the page until someone reaches for it", () => {
|
|
84
|
+
render(<FieldLabelElement title="Ready" tooltip={READY} />);
|
|
85
|
+
|
|
86
|
+
expect(screen.queryByText(READY)).not.toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("hovering the (i) shows the text", async () => {
|
|
90
|
+
render(<FieldLabelElement title="Ready" tooltip={READY} />);
|
|
91
|
+
|
|
92
|
+
await hover(screen.getByRole("button", { name: "About Ready" }));
|
|
93
|
+
|
|
94
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(READY);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("keyboard focus shows the text too", async () => {
|
|
98
|
+
render(<FieldLabelElement title="Ready" tooltip={READY} />);
|
|
99
|
+
|
|
100
|
+
const info: HTMLElement = screen.getByRole("button", {
|
|
101
|
+
name: "About Ready",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
act(() => {
|
|
105
|
+
info.focus();
|
|
106
|
+
});
|
|
107
|
+
await act(async () => {
|
|
108
|
+
jest.advanceTimersByTime(200);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(READY);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("a visible description and a tooltip can sit together", () => {
|
|
115
|
+
render(
|
|
116
|
+
<FieldLabelElement
|
|
117
|
+
title="Ready"
|
|
118
|
+
description="Shown under the title."
|
|
119
|
+
tooltip={READY}
|
|
120
|
+
/>,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(screen.getByText("Shown under the title.")).toBeInTheDocument();
|
|
124
|
+
expect(
|
|
125
|
+
screen.getByRole("button", { name: "About Ready" }),
|
|
126
|
+
).toBeInTheDocument();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("no title means no label and so no (i)", () => {
|
|
130
|
+
render(<FieldLabelElement tooltip={READY} />);
|
|
131
|
+
|
|
132
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("the (i) is not inside the side link", () => {
|
|
136
|
+
render(
|
|
137
|
+
<FieldLabelElement
|
|
138
|
+
title="Ready"
|
|
139
|
+
tooltip={READY}
|
|
140
|
+
sideLink={{ text: "View pods", url: new Route("/pods") }}
|
|
141
|
+
/>,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const info: HTMLElement = screen.getByRole("button", {
|
|
145
|
+
name: "About Ready",
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(info.closest("a")).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("InfoCard tooltip", () => {
|
|
153
|
+
test("a card without a tooltip draws no (i)", () => {
|
|
154
|
+
render(<InfoCard title="Ready" value="3/3" />);
|
|
155
|
+
|
|
156
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("a card passes its tooltip to the title", async () => {
|
|
160
|
+
render(<InfoCard title="Ready" value="3/3" tooltip={READY} />);
|
|
161
|
+
|
|
162
|
+
const info: HTMLElement = screen.getByRole("button", {
|
|
163
|
+
name: "About Ready",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await hover(info);
|
|
167
|
+
|
|
168
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(READY);
|
|
169
|
+
expect(screen.getByText("3/3")).toBeInTheDocument();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("an inert card stays inert - the (i) is the only control", () => {
|
|
173
|
+
const { container } = render(
|
|
174
|
+
<InfoCard title="Ready" value="3/3" tooltip={READY} />,
|
|
175
|
+
);
|
|
176
|
+
const card: HTMLElement = container.firstElementChild as HTMLElement;
|
|
177
|
+
|
|
178
|
+
expect(screen.getAllByRole("button")).toHaveLength(1);
|
|
179
|
+
expect(card).not.toHaveAttribute("role");
|
|
180
|
+
expect(card).not.toHaveAttribute("tabindex");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("the card's value can be an element and still sits beside the (i)", () => {
|
|
184
|
+
render(
|
|
185
|
+
<InfoCard
|
|
186
|
+
title="CPU"
|
|
187
|
+
value={<span data-testid="cpu-value">42%</span>}
|
|
188
|
+
tooltip="Processor time the pod used, as a share of one core."
|
|
189
|
+
/>,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(screen.getByTestId("cpu-value")).toHaveTextContent("42%");
|
|
193
|
+
expect(
|
|
194
|
+
screen.getByRole("button", { name: "About CPU" }),
|
|
195
|
+
).toBeInTheDocument();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("a clickable InfoCard with a tooltip", () => {
|
|
200
|
+
interface Rendered {
|
|
201
|
+
card: HTMLElement;
|
|
202
|
+
info: HTMLElement;
|
|
203
|
+
onClick: MockFunction;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function renderClickable(isSelected?: boolean): Rendered {
|
|
207
|
+
const onClick: MockFunction = getJestMockFunction();
|
|
208
|
+
|
|
209
|
+
render(
|
|
210
|
+
<InfoCard
|
|
211
|
+
title="Ready"
|
|
212
|
+
value="3/3"
|
|
213
|
+
tooltip={READY}
|
|
214
|
+
onClick={onClick}
|
|
215
|
+
isSelected={isSelected}
|
|
216
|
+
ariaLabel="Show ready pods"
|
|
217
|
+
/>,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
card: screen.getByRole("button", { name: "Show ready pods" }),
|
|
222
|
+
info: screen.getByRole("button", { name: "About Ready" }),
|
|
223
|
+
onClick,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* A button's children are presentational, so an (i) inside the card's own
|
|
229
|
+
* button would be unreachable for a screen reader. The card is a plain
|
|
230
|
+
* container with a real button laid over it, and the (i) is a sibling of
|
|
231
|
+
* that button, lifted above it.
|
|
232
|
+
*/
|
|
233
|
+
test("the (i) is its own button, never nested in the card's button", () => {
|
|
234
|
+
const { card, info } = renderClickable();
|
|
235
|
+
|
|
236
|
+
expect(card).not.toContainElement(info);
|
|
237
|
+
expect(info.closest("[role='button']")).toBeNull();
|
|
238
|
+
expect(card.tagName).toBe("BUTTON");
|
|
239
|
+
expect(card).toHaveAttribute("type", "button");
|
|
240
|
+
expect(info.tagName).toBe("BUTTON");
|
|
241
|
+
expect(info).toHaveAttribute("type", "button");
|
|
242
|
+
expect(card.parentElement).toContainElement(info);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("the card's button covers the whole card and the (i) sits above it", () => {
|
|
246
|
+
const { card, info } = renderClickable();
|
|
247
|
+
|
|
248
|
+
expect(card).toHaveClass("absolute", "inset-0");
|
|
249
|
+
expect(card.parentElement).toHaveClass("relative");
|
|
250
|
+
expect(info).toHaveClass("relative", "z-10");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("clicking the (i) does not activate the card", () => {
|
|
254
|
+
const { info, onClick } = renderClickable();
|
|
255
|
+
|
|
256
|
+
fireEvent.click(info);
|
|
257
|
+
|
|
258
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test.each([
|
|
262
|
+
["Enter", "Enter"],
|
|
263
|
+
["Space", " "],
|
|
264
|
+
])(
|
|
265
|
+
"pressing %s on the (i) does not activate the card",
|
|
266
|
+
(_: string, key: string) => {
|
|
267
|
+
const { info, onClick } = renderClickable();
|
|
268
|
+
|
|
269
|
+
fireEvent.keyDown(info, { key });
|
|
270
|
+
|
|
271
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
test.each([["{Enter}"], ["[Space]"]])(
|
|
276
|
+
"a real keyboard %s on the focused (i) does not activate the card",
|
|
277
|
+
async (keys: string) => {
|
|
278
|
+
const user: ReturnType<typeof userEvent.setup> = userEvent.setup({
|
|
279
|
+
advanceTimers: (ms: number) => {
|
|
280
|
+
jest.advanceTimersByTime(ms);
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
const { info, onClick } = renderClickable();
|
|
284
|
+
|
|
285
|
+
act(() => {
|
|
286
|
+
info.focus();
|
|
287
|
+
});
|
|
288
|
+
await user.keyboard(keys);
|
|
289
|
+
|
|
290
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
test("clicking the card itself still activates it", () => {
|
|
295
|
+
const { card, onClick } = renderClickable();
|
|
296
|
+
|
|
297
|
+
fireEvent.click(card);
|
|
298
|
+
|
|
299
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("the title and value are not inside the card's button - they stay readable text", () => {
|
|
303
|
+
const { card } = renderClickable();
|
|
304
|
+
|
|
305
|
+
expect(card).not.toContainElement(screen.getByText("Ready"));
|
|
306
|
+
expect(card).not.toContainElement(screen.getByText("3/3"));
|
|
307
|
+
expect(card).toBeEmptyDOMElement();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test.each([["{Enter}"], ["[Space]"]])(
|
|
311
|
+
"a real keyboard %s on the focused card activates it",
|
|
312
|
+
async (keys: string) => {
|
|
313
|
+
const user: ReturnType<typeof userEvent.setup> = userEvent.setup({
|
|
314
|
+
advanceTimers: (ms: number) => {
|
|
315
|
+
jest.advanceTimersByTime(ms);
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
const { card, onClick } = renderClickable();
|
|
319
|
+
|
|
320
|
+
act(() => {
|
|
321
|
+
card.focus();
|
|
322
|
+
});
|
|
323
|
+
await user.keyboard(keys);
|
|
324
|
+
|
|
325
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
test("Tab reaches the card first and then its (i)", async () => {
|
|
330
|
+
const user: ReturnType<typeof userEvent.setup> = userEvent.setup({
|
|
331
|
+
advanceTimers: (ms: number) => {
|
|
332
|
+
jest.advanceTimersByTime(ms);
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
const { card, info } = renderClickable();
|
|
336
|
+
|
|
337
|
+
await user.tab();
|
|
338
|
+
expect(card).toHaveFocus();
|
|
339
|
+
|
|
340
|
+
await user.tab();
|
|
341
|
+
expect(info).toHaveFocus();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("other keys on the (i) still reach the card and are ignored by it", () => {
|
|
345
|
+
const { info, onClick } = renderClickable();
|
|
346
|
+
|
|
347
|
+
fireEvent.keyDown(info, { key: "a" });
|
|
348
|
+
fireEvent.keyDown(info, { key: "Tab" });
|
|
349
|
+
|
|
350
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("hovering the (i) of a clickable card shows the text without activating it", async () => {
|
|
354
|
+
const { info, onClick } = renderClickable();
|
|
355
|
+
|
|
356
|
+
await hover(info);
|
|
357
|
+
|
|
358
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(READY);
|
|
359
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("a toggle card keeps its pressed state when it gains a tooltip", () => {
|
|
363
|
+
const { card } = renderClickable(true);
|
|
364
|
+
|
|
365
|
+
expect(card).toHaveAttribute("aria-pressed", "true");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("the card keeps its own accessible name when it has one", () => {
|
|
369
|
+
const { card } = renderClickable();
|
|
370
|
+
|
|
371
|
+
expect(card).toHaveAttribute("aria-label", "Show ready pods");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("without an aria-label, the card is named by its title and value - the (i)'s 'About' name does not leak in", () => {
|
|
375
|
+
render(
|
|
376
|
+
<InfoCard title="Ready" value="3/3" tooltip={READY} onClick={() => {}} />,
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
expect(
|
|
380
|
+
screen.getByRole("button", { name: "Ready 3/3" }),
|
|
381
|
+
).toBeInTheDocument();
|
|
382
|
+
expect(
|
|
383
|
+
screen.getByRole("button", { name: "About Ready" }),
|
|
384
|
+
).toBeInTheDocument();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("an element value has no text to borrow, so the card is named by its title", () => {
|
|
388
|
+
render(
|
|
389
|
+
<InfoCard
|
|
390
|
+
title="Ready"
|
|
391
|
+
value={<span>3 of 3</span>}
|
|
392
|
+
tooltip={READY}
|
|
393
|
+
onClick={() => {}}
|
|
394
|
+
/>,
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
expect(screen.getByRole("button", { name: "Ready" })).toBeInTheDocument();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("a whitespace-only tooltip leaves a clickable card exactly as it was", () => {
|
|
401
|
+
render(
|
|
402
|
+
<InfoCard title="Ready" value="3/3" tooltip=" " onClick={() => {}} />,
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const card: HTMLElement = screen.getByRole("button");
|
|
406
|
+
|
|
407
|
+
expect(card.tagName).toBe("DIV");
|
|
408
|
+
expect(card).toHaveAttribute("tabindex", "0");
|
|
409
|
+
expect(
|
|
410
|
+
screen.queryByRole("button", { name: /About/ }),
|
|
411
|
+
).not.toBeInTheDocument();
|
|
412
|
+
});
|
|
413
|
+
});
|