Setup tests and absolute paths
This commit is contained in:
parent
9c9a18516d
commit
d5e39c03f0
32 changed files with 3356 additions and 106 deletions
|
|
@ -8,7 +8,10 @@
|
|||
"typecheck": "tsc -b",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:watch": "vitest watch",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.22.0",
|
||||
|
|
@ -18,6 +21,10 @@
|
|||
"@react-aria/toast": "^3.0.0",
|
||||
"@react-stately/toast": "^3.0.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
|
|
@ -28,6 +35,8 @@
|
|||
"eslint": "^9.22.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"lucide-react": "^0.460.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"react": "19.0.0",
|
||||
|
|
@ -40,7 +49,9 @@
|
|||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.7.0",
|
||||
"typescript-eslint": "^8.26.1",
|
||||
"vite": "^6.2.2"
|
||||
"vite": "^6.2.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-stately/calendar": "^3.7.1",
|
||||
|
|
|
|||
2588
ui/pnpm-lock.yaml
2588
ui/pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
2
ui/src/components/BrandButtons/LoginWithGoogle.d.ts
vendored
Normal file
2
ui/src/components/BrandButtons/LoginWithGoogle.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import "./login-with-google.css";
|
||||
export declare function LoginWithGoogle(): import("react/jsx-runtime").JSX.Element;
|
||||
1
ui/src/components/BrandButtons/LoginWithGoogle.test.d.ts
vendored
Normal file
1
ui/src/components/BrandButtons/LoginWithGoogle.test.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
export {};
|
||||
41
ui/src/components/BrandButtons/LoginWithGoogle.test.tsx
Normal file
41
ui/src/components/BrandButtons/LoginWithGoogle.test.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { LoginWithGoogle } from "./LoginWithGoogle";
|
||||
import { useLoginGoogle } from "../../hooks/auth";
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.mock("../../hooks/auth", () => ({
|
||||
useLoginGoogle: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("LoginWithGoogle", () => {
|
||||
it("renders the Google login button", () => {
|
||||
const mockLoginWithGoogle = vi.fn();
|
||||
(useLoginGoogle as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
loginWithGoogle: mockLoginWithGoogle,
|
||||
});
|
||||
|
||||
render(<LoginWithGoogle />);
|
||||
|
||||
const button = screen.getByRole("button", {
|
||||
name: /continue with google/i,
|
||||
});
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveClass("login-with-google");
|
||||
});
|
||||
|
||||
it("calls loginWithGoogle when clicked", () => {
|
||||
const mockLoginWithGoogle = vi.fn();
|
||||
(useLoginGoogle as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
loginWithGoogle: mockLoginWithGoogle,
|
||||
});
|
||||
|
||||
render(<LoginWithGoogle />);
|
||||
|
||||
const button = screen.getByRole("button", {
|
||||
name: /continue with google/i,
|
||||
});
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(mockLoginWithGoogle).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
92
ui/src/components/Layout.test.tsx
Normal file
92
ui/src/components/Layout.test.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { Layout } from "@ui/components/Layout";
|
||||
import { SessionProvider } from "@ui/contexts/SessionContext";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Mock the SessionContext
|
||||
vi.mock("../../contexts/SessionContext", () => ({
|
||||
...vi.importActual("../../contexts/SessionContext"),
|
||||
SessionProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||
useSession: () => ({
|
||||
session: {
|
||||
user: {
|
||||
user_metadata: {
|
||||
full_name: "John Doe",
|
||||
avatar_url: "https://example.com/avatar.jpg",
|
||||
first_name: "John",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../ThemeSwitcher", () => ({
|
||||
ThemeSwitcher: () => <div>Theme Switcher</div>,
|
||||
}));
|
||||
|
||||
describe("Layout", () => {
|
||||
it("renders the layout with children", () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
{/* Mock SessionProvider just passes children through */}
|
||||
<Layout>
|
||||
<div>Test Content</div>
|
||||
</Layout>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
// Check if the content is rendered
|
||||
expect(screen.getByText("Test Content")).toBeInTheDocument();
|
||||
|
||||
// Check if the mobile menu button is present
|
||||
expect(screen.getByRole("button", { name: /menu/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.skip("toggles mobile menu when menu button is clicked", () => {
|
||||
// Mock viewport width to mobile size
|
||||
global.innerWidth = 500; // Mobile width
|
||||
global.dispatchEvent(new Event("resize"));
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<SessionProvider>
|
||||
<Layout>
|
||||
<div>Test Content</div>
|
||||
</Layout>
|
||||
</SessionProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
// Get the menu button
|
||||
const menuButton = screen.getByRole("button", { name: /menu/i });
|
||||
|
||||
// Verify initial mobile state
|
||||
const navigation = screen.getByLabelText("Side Navigation");
|
||||
expect(navigation).toHaveClass("-translate-x-full");
|
||||
expect(navigation).not.toHaveClass("translate-x-0");
|
||||
|
||||
// Click the menu button to show
|
||||
fireEvent.click(menuButton);
|
||||
expect(navigation).toHaveClass("translate-x-0");
|
||||
|
||||
// Click again to hide
|
||||
fireEvent.click(menuButton);
|
||||
expect(navigation).toHaveClass("-translate-x-full");
|
||||
});
|
||||
|
||||
it("renders the side navigation", () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<SessionProvider>
|
||||
<Layout>
|
||||
<div>Test Content</div>
|
||||
</Layout>
|
||||
</SessionProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
// Check if the side navigation is present
|
||||
expect(screen.getByRole("navigation")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
76
ui/src/components/NavigationBar.test.tsx
Normal file
76
ui/src/components/NavigationBar.test.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { screen, fireEvent } from "@testing-library/react";
|
||||
import {
|
||||
SideNavigation,
|
||||
MainNavigation,
|
||||
UserMenuPopover,
|
||||
} from "@ui/components/NavigationBar";
|
||||
import { renderWithProviders } from "@ui/utils/testHelpers";
|
||||
describe("NavigationBar", () => {
|
||||
describe("SideNavigation", () => {
|
||||
it("renders the side navigation with correct initial state", () => {
|
||||
renderWithProviders(<SideNavigation isMobileMenuOpen={false} />);
|
||||
|
||||
// Check if the logo is present
|
||||
expect(screen.getByAltText("Logo XTablo")).toBeInTheDocument();
|
||||
|
||||
// Check if the title is present
|
||||
expect(screen.getByText("XTablo")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("collapses and expands when the collapse button is clicked", () => {
|
||||
renderWithProviders(<SideNavigation isMobileMenuOpen={false} />);
|
||||
|
||||
// Find and click the collapse button
|
||||
const collapseButton = screen.getByRole("button", { name: /collapse/i });
|
||||
fireEvent.click(collapseButton);
|
||||
|
||||
// Check if the navigation is collapsed
|
||||
const navigation = screen.getByRole("navigation", {
|
||||
name: "Main navigation",
|
||||
});
|
||||
expect(navigation).toHaveClass("w-16");
|
||||
|
||||
// Click again to expand
|
||||
fireEvent.click(collapseButton);
|
||||
expect(navigation).toHaveClass("w-48");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MainNavigation", () => {
|
||||
it("renders all navigation items", () => {
|
||||
renderWithProviders(<MainNavigation isCollapsed={false} />);
|
||||
|
||||
// Check if all navigation items are present
|
||||
expect(screen.getByText("Tableau de Bord")).toBeInTheDocument();
|
||||
expect(screen.getByText("Devis")).toBeInTheDocument();
|
||||
expect(screen.getByText("Factures")).toBeInTheDocument();
|
||||
expect(screen.getByText("Planning")).toBeInTheDocument();
|
||||
expect(screen.getByText("Chantiers")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("UserMenuPopover", () => {
|
||||
it("renders the user menu with correct user information", () => {
|
||||
renderWithProviders(<UserMenuPopover isCollapsed={false} />);
|
||||
|
||||
// Check if user information is displayed
|
||||
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
||||
expect(screen.getByAltText("Avatar")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens and closes the popover when clicked", () => {
|
||||
renderWithProviders(<UserMenuPopover isCollapsed={false} />);
|
||||
|
||||
// Click the user menu button
|
||||
const userMenuButton = screen.getByRole("button", { name: /user menu/i });
|
||||
fireEvent.click(userMenuButton);
|
||||
|
||||
// Check if the popover is open
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
|
||||
// Click again to close
|
||||
fireEvent.click(userMenuButton);
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -13,24 +13,24 @@ import {
|
|||
} from "lucide-react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { Separator } from "react-aria-components";
|
||||
import { Link } from "../ui-library/link";
|
||||
import { Icon } from "../ui-library/icon";
|
||||
import { Avatar, AvatarBadge } from "../ui-library/avatar";
|
||||
import { Dialog } from "../ui-library/dialog";
|
||||
import { Button } from "../ui-library/button";
|
||||
import { Link } from "@ui/ui-library/link";
|
||||
import { Icon } from "@ui/ui-library/icon";
|
||||
import { Avatar, AvatarBadge } from "@ui/ui-library/avatar";
|
||||
import { Dialog } from "@ui/ui-library/dialog";
|
||||
import { Button } from "@ui/ui-library/button";
|
||||
import {
|
||||
DisclosurePanel,
|
||||
DisclosureControl,
|
||||
Disclosure,
|
||||
} from "../ui-library/disclosure";
|
||||
} from "@ui/ui-library/disclosure";
|
||||
import { LinkProps } from "react-aria-components";
|
||||
import { Popover } from "../ui-library/popover";
|
||||
import { AvailableIcon } from "../ui-library/icons";
|
||||
import { Popover } from "@ui/ui-library/popover";
|
||||
import { AvailableIcon } from "@ui/ui-library/icons";
|
||||
import { useState, useRef } from "react";
|
||||
import logo from "../assets/icon.jpg";
|
||||
import { ThemeSwitcher } from "./ThemeSwitcher";
|
||||
import { useSession } from "../contexts/SessionContext";
|
||||
import { Text } from "../ui-library/text";
|
||||
import { Text } from "@ui/ui-library/text";
|
||||
type NavLinkItem = {
|
||||
isActive?: boolean;
|
||||
} & LinkProps;
|
||||
|
|
@ -156,11 +156,11 @@ export const SideNavigation = ({
|
|||
isMobileMenuOpen: boolean;
|
||||
}) => {
|
||||
const isCollapsable = !isMobileMenuOpen;
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(isCollapsable ? false : true);
|
||||
|
||||
return (
|
||||
<div
|
||||
<nav
|
||||
aria-label="Main navigation"
|
||||
className={twMerge(
|
||||
"group isolate flex flex-col overflow-y-auto overflow-x-hidden bg-navbar-background transition-all duration-300",
|
||||
"fixed md:relative h-[calc(100vh-2rem)] md:h-screen z-50",
|
||||
|
|
@ -179,6 +179,7 @@ export const SideNavigation = ({
|
|||
"flex flex-col items-center gap-2 w-full",
|
||||
isCollapsed ? "justify-center" : ""
|
||||
)}
|
||||
aria-label="Home"
|
||||
>
|
||||
<img
|
||||
src={logo}
|
||||
|
|
@ -202,6 +203,10 @@ export const SideNavigation = ({
|
|||
variant="plain"
|
||||
isIconOnly
|
||||
onPress={() => setIsCollapsed(!isCollapsed)}
|
||||
aria-label={
|
||||
isCollapsed ? "Expand navigation" : "Collapse navigation"
|
||||
}
|
||||
aria-expanded={!isCollapsed}
|
||||
className={twMerge(
|
||||
isCollapsed ? "relative" : "absolute top-2 right-2",
|
||||
"size-5 p-1",
|
||||
|
|
@ -213,7 +218,9 @@ export const SideNavigation = ({
|
|||
"hover:scale-110"
|
||||
)}
|
||||
>
|
||||
<Icon>{isCollapsed ? <PlusIcon /> : <MinusIcon />}</Icon>
|
||||
<Icon aria-hidden="true">
|
||||
{isCollapsed ? <PlusIcon /> : <MinusIcon />}
|
||||
</Icon>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -226,7 +233,7 @@ export const SideNavigation = ({
|
|||
>
|
||||
<UserMenuPopover isCollapsed={isCollapsed} />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -258,10 +265,10 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
|
|||
icon: <ConstructionIcon className="w-5 h-5" />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<nav className="flex flex-1 flex-col" aria-label="Primary navigation">
|
||||
<ul
|
||||
role="list"
|
||||
className={twMerge(
|
||||
"grid gap-y-1 py-3",
|
||||
isCollapsed ? "pl-2.5 pr-3" : ""
|
||||
|
|
@ -270,14 +277,18 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
|
|||
{navItems.map(({ path, label, icon }) => (
|
||||
<li key={label}>
|
||||
<NavLink>
|
||||
<RouterLink to={path} className="w-full">
|
||||
<RouterLink
|
||||
to={path}
|
||||
className="w-full"
|
||||
aria-label={isCollapsed ? label : undefined}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center gap-x-2",
|
||||
isCollapsed ? "" : "pl-2"
|
||||
)}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
<Icon aria-hidden="true">{icon}</Icon>
|
||||
<span
|
||||
className={twMerge(
|
||||
"text-sm transition-all duration-300",
|
||||
|
|
@ -293,6 +304,7 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
|
|||
))}
|
||||
</ul>
|
||||
<ul
|
||||
role="list"
|
||||
className={twMerge(
|
||||
"mt-auto grid gap-y-1 py-1",
|
||||
isCollapsed ? "pl-2.5 pr-3" : ""
|
||||
|
|
@ -303,9 +315,12 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
|
|||
<RouterLink
|
||||
to="/"
|
||||
className={twMerge("w-full", isCollapsed ? "" : "pl-2")}
|
||||
aria-label={isCollapsed ? "Support" : undefined}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<HelpCircleIcon className="w-5 h-5" />
|
||||
<Icon aria-hidden="true">
|
||||
<HelpCircleIcon className="w-5 h-5" />
|
||||
</Icon>
|
||||
<span
|
||||
className={twMerge(
|
||||
"text-sm transition-all duration-300",
|
||||
|
|
@ -323,9 +338,12 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
|
|||
<RouterLink
|
||||
to="/"
|
||||
className={twMerge("w-full", isCollapsed ? "" : "pl-2")}
|
||||
aria-label={isCollapsed ? "Feedback" : undefined}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<SendIcon className="w-5 h-5" />
|
||||
<Icon aria-hidden="true">
|
||||
<SendIcon className="w-5 h-5" />
|
||||
</Icon>
|
||||
<span
|
||||
className={twMerge(
|
||||
"text-sm transition-all duration-300",
|
||||
|
|
|
|||
145
ui/src/components/ProtectedRoute.test.tsx
Normal file
145
ui/src/components/ProtectedRoute.test.tsx
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { ProtectedRoute } from "@ui/components/ProtectedRoute";
|
||||
import { SessionProvider } from "@ui/contexts/SessionContext";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import * as SessionContext from "@ui/contexts/SessionContext";
|
||||
import { Session } from "@supabase/supabase-js";
|
||||
import { vi } from "vitest";
|
||||
|
||||
describe("ProtectedRoute", () => {
|
||||
beforeEach(() => {
|
||||
// Clear localStorage before each test
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("shows loading state initially", () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<SessionProvider>
|
||||
<Routes>
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path="/" element={<div>Protected Content</div>} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</SessionProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
// Check if loading spinner is present
|
||||
expect(screen.getByRole("status")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("redirects to login when user is not authenticated", async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<SessionProvider>
|
||||
<Routes>
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path="/" element={<div>Protected Content</div>} />
|
||||
</Route>
|
||||
<Route path="/login" element={<div>Login Page</div>} />
|
||||
</Routes>
|
||||
</SessionProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
// Wait for the loading state to finish
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Login Page")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("redirects to landing page for first-time users", async () => {
|
||||
// Set up first-time user scenario
|
||||
localStorage.removeItem("xtablo-has-seen-landing-page");
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<SessionProvider>
|
||||
<Routes>
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path="/" element={<div>Protected Content</div>} />
|
||||
</Route>
|
||||
<Route path="/landing" element={<div>Landing Page</div>} />
|
||||
</Routes>
|
||||
</SessionProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
// Wait for the loading state to finish
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Landing Page")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders protected content when user is authenticated", async () => {
|
||||
// Mock authenticated session
|
||||
const mockSession: Session = {
|
||||
access_token: "test-token",
|
||||
refresh_token: "test-refresh-token",
|
||||
expires_in: 3600,
|
||||
token_type: "bearer",
|
||||
user: {
|
||||
id: "123",
|
||||
app_metadata: {},
|
||||
user_metadata: {
|
||||
full_name: "Test User",
|
||||
email: "test@example.com",
|
||||
email_verified: true,
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
business_name: "Test Business",
|
||||
},
|
||||
aud: "authenticated",
|
||||
created_at: new Date().toISOString(),
|
||||
email: "test@example.com",
|
||||
role: "authenticated",
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
vi.spyOn(SessionContext, "useSession").mockImplementation(() => ({
|
||||
session: mockSession,
|
||||
}));
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<SessionProvider>
|
||||
<Routes>
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path="/" element={<div>Protected Content</div>} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</SessionProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
// Wait for the loading state to finish
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Protected Content")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("uses custom fallback route when provided", async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<SessionProvider>
|
||||
<Routes>
|
||||
<Route element={<ProtectedRoute fallback="/custom-login" />}>
|
||||
<Route path="/" element={<div>Protected Content</div>} />
|
||||
</Route>
|
||||
<Route
|
||||
path="/custom-login"
|
||||
element={<div>Custom Login Page</div>}
|
||||
/>
|
||||
</Routes>
|
||||
</SessionProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
// Wait for the loading state to finish
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Custom Login Page")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
87
ui/src/components/PublicRoute.test.tsx
Normal file
87
ui/src/components/PublicRoute.test.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { PublicRoute } from "@ui/components/PublicRoute";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import * as SessionContext from "@ui/contexts/SessionContext";
|
||||
import { Session } from "@supabase/supabase-js";
|
||||
import { vi } from "vitest";
|
||||
|
||||
describe("PublicRoute", () => {
|
||||
it("shows loading state initially", () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<PublicRoute />}>
|
||||
<Route path="/login" element={<div>Login Page</div>} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
// Check if loading spinner is present
|
||||
expect(screen.getByRole("status")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("redirects to home when user is authenticated", async () => {
|
||||
// Mock authenticated session
|
||||
const mockSession: Session = {
|
||||
access_token: "test-token",
|
||||
refresh_token: "test-refresh-token",
|
||||
expires_in: 3600,
|
||||
token_type: "bearer",
|
||||
user: {
|
||||
id: "123",
|
||||
app_metadata: {},
|
||||
user_metadata: {
|
||||
full_name: "Test User",
|
||||
email: "test@example.com",
|
||||
email_verified: true,
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
business_name: "Test Business",
|
||||
},
|
||||
aud: "authenticated",
|
||||
created_at: new Date().toISOString(),
|
||||
email: "test@example.com",
|
||||
role: "authenticated",
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
vi.spyOn(SessionContext, "useSession").mockImplementation(() => ({
|
||||
session: mockSession,
|
||||
}));
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<PublicRoute />}>
|
||||
<Route path="/login" element={<div>Login Page</div>} />
|
||||
</Route>
|
||||
<Route path="/" element={<div>Home Page</div>} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
// Wait for the loading state to finish
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Home Page")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders public content when user is not authenticated", async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<PublicRoute />}>
|
||||
<Route path="/login" element={<div>Login Page</div>} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
// Wait for the loading state to finish
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Login Page")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
61
ui/src/components/SignOutButton.test.tsx
Normal file
61
ui/src/components/SignOutButton.test.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { SignOutButton } from "@ui/components/SignOutButton";
|
||||
import * as AuthHooks from "@ui/hooks/auth";
|
||||
import { UseMutationResult } from "@tanstack/react-query";
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Create a mock mutation result
|
||||
const createMockMutationResult = (
|
||||
mutate: ReturnType<typeof vi.fn>
|
||||
): UseMutationResult<void, Error, void, unknown> => {
|
||||
return {
|
||||
mutate,
|
||||
data: undefined,
|
||||
error: null,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
variables: undefined,
|
||||
reset: vi.fn(),
|
||||
status: "idle",
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
isPaused: false,
|
||||
isPlaceholderData: false,
|
||||
fetchStatus: "idle",
|
||||
isIdle: true,
|
||||
context: undefined,
|
||||
submittedAt: 0,
|
||||
} as unknown as UseMutationResult<void, Error, void, unknown>;
|
||||
};
|
||||
|
||||
// Mock the useLogout hook
|
||||
vi.mock("../../hooks/auth", () => ({
|
||||
useLogout: () => createMockMutationResult(vi.fn()),
|
||||
}));
|
||||
|
||||
describe("SignOutButton", () => {
|
||||
it("renders the sign out button", () => {
|
||||
render(<SignOutButton />);
|
||||
|
||||
// Check if the button is rendered with correct text
|
||||
expect(
|
||||
screen.getByRole("button", { name: /se déconnecter/i })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls logout function when clicked", () => {
|
||||
const mockLogout = vi.fn();
|
||||
vi.spyOn(AuthHooks, "useLogout").mockImplementation(() =>
|
||||
createMockMutationResult(mockLogout)
|
||||
);
|
||||
|
||||
render(<SignOutButton />);
|
||||
|
||||
// Click the button
|
||||
fireEvent.click(screen.getByRole("button", { name: /se déconnecter/i }));
|
||||
|
||||
// Check if logout was called
|
||||
expect(mockLogout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
44
ui/src/components/ThemeSwitcher.test.tsx
Normal file
44
ui/src/components/ThemeSwitcher.test.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { ThemeSwitcher } from "@ui/components/ThemeSwitcher";
|
||||
import * as ThemeContext from "@ui/contexts/ThemeContext";
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Mock the ThemeProvider
|
||||
vi.mock("../../contexts/ThemeContext", () => ({
|
||||
...vi.importActual("../../contexts/ThemeContext"),
|
||||
ThemeProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||
useTheme: () => ({
|
||||
theme: "light",
|
||||
setTheme: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("ThemeSwitcher", () => {
|
||||
it("renders the theme switcher with correct initial theme", () => {
|
||||
render(<ThemeSwitcher />);
|
||||
|
||||
// Check if the current theme text is displayed
|
||||
expect(screen.getByText("Thème: Clair")).toBeInTheDocument();
|
||||
|
||||
// Check if all theme buttons are present
|
||||
expect(screen.getByRole("button", { name: /light/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /system/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /dark/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("changes theme when a different theme button is clicked", () => {
|
||||
const setTheme = vi.fn();
|
||||
vi.spyOn(ThemeContext, "useTheme").mockImplementation(() => ({
|
||||
theme: "light",
|
||||
setTheme,
|
||||
}));
|
||||
|
||||
render(<ThemeSwitcher />);
|
||||
|
||||
// Click the dark theme button
|
||||
fireEvent.click(screen.getByRole("button", { name: /dark/i }));
|
||||
|
||||
// Verify that setTheme was called with 'dark'
|
||||
expect(setTheme).toHaveBeenCalledWith("dark");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button } from "../ui-library/button";
|
||||
import { Button } from "@ui/ui-library/button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import logo from "../assets/icon.jpg";
|
||||
import { Link } from "react-router-dom";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import { Session } from "@supabase/supabase-js";
|
||||
import { supabase } from "../hooks/auth";
|
||||
import { Session, User } from "@supabase/supabase-js";
|
||||
import { supabase } from "@ui/hooks/auth";
|
||||
|
||||
const SessionContext = createContext<{
|
||||
session: Session | null;
|
||||
|
|
@ -16,9 +16,19 @@ export const useSession = () => {
|
|||
return context;
|
||||
};
|
||||
|
||||
type Props = { children: React.ReactNode };
|
||||
export const SessionProvider = ({ children }: Props) => {
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
type Props = { children: React.ReactNode; testUser?: User };
|
||||
export const SessionProvider = ({ children, testUser }: Props) => {
|
||||
const [session, setSession] = useState<Session | null>(
|
||||
testUser
|
||||
? {
|
||||
user: testUser,
|
||||
access_token: "test_access_token",
|
||||
refresh_token: "test_refresh_token",
|
||||
expires_in: 3600,
|
||||
token_type: "Bearer",
|
||||
}
|
||||
: null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const authStateListener = supabase.auth.onAuthStateChange(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useMutation } from "@tanstack/react-query";
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { match } from "ts-pattern";
|
||||
import { toast } from "../ui-library/toast/toast-queue";
|
||||
import { toast } from "@ui/ui-library/toast/toast-queue";
|
||||
import {
|
||||
User as SupabaseUser,
|
||||
Session,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api } from "../lib/api";
|
||||
import { api } from "@ui/lib/api";
|
||||
|
||||
interface UserMetadata {
|
||||
email: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import axios from "axios";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { IS_DEV } from "../config";
|
||||
import { IS_DEV } from "@ui/config";
|
||||
|
||||
// Create axios instance with default config
|
||||
export const api = axios.create({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "../ui-library/button";
|
||||
import { Button } from "@ui/ui-library/button";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const NotFoundPage = () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Button } from "../ui-library/button";
|
||||
import { PlusIcon } from "../ui-library/icons";
|
||||
import { Button } from "@ui/ui-library/button";
|
||||
import { PlusIcon } from "@ui/ui-library/icons";
|
||||
import {
|
||||
AllCommunityModule,
|
||||
ModuleRegistry,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Button } from "../ui-library/button";
|
||||
import { Button } from "@ui/ui-library/button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import logo from "../assets/icon.jpg";
|
||||
import { Header } from "../components/header";
|
||||
import { Header } from "@ui/components/header";
|
||||
|
||||
export const LandingPage = () => {
|
||||
localStorage.setItem("xtablo-has-seen-landing-page", "true");
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Button } from "../ui-library/button";
|
||||
import { Button } from "@ui/ui-library/button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useState } from "react";
|
||||
import { Label, Input, TextField, FieldError } from "../ui-library/field";
|
||||
import { useLoginEmail } from "../hooks/auth";
|
||||
import { Form } from "../ui-library/form";
|
||||
import { LoginWithGoogle } from "../components/BrandButtons/LoginWithGoogle";
|
||||
import { Label, Input, TextField, FieldError } from "@ui/ui-library/field";
|
||||
import { useLoginEmail } from "@ui/hooks/auth";
|
||||
import { Form } from "@ui/ui-library/form";
|
||||
import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function LoginPage() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSession } from "../contexts/SessionContext";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
|
||||
export const OAuthSigninPage = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Button } from "../ui-library/button";
|
||||
import { Button } from "@ui/ui-library/button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { Label, Input, TextField, FieldError } from "../ui-library/field";
|
||||
import { Form } from "../ui-library/form";
|
||||
import { Text } from "../ui-library/text";
|
||||
import { Label, Input, TextField, FieldError } from "@ui/ui-library/field";
|
||||
import { Form } from "@ui/ui-library/form";
|
||||
import { Text } from "@ui/ui-library/text";
|
||||
|
||||
export function ResetPasswordPage() {
|
||||
const navigate = useNavigate();
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { Button } from "../ui-library/button";
|
||||
import { Button } from "@ui/ui-library/button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { Label, Input, TextField, FieldError } from "../ui-library/field";
|
||||
import { useSignUp } from "../hooks/auth";
|
||||
import { Form } from "../ui-library/form";
|
||||
import { Text } from "../ui-library/text";
|
||||
import { LoginWithGoogle } from "../components/BrandButtons/LoginWithGoogle";
|
||||
import { Label, Input, TextField, FieldError } from "@ui/ui-library/field";
|
||||
import { useSignUp } from "@ui/hooks/auth";
|
||||
import { Form } from "@ui/ui-library/form";
|
||||
import { Text } from "@ui/ui-library/text";
|
||||
import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle";
|
||||
|
||||
export function SignUpPage() {
|
||||
const navigate = useNavigate();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { SignOutButton } from "../components/SignOutButton";
|
||||
import { useSession } from "../contexts/SessionContext";
|
||||
import { SignOutButton } from "@ui/components/SignOutButton";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
export const TabloPage = () => {
|
||||
const { session } = useSession();
|
||||
return (
|
||||
|
|
|
|||
22
ui/src/setupTests.ts
Normal file
22
ui/src/setupTests.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import "@testing-library/jest-dom";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Cleanup after each test case
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(), // deprecated
|
||||
removeListener: vi.fn(), // deprecated
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
import {
|
||||
ComboBox as RACComboBox,
|
||||
ComboBoxProps as RACComboBoxProps,
|
||||
|
|
@ -6,20 +6,19 @@ import {
|
|||
GroupProps,
|
||||
Group,
|
||||
composeRenderProps,
|
||||
} from 'react-aria-components';
|
||||
import { ButtonProps, Button } from './button';
|
||||
import { inputField } from './utils';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
} from "react-aria-components";
|
||||
import { ButtonProps, Button } from "./button";
|
||||
import { inputField } from "./utils";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import {
|
||||
SelectListBox,
|
||||
SelectListItem,
|
||||
SelectListItemDescription,
|
||||
SelectListItemLabel,
|
||||
SelectPopover,
|
||||
SelectSection,
|
||||
} from './select';
|
||||
import { Input } from './field';
|
||||
import { ChevronDownIcon, XIcon } from './icons';
|
||||
} from "./select";
|
||||
import { Input } from "./field";
|
||||
import { ChevronDownIcon, XIcon } from "./icons";
|
||||
|
||||
export function ComboBox(props: RACComboBoxProps<object>) {
|
||||
return (
|
||||
|
|
@ -27,7 +26,7 @@ export function ComboBox(props: RACComboBoxProps<object>) {
|
|||
{...props}
|
||||
data-ui="comboBox"
|
||||
className={composeRenderProps(props.className, (className) =>
|
||||
twMerge(['w-full min-w-56', inputField, className]),
|
||||
twMerge(["w-full min-w-56", inputField, className])
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
|
@ -40,48 +39,48 @@ export function ComboBoxGroup(props: GroupProps) {
|
|||
{...props}
|
||||
className={composeRenderProps(props.className, (className) =>
|
||||
twMerge([
|
||||
'group/combobox',
|
||||
'isolate',
|
||||
'grid',
|
||||
'grid-cols-[36px_1fr_minmax(40px,max-content)_minmax(40px,max-content)]',
|
||||
'sm:grid-cols-[36px_1fr_minmax(36px,max-content)_minmax(36px,max-content)]',
|
||||
'items-center',
|
||||
"group/combobox",
|
||||
"isolate",
|
||||
"grid",
|
||||
"grid-cols-[36px_1fr_minmax(40px,max-content)_minmax(40px,max-content)]",
|
||||
"sm:grid-cols-[36px_1fr_minmax(36px,max-content)_minmax(36px,max-content)]",
|
||||
"items-center",
|
||||
|
||||
// Icon
|
||||
'sm:[&>[data-ui=icon]:has(+input)]:size-4',
|
||||
'[&>[data-ui=icon]:has(+input)]:size-5',
|
||||
'[&>[data-ui=icon]:has(+input)]:row-start-1',
|
||||
'[&>[data-ui=icon]:has(+input)]:col-start-1',
|
||||
'[&>[data-ui=icon]:has(+input)]:place-self-center',
|
||||
'[&>[data-ui=icon]:has(+input)]:text-muted',
|
||||
'[&>[data-ui=icon]:has(+input)]:z-10',
|
||||
"sm:[&>[data-ui=icon]:has(+input)]:size-4",
|
||||
"[&>[data-ui=icon]:has(+input)]:size-5",
|
||||
"[&>[data-ui=icon]:has(+input)]:row-start-1",
|
||||
"[&>[data-ui=icon]:has(+input)]:col-start-1",
|
||||
"[&>[data-ui=icon]:has(+input)]:place-self-center",
|
||||
"[&>[data-ui=icon]:has(+input)]:text-muted",
|
||||
"[&>[data-ui=icon]:has(+input)]:z-10",
|
||||
|
||||
// Input
|
||||
'[&>input]:row-start-1',
|
||||
'[&>input]:col-span-full',
|
||||
'[&>input:not([class*=pe-])]:pe-10',
|
||||
'sm:[&>input:not([class*=pe-])]:pe-9',
|
||||
"[&>input]:row-start-1",
|
||||
"[&>input]:col-span-full",
|
||||
"[&>input:not([class*=pe-])]:pe-10",
|
||||
"sm:[&>input:not([class*=pe-])]:pe-9",
|
||||
|
||||
'[&>input:has(+[data-ui=clear]:not(:last-of-type))]:pe-20',
|
||||
'sm:[&>input:has(+[data-ui=clear]:not(:last-of-type))]:pe-16',
|
||||
"[&>input:has(+[data-ui=clear]:not(:last-of-type))]:pe-20",
|
||||
"sm:[&>input:has(+[data-ui=clear]:not(:last-of-type))]:pe-16",
|
||||
|
||||
'[&:has([data-ui=icon]+input)>input]:ps-10',
|
||||
'sm:[&:has([data-ui=icon]+input)>input]:ps-8',
|
||||
"[&:has([data-ui=icon]+input)>input]:ps-10",
|
||||
"sm:[&:has([data-ui=icon]+input)>input]:ps-8",
|
||||
|
||||
// Trigger button
|
||||
'*:data-[ui=trigger]:row-start-1',
|
||||
'*:data-[ui=trigger]:-col-end-1',
|
||||
'*:data-[ui=trigger]:place-self-center',
|
||||
"*:data-[ui=trigger]:row-start-1",
|
||||
"*:data-[ui=trigger]:-col-end-1",
|
||||
"*:data-[ui=trigger]:place-self-center",
|
||||
|
||||
// Clear button
|
||||
'*:data-[ui=clear]:row-start-1',
|
||||
'*:data-[ui=clear]:-col-end-2',
|
||||
'*:data-[ui=clear]:justify-self-end',
|
||||
'[&>[data-ui=clear]:last-of-type]:-col-end-1',
|
||||
'[&>[data-ui=clear]:last-of-type]:place-self-center',
|
||||
"*:data-[ui=clear]:row-start-1",
|
||||
"*:data-[ui=clear]:-col-end-2",
|
||||
"*:data-[ui=clear]:justify-self-end",
|
||||
"[&>[data-ui=clear]:last-of-type]:-col-end-1",
|
||||
"[&>[data-ui=clear]:last-of-type]:place-self-center",
|
||||
|
||||
className,
|
||||
]),
|
||||
])
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
|
@ -110,18 +109,18 @@ export function ComboBoxButton({
|
|||
export function ComboBoxClearButton({
|
||||
onPress,
|
||||
}: {
|
||||
onPress?: ButtonProps['onPress'];
|
||||
onPress?: ButtonProps["onPress"];
|
||||
}) {
|
||||
const state = React.useContext(ComboBoxStateContext);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={twMerge(
|
||||
'[&:not(:hover)]:text-muted',
|
||||
'not-last:-me-1',
|
||||
"[&:not(:hover)]:text-muted",
|
||||
"not-last:-me-1",
|
||||
state?.inputValue
|
||||
? 'visible focus-visible:-outline-offset-2'
|
||||
: 'invisible',
|
||||
? "visible focus-visible:-outline-offset-2"
|
||||
: "invisible"
|
||||
)}
|
||||
slot={null}
|
||||
data-ui="clear"
|
||||
|
|
@ -147,8 +146,6 @@ export const ComboBoxSection = SelectSection;
|
|||
|
||||
export const ComboBoxListBox = SelectListBox;
|
||||
|
||||
export const ComboBoxListItem = SelectListItem;
|
||||
|
||||
export const ComboBoxListItemLabel = SelectListItemLabel;
|
||||
|
||||
export const ComboBoxListItemDescription = SelectListItemDescription;
|
||||
|
|
|
|||
35
ui/src/utils/testHelpers.tsx
Normal file
35
ui/src/utils/testHelpers.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { render, RenderResult } from "@testing-library/react";
|
||||
import { SessionProvider } from "@ui/contexts/SessionContext";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { ThemeProvider } from "@ui/contexts/ThemeContext";
|
||||
import { User } from "@supabase/supabase-js";
|
||||
|
||||
export const renderWithProviders = (
|
||||
ui: React.ReactNode,
|
||||
opts: { user?: User } = {}
|
||||
): RenderResult => {
|
||||
return render(
|
||||
<BrowserRouter>
|
||||
<ThemeProvider>
|
||||
<SessionProvider
|
||||
testUser={
|
||||
opts.user ?? {
|
||||
id: "123",
|
||||
app_metadata: {},
|
||||
aud: "test",
|
||||
created_at: "2021-01-01",
|
||||
user_metadata: {
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
avatar_url: "https://example.com/avatar.jpg",
|
||||
full_name: "John Doe",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
{ui}
|
||||
</SessionProvider>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -20,7 +20,13 @@
|
|||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@ui/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,14 +11,16 @@
|
|||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"emitDeclarationOnly": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
"composite": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,21 @@
|
|||
/// <reference types="vitest" />
|
||||
import { defineConfig, type PluginOption } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { visualizer } from "rollup-plugin-visualizer";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), visualizer() as PluginOption, tailwindcss()],
|
||||
plugins: [
|
||||
react(),
|
||||
visualizer() as PluginOption,
|
||||
tailwindcss(),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: "./src/setupTests.ts",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue