Skip to content

Commit 4d6d722

Browse files
Copilotpethers
andauthored
test: Improve coverage for low-coverage widgets and data files (42-73% → 80%+) (#637)
* Initial plan * Improve data layer and common component test coverage to 80%+ Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> * Enhance widget test coverage for CostEstimation and ValueCreation widgets Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> * refactor: use ALL_SECURITY_LEVELS constant and fix import order - Extract securityLevels array to use ALL_SECURITY_LEVELS from constants - Fix import organization in ValueDisplay.test.tsx (Vitest before RTL) - Addresses code review feedback for better code reusability Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> * Changes before error encountered Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> * fix: resolve TypeScript build errors and scope issues in test files - Add securityLevels constant to each describe block scope - Fix undefined type errors with nullish coalescing operator - Fix type guard for ResourceCard description prop - All 1545 tests passing, build successful Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> Co-authored-by: James Pether Sörling <pethers@users.noreply.github.com>
1 parent 963cd85 commit 4d6d722

7 files changed

Lines changed: 1532 additions & 24 deletions

File tree

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { render, screen, fireEvent } from "@testing-library/react";
3+
import ResourceCard from "./ResourceCard";
4+
import { SecurityResource } from "../../types/securityResources";
5+
6+
describe("ResourceCard Component", () => {
7+
const mockResource: SecurityResource = {
8+
id: "test-1",
9+
title: "Test Resource",
10+
description: "This is a test resource description",
11+
url: "https://example.com",
12+
type: "Guide",
13+
component: "availability",
14+
source: "NIST",
15+
level: "Moderate",
16+
tags: ["tag1", "tag2", "tag3"],
17+
};
18+
19+
it("renders without crashing", () => {
20+
render(<ResourceCard resource={mockResource} />);
21+
22+
expect(screen.getByTestId("resource-item")).toBeInTheDocument();
23+
expect(screen.getByText("Test Resource")).toBeInTheDocument();
24+
});
25+
26+
it("displays resource title", () => {
27+
render(<ResourceCard resource={mockResource} />);
28+
29+
expect(screen.getByText("Test Resource")).toBeInTheDocument();
30+
});
31+
32+
it("displays resource description", () => {
33+
render(<ResourceCard resource={mockResource} />);
34+
35+
if (mockResource.description) {
36+
expect(screen.getByText(mockResource.description)).toBeInTheDocument();
37+
}
38+
});
39+
40+
it("displays resource type", () => {
41+
render(<ResourceCard resource={mockResource} />);
42+
43+
expect(screen.getByText("Guide")).toBeInTheDocument();
44+
});
45+
46+
it("displays component information when available", () => {
47+
render(<ResourceCard resource={mockResource} />);
48+
49+
expect(screen.getByText(/Component: availability/i)).toBeInTheDocument();
50+
});
51+
52+
it("displays source information when available", () => {
53+
render(<ResourceCard resource={mockResource} />);
54+
55+
expect(screen.getByText(/Source: NIST/i)).toBeInTheDocument();
56+
});
57+
58+
it("displays level information when available", () => {
59+
render(<ResourceCard resource={mockResource} />);
60+
61+
expect(screen.getByText(/Level: Moderate/i)).toBeInTheDocument();
62+
});
63+
64+
it("displays all tags", () => {
65+
render(<ResourceCard resource={mockResource} />);
66+
67+
mockResource.tags?.forEach((tag) => {
68+
expect(screen.getByText(tag)).toBeInTheDocument();
69+
});
70+
});
71+
72+
it("opens URL in new tab when clicked without custom onClick", () => {
73+
const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => null);
74+
render(<ResourceCard resource={mockResource} />);
75+
76+
const card = screen.getByTestId("resource-item");
77+
fireEvent.click(card);
78+
79+
expect(windowOpenSpy).toHaveBeenCalledWith(
80+
mockResource.url,
81+
"_blank",
82+
"noopener,noreferrer"
83+
);
84+
85+
windowOpenSpy.mockRestore();
86+
});
87+
88+
it("calls custom onClick handler when provided", () => {
89+
const mockOnClick = vi.fn();
90+
render(<ResourceCard resource={mockResource} onClick={mockOnClick} />);
91+
92+
const card = screen.getByTestId("resource-item");
93+
fireEvent.click(card);
94+
95+
expect(mockOnClick).toHaveBeenCalledWith(mockResource);
96+
});
97+
98+
it("handles keyboard Enter key", () => {
99+
const mockOnClick = vi.fn();
100+
render(<ResourceCard resource={mockResource} onClick={mockOnClick} />);
101+
102+
const card = screen.getByTestId("resource-item");
103+
fireEvent.keyDown(card, { key: "Enter" });
104+
105+
expect(mockOnClick).toHaveBeenCalledWith(mockResource);
106+
});
107+
108+
it("handles keyboard Space key", () => {
109+
const mockOnClick = vi.fn();
110+
render(<ResourceCard resource={mockResource} onClick={mockOnClick} />);
111+
112+
const card = screen.getByTestId("resource-item");
113+
fireEvent.keyDown(card, { key: " " });
114+
115+
expect(mockOnClick).toHaveBeenCalledWith(mockResource);
116+
});
117+
118+
it("does not trigger action on other keyboard keys", () => {
119+
const mockOnClick = vi.fn();
120+
render(<ResourceCard resource={mockResource} onClick={mockOnClick} />);
121+
122+
const card = screen.getByTestId("resource-item");
123+
fireEvent.keyDown(card, { key: "a" });
124+
125+
expect(mockOnClick).not.toHaveBeenCalled();
126+
});
127+
128+
it("truncates long descriptions", () => {
129+
const longDescription = "a".repeat(150);
130+
const resourceWithLongDesc: SecurityResource = {
131+
...mockResource,
132+
description: longDescription,
133+
};
134+
135+
render(<ResourceCard resource={resourceWithLongDesc} />);
136+
137+
// Check that the description was truncated by looking for the ellipsis
138+
const descriptionElement = screen.getByText(/\.\.\.$/);
139+
expect(descriptionElement).toBeInTheDocument();
140+
expect(descriptionElement.textContent?.length).toBeLessThanOrEqual(103); // 100 chars + "..."
141+
});
142+
143+
it("applies custom className", () => {
144+
render(<ResourceCard resource={mockResource} className="custom-class" />);
145+
146+
const card = screen.getByTestId("resource-item");
147+
expect(card.className).toContain("custom-class");
148+
});
149+
150+
it("uses custom testId when provided", () => {
151+
render(<ResourceCard resource={mockResource} testId="custom-test-id" />);
152+
153+
expect(screen.getByTestId("custom-test-id")).toBeInTheDocument();
154+
});
155+
156+
it("handles missing description gracefully", () => {
157+
const resourceWithoutDesc: SecurityResource = {
158+
...mockResource,
159+
description: undefined,
160+
};
161+
162+
render(<ResourceCard resource={resourceWithoutDesc} />);
163+
164+
expect(screen.getByTestId("resource-item")).toBeInTheDocument();
165+
});
166+
167+
it("handles empty description", () => {
168+
const resourceWithEmptyDesc: SecurityResource = {
169+
...mockResource,
170+
description: "",
171+
};
172+
173+
render(<ResourceCard resource={resourceWithEmptyDesc} />);
174+
175+
expect(screen.getByTestId("resource-item")).toBeInTheDocument();
176+
});
177+
178+
it("displays General type when type is missing", () => {
179+
const resourceWithoutType: SecurityResource = {
180+
...mockResource,
181+
type: undefined,
182+
};
183+
184+
render(<ResourceCard resource={resourceWithoutType} />);
185+
186+
expect(screen.getByText("General")).toBeInTheDocument();
187+
});
188+
189+
it("does not display component info when not available", () => {
190+
const resourceWithoutComponent: SecurityResource = {
191+
...mockResource,
192+
component: undefined,
193+
};
194+
195+
render(<ResourceCard resource={resourceWithoutComponent} />);
196+
197+
expect(screen.queryByText(/Component:/)).not.toBeInTheDocument();
198+
});
199+
200+
it("does not display source info when not available", () => {
201+
const resourceWithoutSource: SecurityResource = {
202+
...mockResource,
203+
source: undefined,
204+
};
205+
206+
render(<ResourceCard resource={resourceWithoutSource} />);
207+
208+
expect(screen.queryByText(/Source:/)).not.toBeInTheDocument();
209+
});
210+
211+
it("does not display level info when not available", () => {
212+
const resourceWithoutLevel: SecurityResource = {
213+
...mockResource,
214+
level: undefined,
215+
};
216+
217+
render(<ResourceCard resource={resourceWithoutLevel} />);
218+
219+
expect(screen.queryByText(/Level:/)).not.toBeInTheDocument();
220+
});
221+
222+
it("handles resources without tags", () => {
223+
const resourceWithoutTags: SecurityResource = {
224+
...mockResource,
225+
tags: undefined,
226+
};
227+
228+
render(<ResourceCard resource={resourceWithoutTags} />);
229+
230+
expect(screen.getByTestId("resource-item")).toBeInTheDocument();
231+
});
232+
233+
it("handles resources with empty tags array", () => {
234+
const resourceWithEmptyTags: SecurityResource = {
235+
...mockResource,
236+
tags: [],
237+
};
238+
239+
render(<ResourceCard resource={resourceWithEmptyTags} />);
240+
241+
expect(screen.getByTestId("resource-item")).toBeInTheDocument();
242+
});
243+
244+
it("has proper accessibility attributes", () => {
245+
render(<ResourceCard resource={mockResource} />);
246+
247+
const card = screen.getByTestId("resource-item");
248+
expect(card).toHaveAttribute("role", "button");
249+
expect(card).toHaveAttribute("tabIndex", "0");
250+
});
251+
});

src/components/common/ValueDisplay.test.tsx

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { describe, expect, it } from "vitest";
12
import { render, screen } from "@testing-library/react";
23
import ValueDisplay from "./ValueDisplay";
34

@@ -35,4 +36,119 @@ describe("ValueDisplay Component", () => {
3536
// Find the value span and check its class using getByText instead of querySelector
3637
expect(screen.getByText("Large Text").className).contain("text-lg");
3738
});
39+
40+
it("renders without label", () => {
41+
render(<ValueDisplay value="No Label Value" />);
42+
43+
expect(screen.getByText("No Label Value")).toBeInTheDocument();
44+
});
45+
46+
it("applies default variant styling when variant is not specified", () => {
47+
render(<ValueDisplay value="Default" />);
48+
49+
const valueElement = screen.getByText("Default");
50+
expect(valueElement).toBeInTheDocument();
51+
expect(valueElement.className).toContain("text-blue");
52+
});
53+
54+
it("applies default size styling when size is not specified", () => {
55+
render(<ValueDisplay value="Default Size" />);
56+
57+
const valueElement = screen.getByText("Default Size");
58+
expect(valueElement).toBeInTheDocument();
59+
expect(valueElement.className).toContain("text-base");
60+
});
61+
62+
it("applies danger variant styling", () => {
63+
render(<ValueDisplay value="Danger" variant="danger" />);
64+
65+
const valueElement = screen.getByText("Danger");
66+
expect(valueElement.className).toContain("text-red");
67+
});
68+
69+
it("applies warning variant styling", () => {
70+
render(<ValueDisplay value="Warning" variant="warning" />);
71+
72+
const valueElement = screen.getByText("Warning");
73+
expect(valueElement.className).toContain("text-yellow");
74+
});
75+
76+
it("applies info variant styling", () => {
77+
render(<ValueDisplay value="Info" variant="info" />);
78+
79+
const valueElement = screen.getByText("Info");
80+
expect(valueElement.className).toContain("text-blue");
81+
});
82+
83+
it("applies primary variant styling", () => {
84+
render(<ValueDisplay value="Primary" variant="primary" />);
85+
86+
const valueElement = screen.getByText("Primary");
87+
expect(valueElement.className).toContain("text-blue");
88+
});
89+
90+
it("applies small size styling", () => {
91+
render(<ValueDisplay value="Small" size="sm" />);
92+
93+
const valueElement = screen.getByText("Small");
94+
expect(valueElement.className).toContain("text-sm");
95+
});
96+
97+
it("applies medium size styling", () => {
98+
render(<ValueDisplay value="Medium" size="md" />);
99+
100+
const valueElement = screen.getByText("Medium");
101+
expect(valueElement.className).toContain("text-base");
102+
});
103+
104+
it("applies large size styling", () => {
105+
render(<ValueDisplay value="Large" size="lg" />);
106+
107+
const valueElement = screen.getByText("Large");
108+
expect(valueElement.className).toContain("text-lg");
109+
});
110+
111+
it("renders with empty string value", () => {
112+
render(<ValueDisplay value="" />);
113+
114+
expect(screen.getByTestId("value-display")).toBeInTheDocument();
115+
});
116+
117+
it("renders with numeric value", () => {
118+
render(<ValueDisplay value={12345} />);
119+
120+
expect(screen.getByText("12345")).toBeInTheDocument();
121+
});
122+
123+
it("renders label after value", () => {
124+
render(<ValueDisplay label="suffix" value="Test Value" />);
125+
126+
const container = screen.getByTestId("value-display");
127+
expect(container).toBeInTheDocument();
128+
expect(screen.getByText("Test Value")).toBeInTheDocument();
129+
expect(screen.getByText("suffix")).toBeInTheDocument();
130+
});
131+
132+
it("renders with combined variant and size props", () => {
133+
render(<ValueDisplay value="Combined" variant="success" size="lg" />);
134+
135+
const valueElement = screen.getByText("Combined");
136+
expect(valueElement.className).toContain("text-green");
137+
expect(valueElement.className).toContain("text-lg");
138+
});
139+
140+
it("applies font-medium class to value", () => {
141+
render(<ValueDisplay value="Bold Value" />);
142+
143+
const valueElement = screen.getByText("Bold Value");
144+
expect(valueElement.className).toContain("font-medium");
145+
});
146+
147+
it("applies smaller text and gray color to label", () => {
148+
render(<ValueDisplay value="Value" label="Label" />);
149+
150+
const labelElement = screen.getByText("Label");
151+
expect(labelElement.className).toContain("text-xs");
152+
expect(labelElement.className).toContain("text-gray");
153+
});
38154
});

0 commit comments

Comments
 (0)