Fix TypeScript matcher typing issue
CI Pipeline / test (pull_request) Successful in 7m5s
CI Pipeline / lint (pull_request) Has been cancelled
CI Pipeline / build (pull_request) Has been cancelled
CI Pipeline / e2e (webkit) (pull_request) Has been cancelled
CI Pipeline / e2e (chromium) (pull_request) Successful in 54m11s
CI Pipeline / e2e (firefox) (pull_request) Failing after 22m9s
CI Pipeline / visual-regression (pull_request) Successful in 11m50s
CI Pipeline / performance (pull_request) Successful in 13m59s
CI Pipeline / test (pull_request) Successful in 7m5s
CI Pipeline / lint (pull_request) Has been cancelled
CI Pipeline / build (pull_request) Has been cancelled
CI Pipeline / e2e (webkit) (pull_request) Has been cancelled
CI Pipeline / e2e (chromium) (pull_request) Successful in 54m11s
CI Pipeline / e2e (firefox) (pull_request) Failing after 22m9s
CI Pipeline / visual-regression (pull_request) Successful in 11m50s
CI Pipeline / performance (pull_request) Successful in 13m59s
This commit is contained in:
@@ -444,19 +444,6 @@ jobs:
|
|||||||
.next/test-results
|
.next/test-results
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
storybook:
|
|
||||||
runs-on: [self-hosted, macos-latest]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "${{ env.NODE_VERSION }}"
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci --no-audit --fund=false
|
|
||||||
- run: npm run storybook:build:github
|
|
||||||
# Storybook is used for component documentation only.
|
|
||||||
# Component tests (tests/components/*.test.tsx) provide all test coverage.
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
runs-on: [self-hosted, macos-latest]
|
runs-on: [self-hosted, macos-latest]
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"name": "maxNumericValue",
|
|
||||||
"expected": 5000,
|
|
||||||
"actual": 6316.213500000003,
|
|
||||||
"values": [6352.161499999999, 6316.213500000003, 6810.3611],
|
|
||||||
"operator": "<=",
|
|
||||||
"passed": false,
|
|
||||||
"auditId": "interactive",
|
|
||||||
"level": "warn",
|
|
||||||
"url": "http://127.0.0.1:3010/",
|
|
||||||
"auditTitle": "Time to Interactive",
|
|
||||||
"auditDocumentationLink": "https://developer.chrome.com/docs/lighthouse/performance/interactive/"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Test/build artifacts
|
||||||
|
coverage/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
lhci-results/
|
||||||
|
.lighthouseci/
|
||||||
|
|
||||||
|
# Storybook build output
|
||||||
|
storybook-static/
|
||||||
|
|
||||||
|
# Misc generated
|
||||||
|
*.log
|
||||||
@@ -169,7 +169,9 @@ const ContentLockup = memo<ContentLockupProps>(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subtitle */}
|
{/* Subtitle */}
|
||||||
{subtitle ? <h2 className={styles.subtitle}>{subtitle}</h2> : null}
|
{subtitle ? (
|
||||||
|
<h2 className={styles.subtitle}>{subtitle}</h2>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
|
|||||||
@@ -73,9 +73,7 @@ const FeatureGrid = memo<FeatureGridProps>(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* MiniCard Grid */}
|
{/* MiniCard Grid */}
|
||||||
<div
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-[var(--spacing-scale-012)] mt-[var(--spacing-scale-048)] lg:mt-0 lg:flex-grow lg:shrink-0">
|
||||||
className="grid grid-cols-2 md:grid-cols-4 gap-[var(--spacing-scale-012)] mt-[var(--spacing-scale-048)] lg:mt-0 lg:flex-grow lg:shrink-0"
|
|
||||||
>
|
|
||||||
{features.map((feature, index) => (
|
{features.map((feature, index) => (
|
||||||
<MiniCard
|
<MiniCard
|
||||||
key={index}
|
key={index}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ This directory contains project documentation organized by topic.
|
|||||||
### For Testing
|
### For Testing
|
||||||
|
|
||||||
Start with **[TESTING_GUIDE.md](./TESTING_GUIDE.md)** for:
|
Start with **[TESTING_GUIDE.md](./TESTING_GUIDE.md)** for:
|
||||||
|
|
||||||
- Testing philosophy and approach
|
- Testing philosophy and approach
|
||||||
- Component testing with `componentTestSuite`
|
- Component testing with `componentTestSuite`
|
||||||
- E2E testing with Playwright
|
- E2E testing with Playwright
|
||||||
@@ -27,6 +28,7 @@ Start with **[TESTING_GUIDE.md](./TESTING_GUIDE.md)** for:
|
|||||||
### For Custom Hooks
|
### For Custom Hooks
|
||||||
|
|
||||||
See **[CUSTOM_HOOKS.md](./CUSTOM_HOOKS.md)** for:
|
See **[CUSTOM_HOOKS.md](./CUSTOM_HOOKS.md)** for:
|
||||||
|
|
||||||
- Available custom hooks
|
- Available custom hooks
|
||||||
- Usage examples
|
- Usage examples
|
||||||
- API documentation
|
- API documentation
|
||||||
@@ -34,6 +36,7 @@ See **[CUSTOM_HOOKS.md](./CUSTOM_HOOKS.md)** for:
|
|||||||
### For Content Creation
|
### For Content Creation
|
||||||
|
|
||||||
See **[content-creation.md](./guides/content-creation.md)** for:
|
See **[content-creation.md](./guides/content-creation.md)** for:
|
||||||
|
|
||||||
- How to create blog articles
|
- How to create blog articles
|
||||||
- Content guidelines
|
- Content guidelines
|
||||||
- Contribution workflow
|
- Contribution workflow
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ const config: ComponentTestSuiteConfig<Props> = {
|
|||||||
error: {
|
error: {
|
||||||
props: { error: true } as Partial<Props>,
|
props: { error: true } as Partial<Props>,
|
||||||
assert: (el) => {
|
assert: (el) => {
|
||||||
expect(el).toHaveClass("border-[var(--color-border-default-utility-negative)]");
|
expect(el).toHaveClass(
|
||||||
|
"border-[var(--color-border-default-utility-negative)]",
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -214,4 +216,3 @@ Accessibility is tested at two levels:
|
|||||||
- Tests complete user journeys for accessibility barriers
|
- Tests complete user journeys for accessibility barriers
|
||||||
|
|
||||||
This two-tier approach ensures both individual components and full page experiences meet accessibility standards.
|
This two-tier approach ensures both individual components and full page experiences meet accessibility standards.
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ describe("AskOrganizer (behavioral tests)", () => {
|
|||||||
|
|
||||||
it("renders subtitle when provided", () => {
|
it("renders subtitle when provided", () => {
|
||||||
render(<AskOrganizer title="Test" subtitle="Subtitle" />);
|
render(<AskOrganizer title="Test" subtitle="Subtitle" />);
|
||||||
expect(screen.getByRole("heading", { name: "Subtitle" })).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByRole("heading", { name: "Subtitle" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders button with default text", () => {
|
it("renders button with default text", () => {
|
||||||
|
|||||||
@@ -63,4 +63,3 @@ describe("Button (behavioral tests)", () => {
|
|||||||
expect(link).toHaveAttribute("href", "/learn");
|
expect(link).toHaveAttribute("href", "/learn");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,4 +26,3 @@ componentTestSuite<CheckboxProps>({
|
|||||||
disabledProps: { disabled: true },
|
disabledProps: { disabled: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ vi.mock("next/link", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../lib/assetUtils", async (importOriginal) => {
|
vi.mock("../../lib/assetUtils", async (importOriginal) => {
|
||||||
const actual = (await importOriginal()) as typeof import("../../lib/assetUtils");
|
const actual =
|
||||||
|
(await importOriginal()) as typeof import("../../lib/assetUtils");
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
getAssetPath: vi.fn((asset: string) => `/assets/${asset}`),
|
getAssetPath: vi.fn((asset: string) => `/assets/${asset}`),
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ componentTestSuite<ContextMenuProps>({
|
|||||||
component: ContextMenu,
|
component: ContextMenu,
|
||||||
name: "ContextMenu",
|
name: "ContextMenu",
|
||||||
props: {
|
props: {
|
||||||
children: (
|
children: <ContextMenuItem>Item</ContextMenuItem>,
|
||||||
<ContextMenuItem>
|
|
||||||
Item
|
|
||||||
</ContextMenuItem>
|
|
||||||
),
|
|
||||||
} as ContextMenuProps,
|
} as ContextMenuProps,
|
||||||
requiredProps: [],
|
requiredProps: [],
|
||||||
primaryRole: "menu",
|
primaryRole: "menu",
|
||||||
@@ -25,4 +21,3 @@ componentTestSuite<ContextMenuProps>({
|
|||||||
errorState: false,
|
errorState: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,4 +23,3 @@ componentTestSuite<ContextMenuItemProps>({
|
|||||||
disabledProps: { disabled: true },
|
disabledProps: { disabled: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -60,14 +60,24 @@ describe("Footer (behavioral tests)", () => {
|
|||||||
|
|
||||||
it("renders navigation links", () => {
|
it("renders navigation links", () => {
|
||||||
render(<Footer />);
|
render(<Footer />);
|
||||||
expect(screen.getAllByRole("link", { name: "Use cases" }).length).toBeGreaterThan(0);
|
expect(
|
||||||
expect(screen.getAllByRole("link", { name: "Learn" }).length).toBeGreaterThan(0);
|
screen.getAllByRole("link", { name: "Use cases" }).length,
|
||||||
expect(screen.getAllByRole("link", { name: "About" }).length).toBeGreaterThan(0);
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByRole("link", { name: "Learn" }).length,
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByRole("link", { name: "About" }).length,
|
||||||
|
).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders legal links", () => {
|
it("renders legal links", () => {
|
||||||
render(<Footer />);
|
render(<Footer />);
|
||||||
expect(screen.getAllByRole("link", { name: "Privacy Policy" }).length).toBeGreaterThan(0);
|
expect(
|
||||||
expect(screen.getAllByRole("link", { name: "Terms of Service" }).length).toBeGreaterThan(0);
|
screen.getAllByRole("link", { name: "Privacy Policy" }).length,
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByRole("link", { name: "Terms of Service" }).length,
|
||||||
|
).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,4 +19,3 @@ componentTestSuite<HeaderProps>({
|
|||||||
errorState: false,
|
errorState: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ describe("HeroBanner (behavioral tests)", () => {
|
|||||||
|
|
||||||
it("renders subtitle when provided", () => {
|
it("renders subtitle when provided", () => {
|
||||||
render(<HeroBanner title="Test" subtitle="Subtitle" />);
|
render(<HeroBanner title="Test" subtitle="Subtitle" />);
|
||||||
expect(screen.getByRole("heading", { name: "Subtitle" })).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByRole("heading", { name: "Subtitle" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders hero image", () => {
|
it("renders hero image", () => {
|
||||||
@@ -56,9 +58,7 @@ describe("HeroBanner (behavioral tests)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders CTA button when provided", () => {
|
it("renders CTA button when provided", () => {
|
||||||
render(
|
render(<HeroBanner title="Test" ctaText="Get Started" ctaHref="/start" />);
|
||||||
<HeroBanner title="Test" ctaText="Get Started" ctaHref="/start" />,
|
|
||||||
);
|
|
||||||
expect(
|
expect(
|
||||||
screen.getAllByRole("button", { name: "Get Started" }).length,
|
screen.getAllByRole("button", { name: "Get Started" }).length,
|
||||||
).toBeGreaterThan(0);
|
).toBeGreaterThan(0);
|
||||||
|
|||||||
@@ -27,4 +27,3 @@ componentTestSuite<InputProps>({
|
|||||||
errorProps: { error: true },
|
errorProps: { error: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,7 @@ describe("Logo (behavioral tests)", () => {
|
|||||||
|
|
||||||
it("renders logo icon", () => {
|
it("renders logo icon", () => {
|
||||||
render(<Logo />);
|
render(<Logo />);
|
||||||
expect(
|
expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument();
|
||||||
screen.getByAltText("CommunityRule Logo Icon"),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders text by default", () => {
|
it("renders text by default", () => {
|
||||||
@@ -50,9 +48,7 @@ describe("Logo (behavioral tests)", () => {
|
|||||||
it("hides text when showText is false", () => {
|
it("hides text when showText is false", () => {
|
||||||
render(<Logo showText={false} />);
|
render(<Logo showText={false} />);
|
||||||
expect(screen.queryByText("CommunityRule")).not.toBeInTheDocument();
|
expect(screen.queryByText("CommunityRule")).not.toBeInTheDocument();
|
||||||
expect(
|
expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument();
|
||||||
screen.getByAltText("CommunityRule Logo Icon"),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders with different size variants", () => {
|
it("renders with different size variants", () => {
|
||||||
|
|||||||
@@ -27,4 +27,3 @@ componentTestSuite<RadioButtonProps>({
|
|||||||
disabledProps: { disabled: true },
|
disabledProps: { disabled: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,4 +25,3 @@ componentTestSuite<RadioGroupProps>({
|
|||||||
errorState: false,
|
errorState: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -50,19 +50,13 @@ const mockPosts: BlogPost[] = [
|
|||||||
describe("RelatedArticles", () => {
|
describe("RelatedArticles", () => {
|
||||||
it("renders without crashing", () => {
|
it("renders without crashing", () => {
|
||||||
render(
|
render(
|
||||||
<RelatedArticles
|
<RelatedArticles relatedPosts={mockPosts} currentPostSlug="current" />,
|
||||||
relatedPosts={mockPosts}
|
|
||||||
currentPostSlug="current"
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders related articles", () => {
|
it("renders related articles", () => {
|
||||||
render(
|
render(
|
||||||
<RelatedArticles
|
<RelatedArticles relatedPosts={mockPosts} currentPostSlug="current" />,
|
||||||
relatedPosts={mockPosts}
|
|
||||||
currentPostSlug="current"
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId("thumbnail-article-1")).toBeInTheDocument();
|
expect(screen.getByTestId("thumbnail-article-1")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("thumbnail-article-2")).toBeInTheDocument();
|
expect(screen.getByTestId("thumbnail-article-2")).toBeInTheDocument();
|
||||||
@@ -70,10 +64,7 @@ describe("RelatedArticles", () => {
|
|||||||
|
|
||||||
it("filters out current post", () => {
|
it("filters out current post", () => {
|
||||||
render(
|
render(
|
||||||
<RelatedArticles
|
<RelatedArticles relatedPosts={mockPosts} currentPostSlug="article-1" />,
|
||||||
relatedPosts={mockPosts}
|
|
||||||
currentPostSlug="article-1"
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
expect(screen.queryByTestId("thumbnail-article-1")).not.toBeInTheDocument();
|
expect(screen.queryByTestId("thumbnail-article-1")).not.toBeInTheDocument();
|
||||||
expect(screen.getByTestId("thumbnail-article-2")).toBeInTheDocument();
|
expect(screen.getByTestId("thumbnail-article-2")).toBeInTheDocument();
|
||||||
|
|||||||
@@ -21,4 +21,3 @@ componentTestSuite<SectionHeaderProps>({
|
|||||||
errorState: false,
|
errorState: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -32,4 +32,3 @@ componentTestSuite<SelectProps>({
|
|||||||
errorProps: { error: true },
|
errorProps: { error: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,4 +26,3 @@ componentTestSuite<SwitchProps>({
|
|||||||
disabledProps: { disabled: true },
|
disabledProps: { disabled: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -28,4 +28,3 @@ componentTestSuite<TextAreaProps>({
|
|||||||
errorProps: { error: true },
|
errorProps: { error: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,4 +24,3 @@ componentTestSuite<ToggleProps>({
|
|||||||
disabledProps: { disabled: true },
|
disabledProps: { disabled: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,4 +20,3 @@ componentTestSuite<ToggleGroupProps>({
|
|||||||
errorState: false,
|
errorState: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import React from "react";
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, 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 userEvent from "@testing-library/user-event";
|
||||||
import { axe, toHaveNoViolations } from "jest-axe";
|
import { axe } from "jest-axe";
|
||||||
|
|
||||||
expect.extend(toHaveNoViolations);
|
|
||||||
|
|
||||||
type TestCases = {
|
type TestCases = {
|
||||||
renders?: boolean;
|
renders?: boolean;
|
||||||
@@ -134,9 +132,14 @@ export function componentTestSuite<TProps>(
|
|||||||
|
|
||||||
// Render again with optional props omitted to ensure no runtime error
|
// Render again with optional props omitted to ensure no runtime error
|
||||||
const { unmount } = render(
|
const { unmount } = render(
|
||||||
<Component {...({ ...props, ...Object.fromEntries(
|
<Component
|
||||||
Object.keys(optionalProps).map((k) => [k, undefined]),
|
{...({
|
||||||
) } as TProps)} />,
|
...props,
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.keys(optionalProps).map((k) => [k, undefined]),
|
||||||
|
),
|
||||||
|
} as TProps)}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Basic sanity check: component is mounted
|
// Basic sanity check: component is mounted
|
||||||
@@ -149,7 +152,8 @@ export function componentTestSuite<TProps>(
|
|||||||
it("has no obvious accessibility violations (axe)", async () => {
|
it("has no obvious accessibility violations (axe)", async () => {
|
||||||
const { container } = render(<Component {...props} />);
|
const { container } = render(<Component {...props} />);
|
||||||
const results = await axe(container);
|
const results = await axe(container);
|
||||||
expect(results).toHaveNoViolations();
|
// Avoid relying on Jest matcher typings in Vitest/Next typecheck context.
|
||||||
|
expect(results.violations).toHaveLength(0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,4 +222,3 @@ export function componentTestSuite<TProps>(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user