Skip to content
Draft
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
2 changes: 1 addition & 1 deletion __tests__/ActivitiesContainer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ describe("ActivitiesContainer Search Functionality", () => {

// Should show loading indicator
await waitFor(() => {
expect(screen.getByTestId("loader")).toBeInTheDocument();
expect(screen.getByRole("region", { name: "Activity records" })).toHaveAttribute("aria-busy", "true");
});

// Resolve the search
Expand Down
48 changes: 48 additions & 0 deletions __tests__/AuthPresentation.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import { authenticate, signup } from "@/actions/auth.actions";
import SigninForm from "@/components/auth/SigninForm";
import SignupForm from "@/components/auth/SignupForm";

vi.mock("@/actions/auth.actions", () => ({
authenticate: vi.fn(),
signup: vi.fn(),
}));
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));

it.each(["signin", "signup"])(
"keeps the %s submit label while pending and announces errors",
async (mode) => {
let finish!: (value: "Invalid credentials.") => void;
vi.mocked(authenticate).mockImplementationOnce(
() =>
new Promise<"Invalid credentials.">((resolve) => {
finish = resolve;
}),
);
vi.mocked(signup).mockResolvedValueOnce({ success: true });
render(mode === "signin" ? <SigninForm /> : <SignupForm />);
if (mode === "signup")
fireEvent.change(screen.getByLabelText("Full Name"), {
target: { value: "Local Tester" },
});
fireEvent.change(screen.getByLabelText("Email"), {
target: { value: "test@example.com" },
});
fireEvent.change(screen.getByLabelText("Password"), {
target: { value: "password123" },
});
const button = screen.getByRole("button", {
name: mode === "signin" ? "Login" : "Create an account",
});
await act(async () => {
fireEvent.click(button);
});
expect(button).toBeDisabled();
expect(button).toHaveAttribute("aria-busy", "true");
await act(async () => {
finish("Invalid credentials.");
});
expect(button).toBeEnabled();
expect(screen.getByRole("alert")).toHaveTextContent("Invalid credentials.");
},
);
4 changes: 2 additions & 2 deletions __tests__/JobsActivityCard.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe("JobsActivityCard", () => {
render(<JobsActivityCard data={data} />);

const toggle = screen.getByTestId("jobs-activity-toggle-group");
await user.click(within(toggle).getByRole("button", { name: "30d" }));
await user.click(within(toggle).getByRole("tab", { name: "30d" }));

const total = screen.getByTestId("jobs-activity-total");

Expand All @@ -129,7 +129,7 @@ describe("JobsActivityCard", () => {
render(<JobsActivityCard data={data} />);

const toggle = screen.getByTestId("jobs-activity-toggle-group");
await user.click(within(toggle).getByRole("button", { name: "30d" }));
await user.click(within(toggle).getByRole("tab", { name: "30d" }));

const total = screen.getByTestId("jobs-activity-total");

Expand Down
2 changes: 1 addition & 1 deletion __tests__/JobsContainer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ describe("JobsContainer Search Functionality", () => {

// Should show loading indicator
await waitFor(() => {
expect(screen.getByTestId("loader")).toBeInTheDocument();
expect(screen.getByRole("region", { name: "Job records" })).toHaveAttribute("aria-busy", "true");
});

// Resolve the search
Expand Down
2 changes: 1 addition & 1 deletion __tests__/RecentCardToggle.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("RecentCardToggle - Start Activity", () => {
};

const switchToActivitiesTab = async () => {
await user.click(screen.getByRole("button", { name: "Activities" }));
await user.click(screen.getByRole("tab", { name: "Activities" }));
};

beforeEach(() => {
Expand Down
51 changes: 51 additions & 0 deletions __tests__/RootPage.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { render, screen } from "@testing-library/react";
import db from "@/lib/db";
import { redirect } from "next/navigation";
import RootPage from "@/app/page";

const { mockAuth } = vi.hoisted(() => ({ mockAuth: vi.fn() }));
vi.mock("@/auth", () => ({ auth: mockAuth }));
vi.mock("@/lib/db", () => ({ default: { user: { count: vi.fn() } } }));
vi.mock("next/navigation", () => ({
redirect: vi.fn(() => {
throw new Error("redirect");
}),
}));

describe("public root", () => {
it("redirects authenticated visitors without querying public account state", async () => {
mockAuth.mockResolvedValue({ user: { id: "user-1" } });
await expect(RootPage()).rejects.toThrow("redirect");
expect(redirect).toHaveBeenCalledWith("/dashboard");
expect(db.user.count).not.toHaveBeenCalled();
});

it.each([0, 1])(
"renders the landing page with %i existing accounts",
async (count) => {
mockAuth.mockResolvedValue(null);
vi.mocked(db.user.count).mockResolvedValue(count);
render(await RootPage());
expect(
screen.getByRole("heading", {
level: 1,
name: "Your self-hosted workspace for the job search.",
}),
).toBeInTheDocument();
for (const link of screen.getAllByRole("link", { name: "Get started" })) {
expect(link).toHaveAttribute(
"href",
count === 0 ? "/signup" : "/signin",
);
}
expect(screen.getByRole("link", { name: "Sign in" })).toHaveAttribute(
"href",
"/signin",
);
expect(
screen.getByRole("link", { name: "View source on GitHub" }),
).toHaveAttribute("href", "https://github.com/Gsync/jobsync");
expect(redirect).not.toHaveBeenCalled();
},
);
});
14 changes: 7 additions & 7 deletions __tests__/TasksContainer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ describe("TasksContainer Component", () => {

render(<TasksContainer activityTypes={mockActivityTypes} />);

expect(screen.getByTestId("loader")).toBeInTheDocument();
expect(screen.getByRole("status", { name: "Loading records" })).toBeInTheDocument();
});

it("should show empty state when no tasks are found", async () => {
Expand Down Expand Up @@ -248,7 +248,7 @@ describe("TasksContainer Component", () => {
render(<TasksContainer activityTypes={mockActivityTypes} />);

await waitFor(() => {
expect(screen.queryByTestId("loader")).not.toBeInTheDocument();
expect(screen.queryByRole("status", { name: "Loading records" })).not.toBeInTheDocument();
});
});

Expand Down Expand Up @@ -283,7 +283,7 @@ describe("TasksContainer Component", () => {
});

await waitFor(() => {
expect(screen.queryByTestId("loader")).not.toBeInTheDocument();
expect(screen.queryByRole("status", { name: "Loading records" })).not.toBeInTheDocument();
});

// Find and click the actions menu button for the first task
Expand Down Expand Up @@ -314,7 +314,7 @@ describe("TasksContainer Component", () => {
});

await waitFor(() => {
expect(screen.queryByTestId("loader")).not.toBeInTheDocument();
expect(screen.queryByRole("status", { name: "Loading records" })).not.toBeInTheDocument();
});

// Find and click the actions menu button for the first task
Expand Down Expand Up @@ -369,7 +369,7 @@ describe("TasksContainer Component", () => {
});

await waitFor(() => {
expect(screen.queryByTestId("loader")).not.toBeInTheDocument();
expect(screen.queryByRole("status", { name: "Loading records" })).not.toBeInTheDocument();
});

// Find and click the actions menu button for the first task
Expand All @@ -396,7 +396,7 @@ describe("TasksContainer Component", () => {
});

await waitFor(() => {
expect(screen.queryByTestId("loader")).not.toBeInTheDocument();
expect(screen.queryByRole("status", { name: "Loading records" })).not.toBeInTheDocument();
});

// Find and click the actions menu button for the first task
Expand Down Expand Up @@ -427,7 +427,7 @@ describe("TasksContainer Component", () => {
});

await waitFor(() => {
expect(screen.queryByTestId("loader")).not.toBeInTheDocument();
expect(screen.queryByRole("status", { name: "Loading records" })).not.toBeInTheDocument();
});

// Find and click the actions menu button for the first task
Expand Down
14 changes: 7 additions & 7 deletions e2e/dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,19 +214,19 @@ test.describe("Dashboard page", () => {
page,
}) => {
const jobsActivityToggle = page.getByTestId("jobs-activity-toggle-group");
await jobsActivityToggle.getByRole("button", { name: "30d" }).click();
await jobsActivityToggle.getByRole("tab", { name: "30d" }).click();
await expect(
jobsActivityToggle.getByRole("button", { name: "30d" }),
).toHaveClass(/bg-primary/);
jobsActivityToggle.getByRole("tab", { name: "30d" }),
).toHaveAttribute("aria-selected", "true");

const recentToggle = page.getByTestId("recent-card-toggle-group");
await recentToggle.getByRole("button", { name: "Activities" }).click();
await recentToggle.getByRole("tab", { name: "Activities" }).click();
await expect(
page.getByRole("heading", { name: "Recent Activities", exact: true }),
).toBeVisible();

const weeklyToggle = page.getByTestId("weekly-chart-toggle-group");
await weeklyToggle.getByRole("button", { name: "Activities" }).click();
await weeklyToggle.getByRole("tab", { name: "Activities" }).click();
await expect(
page.getByRole("heading", { name: "Weekly Activities", exact: true }),
).toBeVisible();
Expand Down Expand Up @@ -268,7 +268,7 @@ test.describe("Dashboard page", () => {
await navigateToDashboard(page);
await page
.getByTestId("recent-card-toggle-group")
.getByRole("button", { name: "Activities" })
.getByRole("tab", { name: "Activities" })
.click();
await expect(page.getByText(taskTitle, { exact: true })).not.toBeVisible();
});
Expand All @@ -284,7 +284,7 @@ test.describe("Dashboard page", () => {
await navigateToDashboard(page);
await page
.getByTestId("recent-card-toggle-group")
.getByRole("button", { name: "Activities" })
.getByRole("tab", { name: "Activities" })
.click();

const activityRow = page
Expand Down
2 changes: 1 addition & 1 deletion e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ type Fixtures = {
export const test = base.extend<Fixtures>({
// Auto-login every test and land on the dashboard.
page: async ({ page, baseURL }, use) => {
await page.goto("/");
await page.goto("/signin");
await login(page);
await expect(page).toHaveURL(baseURL + "/dashboard");
await use(page);
Expand Down
32 changes: 32 additions & 0 deletions e2e/landing.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { test, expect } from "@playwright/test";

for (const width of [390, 1280]) {
for (const colorScheme of ["light", "dark"] as const) {
test(`public entry at ${width}px in ${colorScheme} mode`, async ({
page,
}) => {
await page.setViewportSize({ width, height: 900 });
await page.emulateMedia({ colorScheme });
await page.goto("/");
await expect(page.getByRole("heading", { level: 1 })).toHaveText(
"Your self-hosted workspace for the job search.",
);
await expect(page.getByText("Example workspace")).toBeVisible();
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth,
),
).toBe(true);
await page.getByRole("link", { name: "Sign in", exact: true }).click();
await expect(page).toHaveURL(/\/signin$/);
await expect(
page.getByRole("heading", { name: "Welcome back" }),
).toBeVisible();
await page
.getByRole("link", { name: "Create Account", exact: true })
.click();
await expect(page).toHaveURL(/\/signup$/);
await expect(page.getByLabel("Full Name")).toBeVisible();
});
}
}
4 changes: 2 additions & 2 deletions e2e/signin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { test, expect } from "@playwright/test";
import { login } from "./fixtures";

test("Signin page has title", async ({ page }) => {
await page.goto("/");
await page.goto("/signin");

await expect(page).toHaveTitle("Sign In | JobSync");

Expand All @@ -17,7 +17,7 @@ test("Signin page has title", async ({ page }) => {
});

test("Signin and out from app", async ({ page, baseURL }) => {
await page.goto("/");
await page.goto("/signin");
await login(page);

await expect(page).toHaveURL(baseURL + "/dashboard");
Expand Down
2 changes: 1 addition & 1 deletion src/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function AuthLayout({
children: React.ReactNode;
}>) {
return (
<main className="flex min-h-screen items-center justify-center bg-muted/40">
<main className="flex min-h-screen items-center justify-center bg-muted/40 px-4 py-10">
{children}
</main>
);
Expand Down
4 changes: 2 additions & 2 deletions src/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export default async function RootLayout({
<GlobalActivityBanner />
{/* Container, not viewport, queries: the docked chat panel
shrinks this box without changing the viewport width. */}
<main className="@container/main flex-1 p-4 sm:px-4 sm:py-0">
<div className="items-start gap-4 md:gap-4 @3xl/main:grid @3xl/main:grid-cols-3">
<main className="@container/main flex-1 px-4 pb-4">
<div className="flex flex-col gap-4 @3xl/main:grid @3xl/main:items-start @3xl/main:grid-cols-3">
{children}
</div>
</main>
Expand Down
4 changes: 2 additions & 2 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export default async function Dashboard() {
const activityCalendarDataKeys = Object.keys(activityCalendarData);
return (
<>
<div className="@container grid grid-cols-1 auto-rows-max items-start gap-2 md:gap-2 @3xl/main:col-span-2">
<div className="grid gap-2 @lg:grid-cols-4">
<div className="@container grid grid-cols-1 auto-rows-max items-start gap-4 @3xl/main:col-span-2">
<div className="grid gap-4 @lg:grid-cols-4">
<JobsApplied />
<JobsActivityCard
data={[
Expand Down
21 changes: 2 additions & 19 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
/* v4 Preflight renders placeholders at 50% currentColor; v3 used gray-400 */
input::placeholder,
textarea::placeholder {
color: var(--color-gray-400);
color: hsl(var(--muted-foreground));
}
}

Expand Down Expand Up @@ -192,26 +192,9 @@
@apply text-14 lg:text-16 font-normal text-muted-foreground;
}

/* Sidebar */
@utility sidebar {
@apply sticky left-0 top-0 flex h-screen w-fit flex-col justify-between border-r border-border bg-card pt-8 text-foreground max-md:hidden sm:p-4 xl:p-6 2xl:w-[355px];
}

@utility sidebar-logo {
@apply 2xl:text-26 text-[26px] text-blue-500 font-bold max-xl:hidden;
}

@utility sidebar-link {
@apply flex gap-3 items-center text-blue-500 py-1 md:p-3 2xl:p-4 rounded-lg justify-center xl:justify-start;
}

@utility sidebar-label {
@apply text-16 font-semibold text-foreground max-xl:hidden;
}

/* Navlink */
@utility navlink {
@apply relative flex items-center overflow-hidden transition-all duration-200 ease-in-out;
@apply relative flex items-center overflow-hidden transition-colors duration-200 ease-in-out;
}

/* Override: For Calender component to hide month, year labels */
Expand Down
7 changes: 2 additions & 5 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { redirect } from "next/navigation";
import { auth } from "@/auth";
import db from "@/lib/db";
import LandingPage from "@/components/landing/LandingPage";

export default async function RootPage() {
const session = await auth();
Expand All @@ -11,9 +12,5 @@ export default async function RootPage() {

const userCount = await db.user.count();

if (userCount === 0) {
redirect("/signup");
} else {
redirect("/signin");
}
return <LandingPage hasUsers={userCount > 0} />;
}
Loading