Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
63 changes: 15 additions & 48 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,34 @@
import { beforeEach, describe, expect, it } from "vitest";
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import App from "./App";

describe("App", () => {
beforeEach(() => {
it("renders the complete portfolio page", () => {
render(<App />);
});

it("renders the portfolio hero", () => {
expect(
screen.getByRole("heading", { name: /rashell\s*guerrero/i }),
).toBeInTheDocument();
expect(
screen.getByText(/i build reliable, user-focused software/i),
screen.getByRole("navigation", { name: /main navigation/i }),
).toBeInTheDocument();
});

it("renders the about section", () => {
const main = screen.getByRole("main");
expect(main).toBeInTheDocument();

expect(
screen.getByRole("heading", {
name: /think\. design\. build\.\s*refine\./i,
level: 1,
name: /rashell\s*guerrero/i,
}),
).toBeInTheDocument();
expect(
screen.getAllByText(/full-stack software engineer/i).length,
).toBeGreaterThan(0);
expect(screen.getByText(/available for/i)).toBeInTheDocument();
expect(
screen.getByRole("link", { name: /full-time opportunities/i }),
).toHaveAttribute("href", "#contact");
});

it("renders the expected page sections", () => {
expect(document.querySelector("#hero")).toBeInTheDocument();
expect(document.querySelector("#about")).toBeInTheDocument();
});
const sectionIds = ["hero", "about", "projects", "skills", "contact"];

it("renders the main navigation", () => {
expect(
screen.getByRole("navigation", { name: /main navigation/i }),
).toBeInTheDocument();
});
sectionIds.forEach((sectionId) => {
const section = document.getElementById(sectionId);

it("has working primary links", () => {
expect(screen.getByRole("link", { name: /view my work/i })).toHaveAttribute(
"href",
"#projects",
);
expect(screen.getByRole("link", { name: /get in touch/i })).toHaveAttribute(
"href",
"#contact",
);
});
expect(section).toBeInTheDocument();
expect(main).toContainElement(section);
});

it("has accessible social links", () => {
expect(
screen.getByRole("link", { name: /github profile/i }),
).toHaveAttribute("href", "https://github.com/rxshellg");
expect(
screen.getByRole("link", { name: /linkedin profile/i }),
).toHaveAttribute("href", "https://www.linkedin.com/in/rashell-guerrero/");
expect(
screen.getByRole("link", { name: /email rashell/i }),
).toHaveAttribute("href", "mailto:rashellguerrero123@gmail.com");
expect(screen.getByRole("contentinfo")).toBeInTheDocument();
});
});
116 changes: 99 additions & 17 deletions src/components/layout/Navbar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,123 @@
import { beforeEach, describe, expect, it } from "vitest";
import { fireEvent, render, screen, within } from "@testing-library/react";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { navigationLinks } from "../../data/navigation";
import Navbar from "./Navbar";

function expectNavigationLinks(container: HTMLElement) {
expect(navigationLinks).toHaveLength(5);

navigationLinks.forEach(({ label, href }) => {
expect(
within(container).getByRole("link", {
name: new RegExp(label, "i"),
}),
).toHaveAttribute("href", href);
});
}

describe("Navbar", () => {
let nav: HTMLElement;
let mainNavigation: HTMLElement;

beforeEach(() => {
render(<Navbar />);
nav = screen.getByRole("navigation", { name: /main navigation/i });

mainNavigation = screen.getByRole("navigation", {
name: /main navigation/i,
});
});

it("renders as the main navigation landmark", () => {
expect(nav).toBeInTheDocument();
expect(mainNavigation).toBeInTheDocument();
});

it("renders the logo link pointing to #hero", () => {
it("renders the logo link pointing to the hero section", () => {
expect(
within(nav).getByRole("link", { name: /go to top/i }),
within(mainNavigation).getByRole("link", {
name: /go to top/i,
}),
).toHaveAttribute("href", "#hero");
});

it("renders all active navigation links with correct hrefs", () => {
expect(within(nav).getByRole("link", { name: /home/i })).toHaveAttribute(
"href",
"#hero",
);
expect(within(nav).getByRole("link", { name: /about/i })).toHaveAttribute(
"href",
"#about",
it("renders all five navigation links with the correct destinations", () => {
expectNavigationLinks(mainNavigation);
});

it("opens and closes the mobile navigation menu", async () => {
const user = userEvent.setup();

const menuButton = within(mainNavigation).getByRole("button", {
name: /toggle navigation menu/i,
});

expect(menuButton).toHaveAttribute("aria-expanded", "false");
expect(
screen.queryByRole("navigation", {
name: /mobile navigation/i,
}),
).not.toBeInTheDocument();

await user.click(menuButton);

expect(menuButton).toHaveAttribute("aria-expanded", "true");

const mobileNavigation = screen.getByRole("navigation", {
name: /mobile navigation/i,
});

expect(mobileNavigation).toBeInTheDocument();
expectNavigationLinks(mobileNavigation);

await user.click(menuButton);

expect(menuButton).toHaveAttribute("aria-expanded", "false");
expect(
screen.queryByRole("navigation", {
name: /mobile navigation/i,
}),
).not.toBeInTheDocument();
});

it("closes the mobile menu after selecting a navigation link", async () => {
const user = userEvent.setup();

const menuButton = within(mainNavigation).getByRole("button", {
name: /toggle navigation menu/i,
});

await user.click(menuButton);

const mobileNavigation = screen.getByRole("navigation", {
name: /mobile navigation/i,
});

await user.click(
within(mobileNavigation).getByRole("link", {
name: /projects/i,
}),
);

expect(menuButton).toHaveAttribute("aria-expanded", "false");
expect(
screen.queryByRole("navigation", {
name: /mobile navigation/i,
}),
).not.toBeInTheDocument();
});

it("opens the resume modal", () => {
fireEvent.click(within(nav).getByRole("button", { name: /resume\.pdf/i }));
it("opens the resume modal", async () => {
const user = userEvent.setup();

await user.click(
within(mainNavigation).getByRole("button", {
name: /resume\.pdf/i,
}),
);

expect(
screen.getByRole("dialog", { name: /hey there/i }),
screen.getByRole("dialog", {
name: /hey there/i,
}),
).toBeInTheDocument();
});
});
4 changes: 3 additions & 1 deletion src/components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ function Navbar() {
</nav>

{isMobileMenuOpen && (
<div className="mobile-navbar-menu">{navLinks(true)}</div>
<nav className="mobile-navbar-menu" aria-label="Mobile navigation">
{navLinks(true)}
</nav>
)}

<ResumeModal
Expand Down
128 changes: 128 additions & 0 deletions src/components/ui/ProjectCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import type { Project } from "../../data/projects";
import ProjectCard from "./ProjectCard";

const project: Project = {
id: "test-project",
title: "Test Project",
description: "A project created for testing.",
images: ["/image-1.png", "/image-2.png", "/image-3.png"],
techStack: ["React", "TypeScript"],
liveDemoUrl: "https://example.com",
githubUrl: "https://github.com/example/project",
featured: true,
};

describe("ProjectCard", () => {
it("cycles through images and wraps in both directions", async () => {
const user = userEvent.setup();

render(
<ProjectCard project={project} isExpanded={false} onToggle={vi.fn()} />,
);

expect(
screen.getByRole("img", {
name: /test project preview 1 of 3/i,
}),
).toBeInTheDocument();

await user.click(
screen.getByRole("button", {
name: /show previous test project image/i,
}),
);

expect(
screen.getByRole("img", {
name: /test project preview 3 of 3/i,
}),
).toBeInTheDocument();

await user.click(
screen.getByRole("button", {
name: /show next test project image/i,
}),
);

expect(
screen.getByRole("img", {
name: /test project preview 1 of 3/i,
}),
).toBeInTheDocument();
});

it("renders only the available project links", () => {
const { rerender } = render(
<ProjectCard project={project} isExpanded={false} onToggle={vi.fn()} />,
);

expect(screen.getByRole("link", { name: /live demo/i })).toHaveAttribute(
"href",
project.liveDemoUrl,
);

expect(
screen.getByRole("link", {
name: /test project on github/i,
}),
).toHaveAttribute("href", project.githubUrl);

rerender(
<ProjectCard
project={{ ...project, liveDemoUrl: "", githubUrl: "" }}
isExpanded={false}
onToggle={vi.fn()}
/>,
);

expect(
screen.queryByRole("link", { name: /live demo/i }),
).not.toBeInTheDocument();

expect(screen.getByText(/live demo/i)).toHaveAttribute(
"aria-disabled",
"true",
);

expect(
screen.queryByRole("link", { name: /github/i }),
).not.toBeInTheDocument();
});

it("calls onToggle when expanding and collapsing", async () => {
const user = userEvent.setup();
const onToggle = vi.fn();

const { rerender } = render(
<ProjectCard project={project} isExpanded={false} onToggle={onToggle} />,
);

const summaryButton = screen.getByRole("button", {
name: project.title,
});

expect(summaryButton).toHaveAttribute("aria-expanded", "false");

await user.click(summaryButton);

expect(onToggle).toHaveBeenCalledTimes(1);

rerender(<ProjectCard project={project} isExpanded onToggle={onToggle} />);

expect(screen.getByRole("button", { name: project.title })).toHaveAttribute(
"aria-expanded",
"true",
);

await user.click(
screen.getByRole("button", {
name: /collapse test project/i,
}),
);

expect(onToggle).toHaveBeenCalledTimes(2);
});
});
Loading
Loading