Skip to content

Commit 2d65a41

Browse files
test: Add comprehensive unit tests for copy button feature
- Add 20 new test cases for copy-to-clipboard functionality - Test copyable column detection (id, name, url, email, cluster) - Test non-copyable columns don't get copy buttons - Test clipboard API integration with mocked navigator.clipboard - Test visual feedback (CheckIcon on success) - Test event propagation blocking to prevent row clicks - Test multiple copy buttons across rows and columns - Test case-insensitive column name detection - Test cell wrapper structure and accessibility (aria-label) - Test with empty values and numeric values - All 25 tests passing ✓
1 parent 23cc995 commit 2d65a41

File tree

1 file changed

+356
-1
lines changed

1 file changed

+356
-1
lines changed

src/test/components/TableWrapper.test.tsx

Lines changed: 356 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen, fireEvent } from "@testing-library/react";
1+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
22

33
import TableWrapper from "../../components/TableWrapper";
44

@@ -496,4 +496,359 @@ describe("TableWrapper Component", () => {
496496
}
497497
});
498498
});
499+
500+
// ========== Copy Button Feature Tests ==========
501+
502+
describe("Copy Button functionality", () => {
503+
// Mock clipboard API
504+
const originalClipboard = { ...global.navigator.clipboard };
505+
const mockWriteText = vitest.fn();
506+
507+
beforeEach(() => {
508+
mockWriteText.mockClear();
509+
mockWriteText.mockResolvedValue(undefined);
510+
Object.defineProperty(navigator, 'clipboard', {
511+
value: { writeText: mockWriteText },
512+
writable: true,
513+
configurable: true,
514+
});
515+
});
516+
517+
afterEach(() => {
518+
Object.defineProperty(navigator, 'clipboard', {
519+
value: originalClipboard,
520+
writable: true,
521+
configurable: true,
522+
});
523+
});
524+
525+
it("should render copy buttons for copyable columns (ID)", () => {
526+
const dataWithID = {
527+
...mockFieldsData,
528+
fields: [
529+
{
530+
name: "Cluster ID",
531+
data_path: "cluster.id",
532+
data: ["cluster-123"],
533+
},
534+
{
535+
name: "Status",
536+
data_path: "cluster.status",
537+
data: ["Active"],
538+
},
539+
],
540+
};
541+
542+
const { container } = render(<TableWrapper {...dataWithID} />);
543+
544+
// Copy button should exist for ID column
545+
const copyButtons = container.querySelectorAll('.copy-button');
546+
expect(copyButtons.length).toBeGreaterThan(0);
547+
});
548+
549+
it("should render copy buttons for copyable columns (Name)", () => {
550+
const dataWithName = {
551+
...mockFieldsData,
552+
fields: [
553+
{
554+
name: "Cluster Name",
555+
data_path: "cluster.name",
556+
data: ["Production Cluster"],
557+
},
558+
],
559+
};
560+
561+
const { container } = render(<TableWrapper {...dataWithName} />);
562+
563+
const copyButtons = container.querySelectorAll('.copy-button');
564+
expect(copyButtons.length).toBeGreaterThan(0);
565+
});
566+
567+
it("should render copy buttons for copyable columns (URL)", () => {
568+
const dataWithURL = {
569+
...mockFieldsData,
570+
fields: [
571+
{
572+
name: "Console URL",
573+
data_path: "cluster.url",
574+
data: ["https://console.redhat.com"],
575+
},
576+
],
577+
};
578+
579+
const { container } = render(<TableWrapper {...dataWithURL} />);
580+
581+
const copyButtons = container.querySelectorAll('.copy-button');
582+
expect(copyButtons.length).toBeGreaterThan(0);
583+
});
584+
585+
it("should render copy buttons for copyable columns (Email)", () => {
586+
const dataWithEmail = {
587+
...mockFieldsData,
588+
fields: [
589+
{
590+
name: "Owner Email",
591+
data_path: "owner.email",
592+
data: ["admin@example.com"],
593+
},
594+
],
595+
};
596+
597+
const { container } = render(<TableWrapper {...dataWithEmail} />);
598+
599+
const copyButtons = container.querySelectorAll('.copy-button');
600+
expect(copyButtons.length).toBeGreaterThan(0);
601+
});
602+
603+
it("should NOT render copy buttons for non-copyable columns", () => {
604+
const nonCopyableData = {
605+
...mockFieldsData,
606+
fields: [
607+
{
608+
name: "Status",
609+
data_path: "status",
610+
data: ["Active"],
611+
},
612+
{
613+
name: "Count",
614+
data_path: "count",
615+
data: [42],
616+
},
617+
],
618+
};
619+
620+
const { container } = render(<TableWrapper {...nonCopyableData} />);
621+
622+
const copyButtons = container.querySelectorAll('.copy-button');
623+
expect(copyButtons.length).toBe(0);
624+
});
625+
626+
it("should copy cell value to clipboard when copy button is clicked", async () => {
627+
const dataWithID = {
628+
...mockFieldsData,
629+
fields: [
630+
{
631+
name: "ID",
632+
data_path: "id",
633+
data: ["test-id-123"],
634+
},
635+
],
636+
};
637+
638+
const { container } = render(<TableWrapper {...dataWithID} />);
639+
640+
const copyButton = container.querySelector('.copy-button') as HTMLElement;
641+
expect(copyButton).toBeInTheDocument();
642+
643+
fireEvent.click(copyButton);
644+
645+
await waitFor(() => {
646+
expect(mockWriteText).toHaveBeenCalledWith("test-id-123");
647+
});
648+
});
649+
650+
it("should show success icon (CheckIcon) after successful copy", async () => {
651+
const dataWithID = {
652+
...mockFieldsData,
653+
fields: [
654+
{
655+
name: "ID",
656+
data_path: "id",
657+
data: ["test-id-123"],
658+
},
659+
],
660+
};
661+
662+
const { container } = render(<TableWrapper {...dataWithID} />);
663+
664+
const copyButton = container.querySelector('.copy-button') as HTMLElement;
665+
fireEvent.click(copyButton);
666+
667+
await waitFor(() => {
668+
expect(copyButton.getAttribute('title')).toBe('Copied!');
669+
});
670+
});
671+
672+
it("should stop event propagation to prevent row click", async () => {
673+
const mockOnRowClick = vitest.fn();
674+
const dataWithID = {
675+
...mockFieldsData,
676+
fields: [
677+
{
678+
name: "ID",
679+
data_path: "id",
680+
data: ["test-id"],
681+
},
682+
],
683+
};
684+
685+
const { container } = render(<TableWrapper {...dataWithID} />);
686+
687+
const copyButton = container.querySelector('.copy-button') as HTMLElement;
688+
689+
// Create a spy on stopPropagation
690+
const clickEvent = new MouseEvent('click', { bubbles: true });
691+
const stopPropagationSpy = vitest.spyOn(clickEvent, 'stopPropagation');
692+
693+
copyButton.dispatchEvent(clickEvent);
694+
695+
expect(stopPropagationSpy).toHaveBeenCalled();
696+
});
697+
698+
it("should render multiple copy buttons for multiple copyable columns", () => {
699+
const multiCopyableData = {
700+
...mockFieldsData,
701+
fields: [
702+
{
703+
name: "Cluster ID",
704+
data_path: "cluster.id",
705+
data: ["id-1", "id-2"],
706+
},
707+
{
708+
name: "Cluster Name",
709+
data_path: "cluster.name",
710+
data: ["name-1", "name-2"],
711+
},
712+
{
713+
name: "Status",
714+
data_path: "status",
715+
data: ["Active", "Inactive"],
716+
},
717+
],
718+
};
719+
720+
const { container } = render(<TableWrapper {...multiCopyableData} />);
721+
722+
const copyButtons = container.querySelectorAll('.copy-button');
723+
// 2 rows × 2 copyable columns = 4 copy buttons
724+
expect(copyButtons.length).toBe(4);
725+
});
726+
727+
it("should handle case-insensitive column detection", () => {
728+
const mixedCaseData = {
729+
...mockFieldsData,
730+
fields: [
731+
{
732+
name: "CLUSTER_ID",
733+
data_path: "cluster.id",
734+
data: ["id-1"],
735+
},
736+
{
737+
name: "ClusterName",
738+
data_path: "cluster.name",
739+
data: ["name-1"],
740+
},
741+
{
742+
name: "email_ADDRESS",
743+
data_path: "email",
744+
data: ["test@example.com"],
745+
},
746+
],
747+
};
748+
749+
const { container } = render(<TableWrapper {...mixedCaseData} />);
750+
751+
const copyButtons = container.querySelectorAll('.copy-button');
752+
expect(copyButtons.length).toBe(3);
753+
});
754+
755+
it("should render copy button with cell value in wrapper", () => {
756+
const dataWithID = {
757+
...mockFieldsData,
758+
fields: [
759+
{
760+
name: "ID",
761+
data_path: "id",
762+
data: ["test-value"],
763+
},
764+
],
765+
};
766+
767+
const { container } = render(<TableWrapper {...dataWithID} />);
768+
769+
const cellWrapper = container.querySelector('.cell-with-copy');
770+
expect(cellWrapper).toBeInTheDocument();
771+
772+
const cellValue = container.querySelector('.cell-value');
773+
expect(cellValue).toBeInTheDocument();
774+
expect(cellValue?.textContent).toBe("test-value");
775+
});
776+
777+
it("should have correct aria-label for accessibility", () => {
778+
const dataWithID = {
779+
...mockFieldsData,
780+
fields: [
781+
{
782+
name: "ID",
783+
data_path: "id",
784+
data: ["test-id"],
785+
},
786+
],
787+
};
788+
789+
const { container } = render(<TableWrapper {...dataWithID} />);
790+
791+
const copyButton = container.querySelector('.copy-button') as HTMLElement;
792+
expect(copyButton.getAttribute('aria-label')).toBe('Copy to clipboard');
793+
});
794+
795+
it("should work with 'cluster' keyword in column name", () => {
796+
const clusterData = {
797+
...mockFieldsData,
798+
fields: [
799+
{
800+
name: "Cluster",
801+
data_path: "cluster",
802+
data: ["my-cluster"],
803+
},
804+
],
805+
};
806+
807+
const { container } = render(<TableWrapper {...clusterData} />);
808+
809+
const copyButtons = container.querySelectorAll('.copy-button');
810+
expect(copyButtons.length).toBeGreaterThan(0);
811+
});
812+
813+
it("should render copy button for empty cell values", () => {
814+
const emptyData = {
815+
...mockFieldsData,
816+
fields: [
817+
{
818+
name: "ID",
819+
data_path: "id",
820+
data: [""],
821+
},
822+
],
823+
};
824+
825+
const { container } = render(<TableWrapper {...emptyData} />);
826+
827+
// Copy button should still render for empty values
828+
const copyButton = container.querySelector('.copy-button') as HTMLElement;
829+
expect(copyButton).toBeInTheDocument();
830+
});
831+
832+
it("should render copy button for numeric values", () => {
833+
const numberData = {
834+
...mockFieldsData,
835+
fields: [
836+
{
837+
name: "User ID",
838+
data_path: "user.id",
839+
data: [12345],
840+
},
841+
],
842+
};
843+
844+
const { container } = render(<TableWrapper {...numberData} />);
845+
846+
// Copy button should render for numeric values
847+
const copyButton = container.querySelector('.copy-button') as HTMLElement;
848+
expect(copyButton).toBeInTheDocument();
849+
850+
// Verify the cell displays the number
851+
expect(screen.getByText("12345")).toBeInTheDocument();
852+
});
853+
});
499854
});

0 commit comments

Comments
 (0)