Merge pull request 'ESLint Fixes' (#27) from adilallo/maintenance/ESLintFixes into main
Reviewed-on: #27
This commit was merged in pull request #27.
This commit is contained in:
+13
-14
@@ -491,20 +491,19 @@ jobs:
|
|||||||
# - run: npm run test:sb
|
# - run: npm run test:sb
|
||||||
# env: { CI: true }
|
# env: { CI: true }
|
||||||
|
|
||||||
# Temporarily disabled - 523 pre-existing ESLint issues will be addressed in separate ticket
|
lint:
|
||||||
# lint:
|
runs-on: [self-hosted, macos-latest]
|
||||||
# runs-on: [self-hosted, macos-latest]
|
steps:
|
||||||
# steps:
|
- uses: actions/checkout@v4
|
||||||
# - uses: actions/checkout@v4
|
- uses: actions/setup-node@v4
|
||||||
# - uses: actions/setup-node@v4
|
if: ${{ github.server_url == 'https://github.com' }}
|
||||||
# if: ${{ github.server_url == 'https://github.com' }}
|
with: { node-version: 20, cache: npm }
|
||||||
# with: { node-version: 20, cache: npm }
|
- uses: actions/setup-node@v4
|
||||||
# - uses: actions/setup-node@v4
|
if: ${{ github.server_url != 'https://github.com' || !github.server_url }}
|
||||||
# if: ${{ github.server_url != 'https://github.com' || !github.server_url }}
|
with: { node-version: 20 }
|
||||||
# with: { node-version: 20 }
|
- run: npm ci
|
||||||
# - run: npm ci
|
- run: npm run lint
|
||||||
# - run: npm run lint
|
- run: npm exec prettier -- --check "**/*.{js,jsx,ts,tsx,json,css,md}"
|
||||||
# - run: npm exec prettier -- --check "**/*.{js,jsx,ts,tsx,json,css,md}"
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: [self-hosted, macos-latest]
|
runs-on: [self-hosted, macos-latest]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { logger } from "../../../lib/logger";
|
||||||
|
|
||||||
const WEB_VITALS_DIR = path.join(process.cwd(), ".next", "web-vitals");
|
const WEB_VITALS_DIR = path.join(process.cwd(), ".next", "web-vitals");
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ export async function POST(request: NextRequest) {
|
|||||||
existingData = JSON.parse(fileContent) as WebVitalData[];
|
existingData = JSON.parse(fileContent) as WebVitalData[];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
console.warn("Could not parse existing vitals data:", err.message);
|
logger.warn("Could not parse existing vitals data:", err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,13 +80,13 @@ export async function POST(request: NextRequest) {
|
|||||||
fs.writeFileSync(filePath, JSON.stringify(existingData, null, 2));
|
fs.writeFileSync(filePath, JSON.stringify(existingData, null, 2));
|
||||||
|
|
||||||
// Log for monitoring
|
// Log for monitoring
|
||||||
console.log(
|
logger.info(
|
||||||
`Web Vital received: ${metric} = ${data.value}ms (${data.rating})`,
|
`Web Vital received: ${metric} = ${data.value}ms (${data.rating})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing web vital:", error);
|
logger.error("Error processing web vital:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Internal server error" },
|
{ error: "Internal server error" },
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
@@ -141,7 +142,7 @@ export async function GET() {
|
|||||||
|
|
||||||
return NextResponse.json({ metrics });
|
return NextResponse.json({ metrics });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching web vitals:", error);
|
logger.error("Error fetching web vitals:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Internal server error" },
|
{ error: "Internal server error" },
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
getAllBlogPosts as getAllPosts,
|
getAllBlogPosts as getAllPosts,
|
||||||
type BlogPost,
|
type BlogPost,
|
||||||
} from "../../../lib/content";
|
} from "../../../lib/content";
|
||||||
|
import { logger } from "../../../lib/logger";
|
||||||
import ContentBanner from "../../components/ContentBanner";
|
import ContentBanner from "../../components/ContentBanner";
|
||||||
import AskOrganizer from "../../components/AskOrganizer";
|
import AskOrganizer from "../../components/AskOrganizer";
|
||||||
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
||||||
@@ -44,7 +45,7 @@ export async function generateStaticParams() {
|
|||||||
slug: post.slug,
|
slug: post.slug,
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error generating static params:", error);
|
logger.error("Error generating static params:", error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +88,7 @@ export async function generateMetadata({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error generating metadata:", error);
|
logger.error("Error generating metadata:", error);
|
||||||
return {
|
return {
|
||||||
title: "Blog Post",
|
title: "Blog Post",
|
||||||
description: "A blog post from our community.",
|
description: "A blog post from our community.",
|
||||||
@@ -162,7 +163,11 @@ export default async function BlogPostPage({ params }: PageProps) {
|
|||||||
return scoredPosts
|
return scoredPosts
|
||||||
.sort((a, b) => b.score - a.score)
|
.sort((a, b) => b.score - a.score)
|
||||||
.slice(0, limit)
|
.slice(0, limit)
|
||||||
.map(({ score, ...post }) => post); // Remove score from final result
|
.map(({ score, ...post }) => {
|
||||||
|
// Score used for sorting, removed from final result
|
||||||
|
void score;
|
||||||
|
return post;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const relatedArticles = getRelatedArticles(post, allPosts);
|
const relatedArticles = getRelatedArticles(post, allPosts);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface AskOrganizerProps {
|
|||||||
buttonHref?: string;
|
buttonHref?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
variant?: "centered" | "left-aligned" | "compact" | "inverse";
|
variant?: "centered" | "left-aligned" | "compact" | "inverse";
|
||||||
onContactClick?: (data: {
|
onContactClick?: (_data: {
|
||||||
event: string;
|
event: string;
|
||||||
component: string;
|
component: string;
|
||||||
variant: string;
|
variant: string;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
type?: "button" | "submit" | "reset";
|
type?: "button" | "submit" | "reset";
|
||||||
onClick?: (
|
onClick?: (
|
||||||
e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>,
|
_e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>,
|
||||||
) => void;
|
) => void;
|
||||||
href?: string;
|
href?: string;
|
||||||
target?: string;
|
target?: string;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface CheckboxProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange?: (data: {
|
onChange?: (_data: {
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
value?: string;
|
value?: string;
|
||||||
event: React.MouseEvent | React.KeyboardEvent;
|
event: React.MouseEvent | React.KeyboardEvent;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: (
|
onClick?: (
|
||||||
e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
|
_e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
|
||||||
) => void;
|
) => void;
|
||||||
size?: "small" | "medium" | "large";
|
size?: "small" | "medium" | "large";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { Component, type ReactNode } from "react";
|
import React, { Component, type ReactNode } from "react";
|
||||||
|
import { logger } from "../../lib/logger";
|
||||||
|
|
||||||
interface ErrorBoundaryProps {
|
interface ErrorBoundaryProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -24,7 +25,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
// Log the error to an error reporting service
|
// Log the error to an error reporting service
|
||||||
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
logger.error("ErrorBoundary caught an error:", error, errorInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ interface InputProps extends Omit<
|
|||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
onChange?: (_e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
onFocus?: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
onBlur?: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,13 +151,12 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
// Form field handlers with disabled state handling
|
// Form field handlers with disabled state handling
|
||||||
const { handleChange, handleFocus, handleBlur } = useFormField<
|
const { handleChange, handleFocus, handleBlur } =
|
||||||
HTMLInputElement
|
useFormField<HTMLInputElement>(disabled, {
|
||||||
>(disabled, {
|
onChange,
|
||||||
onChange,
|
onFocus,
|
||||||
onFocus,
|
onBlur,
|
||||||
onBlur,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
|
|||||||
@@ -10,25 +10,23 @@ interface NumberedCardProps {
|
|||||||
iconColor?: string;
|
iconColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NumberedCard = memo<NumberedCardProps>(
|
const NumberedCard = memo<NumberedCardProps>(({ number, text }) => {
|
||||||
({ number, text, iconShape: _iconShape, iconColor: _iconColor }) => {
|
return (
|
||||||
return (
|
<div className="bg-[var(--color-surface-inverse-primary)] rounded-[12px] p-5 shadow-lg flex flex-col gap-4 sm:p-8 sm:gap-8 sm:flex-row sm:items-center lg:p-8 lg:gap-0 lg:flex-row lg:items-stretch lg:relative lg:h-[238px]">
|
||||||
<div className="bg-[var(--color-surface-inverse-primary)] rounded-[12px] p-5 shadow-lg flex flex-col gap-4 sm:p-8 sm:gap-8 sm:flex-row sm:items-center lg:p-8 lg:gap-0 lg:flex-row lg:items-stretch lg:relative lg:h-[238px]">
|
{/* Section Number - Top right (lg breakpoint) */}
|
||||||
{/* Section Number - Top right (lg breakpoint) */}
|
<div className="flex justify-end sm:justify-start sm:flex-shrink-0 lg:absolute lg:top-8 lg:right-8">
|
||||||
<div className="flex justify-end sm:justify-start sm:flex-shrink-0 lg:absolute lg:top-8 lg:right-8">
|
<SectionNumber number={number} />
|
||||||
<SectionNumber number={number} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Card Content - Bottom left (lg breakpoint) */}
|
|
||||||
<div className="sm:flex-1 lg:absolute lg:bottom-8 lg:left-8 lg:right-16">
|
|
||||||
<p className="font-bricolage-grotesque font-medium text-[24px] leading-[32px] sm:font-normal sm:leading-[24px] sm:text-[24px] lg:text-[24px] lg:leading-[24px] xl:text-[32px] xl:leading-[32px] text-[#141414]">
|
|
||||||
{text}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
},
|
{/* Card Content - Bottom left (lg breakpoint) */}
|
||||||
);
|
<div className="sm:flex-1 lg:absolute lg:bottom-8 lg:left-8 lg:right-16">
|
||||||
|
<p className="font-bricolage-grotesque font-medium text-[24px] leading-[32px] sm:font-normal sm:leading-[24px] sm:text-[24px] lg:text-[24px] lg:leading-[24px] xl:text-[32px] xl:leading-[32px] text-[#141414]">
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
NumberedCard.displayName = "NumberedCard";
|
NumberedCard.displayName = "NumberedCard";
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, memo } from "react";
|
import { useState, memo } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import QuoteDecor from "./QuoteDecor";
|
import QuoteDecor from "./QuoteDecor";
|
||||||
|
import { logger } from "../../lib/logger";
|
||||||
|
|
||||||
interface QuoteBlockProps {
|
interface QuoteBlockProps {
|
||||||
variant?: "compact" | "standard" | "extended";
|
variant?: "compact" | "standard" | "extended";
|
||||||
@@ -13,7 +14,7 @@ interface QuoteBlockProps {
|
|||||||
avatarSrc?: string;
|
avatarSrc?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
fallbackAvatarSrc?: string;
|
fallbackAvatarSrc?: string;
|
||||||
onError?: (error: {
|
onError?: (_error: {
|
||||||
type: string;
|
type: string;
|
||||||
message: string;
|
message: string;
|
||||||
author?: string;
|
author?: string;
|
||||||
@@ -109,7 +110,7 @@ const QuoteBlock = memo<QuoteBlockProps>(
|
|||||||
|
|
||||||
// Error handling functions
|
// Error handling functions
|
||||||
const handleImageError = (error: unknown) => {
|
const handleImageError = (error: unknown) => {
|
||||||
console.warn(
|
logger.warn(
|
||||||
`QuoteBlock: Failed to load avatar image for ${author}:`,
|
`QuoteBlock: Failed to load avatar image for ${author}:`,
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
@@ -135,7 +136,7 @@ const QuoteBlock = memo<QuoteBlockProps>(
|
|||||||
|
|
||||||
// Validate required props
|
// Validate required props
|
||||||
if (!quote || !author) {
|
if (!quote || !author) {
|
||||||
console.error("QuoteBlock: Missing required props (quote or author)");
|
logger.error("QuoteBlock: Missing required props (quote or author)");
|
||||||
if (onError) {
|
if (onError) {
|
||||||
onError({
|
onError({
|
||||||
type: "missing_props",
|
type: "missing_props",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface RadioButtonProps {
|
|||||||
state?: "default" | "hover" | "focus";
|
state?: "default" | "hover" | "focus";
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
onChange?: (data: { checked: boolean; value?: string }) => void;
|
onChange?: (_data: { checked: boolean; value?: string }) => void;
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface RadioOption {
|
|||||||
interface RadioGroupProps {
|
interface RadioGroupProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (data: { value: string }) => void;
|
onChange?: (_data: { value: string }) => void;
|
||||||
mode?: "standard" | "inverse";
|
mode?: "standard" | "inverse";
|
||||||
state?: "default" | "hover" | "focus";
|
state?: "default" | "hover" | "focus";
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const RelatedArticles = memo<RelatedArticlesProps>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className="py-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-064)]"
|
className="py-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-064)]"
|
||||||
data-testid="related-articles"
|
data-testid="related-articles"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ interface RuleCardProps {
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
gtag?: (
|
gtag?: (
|
||||||
command: string,
|
_command: string,
|
||||||
eventName: string,
|
_eventName: string,
|
||||||
params?: Record<string, unknown>,
|
_params?: Record<string, unknown>,
|
||||||
) => void;
|
) => void;
|
||||||
analytics?: {
|
analytics?: {
|
||||||
track: (eventName: string, params?: Record<string, unknown>) => void;
|
track: (_eventName: string, _params?: Record<string, unknown>) => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Image from "next/image";
|
|||||||
import RuleCard from "./RuleCard";
|
import RuleCard from "./RuleCard";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
import { getAssetPath } from "../../lib/assetUtils";
|
import { getAssetPath } from "../../lib/assetUtils";
|
||||||
|
import { logger } from "../../lib/logger";
|
||||||
|
|
||||||
interface RuleStackProps {
|
interface RuleStackProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -13,12 +14,12 @@ interface RuleStackProps {
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
gtag?: (
|
gtag?: (
|
||||||
command: string,
|
_command: string,
|
||||||
eventName: string,
|
_eventName: string,
|
||||||
params?: Record<string, unknown>,
|
_params?: Record<string, unknown>,
|
||||||
) => void;
|
) => void;
|
||||||
analytics?: {
|
analytics?: {
|
||||||
track: (eventName: string, params?: Record<string, unknown>) => void;
|
track: (_eventName: string, _params?: Record<string, unknown>) => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +39,7 @@ const RuleStack = memo<RuleStackProps>(({ className = "" }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(`${templateName} template clicked`);
|
logger.debug(`${templateName} template clicked`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ interface SelectProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (data: { target: { value: string; text: string } }) => void;
|
onChange?: (_data: { target: { value: string; text: string } }) => void;
|
||||||
options?: SelectOptionData[];
|
options?: SelectOptionData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,10 +115,11 @@ const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
const baseStyles = "w-full";
|
const baseStyles = "w-full";
|
||||||
|
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case "small":
|
case "small": {
|
||||||
const smallHeight =
|
const smallHeight =
|
||||||
labelVariant === "horizontal" ? "h-[30px]" : "h-[32px]";
|
labelVariant === "horizontal" ? "h-[30px]" : "h-[32px]";
|
||||||
return `${baseStyles} ${smallHeight} pl-[12px] pr-[36px] py-[8px] text-[10px] leading-[14px]`;
|
return `${baseStyles} ${smallHeight} pl-[12px] pr-[36px] py-[8px] text-[10px] leading-[14px]`;
|
||||||
|
}
|
||||||
case "medium":
|
case "medium":
|
||||||
return `${baseStyles} h-[36px] pl-[12px] pr-[36px] py-[8px] text-[14px] leading-[20px]`;
|
return `${baseStyles} h-[36px] pl-[12px] pr-[36px] py-[8px] text-[14px] leading-[20px]`;
|
||||||
case "large":
|
case "large":
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface SelectOptionProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: (
|
onClick?: (
|
||||||
e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
|
_e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
|
||||||
) => void;
|
) => void;
|
||||||
size?: "small" | "medium" | "large";
|
size?: "small" | "medium" | "large";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ interface SwitchProps extends Omit<
|
|||||||
> {
|
> {
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
onChange?: (
|
onChange?: (
|
||||||
e:
|
_e:
|
||||||
| React.MouseEvent<HTMLButtonElement>
|
| React.MouseEvent<HTMLButtonElement>
|
||||||
| React.KeyboardEvent<HTMLButtonElement>,
|
| React.KeyboardEvent<HTMLButtonElement>,
|
||||||
) => void;
|
) => void;
|
||||||
onFocus?: (e: React.FocusEvent<HTMLButtonElement>) => void;
|
onFocus?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
|
||||||
onBlur?: (e: React.FocusEvent<HTMLButtonElement>) => void;
|
onBlur?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
|
||||||
state?: "default" | "hover" | "focus";
|
state?: "default" | "hover" | "focus";
|
||||||
label?: string;
|
label?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ interface TextAreaProps extends Omit<
|
|||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
onChange?: (_e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||||
onFocus?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
onFocus?: (_e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
||||||
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
onBlur?: (_e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
}
|
}
|
||||||
@@ -155,13 +155,12 @@ const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
// Form field handlers with disabled state handling
|
// Form field handlers with disabled state handling
|
||||||
const { handleChange, handleFocus, handleBlur } = useFormField<
|
const { handleChange, handleFocus, handleBlur } =
|
||||||
HTMLTextAreaElement
|
useFormField<HTMLTextAreaElement>(disabled, {
|
||||||
>(disabled, {
|
onChange,
|
||||||
onChange,
|
onFocus,
|
||||||
onFocus,
|
onBlur,
|
||||||
onBlur,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ interface ToggleProps extends Omit<
|
|||||||
label?: string;
|
label?: string;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
onChange?: (
|
onChange?: (
|
||||||
e:
|
_e:
|
||||||
| React.MouseEvent<HTMLButtonElement>
|
| React.MouseEvent<HTMLButtonElement>
|
||||||
| React.KeyboardEvent<HTMLButtonElement>,
|
| React.KeyboardEvent<HTMLButtonElement>,
|
||||||
) => void;
|
) => void;
|
||||||
onFocus?: (e: React.FocusEvent<HTMLButtonElement>) => void;
|
onFocus?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
|
||||||
onBlur?: (e: React.FocusEvent<HTMLButtonElement>) => void;
|
onBlur?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
state?: "default" | "hover" | "focus";
|
state?: "default" | "hover" | "focus";
|
||||||
showIcon?: boolean;
|
showIcon?: boolean;
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ interface ToggleGroupProps extends Omit<
|
|||||||
showText?: boolean;
|
showText?: boolean;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
onChange?: (
|
onChange?: (
|
||||||
e:
|
_e:
|
||||||
| React.MouseEvent<HTMLButtonElement>
|
| React.MouseEvent<HTMLButtonElement>
|
||||||
| React.KeyboardEvent<HTMLButtonElement>,
|
| React.KeyboardEvent<HTMLButtonElement>,
|
||||||
) => void;
|
) => void;
|
||||||
onFocus?: (e: React.FocusEvent<HTMLButtonElement>) => void;
|
onFocus?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
|
||||||
onBlur?: (e: React.FocusEvent<HTMLButtonElement>) => void;
|
onBlur?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToggleGroup = memo(
|
const ToggleGroup = memo(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, memo } from "react";
|
import { useState, useEffect, memo } from "react";
|
||||||
|
import { logger } from "../../lib/logger";
|
||||||
|
|
||||||
interface VitalData {
|
interface VitalData {
|
||||||
value: number;
|
value: number;
|
||||||
@@ -50,7 +51,7 @@ const WebVitalsDashboard = memo(() => {
|
|||||||
const data = (await response.json()) as { metrics?: Metrics };
|
const data = (await response.json()) as { metrics?: Metrics };
|
||||||
setMetrics(data.metrics || {});
|
setMetrics(data.metrics || {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching web vitals:", error);
|
logger.error("Error fetching web vitals:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Custom hooks for reusable component logic
|
* Custom hooks for reusable component logic
|
||||||
*
|
*
|
||||||
* This module exports all custom hooks used throughout the application.
|
* This module exports all custom hooks used throughout the application.
|
||||||
* Hooks encapsulate complex logic and state management that can be reused
|
* Hooks encapsulate complex logic and state management that can be reused
|
||||||
* across multiple components.
|
* across multiple components.
|
||||||
@@ -12,7 +12,12 @@ export { useComponentId } from "./useComponentId";
|
|||||||
export { useFormField } from "./useFormField";
|
export { useFormField } from "./useFormField";
|
||||||
export { useComponentStyles } from "./useComponentStyles";
|
export { useComponentStyles } from "./useComponentStyles";
|
||||||
export { useSchemaData } from "./useSchemaData";
|
export { useSchemaData } from "./useSchemaData";
|
||||||
export { useMediaQuery, useIsMobile, useIsDesktop, BREAKPOINTS } from "./useMediaQuery";
|
export {
|
||||||
|
useMediaQuery,
|
||||||
|
useIsMobile,
|
||||||
|
useIsDesktop,
|
||||||
|
BREAKPOINTS,
|
||||||
|
} from "./useMediaQuery";
|
||||||
export { useFormValidation, validationRules } from "./useFormValidation";
|
export { useFormValidation, validationRules } from "./useFormValidation";
|
||||||
export type {
|
export type {
|
||||||
SizeStyleConfig,
|
SizeStyleConfig,
|
||||||
|
|||||||
@@ -28,20 +28,20 @@ interface AnalyticsEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UseAnalyticsReturn {
|
interface UseAnalyticsReturn {
|
||||||
trackEvent: (event: AnalyticsEvent) => void;
|
trackEvent: (_event: AnalyticsEvent) => void;
|
||||||
trackCustomEvent: (
|
trackCustomEvent: (
|
||||||
event: string,
|
_event: string,
|
||||||
data: Record<string, unknown>,
|
_data: Record<string, unknown>,
|
||||||
callback?: (data: Record<string, unknown>) => void,
|
_callback?: (_data: Record<string, unknown>) => void,
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
gtag?: (
|
gtag?: (
|
||||||
command: string,
|
_command: string,
|
||||||
eventName: string,
|
_eventName: string,
|
||||||
params?: Record<string, unknown>,
|
_params?: Record<string, unknown>,
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ export function useAnalytics(): UseAnalyticsReturn {
|
|||||||
const trackCustomEvent = (
|
const trackCustomEvent = (
|
||||||
event: string,
|
event: string,
|
||||||
data: Record<string, unknown>,
|
data: Record<string, unknown>,
|
||||||
callback?: (data: Record<string, unknown>) => void,
|
callback?: (_data: Record<string, unknown>) => void,
|
||||||
) => {
|
) => {
|
||||||
// Execute custom callback if provided
|
// Execute custom callback if provided
|
||||||
if (callback) {
|
if (callback) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { useEffect, RefObject } from "react";
|
|||||||
*/
|
*/
|
||||||
export function useClickOutside(
|
export function useClickOutside(
|
||||||
refs: Array<RefObject<HTMLElement>>,
|
refs: Array<RefObject<HTMLElement>>,
|
||||||
handler: (event: MouseEvent | TouchEvent) => void,
|
handler: (_event: MouseEvent | TouchEvent) => void,
|
||||||
enabled: boolean = true,
|
enabled: boolean = true,
|
||||||
): void {
|
): void {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export interface UseComponentStylesOptions {
|
|||||||
error?: boolean;
|
error?: boolean;
|
||||||
sizeStyles: SizeStyleConfig;
|
sizeStyles: SizeStyleConfig;
|
||||||
stateStyles: StateStyleConfig;
|
stateStyles: StateStyleConfig;
|
||||||
getStateStyles?: (params: {
|
getStateStyles?: (_params: {
|
||||||
state?: string;
|
state?: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
@@ -61,9 +61,7 @@ export interface UseComponentStylesOptions {
|
|||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function useComponentStyles(
|
export function useComponentStyles(options: UseComponentStylesOptions): {
|
||||||
options: UseComponentStylesOptions,
|
|
||||||
): {
|
|
||||||
sizeClasses: Record<string, string>;
|
sizeClasses: Record<string, string>;
|
||||||
stateClasses: Record<string, string>;
|
stateClasses: Record<string, string>;
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -19,15 +19,15 @@ import { useCallback } from "react";
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
interface FormFieldHandlers<T = HTMLElement> {
|
interface FormFieldHandlers<T = HTMLElement> {
|
||||||
onChange?: (e: React.ChangeEvent<T>) => void;
|
onChange?: (_e: React.ChangeEvent<T>) => void;
|
||||||
onFocus?: (e: React.FocusEvent<T>) => void;
|
onFocus?: (_e: React.FocusEvent<T>) => void;
|
||||||
onBlur?: (e: React.FocusEvent<T>) => void;
|
onBlur?: (_e: React.FocusEvent<T>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseFormFieldReturn<T = HTMLElement> {
|
interface UseFormFieldReturn<T = HTMLElement> {
|
||||||
handleChange: (e: React.ChangeEvent<T>) => void;
|
handleChange: (_e: React.ChangeEvent<T>) => void;
|
||||||
handleFocus: (e: React.FocusEvent<T>) => void;
|
handleFocus: (_e: React.FocusEvent<T>) => void;
|
||||||
handleBlur: (e: React.FocusEvent<T>) => void;
|
handleBlur: (_e: React.FocusEvent<T>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFormField<T extends HTMLElement = HTMLElement>(
|
export function useFormField<T extends HTMLElement = HTMLElement>(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useState, useCallback, useMemo } from "react";
|
|||||||
/**
|
/**
|
||||||
* Validation rule function type
|
* Validation rule function type
|
||||||
*/
|
*/
|
||||||
export type ValidationRule<T = string> = (value: T) => string | null;
|
export type ValidationRule<T = string> = (_value: T) => string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validation rules for common patterns
|
* Validation rules for common patterns
|
||||||
@@ -22,24 +22,30 @@ export const validationRules = {
|
|||||||
return emailRegex.test(value) ? null : "Please enter a valid email address";
|
return emailRegex.test(value) ? null : "Please enter a valid email address";
|
||||||
},
|
},
|
||||||
|
|
||||||
minLength: (min: number) => (value: string): string | null => {
|
minLength:
|
||||||
if (!value) return null;
|
(min: number) =>
|
||||||
return value.length >= min
|
(value: string): string | null => {
|
||||||
? null
|
if (!value) return null;
|
||||||
: `Must be at least ${min} characters long`;
|
return value.length >= min
|
||||||
},
|
? null
|
||||||
|
: `Must be at least ${min} characters long`;
|
||||||
|
},
|
||||||
|
|
||||||
maxLength: (max: number) => (value: string): string | null => {
|
maxLength:
|
||||||
if (!value) return null;
|
(max: number) =>
|
||||||
return value.length <= max
|
(value: string): string | null => {
|
||||||
? null
|
if (!value) return null;
|
||||||
: `Must be no more than ${max} characters long`;
|
return value.length <= max
|
||||||
},
|
? null
|
||||||
|
: `Must be no more than ${max} characters long`;
|
||||||
|
},
|
||||||
|
|
||||||
pattern: (regex: RegExp, message: string) => (value: string): string | null => {
|
pattern:
|
||||||
if (!value) return null;
|
(regex: RegExp, message: string) =>
|
||||||
return regex.test(value) ? null : message;
|
(value: string): string | null => {
|
||||||
},
|
if (!value) return null;
|
||||||
|
return regex.test(value) ? null : message;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -182,13 +188,16 @@ export function useFormValidation(options: UseFormValidationOptions) {
|
|||||||
}, [initialValues]);
|
}, [initialValues]);
|
||||||
|
|
||||||
// Set field value programmatically
|
// Set field value programmatically
|
||||||
const setValue = useCallback((name: string, value: string) => {
|
const setValue = useCallback(
|
||||||
setValues((prev) => ({ ...prev, [name]: value }));
|
(name: string, value: string) => {
|
||||||
if (validateOnChange) {
|
setValues((prev) => ({ ...prev, [name]: value }));
|
||||||
const error = validateField(name, value);
|
if (validateOnChange) {
|
||||||
setErrors((prev) => ({ ...prev, [name]: error }));
|
const error = validateField(name, value);
|
||||||
}
|
setErrors((prev) => ({ ...prev, [name]: error }));
|
||||||
}, [validateOnChange, validateField]);
|
}
|
||||||
|
},
|
||||||
|
[validateOnChange, validateField],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
values,
|
values,
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export function useMediaQuery(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const media = window.matchMedia(mediaQuery);
|
const media = window.matchMedia(mediaQuery);
|
||||||
|
// Initialize matches synchronously - this is safe for media queries
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
setMatches(media.matches);
|
setMatches(media.matches);
|
||||||
|
|
||||||
// Create listener for changes
|
// Create listener for changes
|
||||||
|
|||||||
@@ -144,7 +144,12 @@ export function useSchemaData(
|
|||||||
type: "BreadcrumbList";
|
type: "BreadcrumbList";
|
||||||
items: Array<{ name: string; url: string }>;
|
items: Array<{ name: string; url: string }>;
|
||||||
},
|
},
|
||||||
): SchemaOrganization | SchemaWebSite | SchemaHowTo | SchemaArticle | SchemaBreadcrumbList {
|
):
|
||||||
|
| SchemaOrganization
|
||||||
|
| SchemaWebSite
|
||||||
|
| SchemaHowTo
|
||||||
|
| SchemaArticle
|
||||||
|
| SchemaBreadcrumbList {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
switch (config.type) {
|
switch (config.type) {
|
||||||
case "Organization":
|
case "Organization":
|
||||||
@@ -216,7 +221,9 @@ export function useSchemaData(
|
|||||||
"@id": config.mainEntityOfPage,
|
"@id": config.mainEntityOfPage,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
...(config.articleSection && { articleSection: config.articleSection }),
|
...(config.articleSection && {
|
||||||
|
articleSection: config.articleSection,
|
||||||
|
}),
|
||||||
...(config.keywords && { keywords: config.keywords }),
|
...(config.keywords && { keywords: config.keywords }),
|
||||||
} as SchemaArticle;
|
} as SchemaArticle;
|
||||||
|
|
||||||
|
|||||||
+26
-12
@@ -15,6 +15,7 @@ Detects clicks outside of specified elements. Useful for closing dropdowns, moda
|
|||||||
**Location:** `app/hooks/useClickOutside.ts`
|
**Location:** `app/hooks/useClickOutside.ts`
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useClickOutside } from "../hooks";
|
import { useClickOutside } from "../hooks";
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ useClickOutside([menuRef, buttonRef], () => setIsOpen(false), isOpen);
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
|
|
||||||
- `refs`: Array of refs to elements that should not trigger the callback
|
- `refs`: Array of refs to elements that should not trigger the callback
|
||||||
- `handler`: Callback function to execute when clicking outside
|
- `handler`: Callback function to execute when clicking outside
|
||||||
- `enabled`: Whether the hook is enabled (default: true)
|
- `enabled`: Whether the hook is enabled (default: true)
|
||||||
@@ -41,6 +43,7 @@ Centralized analytics tracking for component interactions. Supports both Google
|
|||||||
**Location:** `app/hooks/useAnalytics.ts`
|
**Location:** `app/hooks/useAnalytics.ts`
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useAnalytics } from "../hooks";
|
import { useAnalytics } from "../hooks";
|
||||||
|
|
||||||
@@ -66,6 +69,7 @@ trackCustomEvent(
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Returns:**
|
**Returns:**
|
||||||
|
|
||||||
- `trackEvent`: Function to track standard analytics events
|
- `trackEvent`: Function to track standard analytics events
|
||||||
- `trackCustomEvent`: Function to track custom events with optional callback
|
- `trackCustomEvent`: Function to track custom events with optional callback
|
||||||
|
|
||||||
@@ -80,6 +84,7 @@ Generates unique component IDs for accessibility. Provides consistent ID generat
|
|||||||
**Location:** `app/hooks/useComponentId.ts`
|
**Location:** `app/hooks/useComponentId.ts`
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useComponentId } from "../hooks";
|
import { useComponentId } from "../hooks";
|
||||||
|
|
||||||
@@ -89,10 +94,12 @@ const { id, labelId } = useComponentId("input", props.id);
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
|
|
||||||
- `prefix`: Prefix for the generated ID (e.g., "input", "select")
|
- `prefix`: Prefix for the generated ID (e.g., "input", "select")
|
||||||
- `providedId`: Optional ID provided via props (takes precedence)
|
- `providedId`: Optional ID provided via props (takes precedence)
|
||||||
|
|
||||||
**Returns:**
|
**Returns:**
|
||||||
|
|
||||||
- `id`: Component ID
|
- `id`: Component ID
|
||||||
- `labelId`: Associated label ID for accessibility
|
- `labelId`: Associated label ID for accessibility
|
||||||
|
|
||||||
@@ -107,6 +114,7 @@ Manages form field event handlers with disabled state handling. Ensures handlers
|
|||||||
**Location:** `app/hooks/useFormField.ts`
|
**Location:** `app/hooks/useFormField.ts`
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useFormField } from "../hooks";
|
import { useFormField } from "../hooks";
|
||||||
|
|
||||||
@@ -117,18 +125,16 @@ const { handleChange, handleFocus, handleBlur } = useFormField(disabled, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Use in component
|
// Use in component
|
||||||
<input
|
<input onChange={handleChange} onFocus={handleFocus} onBlur={handleBlur} />;
|
||||||
onChange={handleChange}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
/>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
|
|
||||||
- `disabled`: Whether the field is disabled
|
- `disabled`: Whether the field is disabled
|
||||||
- `handlers`: Object containing onChange, onFocus, onBlur handlers
|
- `handlers`: Object containing onChange, onFocus, onBlur handlers
|
||||||
|
|
||||||
**Returns:**
|
**Returns:**
|
||||||
|
|
||||||
- `handleChange`: Wrapped onChange handler
|
- `handleChange`: Wrapped onChange handler
|
||||||
- `handleFocus`: Wrapped onFocus handler
|
- `handleFocus`: Wrapped onFocus handler
|
||||||
- `handleBlur`: Wrapped onBlur handler
|
- `handleBlur`: Wrapped onBlur handler
|
||||||
@@ -144,6 +150,7 @@ Manages component size and state styles. Provides a consistent pattern for styli
|
|||||||
**Location:** `app/hooks/useComponentStyles.ts`
|
**Location:** `app/hooks/useComponentStyles.ts`
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useComponentStyles } from "../hooks";
|
import { useComponentStyles } from "../hooks";
|
||||||
|
|
||||||
@@ -178,6 +185,7 @@ Generates Schema.org structured data (JSON-LD) for SEO and search engines.
|
|||||||
**Location:** `app/hooks/useSchemaData.ts`
|
**Location:** `app/hooks/useSchemaData.ts`
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useSchemaData } from "../hooks";
|
import { useSchemaData } from "../hooks";
|
||||||
|
|
||||||
@@ -205,10 +213,11 @@ const orgSchema = useSchemaData({
|
|||||||
<script
|
<script
|
||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
|
||||||
**Supported Types:**
|
**Supported Types:**
|
||||||
|
|
||||||
- `Organization` - Organization information
|
- `Organization` - Organization information
|
||||||
- `WebSite` - Website navigation and search
|
- `WebSite` - Website navigation and search
|
||||||
- `HowTo` - Step-by-step instructions
|
- `HowTo` - Step-by-step instructions
|
||||||
@@ -226,6 +235,7 @@ Responsive breakpoint detection using window.matchMedia.
|
|||||||
**Location:** `app/hooks/useMediaQuery.ts`
|
**Location:** `app/hooks/useMediaQuery.ts`
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useMediaQuery, useIsMobile, useIsDesktop } from "../hooks";
|
import { useMediaQuery, useIsMobile, useIsDesktop } from "../hooks";
|
||||||
|
|
||||||
@@ -242,6 +252,7 @@ const isDesktop = useIsDesktop(); // lg breakpoint and above
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Available Breakpoints:**
|
**Available Breakpoints:**
|
||||||
|
|
||||||
- `sm`: 640px
|
- `sm`: 640px
|
||||||
- `md`: 768px
|
- `md`: 768px
|
||||||
- `lg`: 1024px
|
- `lg`: 1024px
|
||||||
@@ -259,6 +270,7 @@ Form validation with field-level error handling.
|
|||||||
**Location:** `app/hooks/useFormValidation.ts`
|
**Location:** `app/hooks/useFormValidation.ts`
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useFormValidation, validationRules } from "../hooks";
|
import { useFormValidation, validationRules } from "../hooks";
|
||||||
|
|
||||||
@@ -275,10 +287,7 @@ const {
|
|||||||
initialValues: { email: "", password: "" },
|
initialValues: { email: "", password: "" },
|
||||||
validationRules: {
|
validationRules: {
|
||||||
email: [validationRules.required, validationRules.email],
|
email: [validationRules.required, validationRules.email],
|
||||||
password: [
|
password: [validationRules.required, validationRules.minLength(8)],
|
||||||
validationRules.required,
|
|
||||||
validationRules.minLength(8),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
validateOnChange: true,
|
validateOnChange: true,
|
||||||
validateOnBlur: true,
|
validateOnBlur: true,
|
||||||
@@ -290,11 +299,14 @@ const {
|
|||||||
value={values.email}
|
value={values.email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
/>
|
/>;
|
||||||
{errors.email && touched.email && <span>{errors.email}</span>}
|
{
|
||||||
|
errors.email && touched.email && <span>{errors.email}</span>;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Available Validation Rules:**
|
**Available Validation Rules:**
|
||||||
|
|
||||||
- `validationRules.required` - Field is required
|
- `validationRules.required` - Field is required
|
||||||
- `validationRules.email` - Valid email format
|
- `validationRules.email` - Valid email format
|
||||||
- `validationRules.minLength(n)` - Minimum length
|
- `validationRules.minLength(n)` - Minimum length
|
||||||
@@ -302,6 +314,7 @@ const {
|
|||||||
- `validationRules.pattern(regex, message)` - Custom regex pattern
|
- `validationRules.pattern(regex, message)` - Custom regex pattern
|
||||||
|
|
||||||
**Returns:**
|
**Returns:**
|
||||||
|
|
||||||
- `values` - Current form values
|
- `values` - Current form values
|
||||||
- `errors` - Field error messages
|
- `errors` - Field error messages
|
||||||
- `touched` - Fields that have been interacted with
|
- `touched` - Fields that have been interacted with
|
||||||
@@ -317,6 +330,7 @@ const {
|
|||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
1. **Import from index:** Always import hooks from `app/hooks` index file:
|
1. **Import from index:** Always import hooks from `app/hooks` index file:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useAnalytics, useComponentId } from "../hooks";
|
import { useAnalytics, useComponentId } from "../hooks";
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -9,29 +9,35 @@ This directory contains all project documentation organized by topic.
|
|||||||
Comprehensive guides for different aspects of the project:
|
Comprehensive guides for different aspects of the project:
|
||||||
|
|
||||||
#### Testing
|
#### Testing
|
||||||
|
|
||||||
- **[testing.md](./guides/testing.md)** - Complete testing strategy and philosophy
|
- **[testing.md](./guides/testing.md)** - Complete testing strategy and philosophy
|
||||||
- **[testing-framework.md](./guides/testing-framework.md)** - Detailed testing framework documentation
|
- **[testing-framework.md](./guides/testing-framework.md)** - Detailed testing framework documentation
|
||||||
- **[testing-quick-reference.md](./guides/testing-quick-reference.md)** - Quick reference for daily development
|
- **[testing-quick-reference.md](./guides/testing-quick-reference.md)** - Quick reference for daily development
|
||||||
- **[visual-regression.md](./guides/visual-regression.md)** - Visual regression testing guide
|
- **[visual-regression.md](./guides/visual-regression.md)** - Visual regression testing guide
|
||||||
|
|
||||||
#### Performance
|
#### Performance
|
||||||
|
|
||||||
- **[performance.md](./guides/performance.md)** - Performance optimization and monitoring guide
|
- **[performance.md](./guides/performance.md)** - Performance optimization and monitoring guide
|
||||||
|
|
||||||
#### Content
|
#### Content
|
||||||
|
|
||||||
- **[content-creation.md](./guides/content-creation.md)** - Content creation guidelines
|
- **[content-creation.md](./guides/content-creation.md)** - Content creation guidelines
|
||||||
|
|
||||||
## 🎯 Quick Navigation
|
## 🎯 Quick Navigation
|
||||||
|
|
||||||
### For New Team Members
|
### For New Team Members
|
||||||
|
|
||||||
1. Start with **[testing.md](./guides/testing.md)** to understand the testing strategy
|
1. Start with **[testing.md](./guides/testing.md)** to understand the testing strategy
|
||||||
2. Use **[testing-quick-reference.md](./guides/testing-quick-reference.md)** for daily development
|
2. Use **[testing-quick-reference.md](./guides/testing-quick-reference.md)** for daily development
|
||||||
3. Reference **[performance.md](./guides/performance.md)** for performance optimization
|
3. Reference **[performance.md](./guides/performance.md)** for performance optimization
|
||||||
|
|
||||||
### For Daily Development
|
### For Daily Development
|
||||||
|
|
||||||
- **[testing-quick-reference.md](./guides/testing-quick-reference.md)** - Essential commands and troubleshooting
|
- **[testing-quick-reference.md](./guides/testing-quick-reference.md)** - Essential commands and troubleshooting
|
||||||
- **[testing-framework.md](./guides/testing-framework.md)** - Detailed testing explanations
|
- **[testing-framework.md](./guides/testing-framework.md)** - Detailed testing explanations
|
||||||
|
|
||||||
### For Specific Topics
|
### For Specific Topics
|
||||||
|
|
||||||
- **Visual Testing**: [visual-regression.md](./guides/visual-regression.md)
|
- **Visual Testing**: [visual-regression.md](./guides/visual-regression.md)
|
||||||
- **Performance**: [performance.md](./guides/performance.md)
|
- **Performance**: [performance.md](./guides/performance.md)
|
||||||
- **Content**: [content-creation.md](./guides/content-creation.md)
|
- **Content**: [content-creation.md](./guides/content-creation.md)
|
||||||
|
|||||||
+85
-1
@@ -7,6 +7,8 @@ import tseslint from "@typescript-eslint/eslint-plugin";
|
|||||||
import tsparser from "@typescript-eslint/parser";
|
import tsparser from "@typescript-eslint/parser";
|
||||||
import nextPlugin from "@next/eslint-plugin-next";
|
import nextPlugin from "@next/eslint-plugin-next";
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
|
import react from "eslint-plugin-react";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
// Base JavaScript recommended rules
|
// Base JavaScript recommended rules
|
||||||
@@ -38,6 +40,7 @@ const eslintConfig = [
|
|||||||
...globals.node,
|
...globals.node,
|
||||||
...globals.browser,
|
...globals.browser,
|
||||||
...globals.es2021,
|
...globals.es2021,
|
||||||
|
React: "readonly",
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
@@ -45,6 +48,21 @@ const eslintConfig = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
plugins: {
|
||||||
|
react,
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react/react-in-jsx-scope": "off", // React 19 doesn't require React import
|
||||||
|
"react/prop-types": "off", // Using TypeScript for prop validation
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// TypeScript files configuration
|
// TypeScript files configuration
|
||||||
{
|
{
|
||||||
@@ -55,6 +73,8 @@ const eslintConfig = [
|
|||||||
...globals.node,
|
...globals.node,
|
||||||
...globals.browser,
|
...globals.browser,
|
||||||
...globals.es2021,
|
...globals.es2021,
|
||||||
|
React: "readonly",
|
||||||
|
process: "readonly",
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: "latest",
|
ecmaVersion: "latest",
|
||||||
@@ -68,13 +88,33 @@ const eslintConfig = [
|
|||||||
plugins: {
|
plugins: {
|
||||||
"@typescript-eslint": tseslint,
|
"@typescript-eslint": tseslint,
|
||||||
"@next/next": nextPlugin,
|
"@next/next": nextPlugin,
|
||||||
|
react,
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react/react-in-jsx-scope": "off", // React 19 doesn't require React import
|
||||||
|
"react/prop-types": "off", // Using TypeScript for prop validation
|
||||||
"@typescript-eslint/no-unused-vars": [
|
"@typescript-eslint/no-unused-vars": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
argsIgnorePattern: "^_",
|
argsIgnorePattern: "^_",
|
||||||
varsIgnorePattern: "^_",
|
varsIgnorePattern: "^_",
|
||||||
|
caughtErrorsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
caughtErrorsIgnorePattern: "^_",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
@@ -89,7 +129,51 @@ const eslintConfig = [
|
|||||||
rules: {
|
rules: {
|
||||||
// Basic rules
|
// Basic rules
|
||||||
"react/no-unescaped-entities": "off",
|
"react/no-unescaped-entities": "off",
|
||||||
"no-console": "warn",
|
// Default: discourage console usage, but allow warn/error as "standard practice"
|
||||||
|
"no-console": ["warn", { allow: ["warn", "error"] }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// App/lib code: no console.* (enforced)
|
||||||
|
{
|
||||||
|
files: ["app/**/*.{ts,tsx,js,jsx}", "lib/**/*.{ts,tsx,js,jsx}"],
|
||||||
|
rules: {
|
||||||
|
"no-console": "error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Tests/Storybook/scripts: console is acceptable
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
"tests/**/*.{ts,tsx,js,jsx}",
|
||||||
|
"stories/**/*.{ts,tsx,js,jsx}",
|
||||||
|
"scripts/**/*.{ts,js}",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"no-console": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Config files - allow Node.js globals
|
||||||
|
{
|
||||||
|
files: ["*.config.{js,mjs,ts}", "scripts/**/*.{js,ts}"],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
process: "readonly",
|
||||||
|
require: "readonly",
|
||||||
|
console: "readonly",
|
||||||
|
__dirname: "readonly",
|
||||||
|
__filename: "readonly",
|
||||||
|
module: "readonly",
|
||||||
|
exports: "readonly",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Storybook files - disable React hooks rules (render functions are called by Storybook)
|
||||||
|
// This must come AFTER the general rules to override them
|
||||||
|
{
|
||||||
|
files: ["**/*.stories.{js,jsx,ts,tsx}"],
|
||||||
|
rules: {
|
||||||
|
"react-hooks/rules-of-hooks": "off",
|
||||||
|
"react-hooks/exhaustive-deps": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
+5
-3
@@ -2,6 +2,8 @@
|
|||||||
* Content caching utilities for improved performance
|
* Content caching utilities for improved performance
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { logger } from "./logger";
|
||||||
|
|
||||||
// In-memory cache for blog posts
|
// In-memory cache for blog posts
|
||||||
const blogPostCache = new Map<string, CacheEntry<unknown>>();
|
const blogPostCache = new Map<string, CacheEntry<unknown>>();
|
||||||
const blogListCache = new Map<string, CacheEntry<unknown[]>>();
|
const blogListCache = new Map<string, CacheEntry<unknown[]>>();
|
||||||
@@ -243,9 +245,9 @@ export async function warmCache<T>(
|
|||||||
cacheBlogPost(postWithSlug.slug, post);
|
cacheBlogPost(postWithSlug.slug, post);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Cache warmed up successfully");
|
logger.info("Cache warmed up successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error warming up cache:", error);
|
logger.error("Error warming up cache:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +260,7 @@ export function isCacheHealthy(): boolean {
|
|||||||
clearExpiredCache();
|
clearExpiredCache();
|
||||||
return blogPostCache.size < MAX_CACHE_SIZE;
|
return blogPostCache.size < MAX_CACHE_SIZE;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Cache health check failed:", error);
|
logger.error("Cache health check failed:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -3,6 +3,7 @@ import path from "path";
|
|||||||
import matter from "gray-matter";
|
import matter from "gray-matter";
|
||||||
import { validateBlogPost, sanitizeBlogPost } from "./validation";
|
import { validateBlogPost, sanitizeBlogPost } from "./validation";
|
||||||
import type { BlogPostFrontmatter } from "./validation";
|
import type { BlogPostFrontmatter } from "./validation";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content processing utilities for blog posts
|
* Content processing utilities for blog posts
|
||||||
@@ -73,7 +74,7 @@ export function getBlogPostFiles(): string[] {
|
|||||||
(file) => file.endsWith(".md") || file.endsWith(".mdx"),
|
(file) => file.endsWith(".md") || file.endsWith(".mdx"),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error reading blog content directory:", error);
|
logger.error("Error reading blog content directory:", error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +93,7 @@ export function parseBlogPost(filePath: string): BlogPost | null {
|
|||||||
|
|
||||||
const validationResult = validateBlogPost(data);
|
const validationResult = validateBlogPost(data);
|
||||||
if (!validationResult.isValid) {
|
if (!validationResult.isValid) {
|
||||||
console.error(
|
logger.error(
|
||||||
`Validation errors for ${filePath}:`,
|
`Validation errors for ${filePath}:`,
|
||||||
validationResult.errors,
|
validationResult.errors,
|
||||||
);
|
);
|
||||||
@@ -111,7 +112,7 @@ export function parseBlogPost(filePath: string): BlogPost | null {
|
|||||||
lastModified: fs.statSync(fullPath).mtime,
|
lastModified: fs.statSync(fullPath).mtime,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error parsing blog post file ${filePath}:`, error);
|
logger.error(`Error parsing blog post file ${filePath}:`, error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
/**
|
||||||
|
* Minimal logger wrapper.
|
||||||
|
*
|
||||||
|
* - Centralizes logging so we can swap implementations later (e.g. pino/sentry).
|
||||||
|
* - Avoids sprinkling `console.*` throughout app code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type LoggerArgs = unknown[];
|
||||||
|
|
||||||
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
|
export const logger = {
|
||||||
|
debug: (...args: LoggerArgs) => {
|
||||||
|
if (!isProd) console.debug(...args);
|
||||||
|
},
|
||||||
|
info: (...args: LoggerArgs) => {
|
||||||
|
console.info(...args);
|
||||||
|
},
|
||||||
|
warn: (...args: LoggerArgs) => {
|
||||||
|
console.warn(...args);
|
||||||
|
},
|
||||||
|
error: (...args: LoggerArgs) => {
|
||||||
|
console.error(...args);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import createMDX from "@next/mdx";
|
import createMDX from "@next/mdx";
|
||||||
|
|
||||||
|
/* eslint-env node */
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
// Performance optimizations
|
// Performance optimizations
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 9999",
|
||||||
"postinstall": "npm rebuild lightningcss",
|
"postinstall": "npm rebuild lightningcss",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"storybook:local": "storybook dev -p 6006",
|
"storybook:local": "storybook dev -p 6006",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
* Monitors Core Web Vitals and performance metrics
|
* Monitors Core Web Vitals and performance metrics
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { execSync } = require("child_process");
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
@@ -67,7 +66,7 @@ class PerformanceMonitor {
|
|||||||
execSync("curl -s http://localhost:3000 > /dev/null", {
|
execSync("curl -s http://localhost:3000 > /dev/null", {
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.warn(
|
console.warn(
|
||||||
"⚠️ Development server not running, skipping Lighthouse CI...",
|
"⚠️ Development server not running, skipping Lighthouse CI...",
|
||||||
);
|
);
|
||||||
@@ -82,7 +81,7 @@ class PerformanceMonitor {
|
|||||||
|
|
||||||
// Parse Lighthouse results
|
// Parse Lighthouse results
|
||||||
await this.parseLighthouseResults();
|
await this.parseLighthouseResults();
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.warn("⚠️ Lighthouse CI failed, continuing with other metrics...");
|
console.warn("⚠️ Lighthouse CI failed, continuing with other metrics...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ class PerformanceTester {
|
|||||||
execSync("curl -s http://localhost:3000 > /dev/null", {
|
execSync("curl -s http://localhost:3000 > /dev/null", {
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.warn(
|
console.warn(
|
||||||
"⚠️ Development server not running, skipping Lighthouse CI...",
|
"⚠️ Development server not running, skipping Lighthouse CI...",
|
||||||
);
|
);
|
||||||
|
|||||||
+16
-16
@@ -54,25 +54,25 @@ export const Variants = {
|
|||||||
children: "Button",
|
children: "Button",
|
||||||
size: "large",
|
size: "large",
|
||||||
},
|
},
|
||||||
render: (args) => (
|
render: (_args) => (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<Button {...args} variant="default">
|
<Button {..._args} variant="default">
|
||||||
Default
|
Default
|
||||||
</Button>
|
</Button>
|
||||||
<Button {...args} variant="secondary">
|
<Button {..._args} variant="secondary">
|
||||||
Secondary
|
Secondary
|
||||||
</Button>
|
</Button>
|
||||||
<Button {...args} variant="primary">
|
<Button {..._args} variant="primary">
|
||||||
Primary
|
Primary
|
||||||
</Button>
|
</Button>
|
||||||
<Button {...args} variant="outlined">
|
<Button {..._args} variant="outlined">
|
||||||
Outlined
|
Outlined
|
||||||
</Button>
|
</Button>
|
||||||
<Button {...args} variant="dark">
|
<Button {..._args} variant="dark">
|
||||||
Dark
|
Dark
|
||||||
</Button>
|
</Button>
|
||||||
<Button {...args} variant="inverse">
|
<Button {..._args} variant="inverse">
|
||||||
Inverse
|
Inverse
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,22 +92,22 @@ export const Sizes = {
|
|||||||
children: "Button",
|
children: "Button",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
render: (args) => (
|
render: (_args) => (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<Button {...args} size="xsmall">
|
<Button {..._args} size="xsmall">
|
||||||
XSmall
|
XSmall
|
||||||
</Button>
|
</Button>
|
||||||
<Button {...args} size="small">
|
<Button {..._args} size="small">
|
||||||
Small
|
Small
|
||||||
</Button>
|
</Button>
|
||||||
<Button {...args} size="medium">
|
<Button {..._args} size="medium">
|
||||||
Medium
|
Medium
|
||||||
</Button>
|
</Button>
|
||||||
<Button {...args} size="large">
|
<Button {..._args} size="large">
|
||||||
Large
|
Large
|
||||||
</Button>
|
</Button>
|
||||||
<Button {...args} size="xlarge">
|
<Button {..._args} size="xlarge">
|
||||||
XLarge
|
XLarge
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,11 +128,11 @@ export const States = {
|
|||||||
size: "large",
|
size: "large",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
render: (args) => (
|
render: (_args) => (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<Button {...args}>Normal</Button>
|
<Button {..._args}>Normal</Button>
|
||||||
<Button {...args} disabled>
|
<Button {..._args} disabled>
|
||||||
Disabled
|
Disabled
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import {
|
|||||||
CheckedInteraction,
|
CheckedInteraction,
|
||||||
StandardInteraction,
|
StandardInteraction,
|
||||||
InverseInteraction,
|
InverseInteraction,
|
||||||
KeyboardInteraction,
|
|
||||||
AccessibilityInteraction,
|
|
||||||
FormIntegration,
|
|
||||||
} from "../tests/storybook/Checkbox.interactions.test";
|
} from "../tests/storybook/Checkbox.interactions.test";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -19,3 +19,22 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Default = {
|
||||||
|
args: {
|
||||||
|
children: <div>Normal content</div>,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithError = {
|
||||||
|
render: () => {
|
||||||
|
const ThrowError = () => {
|
||||||
|
throw new Error("Test error for ErrorBoundary");
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ThrowError />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import {
|
|||||||
CheckedInteraction,
|
CheckedInteraction,
|
||||||
StandardInteraction,
|
StandardInteraction,
|
||||||
InverseInteraction,
|
InverseInteraction,
|
||||||
KeyboardInteraction,
|
|
||||||
AccessibilityInteraction,
|
|
||||||
FormIntegration,
|
|
||||||
} from "../tests/storybook/RadioButton.interactions.test";
|
} from "../tests/storybook/RadioButton.interactions.test";
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
|
|||||||
@@ -5,10 +5,6 @@ import {
|
|||||||
StandardInteraction,
|
StandardInteraction,
|
||||||
InverseInteraction,
|
InverseInteraction,
|
||||||
InteractiveInteraction,
|
InteractiveInteraction,
|
||||||
KeyboardInteraction,
|
|
||||||
AccessibilityInteraction,
|
|
||||||
SingleSelectionInteraction,
|
|
||||||
FormIntegration,
|
|
||||||
} from "../tests/storybook/RadioGroup.interactions.test";
|
} from "../tests/storybook/RadioGroup.interactions.test";
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ export const Default = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AllVariants = {
|
export const AllVariants = {
|
||||||
render: (args) => (
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
render: (_args) => (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl">
|
||||||
<RuleCard
|
<RuleCard
|
||||||
title="Consensus clusters"
|
title="Consensus clusters"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { expect, test, describe, it, vi } from "vitest";
|
import { expect, describe, it, vi } from "vitest";
|
||||||
import { axe, toHaveNoViolations } from "jest-axe";
|
import { axe, toHaveNoViolations } from "jest-axe";
|
||||||
import ContextMenu from "../../app/components/ContextMenu";
|
import ContextMenu from "../../app/components/ContextMenu";
|
||||||
import ContextMenuItem from "../../app/components/ContextMenuItem";
|
import ContextMenuItem from "../../app/components/ContextMenuItem";
|
||||||
@@ -39,7 +39,6 @@ describe("ContextMenu Components Accessibility", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("has proper focus management", async () => {
|
it("has proper focus management", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
render(
|
render(
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
|
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
|
||||||
@@ -249,7 +248,6 @@ describe("ContextMenu Components Accessibility", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maintains proper focus order", async () => {
|
it("maintains proper focus order", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
render(<TestMenu />);
|
render(<TestMenu />);
|
||||||
|
|
||||||
const items = screen.getAllByRole("menuitem");
|
const items = screen.getAllByRole("menuitem");
|
||||||
@@ -340,7 +338,6 @@ describe("ContextMenu Components Accessibility", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("announces selection state changes", async () => {
|
it("announces selection state changes", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
<ContextMenuItem onClick={vi.fn()} selected={false}>
|
<ContextMenuItem onClick={vi.fn()} selected={false}>
|
||||||
Test Item
|
Test Item
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { expect, test, describe, it, vi } from "vitest";
|
import { expect, describe, it, vi } from "vitest";
|
||||||
import { axe, toHaveNoViolations } from "jest-axe";
|
import { axe, toHaveNoViolations } from "jest-axe";
|
||||||
import Select from "../../app/components/Select";
|
import Select from "../../app/components/Select";
|
||||||
|
|
||||||
@@ -136,7 +136,6 @@ describe("Select Component Accessibility", () => {
|
|||||||
|
|
||||||
describe("Screen Reader Support", () => {
|
describe("Screen Reader Support", () => {
|
||||||
it("announces selected option", async () => {
|
it("announces selected option", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
render(<Select {...defaultProps} value="option2" />);
|
render(<Select {...defaultProps} value="option2" />);
|
||||||
|
|
||||||
const selectButton = screen.getByRole("button");
|
const selectButton = screen.getByRole("button");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, test, describe, it, vi } from "vitest";
|
import { expect, test, describe, vi } from "vitest";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { axe, toHaveNoViolations } from "jest-axe";
|
import { axe, toHaveNoViolations } from "jest-axe";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, test, describe, it, vi } from "vitest";
|
import { expect, test, describe, vi } from "vitest";
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
import { axe, toHaveNoViolations } from "jest-axe";
|
import { axe, toHaveNoViolations } from "jest-axe";
|
||||||
import Toggle from "../../app/components/Toggle";
|
import Toggle from "../../app/components/Toggle";
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ test.describe("Accessibility Testing", () => {
|
|||||||
// Test Enter key activation
|
// Test Enter key activation
|
||||||
await page.keyboard.press("Enter");
|
await page.keyboard.press("Enter");
|
||||||
await page.waitForTimeout(100); // Brief pause to see if action occurs
|
await page.waitForTimeout(100); // Brief pause to see if action occurs
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
// If focus fails, skip this button
|
// If focus fails, skip this button
|
||||||
console.log(`Could not focus button ${i}: ${error.message}`);
|
console.log(`Could not focus button ${i}: ${_error.message}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,7 +332,7 @@ test.describe("Accessibility Testing", () => {
|
|||||||
|
|
||||||
// Page should handle errors gracefully
|
// Page should handle errors gracefully
|
||||||
await expect(page.locator("body")).toBeVisible();
|
await expect(page.locator("body")).toBeVisible();
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
// If reload fails, that's also acceptable - page should handle errors gracefully
|
// If reload fails, that's also acceptable - page should handle errors gracefully
|
||||||
await expect(page.locator("body")).toBeVisible();
|
await expect(page.locator("body")).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,6 @@ describe("RadioButton Accessibility", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maintains focus management", async () => {
|
it("maintains focus management", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
|
|
||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
|
|||||||
@@ -227,7 +227,6 @@ describe("RadioGroup Accessibility", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maintains focus management", async () => {
|
it("maintains focus management", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
const handleChange = vi.fn();
|
const handleChange = vi.fn();
|
||||||
|
|
||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ describe("Accessibility - Component Level", () => {
|
|||||||
|
|
||||||
// Check for proper heading structure (optional for header components)
|
// Check for proper heading structure (optional for header components)
|
||||||
try {
|
try {
|
||||||
const headings = screen.getAllByRole("heading");
|
screen.getAllByRole("heading");
|
||||||
// Headings are not required in header components, so this is optional
|
// Headings are not required in header components, so this is optional
|
||||||
} catch (error) {
|
} catch {
|
||||||
// No headings found, which is fine for a header component
|
// No headings found, which is fine for a header component
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -119,10 +119,10 @@ describe("Accessibility - Component Level", () => {
|
|||||||
try {
|
try {
|
||||||
element.focus();
|
element.focus();
|
||||||
expect(element).toHaveFocus();
|
expect(element).toHaveFocus();
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Some elements might not be focusable in test environment
|
// Some elements might not be focusable in test environment
|
||||||
// This is acceptable for accessibility testing
|
// This is acceptable for accessibility testing
|
||||||
console.log(`Could not focus element: ${error.message}`);
|
// Intentionally ignore focus failures in JSDOM
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -144,7 +144,7 @@ describe("Accessibility - Component Level", () => {
|
|||||||
let headings;
|
let headings;
|
||||||
try {
|
try {
|
||||||
headings = screen.getAllByRole("heading");
|
headings = screen.getAllByRole("heading");
|
||||||
} catch (error) {
|
} catch {
|
||||||
// No headings found, which is fine for a header component
|
// No headings found, which is fine for a header component
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,27 +33,6 @@ const mockBlogPost = {
|
|||||||
"<p>This is the main content of the test article.</p><p>It has multiple paragraphs.</p>",
|
"<p>This is the main content of the test article.</p><p>It has multiple paragraphs.</p>",
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRelatedPosts = [
|
|
||||||
{
|
|
||||||
slug: "related-article-1",
|
|
||||||
frontmatter: {
|
|
||||||
title: "Related Article 1",
|
|
||||||
description: "First related article",
|
|
||||||
author: "Test Author",
|
|
||||||
date: "2025-04-14",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: "related-article-2",
|
|
||||||
frontmatter: {
|
|
||||||
title: "Related Article 2",
|
|
||||||
description: "Second related article",
|
|
||||||
author: "Test Author",
|
|
||||||
date: "2025-04-13",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe("Content Page Rendering E2E", () => {
|
describe("Content Page Rendering E2E", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import Logo from "../../app/components/Logo";
|
import Logo from "../../app/components/Logo";
|
||||||
|
|
||||||
// Mock Next.js Link component
|
// Mock Next.js Link component
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ test.describe("Edge Cases and Error Scenarios", () => {
|
|||||||
// Trigger a harmless error
|
// Trigger a harmless error
|
||||||
try {
|
try {
|
||||||
throw new Error("Test error");
|
throw new Error("Test error");
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
// Error handled
|
// Error handled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { expect, test, describe, vi } from "vitest";
|
import { expect, test, describe, vi } from "vitest";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import Checkbox from "../../app/components/Checkbox";
|
import Checkbox from "../../app/components/Checkbox";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { render, screen, cleanup } from "@testing-library/react";
|
import { render, screen, cleanup } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import { describe, test, expect, afterEach } from "vitest";
|
||||||
import { vi, describe, test, expect, afterEach } from "vitest";
|
|
||||||
import ContentLockup from "../../app/components/ContentLockup";
|
import ContentLockup from "../../app/components/ContentLockup";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { expect, test, describe, it, vi } from "vitest";
|
import { expect, describe, it, vi } from "vitest";
|
||||||
import ContextMenu from "../../app/components/ContextMenu";
|
import ContextMenu from "../../app/components/ContextMenu";
|
||||||
import ContextMenuItem from "../../app/components/ContextMenuItem";
|
import ContextMenuItem from "../../app/components/ContextMenuItem";
|
||||||
import ContextMenuSection from "../../app/components/ContextMenuSection";
|
import ContextMenuSection from "../../app/components/ContextMenuSection";
|
||||||
@@ -77,7 +77,6 @@ describe("ContextMenu Components Integration", () => {
|
|||||||
it("shows submenu indicators correctly", () => {
|
it("shows submenu indicators correctly", () => {
|
||||||
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
|
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
|
||||||
|
|
||||||
const setting1 = screen.getByText("Setting 1");
|
|
||||||
const arrow = screen
|
const arrow = screen
|
||||||
.getByRole("menuitem", { name: "Setting 1" })
|
.getByRole("menuitem", { name: "Setting 1" })
|
||||||
.querySelector("svg");
|
.querySelector("svg");
|
||||||
@@ -87,7 +86,6 @@ describe("ContextMenu Components Integration", () => {
|
|||||||
|
|
||||||
describe("Keyboard Navigation", () => {
|
describe("Keyboard Navigation", () => {
|
||||||
it("navigates through menu items with arrow keys", async () => {
|
it("navigates through menu items with arrow keys", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
|
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
|
||||||
|
|
||||||
const items = screen.getAllByRole("menuitem");
|
const items = screen.getAllByRole("menuitem");
|
||||||
@@ -137,7 +135,6 @@ describe("ContextMenu Components Integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("skips disabled items during navigation", async () => {
|
it("skips disabled items during navigation", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
|
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
|
||||||
|
|
||||||
const items = screen.getAllByRole("menuitem");
|
const items = screen.getAllByRole("menuitem");
|
||||||
@@ -153,7 +150,7 @@ describe("ContextMenu Components Integration", () => {
|
|||||||
describe("Dynamic Menu Updates", () => {
|
describe("Dynamic Menu Updates", () => {
|
||||||
const DynamicMenu = ({ items, selectedValue, onItemClick }) => (
|
const DynamicMenu = ({ items, selectedValue, onItemClick }) => (
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
{items.map((item, index) => (
|
{items.map((item) => (
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => onItemClick(item.id)}
|
onClick={() => onItemClick(item.id)}
|
||||||
@@ -301,7 +298,6 @@ describe("ContextMenu Components Integration", () => {
|
|||||||
|
|
||||||
describe("Performance", () => {
|
describe("Performance", () => {
|
||||||
it("handles large menu lists efficiently", async () => {
|
it("handles large menu lists efficiently", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
const largeItems = Array.from({ length: 100 }, (_, i) => ({
|
const largeItems = Array.from({ length: 100 }, (_, i) => ({
|
||||||
id: `item${i}`,
|
id: `item${i}`,
|
||||||
label: `Item ${i}`,
|
label: `Item ${i}`,
|
||||||
@@ -329,7 +325,6 @@ describe("ContextMenu Components Integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("handles rapid state changes", async () => {
|
it("handles rapid state changes", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuItem onClick={vi.fn()} selected={false}>
|
<ContextMenuItem onClick={vi.fn()} selected={false}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
import { expect, test, describe, vi } from "vitest";
|
import { expect, test, describe, vi } from "vitest";
|
||||||
import Input from "../../app/components/Input";
|
import Input from "../../app/components/Input";
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ describe("RadioButton Integration", () => {
|
|||||||
|
|
||||||
render(<TestForm />);
|
render(<TestForm />);
|
||||||
|
|
||||||
const option1 = screen.getByText("Option 1").closest("label");
|
|
||||||
const option2 = screen.getByText("Option 2").closest("label");
|
const option2 = screen.getByText("Option 2").closest("label");
|
||||||
const submitButton = screen.getByRole("button");
|
const submitButton = screen.getByRole("button");
|
||||||
|
|
||||||
@@ -55,7 +54,6 @@ describe("RadioButton Integration", () => {
|
|||||||
|
|
||||||
it("handles keyboard navigation", async () => {
|
it("handles keyboard navigation", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const handleChange = vi.fn();
|
|
||||||
|
|
||||||
function KeyboardForm() {
|
function KeyboardForm() {
|
||||||
const [value, setValue] = useState("option1");
|
const [value, setValue] = useState("option1");
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ describe("RadioGroup Integration", () => {
|
|||||||
|
|
||||||
it("handles keyboard navigation", async () => {
|
it("handles keyboard navigation", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const handleChange = vi.fn();
|
|
||||||
|
|
||||||
function KeyboardForm() {
|
function KeyboardForm() {
|
||||||
const [value, setValue] = useState("option1");
|
const [value, setValue] = useState("option1");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import RelatedArticles from "../../app/components/RelatedArticles";
|
import RelatedArticles from "../../app/components/RelatedArticles";
|
||||||
|
|
||||||
// Mock ContentThumbnailTemplate
|
// Mock ContentThumbnailTemplate
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { expect, test, describe, it, vi } from "vitest";
|
import { expect, describe, it } from "vitest";
|
||||||
import Select from "../../app/components/Select";
|
import Select from "../../app/components/Select";
|
||||||
|
|
||||||
describe("Select Component Integration", () => {
|
describe("Select Component Integration", () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
import { describe, it, expect, vi } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import Switch from "../../app/components/Switch";
|
import Switch from "../../app/components/Switch";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { expect, test, describe, it, vi } from "vitest";
|
import { expect, test, describe, vi } from "vitest";
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import TextArea from "../../app/components/TextArea";
|
import TextArea from "../../app/components/TextArea";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { expect, test, describe, it, vi } from "vitest";
|
import { expect, test, describe, vi } from "vitest";
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import Toggle from "../../app/components/Toggle";
|
import Toggle from "../../app/components/Toggle";
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ describe("ToggleGroup Integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("handles state changes", async () => {
|
it("handles state changes", async () => {
|
||||||
const { rerender } = render(<TestForm />);
|
render(<TestForm />);
|
||||||
const toggleGroups = screen.getAllByRole("button");
|
const toggleGroups = screen.getAllByRole("button");
|
||||||
|
|
||||||
// Initially, left should be selected
|
// Initially, left should be selected
|
||||||
@@ -182,7 +182,7 @@ describe("ToggleGroup Integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("handles rapid state changes", async () => {
|
it("handles rapid state changes", async () => {
|
||||||
const { rerender } = render(<TestForm />);
|
render(<TestForm />);
|
||||||
const toggleGroups = screen.getAllByRole("button");
|
const toggleGroups = screen.getAllByRole("button");
|
||||||
|
|
||||||
// Rapidly change states
|
// Rapidly change states
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { render, screen, cleanup } from "@testing-library/react";
|
import { render, screen, cleanup } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { vi, describe, test, expect, afterEach } from "vitest";
|
import { describe, test, expect, afterEach } from "vitest";
|
||||||
import HeroBanner from "../../app/components/HeroBanner";
|
import HeroBanner from "../../app/components/HeroBanner";
|
||||||
import NumberedCards from "../../app/components/NumberedCards";
|
import NumberedCards from "../../app/components/NumberedCards";
|
||||||
import RuleStack from "../../app/components/RuleStack";
|
import RuleStack from "../../app/components/RuleStack";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { render, screen, cleanup } from "@testing-library/react";
|
import { render, screen, cleanup } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { vi, describe, test, expect, afterEach } from "vitest";
|
import { describe, test, expect, afterEach } from "vitest";
|
||||||
import Header from "../../app/components/Header";
|
import Header from "../../app/components/Header";
|
||||||
import Footer from "../../app/components/Footer";
|
import Footer from "../../app/components/Footer";
|
||||||
|
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ describe("Page Flow Integration", () => {
|
|||||||
);
|
);
|
||||||
expect(cards.length).toBeGreaterThan(0);
|
expect(cards.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check that all three cards are rendered
|
// Check that all three cards are rendered
|
||||||
const cards = screen.getAllByText(
|
const cards = screen.getAllByText(
|
||||||
/Document how your community|Build an operating manual|Get a link to your manual/,
|
/Document how your community|Build an operating manual|Get a link to your manual/,
|
||||||
@@ -206,7 +206,7 @@ describe("Page Flow Integration", () => {
|
|||||||
const headings = screen.getAllByRole("heading");
|
const headings = screen.getAllByRole("heading");
|
||||||
expect(headings.length).toBeGreaterThan(4); // Should have multiple headings
|
expect(headings.length).toBeGreaterThan(4); // Should have multiple headings
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check for proper heading hierarchy
|
// Check for proper heading hierarchy
|
||||||
const headings = screen.getAllByRole("heading");
|
const headings = screen.getAllByRole("heading");
|
||||||
expect(headings.length).toBeGreaterThan(4); // Should have multiple headings
|
expect(headings.length).toBeGreaterThan(4); // Should have multiple headings
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ describe("User Journey Integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("user explores the process through numbered cards", async () => {
|
test("user explores the process through numbered cards", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
render(<Page />);
|
render(<Page />);
|
||||||
|
|
||||||
// Wait for dynamically imported NumberedCards component
|
// Wait for dynamically imported NumberedCards component
|
||||||
@@ -153,7 +152,6 @@ describe("User Journey Integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("user accesses contact information through footer", async () => {
|
test("user accesses contact information through footer", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
render(
|
render(
|
||||||
<div>
|
<div>
|
||||||
<Header />
|
<Header />
|
||||||
@@ -179,7 +177,6 @@ describe("User Journey Integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("user explores features and benefits", async () => {
|
test("user explores features and benefits", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
render(<Page />);
|
render(<Page />);
|
||||||
|
|
||||||
// Wait for dynamically imported FeatureGrid component
|
// Wait for dynamically imported FeatureGrid component
|
||||||
@@ -201,7 +198,6 @@ describe("User Journey Integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("user interacts with logo wall and social proof", async () => {
|
test("user interacts with logo wall and social proof", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
render(
|
render(
|
||||||
<div>
|
<div>
|
||||||
<Page />
|
<Page />
|
||||||
@@ -232,7 +228,6 @@ describe("User Journey Integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("user completes the full journey from discovery to action", async () => {
|
test("user completes the full journey from discovery to action", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
render(
|
render(
|
||||||
<div>
|
<div>
|
||||||
<Header />
|
<Header />
|
||||||
@@ -278,7 +273,6 @@ describe("User Journey Integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("user can access all navigation options consistently", async () => {
|
test("user can access all navigation options consistently", async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
render(
|
render(
|
||||||
<div>
|
<div>
|
||||||
<Header />
|
<Header />
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ class WebPerformanceMonitor extends PerformanceMonitor {
|
|||||||
/**
|
/**
|
||||||
* Measure page load performance
|
* Measure page load performance
|
||||||
*/
|
*/
|
||||||
async measurePageLoad(url) {
|
async measurePageLoad() {
|
||||||
return this.measureFunction("page_load", async () => {
|
return this.measureFunction("page_load", async () => {
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
|
|
||||||
@@ -326,18 +326,26 @@ class PlaywrightPerformanceMonitor extends PerformanceMonitor {
|
|||||||
// Navigate to the page
|
// Navigate to the page
|
||||||
// Use "load" instead of "networkidle" to handle dynamically imported components
|
// Use "load" instead of "networkidle" to handle dynamically imported components
|
||||||
// "networkidle" can timeout with code splitting as chunks load asynchronously
|
// "networkidle" can timeout with code splitting as chunks load asynchronously
|
||||||
await this.page.goto(url, {
|
await this.page.goto(url, {
|
||||||
waitUntil: "load",
|
waitUntil: "load",
|
||||||
timeout: 60000, // 60 second timeout for slower networks
|
timeout: 60000, // 60 second timeout for slower networks
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle interstitial/blocking errors
|
// Handle interstitial/blocking errors
|
||||||
if (error.message.includes("interstitial") || error.message.includes("prevented")) {
|
if (
|
||||||
console.warn("Page load was blocked, attempting to continue:", error.message);
|
error.message.includes("interstitial") ||
|
||||||
|
error.message.includes("prevented")
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
"Page load was blocked, attempting to continue:",
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
// Try to wait for the page to be in a usable state
|
// Try to wait for the page to be in a usable state
|
||||||
try {
|
try {
|
||||||
await this.page.waitForLoadState("domcontentloaded", { timeout: 10000 });
|
await this.page.waitForLoadState("domcontentloaded", {
|
||||||
} catch (e) {
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
throw new Error(`Page failed to load: ${error.message}`);
|
throw new Error(`Page failed to load: ${error.message}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -349,9 +357,11 @@ class PlaywrightPerformanceMonitor extends PerformanceMonitor {
|
|||||||
// This ensures code-split components have loaded
|
// This ensures code-split components have loaded
|
||||||
try {
|
try {
|
||||||
// Wait for main content sections that use dynamic imports
|
// Wait for main content sections that use dynamic imports
|
||||||
await this.page.waitForSelector("section", { timeout: 10000 }).catch(() => {
|
await this.page
|
||||||
// Ignore if sections don't appear - page might still be valid
|
.waitForSelector("section", { timeout: 10000 })
|
||||||
});
|
.catch(() => {
|
||||||
|
// Ignore if sections don't appear - page might still be valid
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Continue even if some components haven't loaded - we still want to measure performance
|
// Continue even if some components haven't loaded - we still want to measure performance
|
||||||
console.warn("Some components may not have loaded:", error.message);
|
console.warn("Some components may not have loaded:", error.message);
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ export const SingleSelectionInteraction = {
|
|||||||
export const FormIntegration = {
|
export const FormIntegration = {
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
const radioGroup = canvas.getByRole("radiogroup");
|
|
||||||
const radioButtons = canvas.getAllByRole("radio");
|
const radioButtons = canvas.getAllByRole("radio");
|
||||||
|
|
||||||
// Should have hidden inputs for form submission
|
// Should have hidden inputs for form submission
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ test.describe("RadioGroup Storybook Tests", () => {
|
|||||||
test("interacts with controls", async ({ page }) => {
|
test("interacts with controls", async ({ page }) => {
|
||||||
// Test mode control
|
// Test mode control
|
||||||
await page.selectOption('[data-testid="mode-control"]', "inverse");
|
await page.selectOption('[data-testid="mode-control"]', "inverse");
|
||||||
const radioGroup = page.locator('[role="radiogroup"]');
|
|
||||||
const radioButtons = page.locator('[role="radio"]');
|
const radioButtons = page.locator('[role="radio"]');
|
||||||
|
|
||||||
// All radio buttons should have inverse styling
|
// All radio buttons should have inverse styling
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ vi.mock("../../app/components/ContentBanner", () => {
|
|||||||
|
|
||||||
vi.mock("../../app/components/RelatedArticles", () => {
|
vi.mock("../../app/components/RelatedArticles", () => {
|
||||||
return {
|
return {
|
||||||
default: ({ relatedPosts, currentPostSlug }) => (
|
default: ({ relatedPosts }) => (
|
||||||
<div data-testid="related-articles">
|
<div data-testid="related-articles">
|
||||||
<h2>Related Articles</h2>
|
<h2>Related Articles</h2>
|
||||||
{relatedPosts.map((post) => (
|
{relatedPosts.map((post) => (
|
||||||
@@ -200,7 +200,7 @@ describe("BlogPostPage", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("related-articles")).toBeInTheDocument();
|
expect(screen.getByTestId("related-articles")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText("Related Articles")).toBeInTheDocument();
|
expect(screen.getByText("Related Articles")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("related-related-1")).toBeInTheDocument();
|
expect(screen.getByTestId("related-related-1")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("related-related-2")).toBeInTheDocument();
|
expect(screen.getByTestId("related-related-2")).toBeInTheDocument();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { expect, test, describe, it, vi, beforeEach } from "vitest";
|
import { expect, describe, it, vi, beforeEach } from "vitest";
|
||||||
import { axe, toHaveNoViolations } from "jest-axe";
|
import { axe, toHaveNoViolations } from "jest-axe";
|
||||||
import ContextMenu from "../../app/components/ContextMenu";
|
import ContextMenu from "../../app/components/ContextMenu";
|
||||||
import ContextMenuItem from "../../app/components/ContextMenuItem";
|
import ContextMenuItem from "../../app/components/ContextMenuItem";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { render, screen, cleanup } from "@testing-library/react";
|
import { render, screen, cleanup } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import { describe, test, expect, afterEach } from "vitest";
|
||||||
import { vi, describe, test, expect, afterEach } from "vitest";
|
|
||||||
import FeatureGrid from "../../app/components/FeatureGrid";
|
import FeatureGrid from "../../app/components/FeatureGrid";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, test, expect } from "vitest";
|
import { describe, test, expect } from "vitest";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import Footer from "../../app/components/Footer";
|
import Footer from "../../app/components/Footer";
|
||||||
|
|
||||||
describe("Footer", () => {
|
describe("Footer", () => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, test, expect, vi, beforeEach } from "vitest";
|
import { describe, test, expect, beforeEach } from "vitest";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import Header, {
|
import Header, {
|
||||||
navigationItems,
|
navigationItems,
|
||||||
avatarImages,
|
avatarImages,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { render, screen, cleanup } from "@testing-library/react";
|
import { render, screen, cleanup } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import { describe, test, expect, afterEach } from "vitest";
|
||||||
import { vi, describe, test, expect, afterEach } from "vitest";
|
|
||||||
import HeroBanner from "../../app/components/HeroBanner";
|
import HeroBanner from "../../app/components/HeroBanner";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { render, screen, cleanup } from "@testing-library/react";
|
import { render, screen, cleanup } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import { describe, test, expect, afterEach } from "vitest";
|
||||||
import { vi, describe, test, expect, afterEach } from "vitest";
|
|
||||||
import LogoWall from "../../app/components/LogoWall";
|
import LogoWall from "../../app/components/LogoWall";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { render, screen, cleanup } from "@testing-library/react";
|
import { render, screen, cleanup } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import { describe, test, expect, afterEach } from "vitest";
|
||||||
import { vi, describe, test, expect, afterEach } from "vitest";
|
|
||||||
import NumberedCards from "../../app/components/NumberedCards";
|
import NumberedCards from "../../app/components/NumberedCards";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -100,7 +100,8 @@ describe("Page", () => {
|
|||||||
// Wait for dynamically imported FeatureGrid component to load
|
// Wait for dynamically imported FeatureGrid component to load
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
expect(
|
||||||
screen.getAllByText("We've got your back, every step of the way").length,
|
screen.getAllByText("We've got your back, every step of the way")
|
||||||
|
.length,
|
||||||
).toBeGreaterThan(0);
|
).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
@@ -143,7 +144,8 @@ describe("Page", () => {
|
|||||||
// FeatureGrid
|
// FeatureGrid
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
expect(
|
||||||
screen.getAllByText("We've got your back, every step of the way").length,
|
screen.getAllByText("We've got your back, every step of the way")
|
||||||
|
.length,
|
||||||
).toBeGreaterThan(0);
|
).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -212,7 +214,7 @@ describe("Page", () => {
|
|||||||
|
|
||||||
// Check all section titles (using getAllByText since there are multiple instances)
|
// Check all section titles (using getAllByText since there are multiple instances)
|
||||||
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0);
|
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Wait for dynamically imported components
|
// Wait for dynamically imported components
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
expect(
|
||||||
@@ -221,7 +223,8 @@ describe("Page", () => {
|
|||||||
});
|
});
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
expect(
|
||||||
screen.getAllByText("We've got your back, every step of the way").length,
|
screen.getAllByText("We've got your back, every step of the way")
|
||||||
|
.length,
|
||||||
).toBeGreaterThan(0);
|
).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
expect(screen.getAllByText("Still have questions?").length).toBeGreaterThan(
|
expect(screen.getAllByText("Still have questions?").length).toBeGreaterThan(
|
||||||
@@ -236,7 +239,8 @@ describe("Page", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// Check all three numbered card items (using getAllByText since there are multiple instances)
|
// Check all three numbered card items (using getAllByText since there are multiple instances)
|
||||||
expect(
|
expect(
|
||||||
screen.getAllByText("Document how your community makes decisions").length,
|
screen.getAllByText("Document how your community makes decisions")
|
||||||
|
.length,
|
||||||
).toBeGreaterThan(0);
|
).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
@@ -256,7 +260,7 @@ describe("Page", () => {
|
|||||||
|
|
||||||
// Check subtitles (using getAllByText since there are multiple instances)
|
// Check subtitles (using getAllByText since there are multiple instances)
|
||||||
expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0);
|
expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Wait for dynamically imported components
|
// Wait for dynamically imported components
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { render, screen, cleanup } from "@testing-library/react";
|
import { render, screen, cleanup } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import { vi, describe, test, expect, afterEach } from "vitest";
|
import { vi, describe, test, expect, afterEach } from "vitest";
|
||||||
import QuoteBlock from "../../app/components/QuoteBlock";
|
import QuoteBlock from "../../app/components/QuoteBlock";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { describe, it, expect, vi } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import RadioButton from "../../app/components/RadioButton";
|
import RadioButton from "../../app/components/RadioButton";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, expect, vi, beforeEach, it } from "vitest";
|
||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import RelatedArticles from "../../app/components/RelatedArticles";
|
import RelatedArticles from "../../app/components/RelatedArticles";
|
||||||
|
|
||||||
// Mock Next.js components
|
// Mock Next.js components
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { render, screen, cleanup } from "@testing-library/react";
|
import { render, screen, cleanup } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { vi, describe, test, expect, afterEach } from "vitest";
|
import { vi, describe, test, expect, afterEach } from "vitest";
|
||||||
|
import { logger } from "../../lib/logger";
|
||||||
import RuleStack from "../../app/components/RuleStack";
|
import RuleStack from "../../app/components/RuleStack";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -99,16 +100,18 @@ describe("RuleStack Component", () => {
|
|||||||
|
|
||||||
test("handles template click events", async () => {
|
test("handles template click events", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
const debugSpy = vi
|
||||||
|
.spyOn(logger, "debug")
|
||||||
|
.mockImplementation(() => undefined);
|
||||||
|
|
||||||
render(<RuleStack />);
|
render(<RuleStack />);
|
||||||
|
|
||||||
const consensusCard = screen.getByText("Consensus").closest("div");
|
const consensusCard = screen.getByText("Consensus").closest("div");
|
||||||
await user.click(consensusCard);
|
await user.click(consensusCard);
|
||||||
|
|
||||||
expect(consoleSpy).toHaveBeenCalledWith("Consensus template clicked");
|
expect(debugSpy).toHaveBeenCalledWith("Consensus template clicked");
|
||||||
|
|
||||||
consoleSpy.mockRestore();
|
debugSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders with proper semantic structure", () => {
|
test("renders with proper semantic structure", () => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from "react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { expect, test, describe, it, vi } from "vitest";
|
import { expect, describe, it, vi } from "vitest";
|
||||||
import { axe, toHaveNoViolations } from "jest-axe";
|
import { axe, toHaveNoViolations } from "jest-axe";
|
||||||
import Select from "../../app/components/Select";
|
import Select from "../../app/components/Select";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, test, describe, it, vi } from "vitest";
|
import { expect, test, describe, vi } from "vitest";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import TextArea from "../../app/components/TextArea";
|
import TextArea from "../../app/components/TextArea";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, test, describe, it, vi } from "vitest";
|
import { expect, test, describe, vi } from "vitest";
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
import Toggle from "../../app/components/Toggle";
|
import Toggle from "../../app/components/Toggle";
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useClickOutside } from "../../../app/hooks/useClickOutside";
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
|
||||||
describe("useClickOutside", () => {
|
describe("useClickOutside", () => {
|
||||||
let handler: ReturnType<typeof vi.fn>;
|
let handler;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
handler = vi.fn();
|
handler = vi.fn();
|
||||||
@@ -26,7 +26,9 @@ describe("useClickOutside", () => {
|
|||||||
result.current.current = div;
|
result.current.current = div;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
document.body.dispatchEvent(
|
||||||
|
new MouseEvent("mousedown", { bubbles: true }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(handler).toHaveBeenCalledTimes(1);
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
@@ -55,14 +57,16 @@ describe("useClickOutside", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("does not call handler when disabled", () => {
|
test("does not call handler when disabled", () => {
|
||||||
const { result } = renderHook(() => {
|
renderHook(() => {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
useClickOutside([ref], handler, false);
|
useClickOutside([ref], handler, false);
|
||||||
return ref;
|
return ref;
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
document.body.dispatchEvent(
|
||||||
|
new MouseEvent("mousedown", { bubbles: true }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(handler).not.toHaveBeenCalled();
|
expect(handler).not.toHaveBeenCalled();
|
||||||
@@ -84,7 +88,9 @@ describe("useClickOutside", () => {
|
|||||||
result.current.ref2.current = div2;
|
result.current.ref2.current = div2;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
document.body.dispatchEvent(
|
||||||
|
new MouseEvent("mousedown", { bubbles: true }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(handler).toHaveBeenCalledTimes(1);
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
|
|||||||
+1
-5
@@ -1,9 +1,5 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import path from "path";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -64,7 +60,7 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
thresholds: { lines: 50, functions: 50, statements: 50, branches: 50 },
|
thresholds: { lines: 50, functions: 50, statements: 50, branches: 50 },
|
||||||
// Disable coverage collection in CI to prevent test failures
|
// Disable coverage collection in CI to prevent test failures
|
||||||
enabled: !process.env.CI,
|
enabled: !(typeof process !== "undefined" && process.env.CI),
|
||||||
},
|
},
|
||||||
pool: "threads", // Use threads for better performance
|
pool: "threads", // Use threads for better performance
|
||||||
testTimeout: 60000, // 60s timeout for all tests
|
testTimeout: 60000, // 60s timeout for all tests
|
||||||
|
|||||||
+9
-8
@@ -16,7 +16,7 @@ vi.mock("next/dynamic", () => {
|
|||||||
return function DynamicComponent(props: any) {
|
return function DynamicComponent(props: any) {
|
||||||
const [Component, setComponent] = React.useState(null);
|
const [Component, setComponent] = React.useState(null);
|
||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
importFn()
|
importFn()
|
||||||
.then((mod: any) => {
|
.then((mod: any) => {
|
||||||
@@ -27,15 +27,15 @@ vi.mock("next/dynamic", () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading && options?.loading) {
|
if (loading && options?.loading) {
|
||||||
return options.loading();
|
return options.loading();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Component) {
|
if (Component) {
|
||||||
return React.createElement(Component, props);
|
return React.createElement(Component, props);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -49,18 +49,19 @@ Object.defineProperty(window, "matchMedia", {
|
|||||||
// Parse the media query to determine if it matches
|
// Parse the media query to determine if it matches
|
||||||
const minWidthMatch = query.match(/min-width:\s*(\d+)px/);
|
const minWidthMatch = query.match(/min-width:\s*(\d+)px/);
|
||||||
const maxWidthMatch = query.match(/max-width:\s*(\d+)px/);
|
const maxWidthMatch = query.match(/max-width:\s*(\d+)px/);
|
||||||
|
|
||||||
// Use window.innerWidth if set by tests, otherwise default to desktop (1200px)
|
// Use window.innerWidth if set by tests, otherwise default to desktop (1200px)
|
||||||
// This allows tests to override viewport width by setting window.innerWidth
|
// This allows tests to override viewport width by setting window.innerWidth
|
||||||
const viewportWidth = (typeof window !== "undefined" && window.innerWidth) || 1200;
|
const viewportWidth =
|
||||||
|
(typeof window !== "undefined" && window.innerWidth) || 1200;
|
||||||
let matches = true;
|
let matches = true;
|
||||||
|
|
||||||
if (minWidthMatch) {
|
if (minWidthMatch) {
|
||||||
matches = viewportWidth >= parseInt(minWidthMatch[1], 10);
|
matches = viewportWidth >= parseInt(minWidthMatch[1], 10);
|
||||||
} else if (maxWidthMatch) {
|
} else if (maxWidthMatch) {
|
||||||
matches = viewportWidth <= parseInt(maxWidthMatch[1], 10);
|
matches = viewportWidth <= parseInt(maxWidthMatch[1], 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
matches,
|
matches,
|
||||||
media: query,
|
media: query,
|
||||||
|
|||||||
Reference in New Issue
Block a user