Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ladle stories #500

Merged
merged 19 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b8ced15
✨ feat(stories): add rotating loader story
w3bdesign Dec 4, 2024
5282bfe
✨ feat(error): add compact mode to error boundary
w3bdesign Dec 4, 2024
8b4aec7
✅ test: add storybook tests for hamburger component
w3bdesign Dec 4, 2024
728f88e
✨ feat: add NavigationLink component stories
w3bdesign Dec 4, 2024
f373299
✅ test: add storybook tests for footer component
w3bdesign Dec 4, 2024
ec8174c
🔥 cleanup: remove unused Story import from Footer stories
w3bdesign Dec 4, 2024
5b9ccb3
♻️ refactor: extract ErrorFallbackWrapper component
w3bdesign Dec 4, 2024
1e1f40b
♻️ refactor: simplify error boundary stories and clean up styles
w3bdesign Dec 4, 2024
7d89b93
♻️ refactor: update PageHeader stories with improved examples
w3bdesign Dec 4, 2024
16877c4
♻️ refactor: remove unused Story import from ErrorBoundary story
w3bdesign Dec 4, 2024
5905464
♻️ refactor: improve error boundary component organization
w3bdesign Dec 4, 2024
3452cce
♻️ refactor: improve error handling typing in ErrorBoundary
w3bdesign Dec 4, 2024
c8a7434
✅ test: enhance tabs component test coverage
w3bdesign Dec 4, 2024
35e7c10
♻️ refactor: simplify and enhance tabs component test suite
w3bdesign Dec 4, 2024
0600f82
🎨 style: improve code formatting in Tabs test file
w3bdesign Dec 4, 2024
b787f14
🎨 style: improve code formatting in ErrorBoundary stories
w3bdesign Dec 4, 2024
1f164a1
🎨 style: format PageHeader stories file for consistency
w3bdesign Dec 4, 2024
4612eb0
♻️ refactor: extract error fallback component from boundary
w3bdesign Dec 4, 2024
e486ebe
♻️ refactor: extract fallback component to separate file
w3bdesign Dec 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 70 additions & 115 deletions __tests__/UI/Tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,155 +2,110 @@
* @jest-environment jsdom
*/

/// <reference types="@testing-library/jest-dom" />

import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom";
import Tabs from "../../src/components/UI/Tabs.component";

// Mock motion to avoid issues with animations in tests
jest.mock("motion", () => ({
// Component that throws an error immediately
const ImmediateCrash = () => {
throw new Error("Immediate crash!");
};

const mockMotion = {
motion: {
div: "div",
button: "button",
div: (props: React.ComponentProps<"div">) => (
<div {...props}>{props.children}</div>
),
button: (props: React.ComponentProps<"button">) => (
<button type="button" {...props}>
{props.children}
</button>
),
},
AnimatePresence: ({ children }: { children: React.ReactNode }) => (
<>{children}</>
),
}));

const mockCVData = {
keyQualifications: ["Qualification 1", "Qualification 2"],
experience: [
{
period: "2020-2022",
company: "Example Company",
role: "Software Developer",
description: "Worked on various projects",
},
],
education: [
{
period: "2016-2020",
institution: "University of Example",
degree: "Bachelor in Computer Science",
description: "Studied various aspects of computer science",
},
],
};

jest.mock("motion", () => mockMotion);

const mockTabs = [
{
id: "qualifications",
label: "Nøkkelkvalifikasjoner",
content: (
<ul className="list-disc pl-5 text-gray-300">
{mockCVData.keyQualifications.map((qual) => (
<li key={qual} className="mb-2">
{qual}
</li>
))}
</ul>
),
expectedTexts: mockCVData.keyQualifications,
unexpectedTexts: ["Example Company", "University of Example"],
},
{
id: "experience",
label: "Erfaring",
content: (
<div className="text-gray-300">
{mockCVData.experience.map((exp) => (
<div key={exp.description} className="mb-6">
<h3 className="font-semibold text-white">
{exp.period} - {exp.company}
</h3>
{exp.role && <p className="italic">{exp.role}</p>}
<p>{exp.description}</p>
</div>
))}
</div>
),
expectedTexts: [
`${mockCVData.experience[0].period} - ${mockCVData.experience[0].company}`,
mockCVData.experience[0].role,
mockCVData.experience[0].description,
],
unexpectedTexts: ["Qualification 1"],
id: "tab1",
label: "Normal Tab",
content: <div>Normal content</div>,
},
{
id: "education",
label: "Utdanning",
content: (
<div className="text-gray-300">
{mockCVData.education.map((edu) => (
<div key={edu.description} className="mb-6">
<h3 className="font-semibold text-white">
{edu.period} - {edu.institution}
</h3>
{edu.degree && <p className="italic">{edu.degree}</p>}
<p>{edu.description}</p>
</div>
))}
</div>
),
expectedTexts: [
`${mockCVData.education[0].period} - ${mockCVData.education[0].institution}`,
mockCVData.education[0].degree,
mockCVData.education[0].description,
],
unexpectedTexts: ["Qualification 1"],
id: "tab2",
label: "Crashing Tab",
content: <ImmediateCrash />,
},
];

describe("Tabs", () => {
const renderTabs = () => render(<Tabs tabs={mockTabs} />);

const expectTextsToBePresent = async (texts: string[]) => {
await Promise.all(
texts.map(async (text) => {
await waitFor(() => {
expect(screen.getByText(text)).toBeInTheDocument();
});
}),
);
};

const expectTextsNotToBePresent = (texts: string[]) => {
texts.forEach((text) => {
expect(screen.queryByText(text)).not.toBeInTheDocument();
});
};
const renderTabs = (orientation?: "horizontal" | "vertical") =>
render(<Tabs tabs={mockTabs} orientation={orientation} />);

it("renders all CV tab labels", () => {
it("renders tabs with correct layout in vertical orientation", () => {
renderTabs();
mockTabs.forEach((tab) => {
expect(screen.getByRole("tab", { name: tab.label })).toBeInTheDocument();
});
const tabList = screen.getByRole("tablist");
expect(tabList).toHaveClass("sm:flex-col");
});

it.each(mockTabs)("renders correct content for $label tab", async (tab) => {
renderTabs();
if (tab.id !== mockTabs[0].id) {
fireEvent.click(screen.getByRole("tab", { name: tab.label }));
}
await expectTextsToBePresent(tab.expectedTexts);
expectTextsNotToBePresent(tab.unexpectedTexts);
it("renders tabs with correct layout in horizontal orientation", () => {
renderTabs("horizontal");
const tabList = screen.getByRole("tablist");
expect(tabList).toHaveClass("flex-row");
expect(tabList).not.toHaveClass("sm:flex-col");
});

it("applies correct ARIA attributes to CV tabs", () => {
it("applies correct ARIA attributes to tabs", () => {
renderTabs();
mockTabs.forEach((tab, index) => {
const tabElement = screen.getByRole("tab", { name: tab.label });
expect(tabElement).toHaveAttribute(
"aria-selected",
index === 0 ? "true" : "false",
index === 0 ? "true" : "false"
);
expect(tabElement).toHaveAttribute("aria-controls", `tabpanel-${tab.id}`);
});
});

it("renders in vertical orientation by default", () => {
it("switches tab content when clicking tabs", () => {
renderTabs();
const tabList = screen.getByRole("tablist");
expect(tabList).toHaveClass("sm:flex-col");

// Initial tab should be visible
expect(screen.getByText("Normal content")).toBeInTheDocument();

// Click second tab
const crashingTab = screen.getByRole("tab", { name: "Crashing Tab" });
fireEvent.click(crashingTab);

expect(() => {
render(<ImmediateCrash />);
}).toThrow("Immediate crash!");
});

it("applies correct border styles to tabs", () => {
renderTabs();
const tabs = screen.getAllByRole("tab");

// First tab should not have top border
expect(tabs[0]).not.toHaveClass("border-t");

// Second tab should have top border
expect(tabs[1]).toHaveClass("border-t", "border-gray-600");
});

it("renders tab panels with correct attributes", () => {
renderTabs();

const activePanel = screen.getByRole("tabpanel");
expect(activePanel).toHaveAttribute("id", "tabpanel-tab1");
expect(activePanel).toHaveAttribute("aria-labelledby", "tab-tab1");
expect(activePanel).toHaveClass("px-8");
});
});
23 changes: 16 additions & 7 deletions src/components/ErrorBoundary/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
"use client";

import React, { ReactNode } from "react";
import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary";
import ErrorFallback from "./ErrorFallback.component";
import React, { ReactNode, ErrorInfo } from "react";
import { ErrorBoundary as ReactErrorBoundary, FallbackProps } from "react-error-boundary";
import Fallback from "./Fallback.component";

interface ErrorBoundaryProps {
children: ReactNode;
compact?: boolean;
}

// Define the fallback component outside of ErrorBoundary
function ErrorFallback(props: FallbackProps) {
return <Fallback {...props} compact={false} />;
}

/**
Expand All @@ -14,15 +20,18 @@ interface ErrorBoundaryProps {
*
* @param {Object} props - The component props
* @param {ReactNode} props.children - The child components to be wrapped by the ErrorBoundary
* @param {boolean} props.compact - Whether to show a compact error fallback (used in stories)
* @returns {JSX.Element} A React component that catches errors in its child components
*/
const ErrorBoundary: React.FC<ErrorBoundaryProps> = ({ children }) => {
const ErrorBoundary: React.FC<ErrorBoundaryProps> = ({ children, compact = false }) => {
const handleError = (error: Error, info: ErrorInfo) => {
console.error("Uventet feil i Matrix:", error, info);
};

return (
<ReactErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
console.error("Uventet feil i Matrix:", error, info);
}}
onError={handleError}
>
{children}
</ReactErrorBoundary>
Expand Down
32 changes: 29 additions & 3 deletions src/components/ErrorBoundary/ErrorFallback.component.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from "react";
import ReactMatrixAnimation from "@/components/Animations/Matrix.component";
import Pill from "@/components/UI/Pill.component";
import ReactMatrixAnimation from "../../components/Animations/Matrix.component";
import Pill from "../../components/UI/Pill.component";

interface ErrorFallbackProps {
error: Error;
compact?: boolean;
}

/**
Expand All @@ -12,9 +13,34 @@ interface ErrorFallbackProps {
*
* @param {Object} props - The component props
* @param {Error} props.error - The error object caught by the ErrorBoundary
* @param {boolean} props.compact - Whether to show a compact version (used in stories)
* @returns {JSX.Element} A React component displaying the error message and reload option
*/
const ErrorFallback: React.FC<ErrorFallbackProps> = ({ error }) => {
const ErrorFallback: React.FC<ErrorFallbackProps> = ({ error, compact = false }) => {
if (compact) {
return (
<div className="relative bg-gray-900 p-4 rounded-lg overflow-hidden">
<div className="absolute inset-0 opacity-30">
<ReactMatrixAnimation />
</div>
<div className="relative z-10 flex flex-col items-center text-center">
<h2 className="text-white text-lg mb-2">
Har du funnet en feil i Matrix?
</h2>
<p className="text-white text-sm mb-3">
{error.message || "En uventet feil har oppstått."}
</p>
<button
onClick={() => window.location.reload()}
className="px-3 py-1 bg-matrix-light text-black rounded text-sm hover:bg-matrix-dark"
>
Returner til Matrix
</button>
</div>
</div>
);
}

return (
<div className="absolute w-full h-full">
<ReactMatrixAnimation />
Expand Down
20 changes: 20 additions & 0 deletions src/components/ErrorBoundary/ErrorFallbackWrapper.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";
import ErrorFallback from "./ErrorFallback.component";

interface ErrorFallbackWrapperProps {
error: Error;
compact?: boolean;
}

/**
* Wrapper component for ErrorFallback that handles compact mode
* @param {Object} props - The component props
* @param {Error} props.error - The error object to display
* @param {boolean} props.compact - Whether to show a compact version
* @returns {JSX.Element} The wrapped ErrorFallback component
*/
const ErrorFallbackWrapper: React.FC<ErrorFallbackWrapperProps> = ({ error, compact }) => (
<ErrorFallback error={error} compact={compact} />
);

export default ErrorFallbackWrapper;
13 changes: 13 additions & 0 deletions src/components/ErrorBoundary/Fallback.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";
import { FallbackProps } from "react-error-boundary";
import ErrorFallbackWrapper from "./ErrorFallbackWrapper.component";

interface FallbackComponentProps extends FallbackProps {
compact: boolean;
}

const Fallback: React.FC<FallbackComponentProps> = ({ error, compact }) => {
return <ErrorFallbackWrapper error={error} compact={compact} />;
};

export default Fallback;
Loading
Loading