docs: added tests for components in api-reference project (#14428)

* add tests (WIP)

* added test for h2

* finished adding tests

* fixes

* fixes

* fixes
This commit is contained in:
Shahed Nasser
2026-01-05 10:56:56 +02:00
committed by GitHub
parent fb772f0f6a
commit 4d632e7a5d
102 changed files with 8278 additions and 127 deletions

View File

@@ -1,5 +1,6 @@
"use server"
import React from "react"
import type { OpenAPI } from "types"
import Section from "../Section"
import MDXContentServer from "../MDXContent/Server"

View File

@@ -1,4 +1,5 @@
import DividedLayout from "@/layouts/Divided"
import React from "react"
import { Loading } from "docs-ui"
type DividedLoadingProps = {

View File

@@ -0,0 +1,41 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
// mock hooks
const mockUseArea = vi.fn(() => ({
area: "store",
}))
// mock components
vi.mock("@/providers/area", () => ({
useArea: () => mockUseArea(),
}))
import DownloadFull from ".."
beforeEach(() => {
cleanup()
vi.clearAllMocks()
})
describe("render", () => {
test("render download link for store area", () => {
const { getByTestId } = render(<DownloadFull />)
const link = getByTestId("download-full-link")
expect(link).toBeInTheDocument()
expect(link).toHaveAttribute("href", "/download/store")
expect(link).toHaveAttribute("download")
expect(link).toHaveAttribute("target", "_blank")
})
test("render download link for admin area", () => {
mockUseArea.mockReturnValue({ area: "admin" })
const { getByTestId } = render(<DownloadFull />)
const link = getByTestId("download-full-link")
expect(link).toBeInTheDocument()
expect(link).toHaveAttribute("href", "/download/admin")
expect(link).toHaveAttribute("download")
expect(link).toHaveAttribute("target", "_blank")
})
})

View File

@@ -1,7 +1,8 @@
"use client"
import React from "react"
import { Button } from "docs-ui"
import { useArea } from "../../providers/area"
import { useArea } from "@/providers/area"
import Link from "next/link"
const DownloadFull = () => {
@@ -9,7 +10,12 @@ const DownloadFull = () => {
return (
<Button variant="secondary">
<Link href={`/download/${area}`} download target="_blank">
<Link
href={`/download/${area}`}
download
target="_blank"
data-testid="download-full-link"
>
Download OpenApi Specs Collection
</Link>
</Button>

View File

@@ -0,0 +1,46 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
// mock hooks
const mockUseArea = vi.fn(() => ({
area: "store",
}))
// mock components
vi.mock("@/providers/area", () => ({
useArea: () => mockUseArea(),
}))
vi.mock("docs-ui", async () => {
const actual = await vi.importActual<typeof React>("docs-ui")
return {
...actual,
Feedback: vi.fn(({ extraData }: { extraData: { area: string } }) => (
<div data-testid="feedback" data-area={extraData.area}>Feedback</div>
)),
}
})
import {Feedback} from ".."
beforeEach(() => {
cleanup()
vi.clearAllMocks()
})
describe("render", () => {
test("render feedback component for store area", () => {
const { getByTestId } = render(<Feedback />)
const feedback = getByTestId("feedback")
expect(feedback).toBeInTheDocument()
expect(feedback).toHaveAttribute("data-area", "store")
})
test("render feedback component for admin area", () => {
mockUseArea.mockReturnValue({ area: "admin" })
const { getByTestId } = render(<Feedback />)
const feedback = getByTestId("feedback")
expect(feedback).toBeInTheDocument()
expect(feedback).toHaveAttribute("data-area", "admin")
})
})

View File

@@ -1,11 +1,12 @@
"use client"
import React from "react"
import {
Feedback as UiFeedback,
FeedbackProps,
DocsTrackingEvents,
} from "docs-ui"
import { useArea } from "../../providers/area"
import { useArea } from "@/providers/area"
export const Feedback = (props: Partial<FeedbackProps>) => {
const { area } = useArea()

View File

@@ -0,0 +1,92 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
// mock functions
const mockScrollToElement = vi.fn()
const mockUseScrollController = vi.fn(() => ({
scrollableElement: null as HTMLElement | null,
scrollToElement: mockScrollToElement,
}))
const mockUseSidebar = vi.fn(() => ({
activePath: null as string | null,
}))
// mock components
vi.mock("docs-ui", () => ({
useScrollController: () => mockUseScrollController(),
useSidebar: () => mockUseSidebar(),
H2: ({
passRef,
...props
}: {
passRef: React.RefObject<HTMLHeadingElement>
} & React.HTMLAttributes<HTMLHeadingElement>) => (
<h2 {...props} ref={passRef} />
),
}))
vi.mock("docs-utils", () => ({
getSectionId: vi.fn(() => "section-id"),
}))
import H2 from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("render", () => {
test("renders children", () => {
const { container } = render(<H2>Test</H2>)
const h2 = container.querySelector("h2")
expect(h2).toBeInTheDocument()
expect(h2).toHaveTextContent("Test")
})
test("renders with custom props", () => {
const { container } = render(<H2 className="test-class">Test</H2>)
const h2 = container.querySelector("h2")
expect(h2).toBeInTheDocument()
expect(h2).toHaveClass("test-class")
})
})
describe("section id", () => {
test("uses generated id", () => {
const { container } = render(<H2>Test</H2>)
const h2 = container.querySelector("h2")
expect(h2).toBeInTheDocument()
expect(h2).toHaveAttribute("id", "section-id")
})
})
describe("scroll", () => {
test("scrolls to element when active path matches id", () => {
mockUseScrollController.mockReturnValueOnce({
scrollableElement: document.body,
scrollToElement: mockScrollToElement,
})
mockUseSidebar.mockReturnValueOnce({
activePath: "section-id",
})
render(<H2>Test</H2>)
const heading = document.querySelector("h2")
expect(heading).toBeInTheDocument()
expect(mockScrollToElement).toHaveBeenCalledWith(heading)
})
test("does not scroll to element when active path does not match id", () => {
mockUseScrollController.mockReturnValueOnce({
scrollableElement: document.body,
scrollToElement: mockScrollToElement,
})
mockUseSidebar.mockReturnValueOnce({
activePath: "other-section-id",
})
render(<H2>Test</H2>)
const heading = document.querySelector("h2")
expect(heading).toBeInTheDocument()
expect(mockScrollToElement).not.toHaveBeenCalled()
})
})

View File

@@ -1,8 +1,8 @@
"use client"
import React, { useEffect, useMemo, useRef, useState } from "react"
import { useScrollController, useSidebar, H2 as UiH2 } from "docs-ui"
import { getSectionId } from "docs-utils"
import { useEffect, useMemo, useRef, useState } from "react"
type H2Props = React.HTMLAttributes<HTMLHeadingElement>

View File

@@ -0,0 +1,104 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, getByTestId, render, waitFor } from "@testing-library/react"
import { OpenAPI } from "types"
// mock data
const mockHttpSecuritySchema: OpenAPI.SecuritySchemeObject = {
type: "http",
scheme: "bearer",
description: "Authentication using Bearer token",
"x-displayName": "Bearer Token",
}
const mockApiKeySecuritySchema: OpenAPI.SecuritySchemeObject = {
type: "apiKey",
name: "Authorization",
description: "Authentication using API key",
"x-displayName": "API Key",
in: "header",
}
// mock components
vi.mock("@/components/MDXContent/Client", () => ({
default: ({ content }: { content: string }) => (
<div data-testid="mdx-content-client">{content}</div>
),
}))
vi.mock("@/components/MDXContent/Server", () => ({
default: ({ content }: { content: string }) => (
<div data-testid="mdx-content-server">{content}</div>
),
}))
vi.mock("@/utils/get-security-schema-type-name", () => ({
default: vi.fn(() => "security-schema-type"),
}))
vi.mock("docs-ui", () => ({
Loading: () => <div>Loading</div>,
}))
import SecurityDescription from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders security information for http security scheme", () => {
const { getByTestId } = render(
<SecurityDescription securitySchema={mockHttpSecuritySchema} />
)
const titleElement = getByTestId("title")
expect(titleElement).toBeInTheDocument()
expect(titleElement).toHaveTextContent(mockHttpSecuritySchema["x-displayName"] as string)
const securitySchemeTypeElement = getByTestId("security-scheme-type")
expect(securitySchemeTypeElement).toBeInTheDocument()
expect(securitySchemeTypeElement).toHaveTextContent("security-schema-type")
const securitySchemeTypeDetailsElement = getByTestId("security-scheme-type-details")
expect(securitySchemeTypeDetailsElement).toBeInTheDocument()
expect(securitySchemeTypeDetailsElement).toHaveTextContent("HTTP Authorization Scheme")
const securitySchemeTypeDetailsValueElement = getByTestId("security-scheme-type-details-value")
expect(securitySchemeTypeDetailsValueElement).toBeInTheDocument()
expect(securitySchemeTypeDetailsValueElement).toHaveTextContent(mockHttpSecuritySchema.scheme)
})
test("renders security information for api key security scheme", () => {
const { getByTestId } = render(
<SecurityDescription securitySchema={mockApiKeySecuritySchema} />
)
const titleElement = getByTestId("title")
expect(titleElement).toBeInTheDocument()
expect(titleElement).toHaveTextContent(mockApiKeySecuritySchema["x-displayName"] as string)
const securitySchemeTypeElement = getByTestId("security-scheme-type")
expect(securitySchemeTypeElement).toBeInTheDocument()
expect(securitySchemeTypeElement).toHaveTextContent("security-schema-type")
const securitySchemeTypeDetailsElement = getByTestId("security-scheme-type-details")
expect(securitySchemeTypeDetailsElement).toBeInTheDocument()
expect(securitySchemeTypeDetailsElement).toHaveTextContent("Cookie parameter name")
const securitySchemeTypeDetailsValueElement = getByTestId("security-scheme-type-details-value")
expect(securitySchemeTypeDetailsValueElement).toBeInTheDocument()
expect(securitySchemeTypeDetailsValueElement).toHaveTextContent(mockApiKeySecuritySchema.name)
})
test("render description with server component", async () => {
const { getByTestId } = render(
<SecurityDescription securitySchema={mockHttpSecuritySchema} isServer={true} />
)
await waitFor(() => {
const mdxContentServerElement = getByTestId("mdx-content-server")
expect(mdxContentServerElement).toBeInTheDocument()
expect(mdxContentServerElement).toHaveTextContent(mockHttpSecuritySchema.description as string)
})
})
test("render description with client component", async () => {
const { getByTestId } = render(
<SecurityDescription securitySchema={mockHttpSecuritySchema} isServer={false} />
)
await waitFor(() => {
const mdxContentClientElement = getByTestId("mdx-content-client")
expect(mdxContentClientElement).toBeInTheDocument()
expect(mdxContentClientElement).toHaveTextContent(mockHttpSecuritySchema.description as string)
})
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import type { MDXContentClientProps } from "@/components/MDXContent/Client"
import type { MDXContentServerProps } from "@/components/MDXContent/Server"
import type { OpenAPI } from "types"
@@ -7,14 +8,14 @@ import { Loading } from "docs-ui"
import dynamic from "next/dynamic"
const MDXContentClient = dynamic<MDXContentClientProps>(
async () => import("../../../MDXContent/Client"),
async () => import("@/components/MDXContent/Client"),
{
loading: () => <Loading />,
}
) as React.FC<MDXContentClientProps>
const MDXContentServer = dynamic<MDXContentServerProps>(
async () => import("../../../MDXContent/Server"),
async () => import("@/components/MDXContent/Server"),
{
loading: () => <Loading />,
}
@@ -31,22 +32,25 @@ const SecurityDescription = ({
}: SecurityDescriptionProps) => {
return (
<>
<h2>{securitySchema["x-displayName"] as string}</h2>
<h2 data-testid="title">{securitySchema["x-displayName"] as string}</h2>
{isServer && <MDXContentServer content={securitySchema.description} />}
{!isServer && <MDXContentClient content={securitySchema.description} />}
<p>
<p data-testid="security-scheme-type">
<strong>Security Scheme Type:</strong>{" "}
{getSecuritySchemaTypeName(securitySchema)}
</p>
{(securitySchema.type === "http" || securitySchema.type === "apiKey") && (
<p className={clsx("bg-medusa-bg-subtle", "p-1")}>
<p
className={clsx("bg-medusa-bg-subtle", "p-1")}
data-testid="security-scheme-type-details"
>
<strong>
{securitySchema.type === "http"
? "HTTP Authorization Scheme"
: "Cookie parameter name"}
:
</strong>{" "}
<code>
<code data-testid="security-scheme-type-details-value">
{securitySchema.type === "http"
? securitySchema.scheme
: securitySchema.name}

View File

@@ -0,0 +1,69 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render, waitFor } from "@testing-library/react"
import { OpenAPI } from "types"
// mock data
const mockSpecs: OpenAPI.OpenAPIV3.Document = {
openapi: "3.0.0",
info: {
title: "Test API",
version: "1.0.0",
},
paths: {},
components: {
securitySchemes: {
bearer: {
type: "http",
scheme: "bearer",
},
},
},
}
const mockSpecsWithRef: OpenAPI.OpenAPIV3.Document = {
...mockSpecs,
components: {
securitySchemes: {
bearer: {
$ref: "#/components/securitySchemes/bearer",
},
},
},
}
// mock components
vi.mock("@/components/MDXComponents/Security/Description", () => ({
default: ({ securitySchema }: { securitySchema: OpenAPI.SecuritySchemeObject }) => (
<div data-testid="security-description">{securitySchema.type}</div>
),
}))
import Security from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("does not render when specs is not provided", () => {
const { container } = render(<Security />)
const securityDescriptionElements = container.querySelectorAll("[data-testid='security-description']")
expect(securityDescriptionElements).toHaveLength(0)
})
test("renders security information for specs", async () => {
const { getByTestId } = render(<Security specs={mockSpecs} />)
await waitFor(() => {
const securityDescriptionElement = getByTestId("security-description")
expect(securityDescriptionElement).toBeInTheDocument()
expect(securityDescriptionElement).toHaveTextContent("http")
})
})
test("does not render when security scheme is a $ref", () => {
const { container } = render(<Security specs={mockSpecsWithRef} />)
const securityDescriptionElements = container.querySelectorAll("[data-testid='security-description']")
expect(securityDescriptionElements).toHaveLength(0)
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import dynamic from "next/dynamic"
import type { OpenAPI } from "types"
import type { SecurityDescriptionProps } from "./Description"

View File

@@ -1,3 +1,4 @@
import React from "react"
import type { MDXComponents } from "mdx/types"
import Security from "./Security"
import type { OpenAPI } from "types"

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import { useEffect, useState } from "react"
import getCustomComponents from "../../MDXComponents"
import type { ScopeType } from "../../MDXComponents"

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
"use server"
import React from "react"
import { MDXRemote } from "next-mdx-remote/rsc"
import getCustomComponents from "../../MDXComponents"
import type { ScopeType } from "../../MDXComponents"

View File

@@ -0,0 +1,63 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
// mock data
const mockMethod = "get"
const mockMethod2 = "post"
const mockMethod3 = "delete"
// mock components
vi.mock("docs-ui", () => ({
Badge: ({
children,
variant,
className
}: {
children: React.ReactNode,
variant: string,
className: string
}) => (
<div data-testid="badge" data-variant={variant} className={className}>{children}</div>
),
capitalize: vi.fn((text: string) => text.charAt(0).toUpperCase() + text.slice(1)),
}))
import MethodLabel from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders method label for get method", () => {
const { getByTestId } = render(<MethodLabel method={mockMethod} />)
const badgeElement = getByTestId("badge")
expect(badgeElement).toBeInTheDocument()
expect(badgeElement).toHaveTextContent("Get")
expect(badgeElement).toHaveAttribute("data-variant", "green")
})
test("renders method label for post method", () => {
const { getByTestId } = render(<MethodLabel method={mockMethod2} />)
const badgeElement = getByTestId("badge")
expect(badgeElement).toBeInTheDocument()
expect(badgeElement).toHaveTextContent("Post")
expect(badgeElement).toHaveAttribute("data-variant", "blue")
})
test("renders method label for delete method", () => {
const { getByTestId } = render(<MethodLabel method={mockMethod3} />)
const badgeElement = getByTestId("badge")
expect(badgeElement).toBeInTheDocument()
expect(badgeElement).toHaveTextContent("Del")
expect(badgeElement).toHaveAttribute("data-variant", "red")
})
test("renders method label with className", () => {
const { getByTestId } = render(<MethodLabel method={mockMethod} className="test-class" />)
const badgeElement = getByTestId("badge")
expect(badgeElement).toBeInTheDocument()
expect(badgeElement).toHaveTextContent("Get")
expect(badgeElement).toHaveAttribute("data-variant", "green")
expect(badgeElement).toHaveClass("test-class")
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import clsx from "clsx"
import { Badge, capitalize } from "docs-ui"

View File

@@ -0,0 +1,59 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
// mock components
vi.mock("@/components/Section/Divider", () => ({
default: () => <div data-testid="section-divider">Section Divider</div>,
}))
vi.mock("docs-ui", () => ({
WideSection: ({ children }: { children: React.ReactNode }) => (
<div data-testid="wide-section">{children}</div>
),
}))
import SectionContainer from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders section container with children", () => {
const { getByTestId } = render(<SectionContainer>Test</SectionContainer>)
const wideSectionElement = getByTestId("wide-section")
expect(wideSectionElement).toBeInTheDocument()
expect(wideSectionElement).toHaveTextContent("Test")
})
test("renders section container with no top padding", () => {
const { getByTestId } = render(<SectionContainer noTopPadding>Test</SectionContainer>)
const sectionContainerElement = getByTestId("section-container")
expect(sectionContainerElement).not.toHaveClass("pt-7")
})
test("renders section container with top padding", () => {
const { getByTestId } = render(<SectionContainer noTopPadding={false}>Test</SectionContainer>)
const sectionContainerElement = getByTestId("section-container")
expect(sectionContainerElement).toHaveClass("pt-7")
})
test("renders section container with divider", () => {
const { getByTestId } = render(<SectionContainer>Test</SectionContainer>)
const sectionDividerElement = getByTestId("section-divider")
expect(sectionDividerElement).toBeInTheDocument()
})
test("renders section container with no divider", () => {
const { container } = render(<SectionContainer noDivider>Test</SectionContainer>)
const sectionDividerElement = container.querySelectorAll("[data-testid='section-divider']")
expect(sectionDividerElement).toHaveLength(0)
})
test("renders section container with className", () => {
const { getByTestId } = render(<SectionContainer className="test-class">Test</SectionContainer>)
const sectionContainerElement = getByTestId("section-container")
expect(sectionContainerElement).toHaveClass("test-class")
})
test("renders section container with ref", () => {
const ref = vi.fn()
render(<SectionContainer ref={ref}>Test</SectionContainer>)
expect(ref).toHaveBeenCalled()
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import clsx from "clsx"
import SectionDivider from "../Divider"
import { forwardRef } from "react"
@@ -23,6 +24,7 @@ const SectionContainer = forwardRef<HTMLDivElement, SectionContainerProps>(
!noTopPadding && "pt-7",
className
)}
data-testid="section-container"
>
<WideSection>{children}</WideSection>
{!noDivider && <SectionDivider />}

View File

@@ -1,3 +1,4 @@
import React from "react"
import clsx from "clsx"
type SectionDividerProps = {

View File

@@ -0,0 +1,79 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
// mock functions
const mockSetActivePath = vi.fn()
const mockPush = vi.fn()
const mockReplace = vi.fn()
const mockUseActiveOnScroll = vi.fn((options: unknown) => ({
activeItemId: "",
}))
const mockUseSidebar = vi.fn(() => ({
setActivePath: mockSetActivePath,
}))
const mockUseRouter = vi.fn(() => ({
push: mockPush,
replace: mockReplace,
}))
// mock components
vi.mock("docs-ui", () => ({
useActiveOnScroll: (options: unknown) => mockUseActiveOnScroll(options),
useSidebar: () => mockUseSidebar(),
}))
vi.mock("next/navigation", () => ({
useRouter: () => mockUseRouter(),
}))
import Section from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
window.history.scrollRestoration = "auto"
})
describe("rendering", () => {
test("passes checkActiveOnScroll prop to useActiveOnScroll", () => {
render(<Section checkActiveOnScroll>Test</Section>)
expect(mockUseActiveOnScroll).toHaveBeenCalledWith({
rootElm: undefined,
enable: true,
useDefaultIfNoActive: false,
maxLevel: 2,
})
})
})
describe("effect hooks", () => {
test("sets active path when active item id is not empty", () => {
mockUseActiveOnScroll.mockReturnValue({
activeItemId: "test",
})
render(<Section>Test</Section>)
expect(mockSetActivePath).toHaveBeenCalledWith("test")
expect(mockPush).toHaveBeenCalledWith("#test", { scroll: false })
})
test("does not set active path when active item id is empty", () => {
mockUseActiveOnScroll.mockReturnValue({
activeItemId: "",
})
render(<Section>Test</Section>)
expect(mockSetActivePath).not.toHaveBeenCalled()
expect(mockPush).not.toHaveBeenCalled()
})
test("disables scroll restoration when history is available", () => {
render(<Section>Test</Section>)
expect(history.scrollRestoration).toBe("manual")
})
test("does not disable scroll restoration when history is not available", () => {
delete (window.history as any).scrollRestoration
render(<Section>Test</Section>)
expect(history.scrollRestoration).not.toBe("manual")
})
})

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import clsx from "clsx"
import { useActiveOnScroll, useSidebar } from "docs-ui"
import { useRouter } from "next/navigation"

View File

@@ -0,0 +1,39 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
import Space from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders space with default styles", () => {
const { getByTestId } = render(<Space />)
const spaceElement = getByTestId("space")
expect(spaceElement).toBeInTheDocument()
expect(spaceElement).toHaveStyle({
height: "1px",
marginTop: "0px",
marginBottom: "0px",
marginLeft: "0px",
marginRight: "0px",
})
})
test("renders space with correct styles", () => {
const { getByTestId } = render(<Space top={10} bottom={10} left={10} right={10} />)
const spaceElement = getByTestId("space")
expect(spaceElement).toBeInTheDocument()
expect(spaceElement).toHaveStyle({
height: "1px",
marginTop: "9px",
marginBottom: "9px",
marginLeft: "10px",
marginRight: "10px",
})
})
})

View File

@@ -1,3 +1,5 @@
import React from "react"
type SpaceProps = {
top?: number
bottom?: number
@@ -16,6 +18,7 @@ const Space = ({ top = 0, bottom = 0, left = 0, right = 0 }: SpaceProps) => {
marginLeft: `${left}px`,
marginRight: `${right}px`,
}}
data-testid="space"
></div>
)
}

View File

@@ -0,0 +1,85 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
// mock data
const mockCodeSamples = [
{
label: "Request Sample 1",
lang: "javascript",
source: "console.log('Request Sample 1')",
},
{
label: "Request Sample 2",
lang: "bash",
source: "echo 'Request Sample 2'",
},
]
// mock components
vi.mock("docs-ui", () => ({
CodeBlock: ({
source,
collapsed,
className,
lang
}: {
source: string,
collapsed: boolean,
className: string
lang: string
}) => (
<div data-testid="code-block" data-collapsed={collapsed} className={className} data-lang={lang}>{source}</div>
),
CodeTab: ({
children,
label,
value
}: {
children: React.ReactNode,
label: string,
value: string
}) => (
<div data-testid="code-tab" data-label={label} data-value={value}>{children}</div>
),
CodeTabs: ({
children,
group
}: {
children: React.ReactNode,
group: string
}) => (
<div data-testid="code-tabs" data-group={group}>{children}</div>
),
}))
vi.mock("slugify", () => ({
default: vi.fn((text: string) => text.toLowerCase()),
}))
import TagOperationCodeSectionRequestSamples from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders code tabs and blocks for each code sample", () => {
const { getByTestId, getAllByTestId } = render(
<TagOperationCodeSectionRequestSamples codeSamples={mockCodeSamples} />
)
const codeTabsElement = getByTestId("code-tabs")
expect(codeTabsElement).toBeInTheDocument()
expect(codeTabsElement).toHaveAttribute("data-group", "request-examples")
const codeBlocksElement = getAllByTestId("code-block")
expect(codeBlocksElement).toHaveLength(mockCodeSamples.length)
expect(codeBlocksElement[0]).toHaveAttribute("data-collapsed", "true")
expect(codeBlocksElement[1]).toHaveAttribute("data-collapsed", "true")
expect(codeBlocksElement[0]).toHaveClass("!mb-0")
expect(codeBlocksElement[1]).toHaveClass("!mb-0")
expect(codeBlocksElement[0]).toHaveAttribute("data-lang", mockCodeSamples[0].lang)
expect(codeBlocksElement[1]).toHaveAttribute("data-lang", mockCodeSamples[1].lang)
expect(codeBlocksElement[0]).toHaveTextContent(mockCodeSamples[0].source)
expect(codeBlocksElement[1]).toHaveTextContent(mockCodeSamples[1].source)
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import type { OpenAPI } from "types"
import { CodeBlock, CodeTab, CodeTabs } from "docs-ui"
import slugify from "slugify"

View File

@@ -0,0 +1,170 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, fireEvent, render } from "@testing-library/react"
import { OpenAPI } from "types"
// mock data
const mockResponse: OpenAPI.ResponseObject = {
description: "Mock response",
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: {
type: "string",
properties: {}
},
age: {
type: "number",
properties: {}
}
},
},
},
},
}
const mockExamples: OpenAPI.ExampleObject[] = [
{
title: "example 1",
value: "example 1",
content: "Example 1",
},
]
// mock function
const mockUseSchemaExample = vi.fn((options: unknown) => ({
examples: mockExamples
}))
// mock components and hooks
vi.mock("docs-ui", () => ({
CodeBlock: ({ source, collapsed, className, lang }: { source: string, collapsed: boolean, className: string, lang: string }) => (
<div data-testid="code-block" data-collapsed={collapsed} className={className} data-lang={lang}>{source}</div>
),
}))
vi.mock("@/hooks/use-schema-example", () => ({
default: (options: unknown) => mockUseSchemaExample(options),
}))
import TagsOperationCodeSectionResponsesSample from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("selects first example by default", () => {
const { getByTestId, container } = render(
<TagsOperationCodeSectionResponsesSample response={mockResponse} />
)
const codeBlockElement = getByTestId("code-block")
expect(codeBlockElement).toBeInTheDocument()
expect(codeBlockElement).toHaveAttribute("data-collapsed", "true")
expect(codeBlockElement).toHaveAttribute("data-lang", "json")
expect(codeBlockElement).toHaveTextContent("Example 1")
})
test("renders empty response when no examples are available", () => {
mockUseSchemaExample.mockReturnValue({
examples: [],
})
const { container } = render(
<TagsOperationCodeSectionResponsesSample response={mockResponse} />
)
expect(container).toHaveTextContent("Empty Response")
})
test("renders content type when content is available", () => {
const { container } = render(
<TagsOperationCodeSectionResponsesSample response={mockResponse} />
)
const contentTypeElement = container.querySelector("[data-testid='content-type']")
expect(contentTypeElement).toBeInTheDocument()
expect(contentTypeElement).toHaveTextContent("Content type: application/json")
})
test("doesn't render content type when content is empty", () => {
const modifiedResponse: OpenAPI.ResponseObject = {
...mockResponse,
content: {},
}
const { container } = render(
<TagsOperationCodeSectionResponsesSample response={modifiedResponse} />
)
const contentTypeElement = container.querySelector("[data-testid='content-type']")
expect(contentTypeElement).not.toBeInTheDocument()
})
test("doesn't render select for single example", () => {
mockUseSchemaExample.mockReturnValue({
examples: [
{ title: "example 1", value: "example 1", content: "Example 1" },
],
})
const { container } = render(
<TagsOperationCodeSectionResponsesSample response={mockResponse} />
)
const selectElement = container.querySelector("select")
expect(selectElement).not.toBeInTheDocument()
})
test("renders select for multiple examples", () => {
mockUseSchemaExample.mockReturnValue({
examples: [
{ title: "example 1", value: "example 1", content: "Example 1" },
{ title: "example 2", value: "example 2", content: "Example 2" },
],
})
const { container } = render(
<TagsOperationCodeSectionResponsesSample response={mockResponse} />
)
const selectElement = container.querySelector("select")
expect(selectElement).toBeInTheDocument()
const options = selectElement!.querySelectorAll("option")
expect(options).toHaveLength(2)
expect(options[0]).toHaveValue("example 1")
expect(options[1]).toHaveValue("example 2")
expect(options[0]).toHaveTextContent("example 1")
expect(options[1]).toHaveTextContent("example 2")
})
test("renders empty response when no example is selected", () => {
mockUseSchemaExample.mockReturnValue({
examples: [
{ title: "example 1", value: "example 1", content: "Example 1" },
{ title: "example 2", value: "example 2", content: "Example 2" },
],
})
const { container } = render(
<TagsOperationCodeSectionResponsesSample response={mockResponse} />
)
const selectElement = container.querySelector("select")
expect(selectElement).toBeInTheDocument()
fireEvent.change(selectElement!, { target: { value: "example 3" } })
const codeBlockElement = container.querySelector(
"[data-testid='code-block']"
)
expect(codeBlockElement).not.toBeInTheDocument()
expect(container).toHaveTextContent("Empty Response")
})
})
describe("interaction", () => {
test("renders code block for selected example when select is changed", () => {
mockUseSchemaExample.mockReturnValue({
examples: [
{ title: "example 1", value: "example 1", content: "Example 1" },
{ title: "example 2", value: "example 2", content: "Example 2" },
],
})
const { container, getByTestId } = render(
<TagsOperationCodeSectionResponsesSample response={mockResponse} />
)
const selectElement = container.querySelector("select")
fireEvent.change(selectElement!, { target: { value: "example 2" } })
const codeBlockElement = getByTestId("code-block")
expect(codeBlockElement).toHaveTextContent("Example 2")
})
})

View File

@@ -1,7 +1,8 @@
import React from "react"
import { CodeBlock } from "docs-ui"
import type { OpenAPI } from "types"
import { useEffect, useState } from "react"
import useSchemaExample from "../../../../../../hooks/use-schema-example"
import useSchemaExample from "@/hooks/use-schema-example"
export type TagsOperationCodeSectionResponsesSampleProps = {
response: OpenAPI.ResponseObject
@@ -27,11 +28,17 @@ const TagsOperationCodeSectionResponsesSample = ({
setSelectedExample(examples[0])
}, [examples])
const isEmptyResponse =
response?.content === undefined ||
Object.keys(response.content).length === 0
return (
<>
<div className={className}>
{response.content && (
<span>Content type: {Object.keys(response.content)[0]}</span>
{!isEmptyResponse && (
<span data-testid="content-type">
Content type: {Object.keys(response.content)[0]}
</span>
)}
<>
{examples.length > 1 && (
@@ -70,5 +77,5 @@ const TagsOperationCodeSectionResponsesSample = ({
export default TagsOperationCodeSectionResponsesSample
const getLanguageFromMedia = (media: string) => {
return media.substring(media.indexOf("/"))
return media.substring(media.indexOf("/") + 1)
}

View File

@@ -0,0 +1,89 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
import { OpenAPI } from "types"
// mock data
const mockOperation: OpenAPI.Operation = {
operationId: "mockOperation",
summary: "Mock Operation",
description: "Mock Operation",
"x-authenticated": false,
"x-codeSamples": [],
requestBody: {
content: {},
},
parameters: [],
responses: {
"200": {
description: "OK",
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
},
},
}
},
},
}
// mock components
vi.mock("docs-ui", () => ({
Badge: ({ variant, children }: { variant: string, children: React.ReactNode }) => (
<div data-testid="badge" data-variant={variant}>{children}</div>
),
}))
vi.mock("@/components/Tags/Operation/CodeSection/Responses/Sample", () => ({
default: () => <div data-testid="sample">Sample</div>,
}))
import TagsOperationCodeSectionResponses from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders response code badge", () => {
const { getByTestId } = render(
<TagsOperationCodeSectionResponses operation={mockOperation} />
)
const badgeElement = getByTestId("badge")
expect(badgeElement).toBeInTheDocument()
expect(badgeElement).toHaveTextContent("200")
expect(badgeElement).toHaveAttribute("data-variant", "green")
})
test("doesn't render when no response is available", () => {
const mockOperation: OpenAPI.Operation = {
operationId: "mockOperation",
summary: "Mock Operation",
description: "Mock Operation",
"x-authenticated": false,
"x-codeSamples": [],
requestBody: {
content: {},
},
parameters: [],
responses: {},
}
const { container } = render(
<TagsOperationCodeSectionResponses operation={mockOperation} />
)
expect(container).toBeEmptyDOMElement()
})
test("renders sample component", () => {
const { getByTestId } = render(
<TagsOperationCodeSectionResponses operation={mockOperation} />
)
const sampleElement = getByTestId("sample")
expect(sampleElement).toBeInTheDocument()
expect(sampleElement).toHaveTextContent("Sample")
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import type { OpenAPI } from "types"
import dynamic from "next/dynamic"
import type { TagsOperationCodeSectionResponsesSampleProps } from "./Sample"

View File

@@ -0,0 +1,107 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render, waitFor } from "@testing-library/react"
import { OpenAPI } from "types"
// mock data
const mockOperation: OpenAPI.Operation = {
operationId: "mockOperation",
summary: "Mock Operation",
description: "Mock Operation",
"x-authenticated": false,
"x-codeSamples": [
{
label: "Request Sample 1",
lang: "javascript",
source: "console.log('Request Sample 1')",
}
],
requestBody: {
content: {},
},
parameters: [],
responses: {
"200": {
description: "OK",
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
},
},
},
},
},
}
// mock components
vi.mock("@/components/MethodLabel", () => ({
default: ({ method }: { method: string }) => (
<div data-testid="method-label" data-method={method}>{method}</div>
),
}))
vi.mock("@/components/Tags/Operation/CodeSection/RequestSamples", () => ({
default: () => <div data-testid="request-samples">Request Samples</div>,
}))
vi.mock("@/components/Tags/Operation/CodeSection/Responses", () => ({
default: () => <div data-testid="responses">Responses</div>,
}))
vi.mock("docs-ui", () => ({
CopyButton: ({ text }: { text: string }) => (
<div data-testid="copy-button" data-text={text}>{text}</div>
),
}))
import TagsOperationCodeSection from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders method label, request samples, and responses", async () => {
const { getByTestId } = render(
<TagsOperationCodeSection operation={mockOperation} method="GET" endpointPath="/api/v1/users" />
)
const methodLabelElement = getByTestId("method-label")
expect(methodLabelElement).toBeInTheDocument()
expect(methodLabelElement).toHaveTextContent("GET")
const endpointPathElement = getByTestId("endpoint-path")
expect(endpointPathElement).toBeInTheDocument()
expect(endpointPathElement).toHaveTextContent("/api/v1/users")
const responsesElement = getByTestId("responses")
expect(responsesElement).toBeInTheDocument()
const copyButtonElement = getByTestId("copy-button")
expect(copyButtonElement).toBeInTheDocument()
expect(copyButtonElement).toHaveTextContent("/api/v1/users")
await waitFor(() => {
const requestSamplesElement = getByTestId("request-samples")
expect(requestSamplesElement).toBeInTheDocument()
})
})
test("doesn't render request samples when no code samples are available", () => {
const modifiedOperation: OpenAPI.Operation = {
...mockOperation,
"x-codeSamples": [],
}
const { container } = render(
<TagsOperationCodeSection operation={modifiedOperation} method="GET" endpointPath="/api/v1/users" />
)
const requestSamplesElement = container.querySelector("[data-testid='request-samples']")
expect(requestSamplesElement).not.toBeInTheDocument()
})
test("renders code section with className", () => {
const { getByTestId } = render(
<TagsOperationCodeSection operation={mockOperation} method="GET" endpointPath="/api/v1/users" className="test-class" />
)
const codeSectionElement = getByTestId("code-section")
expect(codeSectionElement).toHaveClass("test-class")
})
})

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import MethodLabel from "@/components/MethodLabel"
import type { OpenAPI } from "types"
import TagsOperationCodeSectionResponses from "./Responses"
@@ -27,7 +28,10 @@ const TagOperationCodeSection = ({
className,
}: TagOperationCodeSectionProps) => {
return (
<div className={clsx("mt-2 flex flex-col gap-2", className)}>
<div
className={clsx("mt-2 flex flex-col gap-2", className)}
data-testid="code-section"
>
<div
className={clsx(
"bg-medusa-bg-subtle border-medusa-border-base px-0.75 rounded border py-0.5",
@@ -36,7 +40,10 @@ const TagOperationCodeSection = ({
>
<div className={clsx("flex w-[calc(100%-36px)] gap-1")}>
<MethodLabel method={method} className="h-fit" />
<code className="text-medusa-fg-base =break-words break-all">
<code
className="text-medusa-fg-base =break-words break-all"
data-testid="endpoint-path"
>
{endpointPath}
</code>
</div>
@@ -44,7 +51,7 @@ const TagOperationCodeSection = ({
<SquareTwoStack className="text-medusa-fg-muted" />
</CopyButton>
</div>
{operation["x-codeSamples"] && (
{operation["x-codeSamples"] && operation["x-codeSamples"].length > 0 && (
<TagOperationCodeSectionRequestSamples
codeSamples={operation["x-codeSamples"]}
/>

View File

@@ -0,0 +1,54 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
// mock data
const mockDeprecationMessage = "This operation is deprecated"
// mock components
vi.mock("docs-ui", () => ({
Badge: ({ variant, children }: { variant: string, children: React.ReactNode }) => (
<div data-testid="badge" data-variant={variant}>{children}</div>
),
Tooltip: ({ text, children }: { text: string, children: React.ReactNode }) => (
<div data-testid="tooltip" data-text={text}>{children}</div>
),
}))
import TagsOperationDescriptionSectionDeprecationNotice from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders deprecation notice with tooltip", () => {
const { getByTestId } = render(
<TagsOperationDescriptionSectionDeprecationNotice deprecationMessage={mockDeprecationMessage} />
)
const badgeElement = getByTestId("badge")
expect(badgeElement).toBeInTheDocument()
expect(badgeElement).toHaveTextContent("deprecated")
expect(badgeElement).toHaveAttribute("data-variant", "orange")
const tooltipElement = getByTestId("tooltip")
expect(tooltipElement).toBeInTheDocument()
expect(tooltipElement).toHaveAttribute("data-text", mockDeprecationMessage)
})
test("doesn't render tooltip when no deprecation message is available", () => {
const { container } = render(
<TagsOperationDescriptionSectionDeprecationNotice />
)
const tooltipElement = container.querySelector("[data-testid='tooltip']")
expect(tooltipElement).not.toBeInTheDocument()
})
test("renders deprecation notice with className", () => {
const { getByTestId } = render(
<TagsOperationDescriptionSectionDeprecationNotice deprecationMessage={mockDeprecationMessage} className="test-class" />
)
const deprecationNoticeElement = getByTestId("deprecation-notice")
expect(deprecationNoticeElement).toHaveClass("test-class")
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import clsx from "clsx"
import { Badge, Tooltip } from "docs-ui"
@@ -15,7 +16,10 @@ const TagsOperationDescriptionSectionDeprecationNotice = ({
}
return (
<div className={clsx("inline-block", className)}>
<div
className={clsx("inline-block", className)}
data-testid="deprecation-notice"
>
{deprecationMessage && (
<Tooltip text={deprecationMessage}>{getBadge()}</Tooltip>
)}

View File

@@ -0,0 +1,246 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, fireEvent, render } from "@testing-library/react"
import { MenuItem, OpenAPI } from "types"
// mock data
const mockEvents: OpenAPI.OasEvents[] = [
{
name: "event1",
payload: "payload1",
description: "description1",
},
]
// mock functions
const mockParseEventPayload = vi.fn((payload: string) => {
return {
parsed_payload: payload,
payload_for_snippet: payload,
}
})
const mockHandleCopy = vi.fn()
const mockUseCopied = vi.fn((options: unknown) => ({
handleCopy: mockHandleCopy,
isCopied: false,
}))
const mockUseGenerateSnippet = vi.fn((options: unknown) => ({
snippet: "snippet",
}))
// mock components
vi.mock("docs-ui", () => ({
Badge: ({
variant,
children,
...props
}: { variant: string, children: React.ReactNode } & React.HTMLAttributes<HTMLDivElement>) => (
<div data-testid="badge" data-variant={variant} {...props}>{children}</div>
),
DetailsSummary: ({ title, subtitle }: { title: string, subtitle: React.ReactNode }) => (
<div data-testid="details-summary" data-title={title}>{title}</div>
),
DropdownMenu: ({
dropdownButtonContent,
menuItems
}: { dropdownButtonContent: React.ReactNode, menuItems: MenuItem[] }) => (
<div data-testid="dropdown-menu">
<div data-testid="dropdown-button">{dropdownButtonContent}</div>
<div data-testid="dropdown-menu-items">
{menuItems.map((item, index) => (
<div key={index} data-testid={"dropdown-menu-item"} onClick={() => {
if ("action" in item) {
item.action()
}
}}>
{"title" in item && item.title}
</div>
))}
</div>
</div>
),
Link: ({ href, children }: { href: string, children: React.ReactNode }) => (
<div data-testid="link" data-href={href}>{children}</div>
),
MarkdownContent: ({ children }: { children: React.ReactNode }) => (
<div data-testid="markdown-content">{children}</div>
),
parseEventPayload: (payload: string) => mockParseEventPayload(payload),
Tabs: ({ children }: { children: React.ReactNode }) => (
<div data-testid="tabs">{children}</div>
),
TabsContent: ({ value, children }: { value: string, children: React.ReactNode }) => (
<div data-testid="tabs-content" data-value={value}>{children}</div>
),
TabsContentWrapper: ({ children }: { children: React.ReactNode }) => (
<div data-testid="tabs-content-wrapper">{children}</div>
),
TabsList: ({ children }: { children: React.ReactNode }) => (
<div data-testid="tabs-list">{children}</div>
),
TabsTrigger: ({ value, children }: { value: string, children: React.ReactNode }) => (
<div data-testid="tabs-trigger" data-value={value}>{children}</div>
),
Tooltip: ({
text,
children,
...props
}: { text: string, children: React.ReactNode } & React.HTMLAttributes<HTMLDivElement>) => (
<div data-testid="tooltip" data-text={text} {...props}>{children}</div>
),
useCopy: (options: unknown) => mockUseCopied(options),
useGenerateSnippet: (options: unknown) => mockUseGenerateSnippet(options),
}))
vi.mock("@/components/Tags/Operation/Parameters", () => ({
default: () => <div data-testid="parameters">Parameters</div>,
}))
vi.mock("@medusajs/icons", () => ({
CheckCircle: () => <div data-testid="check-circle">CheckCircle</div>,
SquareTwoStack: () => <div data-testid="square-two-stack">SquareTwoStack</div>,
Tag: () => <div data-testid="tag">Tag</div>,
Brackets: () => <div data-testid="brackets">Brackets</div>,
}))
import TagsOperationDescriptionSectionEvents from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders events", () => {
const { container } = render(
<TagsOperationDescriptionSectionEvents events={mockEvents} />
)
const detailsSummaryElement = container.querySelector("[data-testid='details-summary']")
expect(detailsSummaryElement).toBeInTheDocument()
expect(detailsSummaryElement).toHaveAttribute("data-title", "Emitted Events")
const tabsElement = container.querySelector("[data-testid='tabs']")
expect(tabsElement).toBeInTheDocument()
const tabsContentElement = tabsElement!.querySelectorAll("[data-testid='tabs-content']")
expect(tabsContentElement).toHaveLength(mockEvents.length)
expect(tabsContentElement[0]).toHaveAttribute("data-value", mockEvents[0].name)
expect(tabsContentElement[0]).toHaveTextContent(mockEvents[0].description!)
const deprecatedBadge = tabsContentElement[0].querySelector("[data-testid='deprecated-badge']")
expect(deprecatedBadge).not.toBeInTheDocument()
const sinceBadge = tabsContentElement[0].querySelector("[data-testid='since-badge']")
expect(sinceBadge).not.toBeInTheDocument()
const parameters = tabsContentElement[0].querySelector("[data-testid='parameters']")
expect(parameters).toBeInTheDocument()
})
test("renders deprecated badge without tooltip when event is deprecated and does not have a deprecated message", () => {
const modifiedEvents: OpenAPI.OasEvents[] = [
{
...mockEvents[0],
deprecated: true,
},
]
const { container } = render(
<TagsOperationDescriptionSectionEvents events={modifiedEvents} />
)
const deprecatedBadge = container.querySelector("[data-testid='deprecated-badge']")
expect(deprecatedBadge).toBeInTheDocument()
expect(deprecatedBadge).toHaveAttribute("data-variant", "orange")
expect(deprecatedBadge).toHaveTextContent("Deprecated")
const deprecatedTooltip = container.querySelector("[data-testid='deprecated-tooltip']")
expect(deprecatedTooltip).not.toBeInTheDocument()
})
test("renders deprecated badge with tooltip when event is deprecated and has a deprecated message", () => {
const modifiedEvents: OpenAPI.OasEvents[] = [
{
...mockEvents[0],
deprecated: true,
deprecated_message: "This event is deprecated",
},
]
const { container } = render(
<TagsOperationDescriptionSectionEvents events={modifiedEvents} />
)
const deprecatedBadge = container.querySelector("[data-testid='deprecated-badge']")
expect(deprecatedBadge).toBeInTheDocument()
expect(deprecatedBadge).toHaveAttribute("data-variant", "orange")
expect(deprecatedBadge).toHaveTextContent("Deprecated")
const deprecatedTooltip = container.querySelector("[data-testid='deprecated-tooltip']")
expect(deprecatedTooltip).toBeInTheDocument()
expect(deprecatedTooltip).toHaveAttribute("data-text", "This event is deprecated")
})
test("renders since badge when event has a since version", () => {
const modifiedEvents: OpenAPI.OasEvents[] = [
{
...mockEvents[0],
since: "1.0.0",
},
]
const { container } = render(
<TagsOperationDescriptionSectionEvents events={modifiedEvents} />
)
const sinceBadge = container.querySelector("[data-testid='since-badge']")
expect(sinceBadge).toBeInTheDocument()
expect(sinceBadge).toHaveAttribute("data-variant", "blue")
expect(sinceBadge).toHaveTextContent("v1.0.0")
const sinceTooltip = container.querySelector("[data-testid='since-tooltip']")
expect(sinceTooltip).toBeInTheDocument()
expect(sinceTooltip).toHaveAttribute("data-text", "This event is emitted since v1.0.0")
})
test("renders SquareTwoStack icon when event name and snippet are not copied", () => {
mockUseCopied.mockReturnValue({
handleCopy: mockHandleCopy,
isCopied: false,
})
const { container } = render(
<TagsOperationDescriptionSectionEvents events={mockEvents} />
)
const squareTwoStackIcon = container.querySelector("[data-testid='square-two-stack']")
expect(squareTwoStackIcon).toBeInTheDocument()
const checkCircleIcon = container.querySelector("[data-testid='check-circle']")
expect(checkCircleIcon).not.toBeInTheDocument()
})
test("renders CheckCircle icon when event name and snippet are copied", () => {
mockUseCopied.mockReturnValue({
handleCopy: mockHandleCopy,
isCopied: true,
})
const { container } = render(
<TagsOperationDescriptionSectionEvents events={mockEvents} />
)
const checkCircleIcon = container.querySelector("[data-testid='check-circle']")
expect(checkCircleIcon).toBeInTheDocument()
const squareTwoStackIcon = container.querySelector("[data-testid='square-two-stack']")
expect(squareTwoStackIcon).not.toBeInTheDocument()
})
})
describe("interactions", () => {
test("copies event name when copy button is clicked", () => {
const { container } = render(
<TagsOperationDescriptionSectionEvents events={mockEvents} />
)
expect(mockUseCopied).toHaveBeenCalledWith(mockEvents[0].name)
const dropdownMenuItems = container.querySelectorAll("[data-testid='dropdown-menu-item']")
expect(dropdownMenuItems).toHaveLength(2)
fireEvent.click(dropdownMenuItems[0]!)
expect(mockHandleCopy).toHaveBeenCalledTimes(1)
})
test("copies subscriber for event when copy button is clicked", () => {
const { container } = render(
<TagsOperationDescriptionSectionEvents events={mockEvents} />
)
expect(mockUseCopied).toHaveBeenCalledWith("snippet")
const dropdownMenuItems = container.querySelectorAll("[data-testid='dropdown-menu-item']")
expect(dropdownMenuItems).toHaveLength(2)
fireEvent.click(dropdownMenuItems[1]!)
expect(mockHandleCopy).toHaveBeenCalledTimes(1)
})
})

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import {
Badge,
DetailsSummary,
@@ -105,15 +106,27 @@ const TagsOperationDescriptionSectionEvent = ({
</MarkdownContent>
{event.deprecated &&
(event.deprecated_message ? (
<Tooltip text={event.deprecated_message}>
<Badge variant="orange">Deprecated</Badge>
<Tooltip
text={event.deprecated_message}
data-testid="deprecated-tooltip"
>
<Badge variant="orange" data-testid="deprecated-badge">
Deprecated
</Badge>
</Tooltip>
) : (
<Badge variant="orange">Deprecated</Badge>
<Badge variant="orange" data-testid="deprecated-badge">
Deprecated
</Badge>
))}
{event.since && (
<Tooltip text={`This event is emitted since v${event.since}`}>
<Badge variant="blue">v{event.since}</Badge>
<Tooltip
text={`This event is emitted since v${event.since}`}
data-testid="since-tooltip"
>
<Badge variant="blue" data-testid="since-badge">
v{event.since}
</Badge>
</Tooltip>
)}
</div>

View File

@@ -0,0 +1,135 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
import { OpenAPI } from "types"
import { TagOperationParametersProps } from "../../../Parameters"
// mock data
const mockParameters: OpenAPI.Parameter[] = [
{
name: "parameter1",
in: "query",
description: "description1",
schema: {
type: "string",
properties: {},
} as OpenAPI.SchemaObject,
examples: {},
},
]
// mock components
vi.mock("@/components/Tags/Operation/Parameters", () => ({
default: (props: TagOperationParametersProps) => (
<div data-testid="parameters" {...props}>
{Object.values(props.schemaObject.properties).map((property) => (
<div key={property.parameterName} data-testid={"property"}>
{property.parameterName}
</div>
))}
</div>
),
}))
import TagsOperationDescriptionSectionParameters from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders header parameters when parameters have header in parameter location", () => {
const modifiedParameters: OpenAPI.Parameter[] = [
{
...mockParameters[0],
in: "header",
},
]
const { container } = render(
<TagsOperationDescriptionSectionParameters parameters={modifiedParameters} />
)
const headerParameters = container.querySelector("[data-testid='header-parameters']")
expect(headerParameters).toBeInTheDocument()
const properties = headerParameters!.querySelectorAll("[data-testid='property']")
expect(properties).toHaveLength(1)
expect(properties[0]).toHaveTextContent(mockParameters[0].name)
})
test("does not render header parameters when parameters do not have header in parameter location", () => {
const modifiedParameters: OpenAPI.Parameter[] = [
{
...mockParameters[0],
in: "query",
},
]
const { container } = render(
<TagsOperationDescriptionSectionParameters parameters={modifiedParameters} />
)
const headerParameters = container.querySelector("[data-testid='header-parameters']")
expect(headerParameters).not.toBeInTheDocument()
})
test("renders path parameters when parameters have path in parameter location", () => {
const modifiedParameters: OpenAPI.Parameter[] = [
{
...mockParameters[0],
in: "path",
},
]
const { container } = render(
<TagsOperationDescriptionSectionParameters parameters={modifiedParameters} />
)
const pathParameters = container.querySelector("[data-testid='path-parameters']")
expect(pathParameters).toBeInTheDocument()
const properties = pathParameters!.querySelectorAll("[data-testid='property']")
expect(properties).toHaveLength(1)
expect(properties[0]).toHaveTextContent(mockParameters[0].name)
})
test("does not render path parameters when parameters do not have path in parameter location", () => {
const modifiedParameters: OpenAPI.Parameter[] = [
{
...mockParameters[0],
in: "query",
},
]
const { container } = render(
<TagsOperationDescriptionSectionParameters parameters={modifiedParameters} />
)
const pathParameters = container.querySelector("[data-testid='path-parameters']")
expect(pathParameters).not.toBeInTheDocument()
})
test("renders query parameters when parameters have query in parameter location", () => {
const modifiedParameters: OpenAPI.Parameter[] = [
{
...mockParameters[0],
in: "query",
},
]
const { container } = render(
<TagsOperationDescriptionSectionParameters parameters={modifiedParameters} />
)
const queryParameters = container.querySelector("[data-testid='query-parameters']")
expect(queryParameters).toBeInTheDocument()
const properties = queryParameters!.querySelectorAll("[data-testid='property']")
expect(properties).toHaveLength(1)
expect(properties[0]).toHaveTextContent(mockParameters[0].name)
})
test("does not render query parameters when parameters do not have query in parameter location", () => {
const modifiedParameters: OpenAPI.Parameter[] = [
{
...mockParameters[0],
in: "header",
},
]
const { container } = render(
<TagsOperationDescriptionSectionParameters parameters={modifiedParameters} />
)
const queryParameters = container.querySelector("[data-testid='query-parameters']")
expect(queryParameters).not.toBeInTheDocument()
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import type { OpenAPI } from "types"
import TagOperationParameters from "../../Parameters"
@@ -60,6 +61,7 @@ const TagsOperationDescriptionSectionParameters = ({
<TagOperationParameters
schemaObject={headerParameters}
topLevel={true}
data-testid="header-parameters"
/>
</>
)}
@@ -71,6 +73,7 @@ const TagsOperationDescriptionSectionParameters = ({
<TagOperationParameters
schemaObject={pathParameters}
topLevel={true}
data-testid="path-parameters"
/>
</>
)}
@@ -82,6 +85,7 @@ const TagsOperationDescriptionSectionParameters = ({
<TagOperationParameters
schemaObject={queryParameters}
topLevel={true}
data-testid="query-parameters"
/>
</>
)}

View File

@@ -1,3 +1,4 @@
import React from "react"
import type { OpenAPI } from "types"
import TagOperationParameters from "../../Parameters"
import { DetailsSummary } from "docs-ui"

View File

@@ -0,0 +1,150 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
import { OpenAPI } from "types"
import { TagOperationParametersProps } from "../../../Parameters"
// mock data
const mockResponses: OpenAPI.ResponsesObject = {
"200": {
description: "success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
},
},
}
},
}
// mock components
vi.mock("@/components/Tags/Operation/Parameters", () => ({
default: (props: TagOperationParametersProps) => (
<div data-testid="parameters" {...props}>
{Object.entries(props.schemaObject.properties).map(([key, property]) => (
<div key={key} data-testid={"property"}>
{key}
</div>
))}
</div>
),
}))
vi.mock("docs-ui", () => ({
Badge: ({ variant, children }: { variant: string, children: React.ReactNode }) => (
<div data-testid="badge" data-variant={variant}>{children}</div>
),
DetailsSummary: ({
title,
badge,
...props }: { title: string, [key: string]: any }) => (
<div data-testid={props["data-testid"]} data-title={title}>
{title}
{badge}
</div>
),
Details: ({
children,
summaryElm,
...props
}: { children: React.ReactNode, [key: string]: any }) => (
<div data-testid={props["data-testid"]}>{summaryElm}{children}</div>
),
}))
import TagsOperationDescriptionSectionResponses from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders success response with content", () => {
const { container } = render(
<TagsOperationDescriptionSectionResponses responses={mockResponses} />
)
const successResponse = container.querySelector("[data-testid='response-success']")
expect(successResponse).toBeInTheDocument()
const successResponseParameters = container.querySelector("[data-testid='response-success-parameters']")
expect(successResponseParameters).toBeInTheDocument()
const successResponseParametersProperties = successResponseParameters!.querySelectorAll("[data-testid='property']")
expect(successResponseParametersProperties).toHaveLength(1)
expect(successResponseParametersProperties[0]).toHaveTextContent("name")
const successResponseBadge = container.querySelector("[data-testid='badge']")
expect(successResponseBadge).toBeInTheDocument()
expect(successResponseBadge).toHaveTextContent("Success")
expect(successResponseBadge).toHaveAttribute("data-variant", "green")
})
test("renders error response with content", () => {
const modifiedResponses: OpenAPI.ResponsesObject = {
"400": {
description: "error",
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
},
},
},
},
}
const { container } = render(
<TagsOperationDescriptionSectionResponses responses={modifiedResponses} />
)
const errorResponse = container.querySelector("[data-testid='response-error']")
expect(errorResponse).toBeInTheDocument()
const errorResponseParameters = container.querySelector("[data-testid='response-error-parameters']")
expect(errorResponseParameters).toBeInTheDocument()
const errorResponseParametersProperties = errorResponseParameters!.querySelectorAll("[data-testid='property']")
expect(errorResponseParametersProperties).toHaveLength(1)
expect(errorResponseParametersProperties[0]).toHaveTextContent("name")
const errorResponseBadge = container.querySelector("[data-testid='badge']")
expect(errorResponseBadge).toBeInTheDocument()
expect(errorResponseBadge).toHaveTextContent("Error")
expect(errorResponseBadge).toHaveAttribute("data-variant", "red")
})
test("renders empty success response", () => {
const modifiedResponses: OpenAPI.ResponsesObject = {
"204": {
description: "empty",
content: {},
},
}
const { container } = render(
<TagsOperationDescriptionSectionResponses responses={modifiedResponses} />
)
const emptyResponse = container.querySelector("[data-testid='response-empty']")
expect(emptyResponse).toBeInTheDocument()
const emptyResponseBadge = container.querySelector("[data-testid='badge']")
expect(emptyResponseBadge).toBeInTheDocument()
expect(emptyResponseBadge).toHaveTextContent("Success")
expect(emptyResponseBadge).toHaveAttribute("data-variant", "green")
})
test("renders empty error response", () => {
const modifiedResponses: OpenAPI.ResponsesObject = {
"400": {
description: "empty",
content: {},
},
}
const { container } = render(
<TagsOperationDescriptionSectionResponses responses={modifiedResponses} />
)
const emptyResponse = container.querySelector("[data-testid='response-empty']")
expect(emptyResponse).toBeInTheDocument()
const emptyResponseBadge = container.querySelector("[data-testid='badge']")
expect(emptyResponseBadge).toBeInTheDocument()
expect(emptyResponseBadge).toHaveTextContent("Error")
expect(emptyResponseBadge).toHaveAttribute("data-variant", "red")
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import type { OpenAPI } from "types"
import clsx from "clsx"
import TagOperationParameters from "../../Parameters"
@@ -19,9 +20,12 @@ const TagsOperationDescriptionSectionResponses = ({
>
{Object.entries(responses).map(([code, response], index) => {
const successCode = code.startsWith("20")
const isEmptyResponse =
response?.content === undefined ||
Object.keys(response.content).length === 0
return (
<Fragment key={index}>
{response.content && (
{!isEmptyResponse && (
<>
{successCode && (
<>
@@ -34,6 +38,7 @@ const TagsOperationDescriptionSectionResponses = ({
index !== 0 && "border-t-0",
index === 0 && "border-b-0"
)}
data-testid="response-success"
/>
<TagOperationParameters
schemaObject={
@@ -41,6 +46,7 @@ const TagsOperationDescriptionSectionResponses = ({
.schema
}
topLevel={true}
data-testid="response-success-parameters"
/>
</>
)}
@@ -56,6 +62,7 @@ const TagsOperationDescriptionSectionResponses = ({
}
openInitial={index === 0}
className={clsx(index > 1 && "border-t-0")}
data-testid="response-error"
>
<TagOperationParameters
schemaObject={
@@ -63,12 +70,13 @@ const TagsOperationDescriptionSectionResponses = ({
.schema
}
topLevel={true}
data-testid="response-error-parameters"
/>
</Details>
)}
</>
)}
{!response.content && (
{isEmptyResponse && (
<DetailsSummary
title={`${code} ${response.description}`}
subtitle={"Empty response"}
@@ -84,6 +92,7 @@ const TagsOperationDescriptionSectionResponses = ({
Object.entries(responses).length > 1 &&
"border-b-0"
)}
data-testid="response-empty"
/>
)}
</Fragment>

View File

@@ -0,0 +1,73 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, fireEvent, render } from "@testing-library/react"
import { OpenAPI } from "types"
// mock function
const mockGetSecuritySchema = vi.fn((key: string) => ({
"x-displayName": "Bearer Token",
"x-is-auth": true,
}))
const mockUseBaseSpecs = vi.fn(() => ({
getSecuritySchema: mockGetSecuritySchema,
}))
// mock components and hooks
vi.mock("@/providers/base-specs", () => ({
useBaseSpecs: () => ({
getSecuritySchema: mockGetSecuritySchema,
}),
}))
vi.mock("docs-ui", () => ({
Card: ({ title, text, href }: { title: string, text: string, href: string }) => (
<div data-testid="card" data-title={title} data-href={href}>
{text}
</div>
),
}))
import TagsOperationDescriptionSectionSecurity from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders security with authentication", () => {
const { container } = render(
<TagsOperationDescriptionSectionSecurity security={[{ "bearer": [] }]} />
)
const cardElement = container.querySelector("[data-testid='card']")
expect(cardElement).toBeInTheDocument()
expect(cardElement).toHaveTextContent("Bearer Token")
expect(cardElement).toHaveAttribute("data-href", "#authentication")
})
test("renders security without authentication", () => {
mockGetSecuritySchema.mockReturnValue({
"x-displayName": "Bearer Token",
"x-is-auth": false,
})
const { container } = render(
<TagsOperationDescriptionSectionSecurity security={[{ "bearer": [] }]} />
)
const cardElement = container.querySelector("[data-testid='card']")
expect(cardElement).toBeInTheDocument()
expect(cardElement).toHaveTextContent("Bearer Token")
expect(cardElement).not.toHaveAttribute("data-href")
})
test("renders security with multiple security schemes", () => {
const { container } = render(
<TagsOperationDescriptionSectionSecurity security={[{ "bearer": [] }, { "apiKey": [] }]} />
)
const cardElement = container.querySelector("[data-testid='card']")
expect(cardElement).toBeInTheDocument()
// it renders the same security scheme twice because we're mocking the getSecuritySchema function
expect(cardElement).toHaveTextContent("Bearer Token or Bearer Token")
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import { useBaseSpecs } from "@/providers/base-specs"
import type { OpenAPI } from "types"
import { Card } from "docs-ui"

View File

@@ -0,0 +1,51 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, fireEvent, render } from "@testing-library/react"
import { OpenAPI } from "types"
// mock data
const mockWorkflow = "test-workflow"
// mock components
vi.mock("@/config", () => ({
config: {
baseUrl: "https://example.com",
},
}))
vi.mock("docs-ui", () => ({
SourceCodeLink: ({ link, text, icon }: { link: string, text: string, icon: React.ReactNode }) => (
<div data-testid="source-code-link" data-link={link} data-text={text}>
{text} {icon}
</div>
),
DecisionProcessIcon: () => (
<div data-testid="decision-process-icon">
Icon
</div>
),
}))
import TagsOperationDescriptionSectionWorkflowBadge from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders workflow badge", () => {
const { container } = render(
<TagsOperationDescriptionSectionWorkflowBadge workflow={mockWorkflow} />
)
const sourceCodeLinkElement = container.querySelector("[data-testid='source-code-link']")
expect(sourceCodeLinkElement).toBeInTheDocument()
expect(sourceCodeLinkElement).toHaveTextContent(mockWorkflow)
expect(sourceCodeLinkElement).toHaveAttribute(
"data-link",
`https://example.com/resources/references/medusa-workflows/${mockWorkflow}`
)
const decisionProcessIconElement = container.querySelector("[data-testid='decision-process-icon']")
expect(decisionProcessIconElement).toBeInTheDocument()
})
})

View File

@@ -1,5 +1,6 @@
import React from "react"
import { DecisionProcessIcon, SourceCodeLink } from "docs-ui"
import { config } from "../../../../../config"
import { config } from "@/config"
export type TagsOperationDescriptionSectionWorkflowBadgeProps = {
workflow: string

View File

@@ -0,0 +1,339 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, fireEvent, render, waitFor } from "@testing-library/react"
import { OpenAPI } from "types"
// mock data
const mockOperation: OpenAPI.Operation = {
operationId: "test-operation-id",
"x-authenticated": false,
"x-codeSamples": [],
tags: ["test-tag"],
summary: "test-summary",
description: "test-description",
responses: {},
parameters: [],
requestBody: {
content: {},
},
}
// mock components
vi.mock("@/components/Tags/Operation/DescriptionSection/Security", () => ({
default: ({ security }: { security: OpenAPI.OpenAPIV3.SecurityRequirementObject[] }) => (
<div data-testid="security">{JSON.stringify(security)}</div>
),
}))
vi.mock("@/components/Tags/Operation/DescriptionSection/RequestBody", () => ({
default: ({ requestBody }: { requestBody: OpenAPI.RequestObject }) => (
<div data-testid="request-body">{JSON.stringify(requestBody)}</div>
),
}))
vi.mock("@/components/Tags/Operation/DescriptionSection/Responses", () => ({
default: ({ responses }: { responses: OpenAPI.ResponsesObject }) => (
<div data-testid="responses">{JSON.stringify(responses)}</div>
),
}))
vi.mock("@/components/Tags/Operation/DescriptionSection/Parameters", () => ({
default: ({ parameters }: { parameters: OpenAPI.Parameter[] }) => (
<div data-testid="parameters">{JSON.stringify(parameters)}</div>
),
}))
vi.mock("@/components/Tags/Operation/DescriptionSection/WorkflowBadge", () => ({
default: ({ workflow }: { workflow: string }) => (
<div data-testid="workflow">{workflow}</div>
),
}))
vi.mock("@/components/Tags/Operation/DescriptionSection/Events", () => ({
default: ({ events }: { events: OpenAPI.OasEvents[] }) => (
<div data-testid="events">{JSON.stringify(events)}</div>
),
}))
vi.mock("@/components/Tags/Operation/DescriptionSection/DeprecationNotice", () => ({
default: ({ deprecationMessage }: { deprecationMessage: string }) => (
<div data-testid="deprecation-notice">{deprecationMessage}</div>
),
}))
vi.mock("@/components/Tags/Operation/DescriptionSection/FeatureFlagNotice", () => ({
default: ({ featureFlag }: { featureFlag: string }) => (
<div data-testid="feature-flag">{featureFlag}</div>
),
}))
vi.mock("@/components/MDXContent/Client", () => ({
default: ({ content }: { content: string }) => (
<div data-testid="mdx-content">{content}</div>
),
}))
vi.mock("docs-ui", () => ({
Badge: ({
variant,
children,
...props
}: { variant: string, children: React.ReactNode, [key: string]: unknown }) => (
<div data-testid="badge" data-variant={variant} {...props}>{children}</div>
),
Link: ({
href,
children,
...props
}: { href: string, children: React.ReactNode, [key: string]: unknown }) => (
<div data-testid="link" data-href={href} {...props}>{children}</div>
),
FeatureFlagNotice: ({ featureFlag }: { featureFlag: string }) => (
<div data-testid="feature-flag">{featureFlag}</div>
),
H2: ({ children }: { children: React.ReactNode }) => (
<h2 data-testid="h2">{children}</h2>
),
Tooltip: ({ children }: { children: React.ReactNode }) => (
<div data-testid="tooltip">{children}</div>
),
MarkdownContent: ({ children }: { children: React.ReactNode }) => (
<div data-testid="markdown-content">{children}</div>
),
}))
vi.mock("@/components/Feedback", () => ({
Feedback: ({ question }: { question: string }) => (
<div data-testid="feedback">{question}</div>
),
}))
import TagsOperationDescriptionSection from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders operation summary, description, feedback, responses (default)", async () => {
const { container } = render(<TagsOperationDescriptionSection operation={mockOperation} />)
const h2Element = container.querySelector("[data-testid='h2']")
expect(h2Element).toBeInTheDocument()
expect(h2Element).toHaveTextContent(mockOperation.summary)
const mdxContentElement = container.querySelector("[data-testid='mdx-content']")
expect(mdxContentElement).toBeInTheDocument()
expect(mdxContentElement).toHaveTextContent(mockOperation.description)
const feedbackElement = container.querySelector("[data-testid='feedback']")
expect(feedbackElement).toBeInTheDocument()
expect(feedbackElement).toHaveTextContent("Did this API Route run successfully?")
await waitFor(() => {
const responsesElement = container.querySelector("[data-testid='responses']")
expect(responsesElement).toBeInTheDocument()
expect(responsesElement).toHaveTextContent(JSON.stringify(mockOperation.responses))
})
})
test("renders deprecated notice when operation is deprecated", async () => {
const modifiedOperation: OpenAPI.Operation = {
...mockOperation,
deprecated: true,
"x-deprecated_message": "test-deprecated-message",
}
const { container } = render(<TagsOperationDescriptionSection operation={modifiedOperation} />)
await waitFor(() => {
const deprecationNoticeElement = container.querySelector("[data-testid='deprecation-notice']")
expect(deprecationNoticeElement).toBeInTheDocument()
expect(deprecationNoticeElement).toHaveTextContent("test-deprecated-message")
})
})
test("does not render deprecated notice when operation is not deprecated", () => {
const { container } = render(<TagsOperationDescriptionSection operation={mockOperation} />)
const deprecationNoticeElement = container.querySelector("[data-testid='deprecation-notice']")
expect(deprecationNoticeElement).not.toBeInTheDocument()
})
test("renders feature flag notice when operation has a feature flag", () => {
const modifiedOperation: OpenAPI.Operation = {
...mockOperation,
"x-featureFlag": "test-feature-flag",
}
const { container } = render(<TagsOperationDescriptionSection operation={modifiedOperation} />)
const featureFlagNoticeElement = container.querySelector("[data-testid='feature-flag']")
expect(featureFlagNoticeElement).toBeInTheDocument()
expect(featureFlagNoticeElement).toHaveTextContent("test-feature-flag")
})
test("does not render feature flag notice when operation does not have a feature flag", () => {
const { container } = render(<TagsOperationDescriptionSection operation={mockOperation} />)
const featureFlagNoticeElement = container.querySelector("[data-testid='feature-flag']")
expect(featureFlagNoticeElement).not.toBeInTheDocument()
})
test("renders since badge when operation has a since", () => {
const modifiedOperation: OpenAPI.Operation = {
...mockOperation,
"x-since": "1.0.0",
}
const { container } = render(<TagsOperationDescriptionSection operation={modifiedOperation} />)
const sinceBadgeElement = container.querySelector("[data-testid='since-badge']")
expect(sinceBadgeElement).toBeInTheDocument()
expect(sinceBadgeElement).toHaveTextContent("1.0.0")
})
test("does not render since badge when operation does not have a since", () => {
const { container } = render(<TagsOperationDescriptionSection operation={mockOperation} />)
const sinceBadgeElement = container.querySelector("[data-testid='since-badge']")
expect(sinceBadgeElement).not.toBeInTheDocument()
})
test("renders operation custom badges when operation has custom badges", () => {
const modifiedOperation: OpenAPI.Operation = {
...mockOperation,
"x-badges": [{
text: "test-badge",
description: "test-description",
variant: "blue",
}],
}
const { container } = render(<TagsOperationDescriptionSection operation={modifiedOperation} />)
const customBadgeElement = container.querySelector("[data-testid='custom-badge']")
expect(customBadgeElement).toBeInTheDocument()
expect(customBadgeElement).toHaveTextContent("test-badge")
expect(customBadgeElement).toHaveAttribute("data-variant", "blue")
})
test("does not render custom badges when operation does not have custom badges", () => {
const { container } = render(<TagsOperationDescriptionSection operation={mockOperation} />)
const customBadgeElement = container.querySelector("[data-testid='custom-badge']")
expect(customBadgeElement).not.toBeInTheDocument()
})
test("renders operation's workflow badge when operation has a workflow", async () => {
const modifiedOperation: OpenAPI.Operation = {
...mockOperation,
"x-workflow": "test-workflow",
}
const { container } = render(<TagsOperationDescriptionSection operation={modifiedOperation} />)
await waitFor(() => {
const workflowBadgeElement = container.querySelector("[data-testid='workflow']")
expect(workflowBadgeElement).toBeInTheDocument()
expect(workflowBadgeElement).toHaveTextContent("test-workflow")
})
})
test("does not render workflow badge when operation does not have a workflow", () => {
const { container } = render(<TagsOperationDescriptionSection operation={mockOperation} />)
const workflowBadgeElement = container.querySelector("[data-testid='workflow']")
expect(workflowBadgeElement).not.toBeInTheDocument()
})
test("renders operation's related guide link when operation has a related guide", () => {
const modifiedOperation: OpenAPI.Operation = {
...mockOperation,
externalDocs: {
url: "https://example.com",
description: "test-description",
},
}
const { container } = render(<TagsOperationDescriptionSection operation={modifiedOperation} />)
const relatedGuideLinkElement = container.querySelector("[data-testid='related-guide-link']")
expect(relatedGuideLinkElement).toBeInTheDocument()
expect(relatedGuideLinkElement).toHaveTextContent("test-description")
expect(relatedGuideLinkElement).toHaveAttribute("data-href", "https://example.com")
})
test("does not render related guide link when operation does not have a related guide", () => {
const { container } = render(<TagsOperationDescriptionSection operation={mockOperation} />)
const relatedGuideLinkElement = container.querySelector("[data-testid='related-guide-link']")
expect(relatedGuideLinkElement).not.toBeInTheDocument()
})
test("renders operation's security when operation has security", async () => {
const modifiedOperation: OpenAPI.Operation = {
...mockOperation,
security: [{ "bearer": [] }],
}
const { container } = render(<TagsOperationDescriptionSection operation={modifiedOperation} />)
await waitFor(() => {
const securityElement = container.querySelector("[data-testid='security']")
expect(securityElement).toBeInTheDocument()
expect(securityElement).toHaveTextContent(JSON.stringify(modifiedOperation.security))
})
})
test("does not render security when operation does not have security", () => {
const { container } = render(<TagsOperationDescriptionSection operation={mockOperation} />)
const securityElement = container.querySelector("[data-testid='security']")
expect(securityElement).not.toBeInTheDocument()
})
test("renders operation's parameters when operation has parameters", () => {
const modifiedOperation: OpenAPI.Operation = {
...mockOperation,
parameters: [{
name: "test-parameter",
in: "query",
description: "test-description",
schema: {
type: "string",
properties: {},
},
examples: {},
}],
}
const { container } = render(<TagsOperationDescriptionSection operation={modifiedOperation} />)
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveTextContent(JSON.stringify(modifiedOperation.parameters))
})
test("does not render parameters when operation does not have parameters", () => {
const { container } = render(<TagsOperationDescriptionSection operation={mockOperation} />)
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).not.toBeInTheDocument()
})
test("renders operation's request body when operation has a request body", async () => {
const modifiedOperation: OpenAPI.Operation = {
...mockOperation,
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {},
},
},
},
},
}
const { container } = render(<TagsOperationDescriptionSection operation={modifiedOperation} />)
await waitFor(() => {
const requestBodyElement = container.querySelector("[data-testid='request-body']")
expect(requestBodyElement).toBeInTheDocument()
expect(requestBodyElement).toHaveTextContent(JSON.stringify(modifiedOperation.requestBody))
})
})
test("does not render request body when operation does not have a request body", () => {
const { container } = render(<TagsOperationDescriptionSection operation={mockOperation} />)
const requestBodyElement = container.querySelector("[data-testid='request-body']")
expect(requestBodyElement).not.toBeInTheDocument()
})
test("renders operation's events when operation has events", async () => {
const modifiedOperation: OpenAPI.Operation = {
...mockOperation,
"x-events": [{
name: "test-event",
description: "test-description",
payload: "test-payload",
}],
}
const { container } = render(<TagsOperationDescriptionSection operation={modifiedOperation} />)
await waitFor(() => {
const eventsElement = container.querySelector("[data-testid='events']")
expect(eventsElement).toBeInTheDocument()
expect(eventsElement).toHaveTextContent(JSON.stringify(modifiedOperation["x-events"]))
})
})
test("does not render events when operation does not have events", () => {
const { container } = render(<TagsOperationDescriptionSection operation={mockOperation} />)
const eventsElement = container.querySelector("[data-testid='events']")
expect(eventsElement).not.toBeInTheDocument()
})
})

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import type { OpenAPI } from "types"
import type { TagsOperationDescriptionSectionSecurityProps } from "./Security"
import type { TagsOperationDescriptionSectionRequestProps } from "./RequestBody"
@@ -77,7 +78,7 @@ const TagsOperationDescriptionSection = ({
<Tooltip
text={`This API route is available since v${operation["x-since"]}`}
>
<Badge variant="blue" className="ml-0.5">
<Badge variant="blue" className="ml-0.5" data-testid="since-badge">
v{operation["x-since"]}
</Badge>
</Tooltip>
@@ -95,7 +96,11 @@ const TagsOperationDescriptionSection = ({
}
clickable={true}
>
<Badge variant={badge.variant || "neutral"} className="ml-0.5">
<Badge
variant={badge.variant || "neutral"}
className="ml-0.5"
data-testid="custom-badge"
>
{badge.text}
</Badge>
</Tooltip>
@@ -116,6 +121,7 @@ const TagsOperationDescriptionSection = ({
href={operation.externalDocs.url}
target="_blank"
variant="content"
data-testid="related-guide-link"
>
{operation.externalDocs.description || "Read More"}
</Link>
@@ -133,16 +139,17 @@ const TagsOperationDescriptionSection = ({
security={operation.security}
/>
)}
{operation.parameters && (
{operation.parameters && operation.parameters.length > 0 && (
<TagsOperationDescriptionSectionParameters
parameters={operation.parameters}
/>
)}
{operation.requestBody && (
<TagsOperationDescriptionSectionRequest
requestBody={operation.requestBody}
/>
)}
{operation.requestBody?.content !== undefined &&
Object.keys(operation.requestBody.content).length > 0 && (
<TagsOperationDescriptionSectionRequest
requestBody={operation.requestBody}
/>
)}
<TagsOperationDescriptionSectionResponses
responses={operation.responses}
/>

View File

@@ -0,0 +1,69 @@
import React from "react"
import { cleanup, render } from "@testing-library/react"
import { beforeEach, describe, expect, test, vi } from "vitest"
// mock components
vi.mock("docs-ui", () => ({
Badge: ({ children, className }: BadgeProps) => (
<div data-testid="badge" className={className}>{children}</div>
),
Link: ({ children }: { children: React.ReactNode }) => (
<div data-testid="link">{children}</div>
),
Tooltip: ({ tooltipChildren, children }: TooltipProps) => (
<div data-testid="tooltip">
<div data-testid="tooltip-children">{tooltipChildren}</div>
<div data-testid="children">{children}</div>
</div>
),
}))
import TagsOperationFeatureFlagNotice from ".."
import { BadgeProps, TooltipProps } from "docs-ui"
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders feature flag notice for endpoint type by default", () => {
const { container } = render(<TagsOperationFeatureFlagNotice featureFlag="test-feature-flag" />)
const tooltipElement = container.querySelector("[data-testid='tooltip']")
expect(tooltipElement).toBeInTheDocument()
const tooltipChildrenElement = container.querySelector("[data-testid='tooltip-children']")
expect(tooltipChildrenElement).toBeInTheDocument()
expect(tooltipChildrenElement).toHaveTextContent("To use this endpoint, make sure to enable its feature flag: test-feature-flag")
const badgeElement = container.querySelector("[data-testid='badge']")
expect(badgeElement).toBeInTheDocument()
expect(badgeElement).toHaveTextContent("feature flag")
})
test("renders feature flag notice for parameter type", () => {
const { container } = render(<TagsOperationFeatureFlagNotice featureFlag="test-feature-flag" type="parameter" />)
const tooltipElement = container.querySelector("[data-testid='tooltip']")
expect(tooltipElement).toBeInTheDocument()
const tooltipChildrenElement = container.querySelector("[data-testid='tooltip-children']")
expect(tooltipChildrenElement).toBeInTheDocument()
expect(tooltipChildrenElement).toHaveTextContent("To use this parameter, make sure to enable its feature flag: test-feature-flag")
const badgeElement = container.querySelector("[data-testid='badge']")
expect(badgeElement).toBeInTheDocument()
expect(badgeElement).toHaveTextContent("feature flag")
})
test("renders feature flag notice with tooltipTextClassName", () => {
const { container } = render(<TagsOperationFeatureFlagNotice featureFlag="test-feature-flag" tooltipTextClassName="text-red-500" />)
const tooltipTextElement = container.querySelector("[data-testid='tooltip-text']")
expect(tooltipTextElement).toBeInTheDocument()
expect(tooltipTextElement).toHaveClass("text-red-500")
})
test("renders feature flag notice with badgeClassName", () => {
const { container } = render(<TagsOperationFeatureFlagNotice featureFlag="test-feature-flag" badgeClassName="bg-red-500" />)
const badgeElement = container.querySelector("[data-testid='badge']")
expect(badgeElement).toBeInTheDocument()
expect(badgeElement).toHaveClass("bg-red-500")
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import { Badge, Link, Tooltip } from "docs-ui"
export type TagsOperationFeatureFlagNoticeProps = {
@@ -16,9 +17,8 @@ const TagsOperationFeatureFlagNotice = ({
return (
<Tooltip
tooltipChildren={
<span className={tooltipTextClassName}>
To use this {type}, make sure to
<br />
<span className={tooltipTextClassName} data-testid="tooltip-text">
To use this {type}, make sure to <br />
<Link
href="https://docs.medusajs.com/development/feature-flags/toggle"
target="__blank"

View File

@@ -0,0 +1,154 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render, waitFor } from "@testing-library/react"
import { OpenAPI } from "types"
import { InlineCodeProps } from "docs-ui"
// mock data
const mockSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
}
// mock functions
const mockCapitalize = vi.fn((text: string) => text.charAt(0).toUpperCase() + text.slice(1))
// mock components
vi.mock("docs-ui", () => ({
InlineCode: ({ children }: InlineCodeProps) => (
<div data-testid="inline-code">{children}</div>
),
Link: (props: React.HTMLAttributes<HTMLAnchorElement>) => (
<a {...props} data-testid="link" />
),
capitalize: (text: string) => mockCapitalize(text),
}))
vi.mock("@/components/MDXContent/Client", () => ({
default: ({ content }: { content: string }) => (
<div data-testid="mdx-content">{content}</div>
),
}))
import TagOperationParametersDescription from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders default when schema has a default", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
default: "test-default",
}
const { container } = render(<TagOperationParametersDescription schema={modifiedSchema} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveTextContent("Default: " + JSON.stringify(modifiedSchema.default))
})
})
test("does not render default when schema does not have a default", () => {
const { container } = render(<TagOperationParametersDescription schema={mockSchema} />)
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).not.toBeInTheDocument()
})
test("renders enum when schema has an enum", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
enum: ["test-enum1", "test-enum2"],
}
const { container } = render(<TagOperationParametersDescription schema={modifiedSchema} />)
await waitFor(() => {
const enumElement = container.querySelector("[data-testid='enum']")
expect(enumElement).toBeInTheDocument()
expect(enumElement).toHaveTextContent("Enum: " + modifiedSchema.enum!.map((value) => JSON.stringify(value)).join(", "))
})
})
test("does not render enum when schema does not have an enum", () => {
const { container } = render(<TagOperationParametersDescription schema={mockSchema} />)
const enumElement = container.querySelector("[data-testid='enum']")
expect(enumElement).not.toBeInTheDocument()
})
test("renders example when schema has an example", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
example: "test-example",
}
const { container } = render(<TagOperationParametersDescription schema={modifiedSchema} />)
const exampleElement = container.querySelector("[data-testid='example']")
expect(exampleElement).toBeInTheDocument()
expect(exampleElement).toHaveTextContent("Example: " + JSON.stringify(modifiedSchema.example))
})
test("does not render example when schema does not have an example", () => {
const { container } = render(<TagOperationParametersDescription schema={mockSchema} />)
const exampleElement = container.querySelector("[data-testid='example']")
expect(exampleElement).not.toBeInTheDocument()
})
test("renders description when schema has a description", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
description: "test-description",
}
const { container } = render(<TagOperationParametersDescription schema={modifiedSchema} />)
const descriptionElement = container.querySelector("[data-testid='mdx-content']")
expect(descriptionElement).toBeInTheDocument()
expect(descriptionElement).toHaveTextContent(mockCapitalize(modifiedSchema.description!))
})
test("does not render description when schema does not have a description", () => {
const { container } = render(<TagOperationParametersDescription schema={mockSchema} />)
const descriptionElement = container.querySelector("[data-testid='mdx-content']")
expect(descriptionElement).not.toBeInTheDocument()
})
test("renders related guide when schema has a related guide", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
externalDocs: {
url: "https://example.com",
description: "test-description",
},
}
const { container } = render(<TagOperationParametersDescription schema={modifiedSchema} />)
const relatedGuideElement = container.querySelector("[data-testid='related-guide']")
expect(relatedGuideElement).toBeInTheDocument()
expect(relatedGuideElement).toHaveTextContent("Related guide: " + modifiedSchema.externalDocs!.description)
const link = relatedGuideElement!.querySelector("[data-testid='link']")
expect(link).toHaveAttribute("href", modifiedSchema.externalDocs!.url)
expect(link).toHaveAttribute("target", "_blank")
expect(link).toHaveAttribute("variant", "content")
})
test("renders related guide with default description when schema has a related guide with no description", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
externalDocs: {
url: "https://example.com",
},
}
const { container } = render(<TagOperationParametersDescription schema={modifiedSchema} />)
const relatedGuideElement = container.querySelector("[data-testid='related-guide']")
expect(relatedGuideElement).toBeInTheDocument()
expect(relatedGuideElement).toHaveTextContent("Related guide: " + "Read More")
const link = relatedGuideElement!.querySelector("[data-testid='link']")
expect(link).toHaveAttribute("href", modifiedSchema.externalDocs!.url)
expect(link).toHaveAttribute("target", "_blank")
expect(link).toHaveAttribute("variant", "content")
})
test("does not render related guide when schema does not have a related guide", () => {
const { container } = render(<TagOperationParametersDescription schema={mockSchema} />)
const relatedGuideElement = container.querySelector("[data-testid='related-guide']")
expect(relatedGuideElement).not.toBeInTheDocument()
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import MDXContentClient from "@/components/MDXContent/Client"
import type { OpenAPI } from "types"
import clsx from "clsx"
@@ -9,7 +10,7 @@ const InlineCode = dynamic<InlineCodeProps>(
async () => (await import("docs-ui")).InlineCode
) as React.FC<InlineCodeProps>
type TagOperationParametersDescriptionProps = {
export type TagOperationParametersDescriptionProps = {
schema: OpenAPI.SchemaObject
}
@@ -19,7 +20,7 @@ const TagOperationParametersDescription = ({
return (
<div className={clsx("pb-0.5 flex flex-col gap-0.25")}>
{schema.default !== undefined && (
<span>
<span data-testid="default">
Default:{" "}
<InlineCode className="break-words">
{JSON.stringify(schema.default)}
@@ -27,7 +28,7 @@ const TagOperationParametersDescription = ({
</span>
)}
{schema.enum && (
<span>
<span data-testid="enum">
Enum:{" "}
{schema.enum.map((value, index) => (
<Fragment key={index}>
@@ -38,7 +39,7 @@ const TagOperationParametersDescription = ({
</span>
)}
{schema.example !== undefined && (
<span>
<span data-testid="example">
Example:{" "}
<InlineCode className="break-words">
{JSON.stringify(schema.example)}
@@ -57,7 +58,7 @@ const TagOperationParametersDescription = ({
</>
)}
{schema.externalDocs && (
<span>
<span data-testid="related-guide">
Related guide:{" "}
<Link
href={schema.externalDocs.url}

View File

@@ -0,0 +1,729 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
import { OpenAPI } from "types"
import { BadgeProps, ExpandableNoticeProps, FeatureFlagNoticeProps } from "docs-ui"
// mock data
const mockName = "test-name"
const mockSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
}
// mock components
vi.mock("docs-ui", () => ({
Badge: ({ variant, children, className }: BadgeProps) => (
<div data-testid="badge" className={className}>{children}</div>
),
ExpandableNotice: ({ type, link }: ExpandableNoticeProps) => (
<div data-testid="expandable-notice" data-type={type} data-link={link}>
Expandable Notice
</div>
),
FeatureFlagNotice: ({ featureFlag, type }: FeatureFlagNoticeProps) => (
<div data-testid="feature-flag-notice" data-feature-flag={featureFlag} data-type={type}>
Feature Flag Notice
</div>
),
}))
import TagOperationParametersName from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders name", () => {
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={mockSchema} />
)
const nameElement = container.querySelector("[data-testid='name']")
expect(nameElement).toBeInTheDocument()
expect(nameElement).toHaveTextContent(mockName)
})
test("renders deprecated badge when schema is deprecated", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
deprecated: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={modifiedSchema} />
)
const badgeElement = container.querySelector("[data-testid='badge']")
expect(badgeElement).toBeInTheDocument()
expect(badgeElement).toHaveTextContent("deprecated")
})
test("renders expandable notice when schema is expandable", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
"x-expandable": "expanding-relations",
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={modifiedSchema} />
)
const expandableNoticeElement = container.querySelector("[data-testid='expandable-notice']")
expect(expandableNoticeElement).toBeInTheDocument()
expect(expandableNoticeElement).toHaveTextContent("Expandable Notice")
expect(expandableNoticeElement).toHaveAttribute("data-type", "request")
expect(expandableNoticeElement).toHaveAttribute("data-link", "#expanding-relations")
})
test("renders feature flag notice when schema has a feature flag", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
"x-featureFlag": "test-feature-flag",
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={modifiedSchema} />
)
const featureFlagNoticeElement = container.querySelector("[data-testid='feature-flag-notice']")
expect(featureFlagNoticeElement).toBeInTheDocument()
expect(featureFlagNoticeElement).toHaveTextContent("Feature Flag Notice")
expect(featureFlagNoticeElement).toHaveAttribute("data-feature-flag", "test-feature-flag")
expect(featureFlagNoticeElement).toHaveAttribute("data-type", "type")
})
test("renders optional span when schema is not required", () => {
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={mockSchema} />
)
const optionalElement = container.querySelector("[data-testid='optional']")
expect(optionalElement).toBeInTheDocument()
expect(optionalElement).toHaveTextContent("optional")
})
test("does not render deprecated badge when schema is not deprecated", () => {
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={mockSchema} />
)
const badgeElement = container.querySelector("[data-testid='badge']")
expect(badgeElement).not.toBeInTheDocument()
})
})
describe("object type schema description formatting", () => {
test("formats description for object type schema without title and no nullable", () => {
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={mockSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object")
})
test("formats description for object type schema with title and no nullable", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
title: "test-title",
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={modifiedSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object (test-title)")
})
test("formats description for object type schema with no title and nullable", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={modifiedSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object or null")
})
test("formats description for object type schema with title and nullable", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
title: "test-title",
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={modifiedSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object (test-title) or null")
})
})
describe("array type schema description formatting", () => {
test("formats description for array type schema with no items and not nullable", () => {
const arraySchema: OpenAPI.ArraySchemaObject = {
type: "array",
// @ts-expect-error - we are testing the case where items is undefined
items: undefined,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={arraySchema as OpenAPI.SchemaObject} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("Array")
})
test("formats description for array type schema with no items and nullable", () => {
const arraySchema: OpenAPI.ArraySchemaObject = {
type: "array",
// @ts-expect-error - we are testing the case where items is undefined
items: undefined,
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={arraySchema as OpenAPI.SchemaObject} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("Array or null")
})
test("formats description for array with object items (with title) and not nullable", () => {
const arraySchema: OpenAPI.ArraySchemaObject = {
type: "array",
items: {
type: "object",
title: "test-title",
properties: {}
},
properties: {},
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={arraySchema as OpenAPI.SchemaObject} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("Array of objects (test-title)")
})
test("formats description for array with object items (with title) and nullable", () => {
const arraySchema: OpenAPI.ArraySchemaObject = {
type: "array",
items: {
type: "object",
title: "test-title",
properties: {},
},
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={arraySchema as OpenAPI.SchemaObject} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("Array of objects (test-title) or null")
})
test("formats description for array with object items (without title) and not nullable", () => {
const arraySchema: OpenAPI.ArraySchemaObject = {
type: "array",
items: {
type: "object",
properties: {},
},
properties: {},
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={arraySchema as OpenAPI.SchemaObject} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("Array of objects")
})
test("formats description for array with object items (without title) and nullable", () => {
const arraySchema: OpenAPI.ArraySchemaObject = {
type: "array",
items: {
type: "object",
properties: {},
},
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={arraySchema as OpenAPI.SchemaObject} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("Array of objects or null")
})
test("formats description for array with any items and not nullable", () => {
const arraySchema: OpenAPI.ArraySchemaObject = {
type: "array",
items: {
type: "string",
properties: {},
},
properties: {},
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={arraySchema as OpenAPI.SchemaObject} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("Array of strings")
})
test("formats description for array with any items and nullable", () => {
const arraySchema: OpenAPI.ArraySchemaObject = {
type: "array",
items: {
type: "string",
properties: {},
},
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={arraySchema as OpenAPI.SchemaObject} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("Array of strings or null")
})
test("formats description for array with items of no type and not nullable", () => {
const arraySchema: OpenAPI.ArraySchemaObject = {
type: "array",
items: {
properties: {},
},
properties: {},
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={arraySchema as OpenAPI.SchemaObject} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("Array of objects")
})
test("formats description for array with items of no type and nullable", () => {
const arraySchema: OpenAPI.ArraySchemaObject = {
type: "array",
items: {
properties: {},
},
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={arraySchema as OpenAPI.SchemaObject} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("Array of objects or null")
})
})
describe("union type schema description formatting", () => {
test("formats description for union type schema with allOf and not nullable", () => {
const unionSchema: OpenAPI.SchemaObject = {
allOf: [
{ type: "object", properties: {} },
{ type: "string", properties: {} },
],
properties: {},
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={unionSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object or string")
})
test("formats description for union type schema with allOf and nullable", () => {
const unionSchema: OpenAPI.SchemaObject = {
allOf: [
{ type: "object", properties: {} },
{ type: "string", properties: {} },
],
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={unionSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object or string or null")
})
test("formats description for union type schema with anyOf and not nullable", () => {
const unionSchema: OpenAPI.SchemaObject = {
anyOf: [
{ type: "object", properties: {} },
{ type: "string", properties: {} },
],
properties: {},
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={unionSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object or string")
})
test("formats description for union type schema with anyOf and nullable", () => {
const unionSchema: OpenAPI.SchemaObject = {
anyOf: [
{ type: "object", properties: {} },
{ type: "string", properties: {} },
],
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={unionSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object or string or null")
})
test("formats description for union type schema with same types and not nullable", () => {
const unionSchema: OpenAPI.SchemaObject = {
anyOf: [
{ type: "object", properties: {} },
{ type: "object", properties: {} },
],
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={unionSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object")
})
test("formats description for union type schema with same types and nullable", () => {
const unionSchema: OpenAPI.SchemaObject = {
anyOf: [
{ type: "object", properties: {} },
{ type: "object", properties: {} },
],
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={unionSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object or null")
})
test("gives precedence to allOf over anyOf", () => {
const unionSchema: OpenAPI.SchemaObject = {
allOf: [
{ type: "object", properties: {} },
],
anyOf: [
{ type: "string", properties: {} },
],
properties: {},
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={unionSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object")
expect(typeDescriptionElement).not.toHaveTextContent("string")
})
})
describe("oneOf type schema description formatting", () => {
test("formats description for oneOf type schema with one item (without title) and not nullable", () => {
const oneOfSchema: OpenAPI.SchemaObject = {
oneOf: [
{ type: "object", properties: {} },
],
properties: {},
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={oneOfSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object")
})
test("formats description for oneOf type schema with one item (without title) and nullable", () => {
const oneOfSchema: OpenAPI.SchemaObject = {
oneOf: [
{ type: "object", properties: {} },
],
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={oneOfSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object or null")
})
test("formats description for oneOf type schema with one item (with title) and not nullable", () => {
const oneOfSchema: OpenAPI.SchemaObject = {
oneOf: [
{ type: "object", title: "test-title", properties: {} },
],
properties: {},
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={oneOfSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("test-title")
})
test("formats description for oneOf type schema with one item (with title) and nullable", () => {
const oneOfSchema: OpenAPI.SchemaObject = {
oneOf: [
{ type: "object", title: "test-title", properties: {} },
],
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={oneOfSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("test-title or null")
})
test("formats description for oneOf type schema with array items (with type) and not nullable", () => {
const oneOfSchema: OpenAPI.SchemaObject = {
oneOf: [
{ type: "array", items: { type: "string", properties: {} }, properties: {} },
],
properties: {},
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={oneOfSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("array of strings")
})
test("formats description for oneOf type schema with array items (with type) and nullable", () => {
const oneOfSchema: OpenAPI.SchemaObject = {
oneOf: [
{ type: "array", items: { type: "string", properties: {} }, properties: {} },
],
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={oneOfSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("array of strings or null")
})
test("formats description for oneOf type schema with array items (without type) and not nullable", () => {
const oneOfSchema: OpenAPI.SchemaObject = {
oneOf: [
{ type: "array", items: { properties: {} }, properties: {} },
],
properties: {},
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={oneOfSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("array")
})
test("formats description for oneOf type schema with array items (without type) and nullable", () => {
const oneOfSchema: OpenAPI.SchemaObject = {
oneOf: [
{ type: "array", items: { properties: {} }, properties: {} },
],
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={oneOfSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("array or null")
})
test("formats description for oneOf type schema with mixed items and not nullable", () => {
const oneOfSchema: OpenAPI.SchemaObject = {
oneOf: [
{ type: "object", properties: {} },
{ type: "array", items: { type: "string", properties: {} }, properties: {} },
],
properties: {},
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={oneOfSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object or array of strings")
})
test("formats description for oneOf type schema with mixed items and nullable", () => {
const oneOfSchema: OpenAPI.SchemaObject = {
oneOf: [
{ type: "object", properties: {} },
{ type: "array", items: { type: "string", properties: {} }, properties: {} },
],
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={oneOfSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("object or array of strings or null")
})
})
describe("default type schema description formatting", () => {
test("formats description for default type schema with type, no nullable, no format", () => {
const defaultSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {}
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={defaultSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("string")
})
test("formats description for default type schema with type, nullable, no format", () => {
const defaultSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={defaultSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("string or null")
})
test("formats description for default type schema with type, no nullable, with format", () => {
const defaultSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {},
format: "date-time",
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={defaultSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("string <date-time>")
})
test("formats description for default type schema with type, nullable, with format", () => {
const defaultSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {},
format: "date-time",
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={defaultSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("string <date-time> or null")
})
test("formats description for default type schema without type and no nullable, no format", () => {
const defaultSchema: OpenAPI.SchemaObject = {
properties: {}
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={defaultSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("any")
})
test("formats description for default type schema without type and nullable, no format", () => {
const defaultSchema: OpenAPI.SchemaObject = {
properties: {},
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={defaultSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("any or null")
})
test("formats description for default type schema without type and no nullable, with format", () => {
const defaultSchema: OpenAPI.SchemaObject = {
properties: {},
format: "date-time",
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={defaultSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("any <date-time>")
})
test("formats description for default type schema without type and nullable, with format", () => {
const defaultSchema: OpenAPI.SchemaObject = {
properties: {},
format: "date-time",
nullable: true,
}
const { container } = render(
<TagOperationParametersName name={mockName} isRequired={false} schema={defaultSchema} />
)
const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
expect(typeDescriptionElement).toBeInTheDocument()
expect(typeDescriptionElement).toHaveTextContent("any <date-time> or null")
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import type { OpenAPI } from "types"
import { Badge, ExpandableNotice, FeatureFlagNotice } from "docs-ui"
import { Fragment } from "react"
@@ -26,7 +27,7 @@ const TagOperationParametersName = ({
case schema.type === "array":
typeDescription = (
<>
{schema.type === "array" && formatArrayDescription(schema.items)}
{formatArrayDescription((schema as OpenAPI.ArraySchemaObject).items)}
{schema.nullable ? ` or null` : ""}
</>
)
@@ -35,7 +36,7 @@ const TagOperationParametersName = ({
case schema.allOf !== undefined:
typeDescription = (
<>
{formatUnionDescription(schema.allOf)}
{formatUnionDescription(schema.allOf || schema.anyOf)}
{schema.nullable ? ` or null` : ""}
</>
)
@@ -43,7 +44,7 @@ const TagOperationParametersName = ({
case schema.oneOf !== undefined:
typeDescription = (
<>
{schema.oneOf?.map((item, index) => (
{schema.oneOf!.map((item, index) => (
<Fragment key={index}>
{index !== 0 && <> or </>}
{item.type !== "array" && <>{item.title || item.type}</>}
@@ -60,16 +61,21 @@ const TagOperationParametersName = ({
typeDescription = (
<>
{!schema.type ? "any" : schema.type}
{schema.nullable ? ` or null` : ""}
{schema.format ? ` <${schema.format}>` : ""}
{schema.nullable ? ` or null` : ""}
</>
)
}
return (
<span className="inline-flex gap-0.5 items-center">
<span className="font-monospace">{name}</span>
<span className="text-medusa-fg-muted text-compact-small">
<span className="font-monospace" data-testid="name">
{name}
</span>
<span
className="text-medusa-fg-muted text-compact-small"
data-testid="type-description"
>
{typeDescription}
</span>
{schema.deprecated && (
@@ -84,7 +90,10 @@ const TagOperationParametersName = ({
<FeatureFlagNotice featureFlag={schema["x-featureFlag"]} type="type" />
)}
{!isRequired && (
<span className="text-medusa-tag-blue-text text-compact-x-small">
<span
className="text-medusa-tag-blue-text text-compact-x-small"
data-testid="optional"
>
optional
</span>
)}

View File

@@ -1,3 +1,4 @@
import React from "react"
import clsx from "clsx"
export type TagsOperationParametersNestedProps =

View File

@@ -0,0 +1,68 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render, waitFor } from "@testing-library/react"
import { OpenAPI } from "types"
import { TagOperationParametersProps } from "../../../Parameters"
// mock data
const mockSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
}
// mock components
vi.mock("@/components/Tags/Operation/Parameters", () => ({
default: ({ schemaObject }: TagOperationParametersProps) => (
<div data-testid="parameters">{JSON.stringify(schemaObject)}</div>
),
}))
vi.mock("docs-ui", () => ({
Loading: () => <div data-testid="loading">Loading...</div>,
}))
import TagsOperationParametersSection from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders parameters", async () => {
const { container } = render(<TagsOperationParametersSection schema={mockSchema} />)
await waitFor(() => {
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveTextContent(JSON.stringify(mockSchema))
})
})
test("renders header when header is provided", () => {
const { container } = render(
<TagsOperationParametersSection header="test-header" schema={mockSchema} />
)
const headerElement = container.querySelector("[data-testid='header']")
expect(headerElement).toBeInTheDocument()
expect(headerElement).toHaveTextContent("test-header")
})
test("does not render header when header is not provided", () => {
const { container } = render(<TagsOperationParametersSection schema={mockSchema} />)
const headerElement = container.querySelector("[data-testid='header']")
expect(headerElement).not.toBeInTheDocument()
})
test("renders content type when content type is provided", () => {
const { container } = render(<TagsOperationParametersSection contentType="test-content-type" schema={mockSchema} />)
const contentTypeElement = container.querySelector("[data-testid='content-type']")
expect(contentTypeElement).toBeInTheDocument()
expect(contentTypeElement).toHaveTextContent("Content type: test-content-type")
})
test("does not render content type when content type is not provided", () => {
const { container } = render(<TagsOperationParametersSection schema={mockSchema} />)
const contentTypeElement = container.querySelector("[data-testid='content-type']")
expect(contentTypeElement).not.toBeInTheDocument()
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import type { OpenAPI } from "types"
import clsx from "clsx"
import type { TagOperationParametersProps } from ".."
@@ -27,12 +28,13 @@ const TagsOperationParametersSection = ({
{header && (
<h3
className={clsx(!contentType && "my-2", contentType && "mt-2 mb-0")}
data-testid="header"
>
{header}
</h3>
)}
{contentType && (
<span className={clsx("mb-2 inline-block")}>
<span className={clsx("mb-2 inline-block")} data-testid="content-type">
Content type: {contentType}
</span>
)}

View File

@@ -0,0 +1,150 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render, waitFor } from "@testing-library/react"
import { OpenAPI } from "types"
import { TagOperationParametersProps } from "../../.."
import { TagOperationParametersDefaultProps } from "../../Default"
// mock components
vi.mock("@/components/Tags/Operation/Parameters", () => ({
default: ({ schemaObject }: TagOperationParametersProps) => (
<div data-testid="parameters">{JSON.stringify(schemaObject)}</div>
),
}))
vi.mock("@/components/Tags/Operation/Parameters/Types/Default", () => ({
default: ({ schema }: TagOperationParametersDefaultProps) => (
<div data-testid="default">{JSON.stringify(schema)}</div>
),
}))
vi.mock("@/components/Tags/Operation/Parameters/Nested", () => ({
default: ({ children }: { children: React.ReactNode }) => (
<div data-testid="nested">{children}</div>
),
}))
vi.mock("docs-ui", () => ({
Loading: () => <div data-testid="loading">Loading...</div>,
Details: ({ children, summaryElm }: { children: React.ReactNode, summaryElm: React.ReactNode }) => (
<div data-testid="details">
<div data-testid="summary">{summaryElm}</div>
<div data-testid="content">{children}</div>
</div>
),
}))
import TagsOperationParametersArray from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("does not render when schema is not an array", () => {
const mockSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {}
}
const { container } = render(
<TagsOperationParametersArray name="test-name" schema={mockSchema} />
)
expect(container).toBeEmptyDOMElement()
})
test("renders default when items type is not an array, object, or undefined", async () => {
const mockSchema: OpenAPI.SchemaObject = {
type: "array",
items: { type: "string", properties: {} },
properties: {},
}
const { container } = render(<TagsOperationParametersArray name="test-name" schema={mockSchema} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveTextContent(JSON.stringify(mockSchema))
})
})
test("renders default when items is undefined", async () => {
const mockSchema: OpenAPI.SchemaObject = {
type: "array",
// @ts-expect-error - we are testing the case where items is undefined
items: undefined,
properties: {},
}
const { container } = render(
<TagsOperationParametersArray name="test-name" schema={mockSchema} />
)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveTextContent(JSON.stringify({ ...mockSchema, items: undefined }))
})
})
test("renders default when items type is an object with no properties, allOf, anyOf, or oneOf", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "array",
// @ts-expect-error - we are testing the case where items is an object with no properties, allOf, anyOf, or oneOf
items: { type: "object" },
properties: {},
}
const { container } = render(<TagsOperationParametersArray name="test-name" schema={modifiedSchema} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveTextContent(JSON.stringify(modifiedSchema))
})
})
test("renders details when items type is an object with properties, allOf, anyOf, or oneOf", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "array",
items: { type: "object", properties: { name: { type: "string", properties: {} } } },
properties: {},
}
const { container } = render(<TagsOperationParametersArray name="test-name" schema={modifiedSchema} />)
const detailsElement = container.querySelector("[data-testid='details']")
expect(detailsElement).toBeInTheDocument()
expect(detailsElement).toHaveTextContent(JSON.stringify(modifiedSchema))
})
test("renders details when items type is an array", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "array",
items: {
type: "array",
items: { type: "string", properties: {} },
properties: {},
},
properties: {},
}
const { container } = render(<TagsOperationParametersArray name="test-name" schema={modifiedSchema} />)
const detailsElement = container.querySelector("[data-testid='details']")
expect(detailsElement).toBeInTheDocument()
expect(detailsElement).toHaveTextContent(JSON.stringify(modifiedSchema))
})
test("renders items in nested parameters when items type is an array", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "array",
items: { type: "array", items: { type: "string", properties: {} }, properties: {} },
properties: {},
}
const { container } = render(<TagsOperationParametersArray name="test-name" schema={modifiedSchema} />)
const nestedElement = container.querySelector("[data-testid='nested']")
expect(nestedElement).toBeInTheDocument()
expect(nestedElement).toHaveTextContent(JSON.stringify(modifiedSchema.items))
})
test("renders items in nested parameters when items type is an object", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "array",
items: { type: "object", properties: { name: { type: "string", properties: {} } } },
properties: {},
}
const { container } = render(<TagsOperationParametersArray name="test-name" schema={modifiedSchema} />)
const nestedElement = container.querySelector("[data-testid='nested']")
expect(nestedElement).toBeInTheDocument()
expect(nestedElement).toHaveTextContent(JSON.stringify(modifiedSchema.items))
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import type { OpenAPI } from "types"
import dynamic from "next/dynamic"
import type { TagOperationParametersDefaultProps } from "../Default"
@@ -7,14 +8,14 @@ import { Details, Loading } from "docs-ui"
const TagOperationParametersDefault =
dynamic<TagOperationParametersDefaultProps>(
async () => import("../Default"),
async () => import("@/components/Tags/Operation/Parameters/Types/Default"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersDefaultProps>
const TagOperationParameters = dynamic<TagOperationParametersProps>(
async () => import("../.."),
async () => import("@/components/Tags/Operation/Parameters"),
{
loading: () => <Loading />,
}

View File

@@ -0,0 +1,80 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render, waitFor } from "@testing-library/react"
import { OpenAPI } from "types"
import { TagOperationParametersNameProps } from "../../../Name"
import { TagOperationParametersDescriptionProps } from "../../../Description"
// mock data
const mockSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {},
}
// mock components
vi.mock("@/components/Tags/Operation/Parameters/Description", () => ({
default: ({ schema }: TagOperationParametersDescriptionProps) => (
<div data-testid="description">{JSON.stringify(schema)}</div>
),
}))
vi.mock("@/components/Tags/Operation/Parameters/Name", () => ({
default: ({ name, isRequired, schema }: TagOperationParametersNameProps) => (
<div data-testid="name">{name} {isRequired ? "required" : "optional"} {JSON.stringify(schema)}</div>
),
}))
import TagsOperationParametersDefault from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders description", async () => {
const { container } = render(<TagsOperationParametersDefault schema={mockSchema} />)
const descriptionElement = container.querySelector("[data-testid='description']")
expect(descriptionElement).toBeInTheDocument()
expect(descriptionElement).toHaveTextContent(JSON.stringify(mockSchema))
})
test("renders name when name is provided", () => {
const { container } = render(<TagsOperationParametersDefault name="test-name" schema={mockSchema} />)
const nameElement = container.querySelector("[data-testid='name']")
expect(nameElement).toBeInTheDocument()
expect(nameElement).toHaveTextContent("test-name")
})
test("renders name with required when isRequired is true", () => {
const { container } = render(<TagsOperationParametersDefault name="test-name" schema={mockSchema} isRequired={true} />)
const nameElement = container.querySelector("[data-testid='name']")
expect(nameElement).toBeInTheDocument()
expect(nameElement).toHaveTextContent("test-name required")
})
test("does not render name when name is not provided", () => {
const { container } = render(<TagsOperationParametersDefault schema={mockSchema} />)
const nameElement = container.querySelector("[data-testid='name']")
expect(nameElement).not.toBeInTheDocument()
})
test("add expandable class when expandable is true", () => {
const { container } = render(<TagsOperationParametersDefault schema={mockSchema} expandable={true} />)
const element = container.querySelector("[data-testid='default']")
expect(element).toHaveClass("w-[calc(100%-16px)]")
expect(element).not.toHaveClass("w-full pl-1")
})
test("does not add expandable class when expandable is false", () => {
const { container } = render(<TagsOperationParametersDefault schema={mockSchema} expandable={false} />)
const element = container.querySelector("[data-testid='default']")
expect(element).not.toHaveClass("w-[calc(100%-16px)]")
expect(element).toHaveClass("w-full pl-1")
})
test("adds className when provided", () => {
const { container } = render(<TagsOperationParametersDefault schema={mockSchema} className="test-class" />)
const element = container.querySelector("[data-testid='default']")
expect(element).toHaveClass("test-class")
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import type { OpenAPI } from "types"
import TagOperationParametersDescription from "../../Description"
import clsx from "clsx"
@@ -26,6 +27,7 @@ const TagOperationParametersDefault = ({
!expandable && "w-full pl-1",
className
)}
data-testid="default"
>
{name && (
<TagOperationParametersName

View File

@@ -0,0 +1,327 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render, waitFor } from "@testing-library/react"
import { OpenAPI } from "types"
import { TagOperationParametersDefaultProps } from "../../Default"
import { TagOperationParametersProps } from "../../.."
// mock functions
const mockCheckRequired = vi.fn()
// mock components
vi.mock("@/components/Tags/Operation/Parameters/Types/Default", () => ({
default: ({ schema }: TagOperationParametersDefaultProps) => (
<div data-testid="default">{JSON.stringify(schema)}</div>
),
}))
vi.mock("@/components/Tags/Operation/Parameters/Nested", () => ({
default: ({ children }: { children: React.ReactNode }) => (
<div data-testid="nested">{children}</div>
),
}))
vi.mock("docs-ui", () => ({
Loading: () => <div data-testid="loading">Loading...</div>,
Details: ({ children, summaryElm }: { children: React.ReactNode, summaryElm: React.ReactNode }) => (
<div data-testid="details">
<div data-testid="summary">{summaryElm}</div>
<div data-testid="content">{children}</div>
</div>
),
}))
vi.mock("@/components/Tags/Operation/Parameters", () => ({
default: ({ schemaObject, isRequired, isExpanded }: TagOperationParametersProps) => (
<div data-testid="parameters">
{JSON.stringify(schemaObject)} {isRequired ? "required" : "optional"}
</div>
),
}))
vi.mock("@/utils/check-required", () => ({
default: (schema: OpenAPI.SchemaObject, property: string) => mockCheckRequired(schema, property),
}))
import TagsOperationParametersObject from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("does not render when schema is not an object", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "array",
items: { type: "string", properties: {} },
properties: {},
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} />)
expect(container).toBeEmptyDOMElement()
})
test("does not render when schema type is undefined", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: undefined,
properties: {},
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} />)
expect(container).toBeEmptyDOMElement()
})
test("does not render when properties is empty and additionalProperties is empty and name is not provided", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {},
additionalProperties: undefined,
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} />)
expect(container).toBeEmptyDOMElement()
})
test("renders description only when properties is empty and additionalProperties is empty and name is provided", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {},
additionalProperties: undefined,
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} name="test-name" />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveTextContent("object")
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).not.toBeInTheDocument()
})
})
test("renders parameters only when topLevel is true", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} topLevel={true} />)
await waitFor(() => {
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveTextContent(JSON.stringify({
type: "string",
properties: {},
parameterName: "name",
}))
const nestedElement = container.querySelector("[data-testid='nested']")
expect(nestedElement).not.toBeInTheDocument()
const detailsElement = container.querySelector("[data-testid='details']")
expect(detailsElement).not.toBeInTheDocument()
})
})
test("renders description and properties when properties is not empty", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} />)
await waitFor(() => {
const descriptionElement = container.querySelector("[data-testid='default']")
expect(descriptionElement).toBeInTheDocument()
expect(descriptionElement).toHaveTextContent(JSON.stringify(modifiedSchema))
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveTextContent(JSON.stringify({
type: "string",
properties: {},
parameterName: "name",
}))
const nestedElement = container.querySelector("[data-testid='nested']")
expect(nestedElement).toBeInTheDocument()
})
})
test("renders description and properties when additionalProperties is not empty", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {},
additionalProperties: { type: "object", properties: {
name: { type: "string", properties: {} },
} },
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} />)
await waitFor(() => {
const descriptionElement = container.querySelector("[data-testid='default']")
expect(descriptionElement).toBeInTheDocument()
expect(descriptionElement).toHaveTextContent("object")
})
})
test("renders description in summary when isExpanded is true", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} isExpanded={true} />)
await waitFor(() => {
const summaryElement = container.querySelector("summary")
expect(summaryElement).toBeInTheDocument()
const descriptionElement = summaryElement?.querySelector("[data-testid='default']")
expect(descriptionElement).toBeInTheDocument()
expect(descriptionElement).toHaveTextContent("object")
})
})
})
describe("parameters rendering", () => {
test("renders parameters when not empty", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} />)
await waitFor(() => {
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveTextContent(JSON.stringify({
type: "string",
properties: {},
parameterName: "name",
}))
})
})
test("renders parameters when additionalProperties is not empty", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {},
additionalProperties: { type: "object", properties: {
name: { type: "string", properties: {} },
} },
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} />)
await waitFor(() => {
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveTextContent(JSON.stringify({
type: "string",
properties: {},
parameterName: "name",
}))
})
})
test("gives precedence to properties over additionalProperties", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
additionalProperties: { type: "number", properties: {} },
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} />)
await waitFor(() => {
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveTextContent(JSON.stringify({
type: "string",
properties: {},
parameterName: "name",
}))
})
})
test("sorts properties to show required fields first", async () => {
mockCheckRequired.mockReturnValue(false)
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {}, isRequired: false },
age: { type: "number", properties: {}, isRequired: true },
},
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} />)
await waitFor(() => {
const parametersElement = container.querySelectorAll("[data-testid='parameters']")
expect(parametersElement).toHaveLength(2)
expect(parametersElement[0]).toHaveTextContent(JSON.stringify({
type: "number",
properties: {},
isRequired: true,
parameterName: "age",
}))
expect(parametersElement[1]).toHaveTextContent(JSON.stringify({
type: "string",
properties: {},
isRequired: false,
parameterName: "name",
}))
})
})
test("adds hr between properties", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {} },
age: { type: "number", properties: {} },
},
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} />)
await waitFor(() => {
const hrElements = container.querySelectorAll("hr")
expect(hrElements).toHaveLength(1)
expect(hrElements[0]).toBeInTheDocument()
expect(hrElements[0]).toHaveClass("bg-medusa-border-base my-0")
})
})
test("renders property as required when the property's isRequired is true", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {}, isRequired: true },
},
}
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} />)
await waitFor(() => {
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveTextContent("required")
})
})
test("renders property as required when property's isRequired is true and checkRequired returns true", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {}, isRequired: true },
},
}
mockCheckRequired.mockReturnValue(true)
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} />)
await waitFor(() => {
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveTextContent("required")
})
})
test("renders property as optional when the property's isRequired is false and checkRequired returns false", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {}, isRequired: false },
},
}
mockCheckRequired.mockReturnValue(false)
const { container } = render(<TagsOperationParametersObject schema={modifiedSchema} />)
await waitFor(() => {
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveTextContent("optional")
})
})
})

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import type { OpenAPI } from "types"
import TagOperationParametersDefault from "../Default"
import dynamic from "next/dynamic"
@@ -10,7 +11,7 @@ import { Loading, type DetailsProps } from "docs-ui"
import { Fragment, useMemo } from "react"
const TagOperationParameters = dynamic<TagOperationParametersProps>(
async () => import("../.."),
async () => import("@/components/Tags/Operation/Parameters"),
{
loading: () => <Loading />,
}
@@ -18,7 +19,7 @@ const TagOperationParameters = dynamic<TagOperationParametersProps>(
const TagsOperationParametersNested =
dynamic<TagsOperationParametersNestedProps>(
async () => import("../../Nested"),
async () => import("@/components/Tags/Operation/Parameters/Nested"),
{
loading: () => <Loading />,
}
@@ -89,8 +90,10 @@ const TagOperationParametersObject = ({
// sort properties to show required fields first
const sortedProperties = Object.keys(properties).sort(
(property1, property2) => {
properties[property1].isRequired = checkRequired(schema, property1)
properties[property2].isRequired = checkRequired(schema, property2)
properties[property1].isRequired =
properties[property1].isRequired || checkRequired(schema, property1)
properties[property2].isRequired =
properties[property2].isRequired || checkRequired(schema, property2)
return properties[property1].isRequired &&
properties[property2].isRequired

View File

@@ -0,0 +1,140 @@
import React from "react"
import { cleanup, fireEvent, render, waitFor } from "@testing-library/react"
import { describe, test, expect, vi, beforeEach } from "vitest"
import type { OpenAPI } from "types"
import { TagOperationParametersDefaultProps } from "../../Default"
import { TagOperationParametersProps } from "../../.."
// mock data
const mockSchema: OpenAPI.SchemaObject = {
title: "test-title",
oneOf: [
{
type: "object",
properties: {
name: { type: "string", properties: {} },
},
},
{
type: "string",
properties: {}
}
],
properties: {}
}
// mock components
vi.mock("@/components/Tags/Operation/Parameters/Types/Default", () => ({
default: ({ schema }: TagOperationParametersDefaultProps) => (
<div data-testid="default">{JSON.stringify(schema)}</div>
),
}))
vi.mock("@/components/Tags/Operation/Parameters/Nested", () => ({
default: ({ children }: { children: React.ReactNode }) => (
<div data-testid="nested">{children}</div>
),
}))
vi.mock("@/components/Tags/Operation/Parameters", () => ({
default: ({ schemaObject }: TagOperationParametersProps) => (
<div data-testid="parameters">{JSON.stringify(schemaObject)}</div>
),
}))
vi.mock("docs-ui", () => ({
Details: ({ children, summaryElm }: { children: React.ReactNode, summaryElm: React.ReactNode }) => (
<div data-testid="details">
{summaryElm}
{children}
</div>
),
Loading: () => <div data-testid="loading">Loading...</div>,
}))
import TagsOperationParametersTypesOneOf from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("does not render when schema is not an oneOf type", () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
}
const { container } = render(<TagsOperationParametersTypesOneOf schema={modifiedSchema} />)
expect(container).toBeEmptyDOMElement()
})
test("renders one of type schema not nested by default", async () => {
const { container } = render(<TagsOperationParametersTypesOneOf schema={mockSchema} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).not.toBeInTheDocument()
const nestedElement = container.querySelector("[data-testid='nested']")
expect(nestedElement).not.toBeInTheDocument()
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveTextContent(JSON.stringify(mockSchema.oneOf![0]))
})
})
test("renders nested one of type schema", async () => {
const { container } = render(<TagsOperationParametersTypesOneOf schema={mockSchema} isNested={true} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
const nestedElement = container.querySelector("[data-testid='nested']")
expect(nestedElement).toBeInTheDocument()
const parametersElement = nestedElement!.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveTextContent(JSON.stringify(mockSchema.oneOf![0]))
})
})
test("renders parameter name when provided and nested is enabled", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
parameterName: "test-parameter-name",
}
const { container } = render(<TagsOperationParametersTypesOneOf schema={modifiedSchema} isNested={true} />)
await waitFor(() => {
const detailsElement = container.querySelector("[data-testid='details']")
expect(detailsElement).toBeInTheDocument()
const summaryElement = detailsElement!.querySelector("summary")
expect(summaryElement).toBeInTheDocument()
expect(summaryElement).toHaveTextContent(modifiedSchema.parameterName!)
})
})
test("renders schema title when parameter name is not provided and nested is enabled", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
...mockSchema,
parameterName: undefined,
}
const { container } = render(<TagsOperationParametersTypesOneOf schema={modifiedSchema} isNested={true} />)
await waitFor(() => {
const detailsElement = container.querySelector("[data-testid='details']")
expect(detailsElement).toBeInTheDocument()
const summaryElement = detailsElement!.querySelector("summary")
expect(summaryElement).toBeInTheDocument()
expect(summaryElement).toHaveTextContent(modifiedSchema.title!)
})
})
})
describe("interaction", () => {
test("toggles between one of options when clicking on the tab", async () => {
const { container } = render(<TagsOperationParametersTypesOneOf schema={mockSchema} />)
const tabElements = container.querySelectorAll("[data-testid='tab']")
expect(tabElements).toHaveLength(mockSchema.oneOf!.length)
fireEvent.click(tabElements[1]!)
await waitFor(() => {
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveTextContent(JSON.stringify(mockSchema.oneOf![1]))
})
})
})

View File

@@ -1,7 +1,7 @@
import React, { useState } from "react"
import type { OpenAPI } from "types"
import clsx from "clsx"
import dynamic from "next/dynamic"
import { useState } from "react"
import type { TagOperationParametersDefaultProps } from "../Default"
import type { TagsOperationParametersNestedProps } from "../../Nested"
import type { TagOperationParametersProps } from "../.."
@@ -43,22 +43,15 @@ const TagOperationParamatersOneOf = ({
}: TagOperationParamatersOneOfProps) => {
const [activeTab, setActiveTab] = useState<number>(0)
if (!schema.oneOf) {
return null
}
const getName = (item: OpenAPI.SchemaObject): string => {
if (item.title) {
return item.title
}
if (item.anyOf || item.allOf) {
// return the name of any of the items
const name = item.anyOf
? item.anyOf.find((i) => i.title !== undefined)?.title
: item.allOf?.find((i) => i.title !== undefined)?.title
if (name) {
return name
}
}
return item.type || ""
}
@@ -84,6 +77,7 @@ const TagOperationParamatersOneOf = ({
]
)}
onClick={() => setActiveTab(index)}
data-testid={"tab"}
>
{getName(item)}
</li>
@@ -91,14 +85,10 @@ const TagOperationParamatersOneOf = ({
</ul>
</div>
{schema.oneOf && (
<>
<TagOperationParameters
schemaObject={schema.oneOf[activeTab]}
topLevel={true}
/>
</>
)}
<TagOperationParameters
schemaObject={schema.oneOf![activeTab]}
topLevel={true}
/>
</>
)
}

View File

@@ -0,0 +1,155 @@
import React from "react"
import { cleanup, render, waitFor } from "@testing-library/react"
import { describe, test, expect, vi, beforeEach } from "vitest"
import type { OpenAPI } from "types"
import { TagOperationParametersObjectProps } from "../../Object"
import { TagOperationParametersDefaultProps } from "../../Default"
// mock data
const mockAnyOfSchema: OpenAPI.SchemaObject = {
anyOf: [
{ type: "object", properties: {
name: { type: "string", properties: {} },
} },
{ type: "string", properties: {} },
],
properties: {}
}
const mockAllOfSchema: OpenAPI.SchemaObject = {
allOf: [
{ type: "object", properties: {
name: { type: "string", properties: {} },
} },
{ type: "string", properties: {} },
],
properties: {}
}
// mock functions
const mockMergeAllOfTypes = vi.fn((schema: OpenAPI.SchemaObject) => schema.allOf?.[0])
// mock components
vi.mock("@/components/Tags/Operation/Parameters/Types/Object", () => ({
default: ({ schema }: TagOperationParametersObjectProps) => (
<div data-testid="object" data-description={schema.description}>{JSON.stringify(schema)}</div>
),
}))
vi.mock("@/components/Tags/Operation/Parameters/Types/Default", () => ({
default: ({ schema }: TagOperationParametersDefaultProps) => (
<div data-testid="default">{JSON.stringify(schema)}</div>
),
}))
vi.mock("docs-ui", () => ({
Loading: () => <div data-testid="loading">Loading...</div>,
}))
vi.mock("@/utils/merge-all-of-types", () => ({
default: (schema: OpenAPI.SchemaObject) => mockMergeAllOfTypes(schema),
}))
import TagOperationParametersUnion from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders default when schema is not an anyOf or allOf type", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
}
const { container } = render(<TagOperationParametersUnion name="test-name" schema={modifiedSchema} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveTextContent(JSON.stringify(modifiedSchema))
})
})
test("renders default when schema is any of and type is not object", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
anyOf: [
{ type: "string", properties: {} },
],
properties: {}
}
const { container } = render(<TagOperationParametersUnion name="test-name" schema={modifiedSchema} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveTextContent(JSON.stringify(modifiedSchema))
})
})
test("renders default when schema is any of and type is object and properties is not defined", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
anyOf: [
{ type: "object", properties: {} },
],
properties: {}
}
const { container } = render(<TagOperationParametersUnion name="test-name" schema={modifiedSchema} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveTextContent(JSON.stringify(modifiedSchema))
})
})
test("renders any of type schema", async () => {
const { container } = render(<TagOperationParametersUnion name="test-name" schema={mockAnyOfSchema} />)
await waitFor(() => {
const objectElement = container.querySelector("[data-testid='object']")
expect(objectElement).toBeInTheDocument()
expect(objectElement).toHaveTextContent(JSON.stringify(mockAnyOfSchema.anyOf![0]))
})
})
test("renders all of type schema", async () => {
const { container } = render(<TagOperationParametersUnion name="test-name" schema={mockAllOfSchema} />)
await waitFor(() => {
const objectElement = container.querySelector("[data-testid='object']")
expect(objectElement).toBeInTheDocument()
expect(objectElement).toHaveTextContent(JSON.stringify(mockAllOfSchema.allOf![0]))
})
expect(mockMergeAllOfTypes).toHaveBeenCalledWith(mockAllOfSchema)
})
test("renders schema description from selected object schema when provided", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
anyOf: [
{ ...mockAllOfSchema.allOf![0], description: "test-description 1" },
{ ...mockAllOfSchema.allOf![1], description: "test-description 2" },
],
properties: {},
}
const { container } = render(<TagOperationParametersUnion name="test-name" schema={modifiedSchema} />)
await waitFor(() => {
const objectElement = container.querySelector("[data-testid='object']")
expect(objectElement).toBeInTheDocument()
expect(objectElement).toHaveAttribute("data-description", "test-description 1")
})
})
test("renders schema description from any item having description when selected object doesn't have description", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
anyOf: [
{ ...mockAllOfSchema.allOf![0] },
{ ...mockAllOfSchema.allOf![1], description: "test-description" },
],
properties: {},
}
const { container } = render(<TagOperationParametersUnion name="test-name" schema={modifiedSchema} />)
await waitFor(() => {
const objectElement = container.querySelector("[data-testid='object']")
expect(objectElement).toBeInTheDocument()
expect(objectElement).toHaveAttribute("data-description", "test-description")
})
})
})

View File

@@ -1,9 +1,10 @@
import React from "react"
import type { OpenAPI } from "types"
import dynamic from "next/dynamic"
import type { TagOperationParametersDefaultProps } from "../Default"
import { TagOperationParametersObjectProps } from "../Object"
import { Loading } from "docs-ui"
import mergeAllOfTypes from "../../../../../../utils/merge-all-of-types"
import mergeAllOfTypes from "@/utils/merge-all-of-types"
const TagOperationParametersObject = dynamic<TagOperationParametersObjectProps>(
async () => import("../Object"),
@@ -34,7 +35,12 @@ const TagOperationParametersUnion = ({
topLevel,
}: TagOperationParametersUnionProps) => {
const objectSchema = schema.anyOf
? schema.anyOf.find((item) => item.type === "object" && item.properties)
? schema.anyOf.find(
(item) =>
item.type === "object" &&
item.properties &&
Object.keys(item.properties).length > 0
)
: schema.allOf
? mergeAllOfTypes(schema)
: undefined

View File

@@ -0,0 +1,293 @@
import React from "react"
import { cleanup, render, waitFor } from "@testing-library/react"
import { describe, test, expect, vi, beforeEach } from "vitest"
import type { OpenAPI } from "types"
import { TagOperationParametersUnionProps } from "../Types/Union"
import { TagOperationParametersArrayProps } from "../Types/Array"
import { TagOperationParamatersOneOfProps } from "../Types/OneOf"
import { TagOperationParametersObjectProps } from "../Types/Object"
import { TagOperationParametersDefaultProps } from "../Types/Default"
// mock functions
const mockCheckRequired = vi.fn()
// mock components
vi.mock("@/components/Tags/Operation/Parameters/Types/Union", () => ({
default: ({ schema, isRequired, name }: TagOperationParametersUnionProps) => (
<div data-testid="union" data-required={isRequired} data-name={name}>{JSON.stringify(schema)}</div>
),
}))
vi.mock("@/components/Tags/Operation/Parameters/Types/OneOf", () => ({
default: ({ schema, isRequired }: TagOperationParamatersOneOfProps) => (
<div data-testid="one-of" data-required={isRequired}>{JSON.stringify(schema)}</div>
),
}))
vi.mock("@/components/Tags/Operation/Parameters/Types/Array", () => ({
default: ({ schema, isRequired, name }: TagOperationParametersArrayProps) => (
<div data-testid="array" data-required={isRequired} data-name={name}>{JSON.stringify(schema)}</div>
),
}))
vi.mock("@/components/Tags/Operation/Parameters/Types/Object", () => ({
default: ({ schema, isRequired, name }: TagOperationParametersObjectProps) => (
<div data-testid="object" data-required={isRequired} data-name={name}>{JSON.stringify(schema)}</div>
),
}))
vi.mock("@/components/Tags/Operation/Parameters/Types/Default", () => ({
default: ({ schema, isRequired, name }: TagOperationParametersDefaultProps) => (
<div data-testid="default" data-required={isRequired} data-name={name}>{JSON.stringify(schema)}</div>
),
}))
vi.mock("@/utils/check-required", () => ({
default: (schema: OpenAPI.SchemaObject) => mockCheckRequired(schema),
}))
vi.mock("docs-utils", () => ({
Loading: () => <div data-testid="loading">Loading...</div>,
}))
import TagOperationParameters from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders union when schema is anyOf", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
anyOf: [
{ type: "object", properties: {} },
{ type: "string", properties: {} },
],
properties: {},
}
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} />)
await waitFor(() => {
const unionElement = container.querySelector("[data-testid='union']")
expect(unionElement).toBeInTheDocument()
expect(unionElement).toHaveTextContent(JSON.stringify(modifiedSchema))
})
})
test("renders union when schema is allOf", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
allOf: [
{ type: "object", properties: {} },
{ type: "string", properties: {} },
],
properties: {},
}
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} />)
await waitFor(() => {
const unionElement = container.querySelector("[data-testid='union']")
expect(unionElement).toBeInTheDocument()
expect(unionElement).toHaveTextContent(JSON.stringify(modifiedSchema))
})
expect(container.querySelector("[data-testid='object']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='array']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='default']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='one-of']")).not.toBeInTheDocument()
})
test("renders oneOf when schema is oneOf", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
oneOf: [
{ type: "object", properties: {} },
{ type: "string", properties: {} },
],
properties: {},
}
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} />)
await waitFor(() => {
const oneOfElement = container.querySelector("[data-testid='one-of']")
expect(oneOfElement).toBeInTheDocument()
expect(oneOfElement).toHaveTextContent(JSON.stringify(modifiedSchema))
})
expect(container.querySelector("[data-testid='union']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='object']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='array']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='default']")).not.toBeInTheDocument()
})
test("renders array when schema is array", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "array",
items: { type: "string", properties: {} },
properties: {},
}
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} />)
await waitFor(() => {
const arrayElement = container.querySelector("[data-testid='array']")
expect(arrayElement).toBeInTheDocument()
expect(arrayElement).toHaveTextContent(JSON.stringify(modifiedSchema))
})
expect(container.querySelector("[data-testid='union']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='object']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='one-of']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='default']")).not.toBeInTheDocument()
})
test("renders object when schema is object", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
}
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} />)
await waitFor(() => {
const objectElement = container.querySelector("[data-testid='object']")
expect(objectElement).toBeInTheDocument()
expect(objectElement).toHaveTextContent(JSON.stringify(modifiedSchema))
})
expect(container.querySelector("[data-testid='union']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='one-of']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='array']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='default']")).not.toBeInTheDocument()
})
test("renders object when schema's type is undefined", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: undefined,
properties: {}
}
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} />)
await waitFor(() => {
const objectElement = container.querySelector("[data-testid='object']")
expect(objectElement).toBeInTheDocument()
expect(objectElement).toHaveTextContent(JSON.stringify(modifiedSchema))
})
expect(container.querySelector("[data-testid='union']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='one-of']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='array']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='default']")).not.toBeInTheDocument()
})
test("renders default when schema is not an anyOf, allOf, oneOf, array, or object", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {},
}
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveTextContent(JSON.stringify(modifiedSchema))
})
expect(container.querySelector("[data-testid='union']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='one-of']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='array']")).not.toBeInTheDocument()
expect(container.querySelector("[data-testid='object']")).not.toBeInTheDocument()
})
test("adds className when provided", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {},
}
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} className="test-class" />)
const parametersElement = container.querySelector("[data-testid='parameters']")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveClass("test-class")
})
})
describe("isRequired", () => {
test("sets parameter as required when isRequired is true", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {},
}
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} isRequired={true} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveAttribute("data-required", "true")
})
})
test("sets parameter as required when isRequired is false and checkRequired returns true", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {},
}
mockCheckRequired.mockReturnValue(true)
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} isRequired={false} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveAttribute("data-required", "true")
})
})
test("sets parameter as optional when isRequired is false and checkRequired returns false", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {},
}
mockCheckRequired.mockReturnValue(false)
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} isRequired={false} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveAttribute("data-required", "false")
})
})
})
describe("parameterName", () => {
test("sets parameter name to parameterName when provided", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {},
parameterName: "test-name",
}
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveAttribute("data-name", "test-name")
})
})
test("sets parameter name to title when parameterName is not provided", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {},
title: "test-title",
}
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveAttribute("data-name", "test-title")
})
})
test("sets parameter name to empty string when parameterName is not provided and title is not provided", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {},
}
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveAttribute("data-name", "")
})
})
test("gives precedence to parameterName over title", async () => {
const modifiedSchema: OpenAPI.SchemaObject = {
type: "string",
properties: {},
parameterName: "test-name",
title: "test-title",
}
const { container } = render(<TagOperationParameters schemaObject={modifiedSchema} />)
await waitFor(() => {
const defaultElement = container.querySelector("[data-testid='default']")
expect(defaultElement).toBeInTheDocument()
expect(defaultElement).toHaveAttribute("data-name", "test-name")
})
})
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import type { OpenAPI } from "types"
import dynamic from "next/dynamic"
import type { TagOperationParametersObjectProps } from "./Types/Object"
@@ -109,11 +110,13 @@ const TagOperationParameters = ({
isRequired={isRequired}
/>
)
return <></>
}
return <div className={className}>{getElement()}</div>
return (
<div className={className} data-testid="parameters">
{getElement()}
</div>
)
}
export default TagOperationParameters

View File

@@ -0,0 +1,489 @@
import React, { useState } from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render, waitFor, act, fireEvent } from "@testing-library/react"
import { OpenAPI } from "types"
// mock data
const mockOperation: OpenAPI.Operation = {
operationId: "mockOperation",
summary: "Mock Operation",
description: "Mock Operation",
"x-authenticated": false,
"x-codeSamples": [
{
label: "Request Sample 1",
lang: "javascript",
source: "console.log('Request Sample 1')",
}
],
requestBody: {
content: {},
},
parameters: [],
responses: {
"200": {
description: "OK",
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
},
},
},
},
},
}
const mockTag: OpenAPI.OpenAPIV3.TagObject = {
name: "mock-tag",
description: "Mock Tag",
}
const mockEndpointPath = "/mock-endpoint-path"
// mock functions
const mockIsElmWindow = vi.fn(() => false)
const mockUseIsBrowser = vi.fn(() => ({
isBrowser: true,
}))
const mockScrollToTop = vi.fn()
const mockUseScrollController = vi.fn(() => ({
scrollableElement: null as HTMLElement | null,
scrollToTop: mockScrollToTop,
}))
const mockSetActivePath = vi.fn()
const mockUseSidebar = vi.fn(() => ({
activePath: null as string | null,
setActivePath: mockSetActivePath,
}))
const mockRemoveLoading = vi.fn()
const mockUseLoading = vi.fn(() => ({
loading: false,
removeLoading: mockRemoveLoading,
}))
const mockPush = vi.fn()
const mockReplace = vi.fn()
const mockUseRouter = vi.fn(() => ({
push: mockPush,
replace: mockReplace,
}))
const mockGetSectionId = vi.fn((options: unknown) => "mock-section-id")
const mockCheckElementInViewport = vi.fn(() => true)
// mock components
vi.mock("react-intersection-observer", () => ({
InView: ({ children, onChange, root }: { children: React.ReactNode, onChange: (inView: boolean) => void, root: HTMLElement | null }) => {
const [inView, setInView] = useState(false)
return (
<div data-testid="in-view" data-root={root?.tagName}>
{children}
<button type="button" data-testid="in-view-toggle-button" onClick={(e) => {
setInView(!inView)
onChange(!inView)
}}>
{inView.toString()}
</button>
</div>
)
},
}))
vi.mock("docs-ui", () => ({
isElmWindow: () => mockIsElmWindow(),
useIsBrowser: () => mockUseIsBrowser(),
useScrollController: () => mockUseScrollController(),
useSidebar: () => mockUseSidebar(),
}))
vi.mock("@/components/Tags/Operation/CodeSection", () => ({
default: ({ operation, method }: { operation: OpenAPI.Operation, method?: string }) => (
<div data-testid="code-section" data-method={method}>{JSON.stringify(operation)}</div>
),
}))
vi.mock("@/components/Tags/Operation/DescriptionSection", () => ({
default: ({ operation}: { operation: OpenAPI.Operation }) => (
<div data-testid="description-section">{JSON.stringify(operation)}</div>
),
}))
vi.mock("@/layouts/Divided", () => ({
default: ({ mainContent, codeContent }: { mainContent: React.ReactNode, codeContent: React.ReactNode }) => (
<div data-testid="divided">
<div data-testid="divided-main-content">{mainContent}</div>
<div data-testid="divided-code-content">{codeContent}</div>
</div>
),
}))
vi.mock("@/providers/loading", () => ({
useLoading: () => mockUseLoading(),
}))
vi.mock("@/utils/check-element-in-viewport", () => ({
default: () => mockCheckElementInViewport(),
}))
vi.mock("@/components/DividedLoading", () => ({
default: ({ children }: { children: React.ReactNode }) => (
<div data-testid="divided-loading">{children}</div>
),
}))
vi.mock("@/components/Section/Container", () => ({
default: React.forwardRef<HTMLDivElement, { children: React.ReactNode; className?: string }>(
({ children, className }, ref) => (
<div data-testid="section-container" className={className} ref={ref}>
{children}
</div>
)
),
}))
vi.mock("docs-utils", () => ({
getSectionId: (options: unknown) => mockGetSectionId(options),
}))
vi.mock("next/navigation", () => ({
useRouter: () => mockUseRouter(),
}))
import TagOperation from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
// Reset location hash
window.location.hash = ""
})
describe("rendering", () => {
test("renders InView wrapper with correct props", () => {
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
const inViewElement = getByTestId("in-view")
expect(inViewElement).toBeInTheDocument()
})
test("renders SectionContainer", () => {
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
const sectionContainerElement = getByTestId("section-container")
expect(sectionContainerElement).toBeInTheDocument()
})
test("renders with className", () => {
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
className="test-class"
/>
)
const sectionContainerElement = getByTestId("section-container")
expect(sectionContainerElement).toHaveClass("test-class")
})
test("renders loading component when show is false", () => {
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
const loadingElement = getByTestId("divided-loading")
expect(loadingElement).toBeInTheDocument()
})
test("renders loading component initially", () => {
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
const loadingElement = getByTestId("divided-loading")
expect(loadingElement).toBeInTheDocument()
})
})
describe("path generation", () => {
test("generates path using getSectionId with tags and operationId", () => {
const operationWithTags: OpenAPI.Operation = {
...mockOperation,
tags: ["tag1", "tag2"],
operationId: "test-operation",
}
render(
<TagOperation
operation={operationWithTags}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
expect(mockGetSectionId).toHaveBeenCalledWith(["tag1", "tag2", "test-operation"])
})
test("generates path using getSectionId with empty tags array when tags are not provided", () => {
const operationWithoutTags: OpenAPI.Operation = {
...mockOperation,
tags: undefined,
operationId: "test-operation",
}
render(
<TagOperation
operation={operationWithoutTags}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
expect(mockGetSectionId).toHaveBeenCalledWith(["test-operation"])
})
})
describe("hash matching and scrolling", () => {
test("removes loading when nodeRef is set", async () => {
render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
expect(mockRemoveLoading).toHaveBeenCalled()
})
test("scrolls into view when hash matches path", async () => {
const mockPath = "mock-section-id"
mockGetSectionId.mockReturnValue(mockPath)
window.location.hash = `#${mockPath}`
const { container } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
expect(mockRemoveLoading).toHaveBeenCalled()
await waitFor(() => {
const operationContainer = container.querySelector("[data-testid='operation-container']")
expect(operationContainer).toBeInTheDocument()
})
})
test("sets show to true when hash prefix matches path prefix", async () => {
const mockPath = "mock-section-id_subsection"
mockGetSectionId.mockReturnValue(mockPath)
window.location.hash = "#mock-section-id"
const { container } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
expect(mockRemoveLoading).toHaveBeenCalled()
await waitFor(() => {
const operationContainer = container.querySelector("[data-testid='operation-container']")
expect(operationContainer).toBeInTheDocument()
})
})
})
describe("InView behavior", () => {
test("sets active path when in view and activePath is different", () => {
const mockPath = "mock-section-id"
window.location.hash = "#different-hash"
mockGetSectionId.mockReturnValue(mockPath)
mockUseSidebar.mockReturnValue({
activePath: "different-hash",
setActivePath: mockSetActivePath,
})
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
expect(mockSetActivePath).toHaveBeenCalledWith(mockPath)
})
test("updates router hash when in view and hash is different", () => {
const mockPath = "mock-section-id"
mockGetSectionId.mockReturnValue(mockPath)
window.location.hash = "#different-hash"
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
expect(getByTestId("in-view")).toBeInTheDocument()
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
expect(mockPush).toHaveBeenCalledWith(`#${mockPath}`, { scroll: false })
})
test("removes loading when in view and loading is true", () => {
mockUseLoading.mockReturnValue({
loading: true,
removeLoading: mockRemoveLoading,
})
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
expect(getByTestId("in-view")).toBeInTheDocument()
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
expect(mockRemoveLoading).toHaveBeenCalled()
})
test("sets show to false when out of view and element is not in viewport", () => {
mockCheckElementInViewport.mockReturnValue(false)
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
expect(getByTestId("in-view")).toBeInTheDocument()
const inViewToggleButton = getByTestId("in-view-toggle-button")
// toggle it to true
fireEvent.click(inViewToggleButton)
// toggle it to false
fireEvent.click(inViewToggleButton)
// should show the divided loading now
expect(getByTestId("divided-loading")).toBeInTheDocument()
})
})
describe("browser environment", () => {
test("handles non-browser environment", () => {
mockUseIsBrowser.mockReturnValue({ isBrowser: false })
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
const inViewElement = getByTestId("in-view")
expect(inViewElement).toBeInTheDocument()
expect(inViewElement).not.toHaveAttribute("data-root")
})
test("uses document.body as root when scrollableElement is window and isBrowser is true", () => {
mockIsElmWindow.mockReturnValue(true)
mockUseScrollController.mockReturnValue({
scrollableElement: window as unknown as HTMLElement,
scrollToTop: mockScrollToTop,
})
mockUseIsBrowser.mockReturnValue({ isBrowser: true })
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
const inViewElement = getByTestId("in-view")
expect(inViewElement).toBeInTheDocument()
expect(inViewElement).toHaveAttribute("data-root", "BODY")
})
test("uses scrollableElement as root when it is not window and isBrowser is true", () => {
const mockScrollableElement = document.createElement("div")
mockIsElmWindow.mockReturnValue(false)
mockUseIsBrowser.mockReturnValue({ isBrowser: true })
mockUseScrollController.mockReturnValue({
scrollableElement: mockScrollableElement,
scrollToTop: mockScrollToTop,
})
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
const inViewElement = getByTestId("in-view")
expect(inViewElement).toBeInTheDocument()
expect(inViewElement).toHaveAttribute("data-root", "DIV")
})
})
describe("method prop", () => {
test("renders component with method prop", async () => {
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
method="POST"
/>
)
const inViewToggleButton = getByTestId("in-view-toggle-button")
// set show to true
fireEvent.click(inViewToggleButton)
await waitFor(() => {
const codeSectionElement = getByTestId("code-section")
expect(codeSectionElement).toBeInTheDocument()
expect(codeSectionElement).toHaveAttribute("data-method", "POST")
})
})
test("renders component without method prop", async () => {
const { getByTestId } = render(
<TagOperation
operation={mockOperation}
tag={mockTag}
endpointPath={mockEndpointPath}
/>
)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
await waitFor(() => {
const codeSectionElement = getByTestId("code-section")
expect(codeSectionElement).toBeInTheDocument()
expect(codeSectionElement).toHaveAttribute("data-method", "")
})
})
})

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import type { OpenAPI } from "types"
import clsx from "clsx"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
@@ -75,14 +76,16 @@ const TagOperation = ({
}, [nodeRef, isBrowser, scrollToTop])
useEffect(() => {
if (nodeRef && nodeRef.current) {
removeLoading()
const currentHash = location.hash.replace("#", "")
if (currentHash === path) {
setTimeout(scrollIntoView, 200)
} else if (currentHash.split("_")[0] === path.split("_")[0]) {
setShow(true)
}
if (!nodeRef.current) {
return
}
removeLoading()
const currentHash = location.hash.replace("#", "")
if (currentHash === path) {
setTimeout(scrollIntoView, 200)
} else if (currentHash.split("_")[0] === path.split("_")[0]) {
setShow(true)
}
}, [nodeRef, path, scrollIntoView])
@@ -129,6 +132,7 @@ const TagOperation = ({
style={{
animationFillMode: "forwards",
}}
data-testid="operation-container"
>
<DividedLayout
mainContent={

View File

@@ -0,0 +1,349 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
import { OpenAPI, Sidebar } from "types"
// mock data
const mockTag: OpenAPI.TagObject = {
name: "mockTag",
description: "Mock Tag",
}
const mockOperation: OpenAPI.Operation = {
operationId: "mockOperation",
summary: "Mock Operation",
description: "Mock Operation",
"x-authenticated": false,
"x-codeSamples": [],
requestBody: { content: {} },
parameters: [],
responses: {
"200": {
description: "OK",
content: {
"application/json": {
schema: {
type: "object",
properties: { name: { type: "string", properties: {} } }
}
}
}
}
}
}
const mockPaths: OpenAPI.PathsObject = {
"/mock-path": {
get: mockOperation,
},
}
const mockSidebar: Sidebar.Sidebar = {
sidebar_id: "mock-sidebar-id",
title: "Mock Sidebar",
items: [],
}
// mock functions
const mockFindSidebarItem = vi.fn((options: unknown) => undefined as Sidebar.SidebarItem | undefined)
const mockAddItems = vi.fn()
const mockUseSidebar = vi.fn(() => ({
shownSidebar: mockSidebar as Sidebar.Sidebar | Sidebar.SidebarItemSidebar | undefined,
addItems: mockAddItems,
}))
const mockUseLoading = vi.fn(() => ({
loading: false,
}))
const mockGetTagChildSidebarItems = vi.fn(() => [] as Sidebar.SidebarItem[])
const mockCompareOperations = vi.fn((options: unknown) => 0)
// mock components and hooks
vi.mock("docs-ui", () => ({
findSidebarItem: (options: unknown) => mockFindSidebarItem(options),
useSidebar: () => mockUseSidebar(),
}))
vi.mock("react", async () => {
const actual = await vi.importActual<typeof React>("react")
return {
...actual,
Suspense: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}
})
vi.mock("@/utils/get-tag-child-sidebar-items", () => ({
default: () => mockGetTagChildSidebarItems(),
}))
vi.mock("@/providers/loading", () => ({
useLoading: () => mockUseLoading(),
}))
vi.mock("@/components/DividedLoading", () => ({
default: (className: string) => <div data-testid="divided-loading" className={className}>Loading...</div>,
}))
vi.mock("@/utils/sort-operations-utils", () => ({
compareOperations: (options: unknown) => mockCompareOperations(options),
}))
vi.mock("@/components/Tags/Operation", () => ({
default: (props: TagOperationProps) => (
<div
data-testid="operation-container"
data-method={props.method}
data-endpoint-path={props.endpointPath}
data-operation-id={props.operation.operationId}
>Operation</div>
),
}))
import TagPaths from ".."
import { TagOperationProps } from "../../Operation"
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders loading when loading is true", () => {
mockUseLoading.mockReturnValue({ loading: true })
const { container } = render(
<TagPaths tag={mockTag} paths={mockPaths} />
)
const dividedLoadingElement = container.querySelector("[data-testid='divided-loading']")
expect(dividedLoadingElement).toBeInTheDocument()
})
test("does not render loading when loading is false", () => {
mockUseLoading.mockReturnValue({ loading: false })
const { container } = render(
<TagPaths tag={mockTag} paths={mockPaths} />
)
const dividedLoadingElement = container.querySelector("[data-testid='divided-loading']")
expect(dividedLoadingElement).not.toBeInTheDocument()
})
test("renders operations", () => {
const { container } = render(
<TagPaths tag={mockTag} paths={mockPaths} />
)
const operationElements = container.querySelectorAll("[data-testid='operation-container']")
expect(operationElements).toHaveLength(1)
expect(operationElements[0]).toHaveAttribute("data-method", "get")
expect(operationElements[0]).toHaveAttribute("data-endpoint-path", "/mock-path")
expect(operationElements[0]).toHaveAttribute("data-operation-id", "mockOperation")
})
test("renders operations in the correct order", () => {
// change the mockCompareOperations return value to 1
mockCompareOperations.mockReturnValue(-1)
const modifiedMockPaths: OpenAPI.PathsObject = {
"/mock-path": {
get: {
...mockOperation,
operationId: "mockOperation1",
},
post: {
...mockOperation,
operationId: "mockOperation2",
},
},
}
const { container } = render(
<TagPaths tag={mockTag} paths={modifiedMockPaths} />
)
const operationElements = container.querySelectorAll("[data-testid='operation-container']")
expect(operationElements).toHaveLength(2)
expect(operationElements[0]).toHaveAttribute("data-operation-id", "mockOperation2")
expect(operationElements[1]).toHaveAttribute("data-operation-id", "mockOperation1")
})
})
describe("sidebar", () => {
test("doesn't add items to sidebar when shownSidebar is not set", () => {
mockUseSidebar.mockReturnValue({
shownSidebar: undefined,
addItems: mockAddItems,
})
render(
<TagPaths tag={mockTag} paths={mockPaths} />
)
expect(mockAddItems).not.toHaveBeenCalled()
})
test("doesn't add items to sidebar when paths is not set", () => {
mockUseSidebar.mockReturnValue({
shownSidebar: mockSidebar,
addItems: mockAddItems,
})
render(
<TagPaths tag={mockTag} paths={{}} />
)
expect(mockAddItems).not.toHaveBeenCalled()
})
test("adds items to sidebar when shownSidebar is set and paths is set and tag doesn't have an associated schema", () => {
mockUseSidebar.mockReturnValue({
shownSidebar: {
...mockSidebar,
items: [
{
type: "category",
title: mockTag.name,
children: [],
}
]
},
addItems: mockAddItems,
})
mockFindSidebarItem.mockReturnValue({
type: "category",
title: mockTag.name,
children: [],
})
const mockPathItems: Sidebar.SidebarItem[] = [
{
type: "link",
title: "Mock Link",
path: "/mock-link",
children: [],
}
]
mockGetTagChildSidebarItems.mockReturnValue(mockPathItems)
render(
<TagPaths tag={mockTag} paths={mockPaths} />
)
expect(mockAddItems).toHaveBeenCalledWith(mockPathItems, {
sidebar_id: mockSidebar.sidebar_id,
// since there is no associated schema, the index position is 0
indexPosition: 0,
parent: {
type: "category",
title: mockTag.name,
path: "",
changeLoaded: true,
},
})
})
test("adds items to sidebar when shownSidebar is set and paths is set and tag has an associated schema", () => {
mockUseSidebar.mockReturnValue({
shownSidebar: {
...mockSidebar,
items: [
{
type: "category",
title: mockTag.name,
children: [],
}
]
},
addItems: mockAddItems,
})
mockFindSidebarItem.mockReturnValue({
type: "category",
title: mockTag.name,
children: [],
})
const mockPathItems: Sidebar.SidebarItem[] = [
{
type: "link",
title: "Mock Link",
path: "/mock-link",
children: [],
}
]
mockGetTagChildSidebarItems.mockReturnValue(mockPathItems)
render(
<TagPaths tag={{
...mockTag,
"x-associatedSchema": {
$ref: "#/components/schemas/MockSchema",
}
}} paths={mockPaths} />
)
expect(mockAddItems).toHaveBeenCalledWith(mockPathItems, {
sidebar_id: mockSidebar.sidebar_id,
indexPosition: 1,
parent: {
type: "category",
title: mockTag.name,
path: "",
changeLoaded: true,
},
})
})
test("doesn't add items to sidebar when parent item is not found", () => {
mockUseSidebar.mockReturnValue({
shownSidebar: mockSidebar,
addItems: mockAddItems,
})
mockFindSidebarItem.mockReturnValue(undefined)
render(
<TagPaths tag={mockTag} paths={mockPaths} />
)
expect(mockAddItems).not.toHaveBeenCalled()
})
test("doesn't add items to sidebar when parent item has enough children and tag doesn't have an associated schema", () => {
mockUseSidebar.mockReturnValue({
shownSidebar: mockSidebar,
addItems: mockAddItems,
})
mockFindSidebarItem.mockReturnValue({
type: "category",
title: mockTag.name,
children: [{ type: "link", title: "Mock Link", path: "/mock-link", children: [] }],
})
render(
<TagPaths tag={mockTag} paths={mockPaths} />
)
expect(mockAddItems).not.toHaveBeenCalled()
})
test("adds item to sidebar when parent item has an item and tag has an associated schema", () => {
mockUseSidebar.mockReturnValue({
shownSidebar: mockSidebar,
addItems: mockAddItems,
})
mockFindSidebarItem.mockReturnValue({
type: "category",
title: mockTag.name,
children: [{ type: "link", title: "Mock Schema", path: "/mock-schema", children: [] }],
})
render(
<TagPaths tag={{
...mockTag,
"x-associatedSchema": {
$ref: "#/components/schemas/MockSchema",
}
}} paths={mockPaths} />
)
expect(mockAddItems).toHaveBeenCalled()
})
test("doesn't add items to sidebar when parent item has enough children and tag has an associated schema", () => {
mockUseSidebar.mockReturnValue({
shownSidebar: mockSidebar,
addItems: mockAddItems,
})
mockFindSidebarItem.mockReturnValue({
type: "category",
title: mockTag.name,
children: [{
type: "link",
title: "Mock Link",
path: "/mock-link",
children: []
}, {
type: "link",
title: "Mock Link 2",
path: "/mock-link-2",
children: [] }],
})
render(
<TagPaths tag={{
...mockTag,
"x-associatedSchema": {
$ref: "#/components/schemas/MockSchema",
}
}} paths={mockPaths} />
)
expect(mockAddItems).not.toHaveBeenCalled()
})
})

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import type { OpenAPI } from "types"
import { findSidebarItem, useSidebar } from "docs-ui"
import { Fragment, Suspense, useEffect, useMemo } from "react"
@@ -10,7 +11,7 @@ import getTagChildSidebarItems from "@/utils/get-tag-child-sidebar-items"
import { useLoading } from "@/providers/loading"
import DividedLoading from "@/components/DividedLoading"
import { Sidebar } from "types"
import { compareOperations } from "../../../utils/sort-operations-utils"
import { compareOperations } from "@/utils/sort-operations-utils"
const TagOperation = dynamic<TagOperationProps>(
async () => import("../Operation")
@@ -26,34 +27,31 @@ const TagPaths = ({ tag, className, paths }: TagPathsProps) => {
const { loading } = useLoading()
useEffect(() => {
if (!shownSidebar) {
if (!shownSidebar || !Object.keys(paths).length) {
return
}
if (paths) {
const parentItem = findSidebarItem({
sidebarItems:
"items" in shownSidebar
? shownSidebar.items
: shownSidebar.children || [],
item: { title: tag.name, type: "category" },
checkChildren: false,
}) as Sidebar.SidebarItemCategory
const pathItems: Sidebar.SidebarItem[] = getTagChildSidebarItems(paths)
const targetLength =
pathItems.length + (tag["x-associatedSchema"] ? 1 : 0)
if ((parentItem.children?.length || 0) < targetLength) {
addItems(pathItems, {
sidebar_id: shownSidebar.sidebar_id,
parent: {
type: "category",
title: tag.name,
path: "",
changeLoaded: true,
},
indexPosition: tag["x-associatedSchema"] ? 1 : 0,
})
}
const parentItem = findSidebarItem({
sidebarItems:
"items" in shownSidebar
? shownSidebar.items
: shownSidebar.children || [],
item: { title: tag.name, type: "category" },
checkChildren: false,
}) as Sidebar.SidebarItemCategory | undefined
const pathItems: Sidebar.SidebarItem[] = getTagChildSidebarItems(paths)
const targetLength = pathItems.length + (tag["x-associatedSchema"] ? 1 : 0)
if (parentItem && (parentItem.children?.length || 0) < targetLength) {
addItems(pathItems, {
sidebar_id: shownSidebar.sidebar_id,
parent: {
type: "category",
title: tag.name,
path: "",
changeLoaded: true,
},
indexPosition: tag["x-associatedSchema"] ? 1 : 0,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paths, shownSidebar?.sidebar_id])

View File

@@ -0,0 +1,98 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
import { OpenAPI, Sidebar } from "types"
// mock data
const mockTagName = "mockTagName"
const mockOperation: OpenAPI.Operation = {
operationId: "mockOperation",
summary: "Mock Operation",
description: "Mock Operation",
"x-authenticated": false,
"x-codeSamples": [],
requestBody: { content: {} },
parameters: [],
responses: {
"200": {
description: "OK",
content: {
"application/json": {
schema: {
type: "object",
properties: { name: { type: "string", properties: {} } }
}
}
}
}
}
}
const mockPaths: OpenAPI.PathsObject = {
"/mock-path": {
get: mockOperation,
},
}
// mock functions
const mockCompareOperations = vi.fn((options: unknown) => 0)
const mockGetSectionId = vi.fn((options: unknown) => "mock-section-id")
// mock components and hooks
vi.mock("docs-utils", () => ({
getSectionId: (options: unknown) => mockGetSectionId(options),
}))
vi.mock("@/utils/sort-operations-utils", () => ({
compareOperations: (options: unknown) => mockCompareOperations(options),
}))
import { RoutesSummary } from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders nothing when there are no operations", () => {
const { container } = render(
<RoutesSummary tagName={mockTagName} paths={{}} />
)
expect(container).toBeEmptyDOMElement()
})
test("renders operation", () => {
const { container } = render(
<RoutesSummary tagName={mockTagName} paths={mockPaths} />
)
const operationLink = container.querySelector("[data-testid='link']")
expect(operationLink).toBeInTheDocument()
expect(operationLink).toHaveAttribute("href", "#mock-section-id")
expect(operationLink).toHaveTextContent("/mock-path")
})
test("renders operations in the correct order", () => {
const modifiedMockPaths: OpenAPI.PathsObject = {
"/mock-path": {
get: {
...mockOperation,
operationId: "mockOperation1",
},
post: {
...mockOperation,
operationId: "mockOperation2",
},
},
}
mockCompareOperations.mockReturnValue(-1)
mockGetSectionId.mockImplementation(
(options: unknown) => (options as string[]).join("-")
)
const { container } = render(
<RoutesSummary tagName={mockTagName} paths={modifiedMockPaths} />
)
const operationLinks = container.querySelectorAll("[data-testid='link']")
expect(operationLinks).toHaveLength(2)
expect(operationLinks[0]).toHaveAttribute("href", "#mockTagName-mockOperation2")
expect(operationLinks[1]).toHaveAttribute("href", "#mockTagName-mockOperation1")
})
})

View File

@@ -3,7 +3,7 @@ import { getSectionId } from "docs-utils"
import Link from "next/link"
import React, { useMemo } from "react"
import { OpenAPI } from "types"
import { compareOperations } from "../../../../utils/sort-operations-utils"
import { compareOperations } from "@/utils/sort-operations-utils"
type RoutesSummaryProps = {
tagName: string
@@ -85,6 +85,7 @@ export const RoutesSummary = ({ tagName, paths }: RoutesSummaryProps) => {
<Link
href={`#${operationId}`}
className="text-medusa-contrast-fg-secondary hover:text-medusa-contrast-fg-primary w-[85%]"
data-testid="link"
>
{endpointPath}
</Link>

View File

@@ -0,0 +1,592 @@
import React, { useState } from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render, waitFor, fireEvent } from "@testing-library/react"
import { OpenAPI } from "types"
// mock data
const mockSchema: OpenAPI.SchemaObject = {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
}
const mockTagName = "mockTagName"
// mock functions
const mockIsElmWindow = vi.fn(() => false)
const mockUseIsBrowser = vi.fn(() => ({
isBrowser: true,
}))
const mockScrollToElement = vi.fn()
const mockUseScrollController = vi.fn(() => ({
scrollableElement: null as HTMLElement | null,
scrollToElement: mockScrollToElement,
}))
const mockSetActivePath = vi.fn()
const mockUseSidebar = vi.fn(() => ({
activePath: null as string | null,
setActivePath: mockSetActivePath,
}))
const mockUseArea = vi.fn(() => ({
displayedArea: "Store",
}))
const mockUseSchemaExample = vi.fn((_options: unknown) => ({
examples: [
{
content: '{"name": "test"}',
},
],
}))
const mockGetSectionId = vi.fn((options: unknown) => "mock-schema-slug")
const mockCheckElementInViewport = vi.fn(
(_element: HTMLElement, _threshold: number) => true
)
const mockSingular = vi.fn((str: string) => str.replace(/s$/, ""))
// mock components
vi.mock("react-intersection-observer", () => ({
InView: ({
children,
onChange,
root,
id,
}: {
children: React.ReactNode
onChange: (inView: boolean, entry: IntersectionObserverEntry) => void
root?: HTMLElement | null
id?: string
}) => {
const [inView, setInView] = useState(false)
const mockTarget = document.createElement("div")
const mockEntry = {
target: mockTarget,
boundingClientRect: mockTarget.getBoundingClientRect(),
intersectionRatio: 1,
intersectionRect: mockTarget.getBoundingClientRect(),
isIntersecting: true,
rootBounds: null,
time: Date.now(),
} as unknown as IntersectionObserverEntry
return (
<div data-testid="in-view" data-root={root?.tagName} id={id}>
{children}
<button
type="button"
data-testid="in-view-toggle-button"
onClick={() => {
const newInView = !inView
setInView(newInView)
onChange(newInView, mockEntry)
}}
>
{inView.toString()}
</button>
</div>
)
},
}))
vi.mock("docs-ui", () => ({
isElmWindow: () => mockIsElmWindow(),
useIsBrowser: () => mockUseIsBrowser(),
useScrollController: () => mockUseScrollController(),
useSidebar: () => mockUseSidebar(),
CodeBlock: ({
source,
lang,
title,
className,
style,
}: {
source: string
lang: string
title?: string
className?: string
style?: React.CSSProperties
}) => (
<div
data-testid="code-block"
data-lang={lang}
data-title={title}
className={className}
style={style}
>
{source}
</div>
),
Note: ({ children }: { children: React.ReactNode }) => (
<div data-testid="note">{children}</div>
),
Link: ({
href,
children,
variant,
}: {
href: string
children: React.ReactNode
variant?: string
}) => (
<a data-testid="link" href={href} data-variant={variant}>
{children}
</a>
),
}))
vi.mock("@/components/Tags/Operation/Parameters", () => ({
default: ({
schemaObject,
topLevel,
}: {
schemaObject: OpenAPI.SchemaObject
topLevel?: boolean
}) => (
<div
data-testid="tag-operation-parameters"
data-top-level={topLevel?.toString()}
>
{JSON.stringify(schemaObject)}
</div>
),
}))
vi.mock("@/layouts/Divided", () => ({
default: ({
mainContent,
codeContent,
}: {
mainContent: React.ReactNode
codeContent: React.ReactNode
}) => (
<div data-testid="divided">
<div data-testid="divided-main-content">{mainContent}</div>
<div data-testid="divided-code-content">{codeContent}</div>
</div>
),
}))
vi.mock("@/components/Section/Container", () => ({
default: ({ children }: { children: React.ReactNode }) => (
<div data-testid="section-container">{children}</div>
),
}))
vi.mock("@/hooks/use-schema-example", () => ({
default: (options: unknown) => mockUseSchemaExample(options),
}))
vi.mock("@/providers/area", () => ({
useArea: () => mockUseArea(),
}))
vi.mock("@/utils/check-element-in-viewport", () => ({
default: (element: HTMLElement, threshold: number) =>
mockCheckElementInViewport(element, threshold),
}))
vi.mock("docs-utils", () => ({
getSectionId: (options: unknown) => mockGetSectionId(options),
}))
vi.mock("pluralize", () => ({
singular: (str: string) => mockSingular(str),
}))
import TagSectionSchema from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
window.location.hash = ""
// Reset mocks to default values
mockIsElmWindow.mockReturnValue(false)
mockUseIsBrowser.mockReturnValue({ isBrowser: true })
mockUseScrollController.mockReturnValue({
scrollableElement: null,
scrollToElement: mockScrollToElement,
})
mockUseSidebar.mockReturnValue({
activePath: null,
setActivePath: mockSetActivePath,
})
mockUseArea.mockReturnValue({
displayedArea: "Store",
})
mockUseSchemaExample.mockReturnValue({
examples: [
{
content: '{"name": "test"}',
},
],
})
mockGetSectionId.mockReturnValue("mock-schema-slug")
mockCheckElementInViewport.mockReturnValue(true)
mockSingular.mockImplementation((str: string) => str.replace(/s$/, ""))
})
describe("rendering", () => {
test("renders InView wrapper with correct props", () => {
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const inViewElement = getByTestId("in-view")
expect(inViewElement).toBeInTheDocument()
expect(inViewElement).toHaveAttribute("id", "mock-schema-slug")
})
test("renders SectionContainer", () => {
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const sectionContainerElement = getByTestId("section-container")
expect(sectionContainerElement).toBeInTheDocument()
})
test("renders DividedLayout with mainContent and codeContent", () => {
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const dividedElement = getByTestId("divided")
expect(dividedElement).toBeInTheDocument()
expect(getByTestId("divided-main-content")).toBeInTheDocument()
expect(getByTestId("divided-code-content")).toBeInTheDocument()
})
test("renders formatted name in heading", () => {
mockSingular.mockReturnValue("mockTagNam")
const { container } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const heading = container.querySelector("h2")
expect(heading).toBeInTheDocument()
expect(heading).toHaveTextContent("mockTagNam Object")
})
test("renders Note with displayedArea", () => {
mockUseArea.mockReturnValue({
displayedArea: "Admin",
})
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const noteElement = getByTestId("note")
expect(noteElement).toBeInTheDocument()
expect(noteElement).toHaveTextContent("Admin")
})
test("renders Link to Commerce Modules Documentation", () => {
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const linkElement = getByTestId("link")
expect(linkElement).toBeInTheDocument()
expect(linkElement).toHaveAttribute(
"href",
"https://docs.medusajs.com/resources/commerce-modules"
)
expect(linkElement).toHaveTextContent("Commerce Modules Documentation")
})
test("renders Fields heading", () => {
const { container } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const fieldsHeading = container.querySelector("h4")
expect(fieldsHeading).toBeInTheDocument()
expect(fieldsHeading).toHaveTextContent("Fields")
})
test("renders TagOperationParameters with schema and topLevel prop", () => {
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const parametersElement = getByTestId("tag-operation-parameters")
expect(parametersElement).toBeInTheDocument()
expect(parametersElement).toHaveAttribute("data-top-level", "true")
expect(parametersElement).toHaveTextContent(JSON.stringify(mockSchema))
})
test("renders CodeBlock when examples exist", async () => {
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
await waitFor(() => {
const codeBlockElement = getByTestId("code-block")
expect(codeBlockElement).toBeInTheDocument()
expect(codeBlockElement).toHaveAttribute("data-lang", "json")
expect(codeBlockElement).toHaveTextContent('{"name": "test"}')
})
})
test("does not render CodeBlock when examples are empty", () => {
mockUseSchemaExample.mockReturnValue({
examples: [],
})
const { queryByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const codeBlockElement = queryByTestId("code-block")
expect(codeBlockElement).not.toBeInTheDocument()
})
test("renders CodeBlock with formatted name as title", async () => {
mockSingular.mockReturnValue("mockTagNam")
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
await waitFor(() => {
const codeBlockElement = getByTestId("code-block")
expect(codeBlockElement).toHaveAttribute(
"data-title",
"The mockTagNam Object"
)
})
})
})
describe("name formatting", () => {
test("formats tag name using singular", () => {
mockSingular.mockReturnValue("product")
render(<TagSectionSchema schema={mockSchema} tagName="products" />)
expect(mockSingular).toHaveBeenCalledWith("products")
})
test("removes spaces from formatted name", () => {
mockSingular.mockReturnValue("test tag")
const { container } = render(
<TagSectionSchema schema={mockSchema} tagName="test tags" />
)
const heading = container.querySelector("h2")
expect(heading).toHaveTextContent("testtag Object")
})
})
describe("schema slug generation", () => {
test("generates schema slug using getSectionId with tagName, formattedName, and 'schema'", () => {
mockSingular.mockReturnValue("mockTagNam")
render(<TagSectionSchema schema={mockSchema} tagName={mockTagName} />)
expect(mockGetSectionId).toHaveBeenCalledWith([
mockTagName,
"mockTagNam",
"schema",
])
})
})
describe("useEffect scrolling behavior", () => {
test("scrolls to element when hash matches schemaSlug and element is not in viewport", () => {
const mockPath = "mock-schema-slug"
mockGetSectionId.mockReturnValue(mockPath)
window.location.hash = `#${mockPath}`
mockUseSidebar.mockReturnValue({
activePath: mockPath,
setActivePath: mockSetActivePath,
})
mockCheckElementInViewport.mockReturnValue(false)
// Create a mock element with the id
const mockElement = document.createElement("div")
mockElement.id = mockPath
document.body.appendChild(mockElement)
render(<TagSectionSchema schema={mockSchema} tagName={mockTagName} />)
expect(mockScrollToElement).toHaveBeenCalledWith(mockElement)
document.body.removeChild(mockElement)
})
test("does not scroll when element is already in viewport", () => {
const mockPath = "mock-schema-slug"
mockGetSectionId.mockReturnValue(mockPath)
window.location.hash = `#${mockPath}`
mockUseSidebar.mockReturnValue({
activePath: mockPath,
setActivePath: mockSetActivePath,
})
mockCheckElementInViewport.mockReturnValue(true)
const mockElement = document.createElement("div")
mockElement.id = mockPath
document.body.appendChild(mockElement)
render(<TagSectionSchema schema={mockSchema} tagName={mockTagName} />)
expect(mockScrollToElement).not.toHaveBeenCalled()
document.body.removeChild(mockElement)
})
test("does not scroll when hash does not match schemaSlug", () => {
const mockPath = "mock-schema-slug"
mockGetSectionId.mockReturnValue(mockPath)
window.location.hash = "#different-hash"
mockUseSidebar.mockReturnValue({
activePath: "different-hash",
setActivePath: mockSetActivePath,
})
render(<TagSectionSchema schema={mockSchema} tagName={mockTagName} />)
expect(mockScrollToElement).not.toHaveBeenCalled()
})
test("scrolls when activePath matches but hash does not", () => {
const mockPath = "mock-schema-slug"
mockGetSectionId.mockReturnValue(mockPath)
window.location.hash = "#different-hash"
mockUseSidebar.mockReturnValue({
activePath: mockPath,
setActivePath: mockSetActivePath,
})
mockCheckElementInViewport.mockReturnValue(false)
// Create a mock element with the id
const mockElement = document.createElement("div")
mockElement.id = mockPath
document.body.appendChild(mockElement)
render(<TagSectionSchema schema={mockSchema} tagName={mockTagName} />)
expect(mockScrollToElement).toHaveBeenCalledWith(mockElement)
document.body.removeChild(mockElement)
})
test("does not scroll when not in browser", () => {
mockUseIsBrowser.mockReturnValue({ isBrowser: false })
const mockPath = "mock-schema-slug"
mockGetSectionId.mockReturnValue(mockPath)
window.location.hash = `#${mockPath}`
mockUseSidebar.mockReturnValue({
activePath: mockPath,
setActivePath: mockSetActivePath,
})
render(<TagSectionSchema schema={mockSchema} tagName={mockTagName} />)
expect(mockScrollToElement).not.toHaveBeenCalled()
})
})
describe("handleViewChange behavior", () => {
test("updates URL and active path when in view and activePath is different", () => {
const mockPath = "mock-schema-slug"
mockGetSectionId.mockReturnValue(mockPath)
mockUseSidebar.mockReturnValue({
activePath: "different-path",
setActivePath: mockSetActivePath,
})
mockCheckElementInViewport.mockReturnValue(true)
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
expect(mockSetActivePath).toHaveBeenCalledWith(mockPath)
expect(window.location.hash).toBe(`#${mockPath}`)
})
test("does not update when activePath already matches schemaSlug", () => {
const mockPath = "mock-schema-slug"
mockGetSectionId.mockReturnValue(mockPath)
mockUseSidebar.mockReturnValue({
activePath: mockPath,
setActivePath: mockSetActivePath,
})
mockCheckElementInViewport.mockReturnValue(true)
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const inViewToggleButton = getByTestId("in-view-toggle-button")
mockSetActivePath.mockClear()
fireEvent.click(inViewToggleButton)
// Should not be called because activePath === schemaSlug, so the condition fails
expect(mockSetActivePath).not.toHaveBeenCalled()
})
test("updates when element is in viewport even if not in view", () => {
const mockPath = "mock-schema-slug"
mockGetSectionId.mockReturnValue(mockPath)
mockUseSidebar.mockReturnValue({
activePath: "different-path",
setActivePath: mockSetActivePath,
})
mockCheckElementInViewport.mockReturnValue(true)
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const inViewToggleButton = getByTestId("in-view-toggle-button")
// Click to set inView to false, but checkElementInViewport returns true
fireEvent.click(inViewToggleButton)
expect(mockSetActivePath).toHaveBeenCalledWith(mockPath)
})
test("does not update when not in browser", () => {
mockUseIsBrowser.mockReturnValue({ isBrowser: false })
const mockPath = "mock-schema-slug"
mockGetSectionId.mockReturnValue(mockPath)
mockUseSidebar.mockReturnValue({
activePath: "different-path",
setActivePath: mockSetActivePath,
})
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
expect(mockSetActivePath).not.toHaveBeenCalled()
})
})
describe("browser environment", () => {
test("handles non-browser environment", () => {
mockUseIsBrowser.mockReturnValue({ isBrowser: false })
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const inViewElement = getByTestId("in-view")
expect(inViewElement).toBeInTheDocument()
expect(inViewElement).not.toHaveAttribute("data-root")
})
test("uses document.body as root when scrollableElement is window", () => {
mockIsElmWindow.mockReturnValue(true)
mockUseScrollController.mockReturnValue({
scrollableElement: window as unknown as HTMLElement,
scrollToElement: mockScrollToElement,
})
mockUseIsBrowser.mockReturnValue({ isBrowser: true })
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const inViewElement = getByTestId("in-view")
expect(inViewElement).toBeInTheDocument()
expect(inViewElement).toHaveAttribute("data-root", "BODY")
})
test("uses scrollableElement as root when it is not window", () => {
const mockScrollableElement = document.createElement("div")
mockIsElmWindow.mockReturnValue(false)
mockUseIsBrowser.mockReturnValue({ isBrowser: true })
mockUseScrollController.mockReturnValue({
scrollableElement: mockScrollableElement,
scrollToElement: mockScrollToElement,
})
const { getByTestId } = render(
<TagSectionSchema schema={mockSchema} tagName={mockTagName} />
)
const inViewElement = getByTestId("in-view")
expect(inViewElement).toBeInTheDocument()
expect(inViewElement).toHaveAttribute("data-root", "DIV")
})
})

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import { Suspense, useEffect, useMemo } from "react"
import { OpenAPI } from "types"
import TagOperationParameters from "../../Operation/Parameters"
@@ -28,7 +29,7 @@ export type TagSectionSchemaProps = {
}
const TagSectionSchema = ({ schema, tagName }: TagSectionSchemaProps) => {
const { setActivePath, activePath, shownSidebar, updateItems } = useSidebar()
const { setActivePath, activePath } = useSidebar()
const { displayedArea } = useArea()
const formattedName = useMemo(
() => singular(tagName).replaceAll(" ", ""),

View File

@@ -0,0 +1,830 @@
import React, { useState } from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render, waitFor, fireEvent } from "@testing-library/react"
import { OpenAPI } from "types"
// mock data
const mockTag: OpenAPI.TagObject = {
name: "mockTag",
description: "Mock Tag Description",
}
const mockTagWithExternalDocs: OpenAPI.TagObject = {
name: "mockTag",
description: "Mock Tag Description",
externalDocs: {
url: "https://example.com/guide",
description: "Read the guide",
},
}
const mockTagWithSchema: OpenAPI.TagObject = {
name: "mockTag",
description: "Mock Tag Description",
"x-associatedSchema": {
$ref: "#/components/schemas/MockSchema",
},
}
const mockSchemaData = {
schema: {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
} as OpenAPI.SchemaObject,
}
const mockPathsData = {
paths: {
"/mock-path": {
get: {
operationId: "mockOperation",
summary: "Mock Operation",
description: "Mock Operation",
"x-authenticated": false,
"x-codeSamples": [],
requestBody: { content: {} },
parameters: [],
responses: {
"200": {
description: "OK",
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: { type: "string", properties: {} },
},
},
},
},
},
},
},
},
} as OpenAPI.PathsObject,
}
// mock functions
const mockIsElmWindow = vi.fn(() => false)
const mockUseIsBrowser = vi.fn(() => ({
isBrowser: true,
}))
const mockScrollToTop = vi.fn()
const mockUseScrollController = vi.fn(() => ({
scrollableElement: null as HTMLElement | null,
scrollToTop: mockScrollToTop,
}))
const mockSetActivePath = vi.fn()
const mockUseSidebar = vi.fn(() => ({
activePath: null as string | null,
setActivePath: mockSetActivePath,
}))
const mockUseArea = vi.fn(() => ({
area: "store",
}))
const mockGetSectionId = vi.fn((options: unknown) => "mock-slug-tag-name")
const mockCheckElementInViewport = vi.fn(
(_element: HTMLElement, _threshold: number) => true
)
const mockBasePathUrl = vi.fn((url: string) => url)
const mockSwrFetcher = vi.fn()
const mockPush = vi.fn()
const mockReplace = vi.fn()
const mockUseRouter = vi.fn(() => ({
push: mockPush,
replace: mockReplace,
}))
const mockUseSWR = vi.fn((key: string | null, fetcher: unknown, options: unknown) => ({
data: undefined as {paths: OpenAPI.PathsObject} | {schema: OpenAPI.SchemaObject} | undefined,
error: undefined,
isLoading: false,
}))
// mock components
vi.mock("react-intersection-observer", () => ({
InView: ({
children,
onChange,
root,
id,
className,
}: {
children: React.ReactNode
onChange: (inView: boolean) => void
root?: HTMLElement | null
id?: string
className?: string
}) => {
const [inView, setInView] = useState(false)
return (
<div
data-testid="in-view"
data-root={root?.tagName}
id={id}
className={className}
>
{children}
<button
type="button"
data-testid="in-view-toggle-button"
onClick={() => {
const newInView = !inView
setInView(newInView)
onChange(newInView)
}}
>
{inView.toString()}
</button>
</div>
)
},
}))
vi.mock("docs-ui", () => ({
isElmWindow: () => mockIsElmWindow(),
useIsBrowser: () => mockUseIsBrowser(),
useScrollController: () => mockUseScrollController(),
useSidebar: () => mockUseSidebar(),
H2: ({ children }: { children: React.ReactNode }) => (
<h2 data-testid="h2">{children}</h2>
),
Link: ({
href,
children,
target,
variant,
}: {
href: string
children: React.ReactNode
target?: string
variant?: string
}) => (
<a data-testid="link" href={href} target={target} data-variant={variant}>
{children}
</a>
),
Loading: () => <div data-testid="loading">Loading...</div>,
swrFetcher: () => mockSwrFetcher(),
}))
vi.mock("@/providers/area", () => ({
useArea: () => mockUseArea(),
}))
vi.mock("@/components/Section/Container", () => ({
default: ({ children }: { children: React.ReactNode }) => (
<div data-testid="section-container">{children}</div>
),
}))
vi.mock("@/layouts/Divided", () => ({
default: ({
mainContent,
codeContent,
}: {
mainContent: React.ReactNode
codeContent: React.ReactNode
}) => (
<div data-testid="divided">
<div data-testid="divided-main-content">{mainContent}</div>
<div data-testid="divided-code-content">{codeContent}</div>
</div>
),
}))
vi.mock("@/components/Tags/Section/Schema", () => ({
default: ({
schema,
tagName,
}: {
schema: OpenAPI.SchemaObject
tagName: string
}) => (
<div data-testid="tag-section-schema" data-tag-name={tagName}>
{JSON.stringify(schema)}
</div>
),
}))
vi.mock("@/components/Tags/Paths", () => ({
default: ({
tag,
paths,
}: {
tag: OpenAPI.TagObject
paths: OpenAPI.PathsObject
}) => (
<div data-testid="tag-paths" data-tag-name={tag.name}>
{JSON.stringify(paths)}
</div>
),
}))
vi.mock("@/components/Tags/Section/RoutesSummary", () => ({
RoutesSummary: ({
tagName,
paths,
}: {
tagName: string
paths: OpenAPI.PathsObject
}) => (
<div data-testid="routes-summary" data-tag-name={tagName}>
{JSON.stringify(paths)}
</div>
),
}))
vi.mock("@/components/Section/Divider", () => ({
default: ({ className }: { className?: string }) => (
<div data-testid="section-divider" className={className} />
),
}))
vi.mock("@/components/Feedback", () => ({
Feedback: ({
question,
extraData,
}: {
question: string
extraData: { section: string }
}) => (
<div data-testid="feedback" data-section={extraData.section}>
{question}
</div>
),
}))
vi.mock("@/providers/loading", () => ({
default: ({
children,
initialLoading,
}: {
children: React.ReactNode
initialLoading?: boolean
}) => (
<div data-testid="loading-provider" data-initial-loading={initialLoading?.toString()}>
{children}
</div>
),
}))
vi.mock("@/components/MDXContent/Client", () => ({
default: ({
content,
scope,
}: {
content: string
scope: { addToSidebar: boolean }
}) => (
<div data-testid="mdx-content-client" data-add-to-sidebar={scope.addToSidebar.toString()}>
{content}
</div>
),
}))
vi.mock("@/components/Section/Divider", () => ({
default: ({ className }: { className?: string }) => (
<div data-testid="section-divider" className={className} />
),
}))
vi.mock("@/components/Section", () => ({
default: ({ children }: { children: React.ReactNode }) => (
<div data-testid="section">{children}</div>
),
}))
vi.mock("next/navigation", () => ({
useRouter: () => mockUseRouter(),
}))
vi.mock("docs-utils", () => ({
getSectionId: (options: unknown) => mockGetSectionId(options),
}))
vi.mock("@/utils/check-element-in-viewport", () => ({
default: (element: HTMLElement, threshold: number) =>
mockCheckElementInViewport(element, threshold),
}))
vi.mock("@/utils/base-path-url", () => ({
default: (url: string) => mockBasePathUrl(url),
}))
vi.mock("swr", () => ({
default: (key: string | null, fetcher: unknown, options: unknown) =>
mockUseSWR(key, fetcher, options),
}))
import TagSectionComponent from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
window.location.hash = ""
// Reset mocks to default values
mockIsElmWindow.mockReturnValue(false)
mockUseIsBrowser.mockReturnValue({ isBrowser: true })
mockUseScrollController.mockReturnValue({
scrollableElement: null,
scrollToTop: mockScrollToTop,
})
mockUseSidebar.mockReturnValue({
activePath: null,
setActivePath: mockSetActivePath,
})
mockUseArea.mockReturnValue({
area: "store",
})
mockGetSectionId.mockReturnValue("mock-slug-tag-name")
mockCheckElementInViewport.mockReturnValue(true)
mockBasePathUrl.mockImplementation((url: string) => url)
mockUseSWR.mockReturnValue({
data: undefined,
error: undefined,
isLoading: false,
})
})
describe("rendering", () => {
test("renders InView wrapper with correct props", () => {
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewElement = getByTestId("in-view")
expect(inViewElement).toBeInTheDocument()
expect(inViewElement).toHaveAttribute("id", "mock-slug-tag-name")
expect(inViewElement).toHaveClass("min-h-screen", "relative")
})
test("renders SectionContainer", () => {
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const sectionContainerElement = getByTestId("section-container")
expect(sectionContainerElement).toBeInTheDocument()
})
test("renders DividedLayout with mainContent and codeContent", () => {
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const dividedElement = getByTestId("divided")
expect(dividedElement).toBeInTheDocument()
expect(getByTestId("divided-main-content")).toBeInTheDocument()
expect(getByTestId("divided-code-content")).toBeInTheDocument()
})
test("renders H2 with tag name", () => {
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const h2Element = getByTestId("h2")
expect(h2Element).toBeInTheDocument()
expect(h2Element).toHaveTextContent(mockTag.name)
})
test("renders MDXContentClient when tag has description", async () => {
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
await waitFor(() => {
const mdxContentElement = getByTestId("mdx-content-client")
expect(mdxContentElement).toBeInTheDocument()
expect(mdxContentElement).toHaveTextContent(mockTag.description!)
expect(mdxContentElement).toHaveAttribute("data-add-to-sidebar", "false")
})
})
test("does not render MDXContentClient when tag has no description", () => {
const tagWithoutDescription: OpenAPI.TagObject = {
name: "mockTag",
}
const { queryByTestId } = render(
<TagSectionComponent tag={tagWithoutDescription} />
)
const mdxContentElement = queryByTestId("mdx-content-client")
expect(mdxContentElement).not.toBeInTheDocument()
})
test("renders external docs link when tag has externalDocs", () => {
const { getByTestId } = render(
<TagSectionComponent tag={mockTagWithExternalDocs} />
)
const linkElement = getByTestId("link")
expect(linkElement).toBeInTheDocument()
expect(linkElement).toHaveAttribute(
"href",
mockTagWithExternalDocs.externalDocs!.url
)
expect(linkElement).toHaveAttribute("target", "_blank")
expect(linkElement).toHaveTextContent(
mockTagWithExternalDocs.externalDocs!.description!
)
})
test("renders 'Read More' when externalDocs has no description", () => {
const tagWithExternalDocsNoDescription: OpenAPI.TagObject = {
name: "mockTag",
externalDocs: {
url: "https://example.com/guide",
},
}
const { getByTestId } = render(
<TagSectionComponent tag={tagWithExternalDocsNoDescription} />
)
const linkElement = getByTestId("link")
expect(linkElement).toHaveTextContent("Read More")
})
test("does not render external docs link when tag has no externalDocs", () => {
const { queryByTestId } = render(<TagSectionComponent tag={mockTag} />)
const linkElement = queryByTestId("link")
expect(linkElement).not.toBeInTheDocument()
})
test("renders Feedback component", () => {
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const feedbackElement = getByTestId("feedback")
expect(feedbackElement).toBeInTheDocument()
expect(feedbackElement).toHaveAttribute("data-section", mockTag.name)
expect(feedbackElement).toHaveTextContent("Was this section helpful?")
})
test("renders RoutesSummary with empty paths initially", () => {
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const routesSummaryElement = getByTestId("routes-summary")
expect(routesSummaryElement).toBeInTheDocument()
expect(routesSummaryElement).toHaveAttribute("data-tag-name", mockTag.name)
expect(routesSummaryElement).toHaveTextContent("{}")
})
test("renders SectionDivider when loadData is false", () => {
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const dividerElement = getByTestId("section-divider")
expect(dividerElement).toBeInTheDocument()
expect(dividerElement).toHaveClass("lg:!-left-1")
})
test("does not render SectionDivider when loadData is true", () => {
mockUseSWR.mockReturnValue({
data: { paths: mockPathsData },
error: undefined,
isLoading: false,
})
const { getByTestId, queryByTestId } = render(
<TagSectionComponent tag={mockTag} />
)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
waitFor(() => {
const dividerElement = queryByTestId("section-divider")
expect(dividerElement).not.toBeInTheDocument()
})
})
})
describe("slug generation", () => {
test("generates slug using getSectionId with tag name", () => {
render(<TagSectionComponent tag={mockTag} />)
expect(mockGetSectionId).toHaveBeenCalledWith([mockTag.name])
})
})
describe("useSWR hooks", () => {
test("does not fetch schema data when loadData is false", () => {
render(<TagSectionComponent tag={mockTagWithSchema} />)
expect(mockUseSWR).not.toHaveBeenCalledWith(
`/schema?name=${mockTagWithSchema["x-associatedSchema"]!.$ref}&area=store`,
)
})
test("fetches schema data when loadData is true and tag has x-associatedSchema", () => {
mockUseSWR.mockReturnValue({
data: mockSchemaData,
error: undefined,
isLoading: false,
})
const { getByTestId } = render(
<TagSectionComponent tag={mockTagWithSchema} />
)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
expect(mockBasePathUrl).toHaveBeenCalledWith(
`/schema?name=${mockTagWithSchema["x-associatedSchema"]!.$ref}&area=store`
)
})
test("does not fetch paths data when loadData is false", () => {
render(<TagSectionComponent tag={mockTag} />)
expect(mockUseSWR).not.toHaveBeenCalledWith(`/tag?tagName=mock-slug-tag-name&area=store`)
})
test("fetches paths data when loadData is true", () => {
mockUseSWR.mockReturnValue({
data: mockPathsData,
error: undefined,
isLoading: false,
})
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
expect(mockBasePathUrl).toHaveBeenCalledWith(
`/tag?tagName=mock-slug-tag-name&area=store`
)
})
})
describe("conditional rendering", () => {
test("renders TagSectionSchema when schemaData exists", async () => {
mockUseSWR.mockImplementation((key: string | null) => {
if (key?.includes("schema")) {
return {
data: mockSchemaData,
error: undefined,
isLoading: false,
}
}
return {
data: undefined,
error: undefined,
isLoading: false,
}
})
const { getByTestId } = render(
<TagSectionComponent tag={mockTagWithSchema} />
)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
await waitFor(() => {
const schemaElement = getByTestId("tag-section-schema")
expect(schemaElement).toBeInTheDocument()
expect(schemaElement).toHaveAttribute("data-tag-name", mockTagWithSchema.name)
})
})
test("does not render TagSectionSchema when schemaData does not exist", () => {
const { queryByTestId } = render(<TagSectionComponent tag={mockTag} />)
const schemaElement = queryByTestId("tag-section-schema")
expect(schemaElement).not.toBeInTheDocument()
})
test("renders TagPaths when loadData is true and pathsData exists", async () => {
mockUseSWR.mockReturnValue({
data: mockPathsData,
error: undefined,
isLoading: false,
})
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
await waitFor(() => {
const tagPathsElement = getByTestId("tag-paths")
expect(tagPathsElement).toBeInTheDocument()
expect(tagPathsElement).toHaveAttribute("data-tag-name", mockTag.name)
})
})
test("does not render TagPaths when loadData is false", () => {
const { queryByTestId } = render(<TagSectionComponent tag={mockTag} />)
const tagPathsElement = queryByTestId("tag-paths")
expect(tagPathsElement).not.toBeInTheDocument()
})
test("renders LoadingProvider with initialLoading when TagPaths is rendered", async () => {
mockUseSWR.mockReturnValue({
data: mockPathsData,
error: undefined,
isLoading: false,
})
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
await waitFor(() => {
const loadingProviderElement = getByTestId("loading-provider")
expect(loadingProviderElement).toBeInTheDocument()
expect(loadingProviderElement).toHaveAttribute(
"data-initial-loading",
"true"
)
})
})
})
describe("useEffect scrolling behavior", () => {
test("scrolls to element when activePath matches slugTagName and element is not in viewport", () => {
const mockPath = "mock-slug-tag-name"
mockGetSectionId.mockReturnValue(mockPath)
mockUseSidebar.mockReturnValue({
activePath: mockPath,
setActivePath: mockSetActivePath,
})
mockCheckElementInViewport.mockReturnValue(false)
const mockElement = document.createElement("div")
mockElement.id = mockPath
Object.defineProperty(mockElement, "offsetTop", { value: 100 })
Object.defineProperty(mockElement, "offsetParent", { value: { offsetTop: 50 } as HTMLElement })
document.body.appendChild(mockElement)
render(<TagSectionComponent tag={mockTag} />)
expect(mockScrollToTop).toHaveBeenCalledWith(150, 0)
document.body.removeChild(mockElement)
})
test("does not scroll when element is already in viewport", () => {
const mockPath = "mock-slug-tag-name"
mockGetSectionId.mockReturnValue(mockPath)
mockUseSidebar.mockReturnValue({
activePath: mockPath,
setActivePath: mockSetActivePath,
})
mockCheckElementInViewport.mockReturnValue(true)
const mockElement = document.createElement("div")
mockElement.id = mockPath
document.body.appendChild(mockElement)
render(<TagSectionComponent tag={mockTag} />)
expect(mockScrollToTop).not.toHaveBeenCalled()
document.body.removeChild(mockElement)
})
test("sets loadData to true when activePath has multiple parts", () => {
const mockPath = "mock-slug-tag-name_operation"
mockGetSectionId.mockReturnValue("mock-slug-tag-name")
mockUseSidebar.mockReturnValue({
activePath: mockPath,
setActivePath: mockSetActivePath,
})
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
// After useEffect runs, loadData should be true, so divider should not be visible
waitFor(() => {
const dividerElement = getByTestId("section-divider")
expect(dividerElement).not.toBeInTheDocument()
})
})
test("does not scroll when activePath does not include slugTagName", () => {
mockUseSidebar.mockReturnValue({
activePath: "different-path",
setActivePath: mockSetActivePath,
})
render(<TagSectionComponent tag={mockTag} />)
expect(mockScrollToTop).not.toHaveBeenCalled()
})
test("does not scroll when activePath is null", () => {
mockUseSidebar.mockReturnValue({
activePath: null,
setActivePath: mockSetActivePath,
})
render(<TagSectionComponent tag={mockTag} />)
expect(mockScrollToTop).not.toHaveBeenCalled()
})
test("does not scroll when not in browser", () => {
mockUseIsBrowser.mockReturnValue({ isBrowser: false })
const mockPath = "mock-slug-tag-name"
mockGetSectionId.mockReturnValue(mockPath)
mockUseSidebar.mockReturnValue({
activePath: mockPath,
setActivePath: mockSetActivePath,
})
render(<TagSectionComponent tag={mockTag} />)
expect(mockScrollToTop).not.toHaveBeenCalled()
})
})
describe("InView onChange behavior", () => {
test("sets loadData to true when in view", () => {
const { getByTestId, queryByTestId } = render(
<TagSectionComponent tag={mockTag} />
)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
// After loadData is set, divider should disappear
waitFor(() => {
const dividerElement = queryByTestId("section-divider")
expect(dividerElement).not.toBeInTheDocument()
})
})
test("updates router hash when in view and hash does not match", () => {
window.location.hash = "#different-hash"
mockUseSidebar.mockReturnValue({
activePath: "different-path",
setActivePath: mockSetActivePath,
})
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
expect(mockPush).toHaveBeenCalledWith("#mock-slug-tag-name", {
scroll: false,
})
})
test("sets active path when in view and activePath is different", () => {
mockUseSidebar.mockReturnValue({
activePath: "different-path",
setActivePath: mockSetActivePath,
})
window.location.hash = "#mock-slug-tag-name"
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
expect(mockSetActivePath).toHaveBeenCalledWith("mock-slug-tag-name")
})
test("does not update hash when current hash links to inner path", () => {
window.location.hash = "#mock-slug-tag-name_operation"
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewToggleButton = getByTestId("in-view-toggle-button")
mockSetActivePath.mockClear()
fireEvent.click(inViewToggleButton)
// Should not update because hash links to inner path
expect(mockSetActivePath).not.toHaveBeenCalledWith("mock-slug-tag-name")
})
test("does not update when hash already matches slugTagName", () => {
window.location.hash = "#mock-slug-tag-name"
mockUseSidebar.mockReturnValue({
activePath: "mock-slug-tag-name",
setActivePath: mockSetActivePath,
})
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewToggleButton = getByTestId("in-view-toggle-button")
mockSetActivePath.mockClear()
fireEvent.click(inViewToggleButton)
// Should not update because already matches
expect(mockSetActivePath).not.toHaveBeenCalled()
})
test("does not update when not in view", () => {
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewToggleButton = getByTestId("in-view-toggle-button")
// Click once to set inView to true, then click again to set to false
fireEvent.click(inViewToggleButton)
mockSetActivePath.mockClear()
fireEvent.click(inViewToggleButton)
// Should not update when not in view
expect(mockSetActivePath).not.toHaveBeenCalled()
})
})
describe("className behavior", () => {
test("applies 'relative' class when loadData is false", () => {
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewElement = getByTestId("in-view")
expect(inViewElement).toHaveClass("relative")
})
test("does not apply 'relative' class when loadData is true", async () => {
mockUseSWR.mockReturnValue({
data: mockPathsData,
error: undefined,
isLoading: false,
})
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewToggleButton = getByTestId("in-view-toggle-button")
fireEvent.click(inViewToggleButton)
await waitFor(() => {
const inViewElement = getByTestId("in-view")
expect(inViewElement).not.toHaveClass("relative")
})
})
})
describe("browser environment", () => {
test("handles non-browser environment", () => {
mockUseIsBrowser.mockReturnValue({ isBrowser: false })
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewElement = getByTestId("in-view")
expect(inViewElement).toBeInTheDocument()
expect(inViewElement).not.toHaveAttribute("data-root")
})
test("uses document.body as root when scrollableElement is window", () => {
mockIsElmWindow.mockReturnValue(true)
mockUseScrollController.mockReturnValue({
scrollableElement: window as unknown as HTMLElement,
scrollToTop: mockScrollToTop,
})
mockUseIsBrowser.mockReturnValue({ isBrowser: true })
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewElement = getByTestId("in-view")
expect(inViewElement).toBeInTheDocument()
expect(inViewElement).toHaveAttribute("data-root", "BODY")
})
test("uses scrollableElement as root when it is not window", () => {
const mockScrollableElement = document.createElement("div")
mockIsElmWindow.mockReturnValue(false)
mockUseIsBrowser.mockReturnValue({ isBrowser: true })
mockUseScrollController.mockReturnValue({
scrollableElement: mockScrollableElement,
scrollToTop: mockScrollToTop,
})
const { getByTestId } = render(<TagSectionComponent tag={mockTag} />)
const inViewElement = getByTestId("in-view")
expect(inViewElement).toBeInTheDocument()
expect(inViewElement).toHaveAttribute("data-root", "DIV")
})
})

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import { InView } from "react-intersection-observer"
import { useEffect, useMemo, useState } from "react"
import {
@@ -9,6 +10,8 @@ import {
useIsBrowser,
useScrollController,
useSidebar,
Loading,
Link,
} from "docs-ui"
import dynamic from "next/dynamic"
import type { SectionProps } from "../../Section"
@@ -19,7 +22,6 @@ import SectionContainer from "../../Section/Container"
import { useArea } from "@/providers/area"
import SectionDivider from "../../Section/Divider"
import clsx from "clsx"
import { Loading, Link } from "docs-ui"
import { useRouter } from "next/navigation"
import { OpenAPI } from "types"
import TagSectionSchema from "./Schema"
@@ -36,11 +38,11 @@ export type TagSectionProps = {
} & React.HTMLAttributes<HTMLDivElement>
const Section = dynamic<SectionProps>(
async () => import("../../Section")
async () => import("@/components/Section")
) as React.FC<SectionProps>
const MDXContentClient = dynamic<MDXContentClientProps>(
async () => import("../../MDXContent/Client"),
async () => import("@/components/MDXContent/Client"),
{
loading: () => <Loading />,
}

View File

@@ -0,0 +1,49 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render, waitFor } from "@testing-library/react"
import { OpenAPI } from "types"
// mock data
const mockTags: OpenAPI.TagObject[] = [
{
name: "mockTag",
description: "Mock Tag",
},
]
// mock components
vi.mock("@/components/Tags/Section", () => ({
default: ({ tag }: { tag: OpenAPI.TagObject }) => (
<div data-testid="tag-section">{tag.name}</div>
),
}))
vi.mock("react", async () => {
const actual = await vi.importActual<typeof React>("react")
return {
...actual,
Suspense: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}
})
import Tags from ".."
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("Tags", () => {
test("does not render tags when tags is undefined", () => {
const { container } = render(<Tags tags={undefined} />)
expect(container).toBeEmptyDOMElement()
})
test("renders tags when tags is defined", async () => {
const { container } = render(<Tags tags={mockTags} />)
await waitFor(() => {
const tagSections = container.querySelectorAll("[data-testid='tag-section']")
expect(tagSections).toHaveLength(mockTags.length)
})
})
})

View File

@@ -1,7 +1,7 @@
import React, { Suspense } from "react"
import { OpenAPI } from "types"
import { TagSectionProps } from "./Section"
import dynamic from "next/dynamic"
import { Suspense } from "react"
const TagSection = dynamic<TagSectionProps>(
async () => import("./Section")

View File

@@ -29,6 +29,8 @@ export default [
"**/public",
"**/.eslintrc.js",
"**/generated",
"**/__tests__",
"**/__mocks__",
],
},
...compat.extends(

View File

@@ -11,7 +11,8 @@
"start": "next start",
"start:monorepo": "yarn start -p 3000",
"lint": "next lint --fix",
"prep": "node ./scripts/prepare.mjs"
"prep": "node ./scripts/prepare.mjs",
"test": "vitest"
},
"dependencies": {
"@mdx-js/loader": "^3.1.0",
@@ -64,7 +65,9 @@
"eslint": "^9.13.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react-hooks": "^5.0.0",
"types": "*"
"types": "*",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^2.1.8"
},
"engines": {
"node": ">=20"

View File

@@ -0,0 +1,182 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, fireEvent, render } from "@testing-library/react"
import { usePathname } from "next/navigation"
import AreaProvider, { useArea } from "../area"
import { OpenAPI } from "types"
// Mock functions
const mockSetActivePath = vi.fn()
const mockUseSidebar = vi.fn(() => ({
setActivePath: mockSetActivePath,
}))
const mockUsePathname = vi.fn(() => "/store/test")
const mockCapitalize = vi.fn((str: string) => str.charAt(0).toUpperCase() + str.slice(1))
// Track previous values for usePrevious mock
// usePrevious returns the value from the previous render
let previousValue: unknown = undefined
const mockUsePrevious = vi.fn((value: unknown) => {
const result = previousValue
previousValue = value
return result
})
// Test component that uses the hook
const TestComponent = () => {
const { area, prevArea, displayedArea, setArea } = useArea()
return (
<div>
<div data-testid="area">{area}</div>
<div data-testid="prev-area">{prevArea || "undefined"}</div>
<div data-testid="displayed-area">{displayedArea}</div>
<button
data-testid="set-area-store"
onClick={() => setArea("store" as OpenAPI.Area)}
>
Set Store
</button>
<button
data-testid="set-area-admin"
onClick={() => setArea("admin" as OpenAPI.Area)}
>
Set Admin
</button>
</div>
)
}
vi.mock("docs-ui", () => ({
capitalize: (str: string) => mockCapitalize(str),
usePrevious: (value: unknown) => mockUsePrevious(value),
useSidebar: () => mockUseSidebar(),
}))
vi.mock("next/navigation", () => ({
usePathname: () => mockUsePathname(),
}))
describe("AreaProvider", () => {
beforeEach(() => {
vi.clearAllMocks()
cleanup()
previousValue = undefined
mockUsePathname.mockReturnValue("/store/test")
mockCapitalize.mockImplementation((str: string) => str.charAt(0).toUpperCase() + str.slice(1))
})
describe("rendering", () => {
test("renders children", () => {
const { getByText } = render(
<AreaProvider area="store">
<div>Test Content</div>
</AreaProvider>
)
expect(getByText("Test Content")).toBeInTheDocument()
})
})
describe("initial state", () => {
test("initializes with passed area", () => {
const { getByTestId } = render(
<AreaProvider area="store">
<TestComponent />
</AreaProvider>
)
expect(getByTestId("area")).toHaveTextContent("store")
})
test("displays capitalized area", () => {
const { getByTestId } = render(
<AreaProvider area="store">
<TestComponent />
</AreaProvider>
)
expect(getByTestId("displayed-area")).toHaveTextContent("Store")
expect(mockCapitalize).toHaveBeenCalledWith("store")
})
test("prevArea is undefined initially", () => {
const { getByTestId } = render(
<AreaProvider area="store">
<TestComponent />
</AreaProvider>
)
expect(getByTestId("prev-area")).toHaveTextContent("undefined")
})
})
describe("setArea", () => {
test("updates area when setArea is called", () => {
const { getByTestId } = render(
<AreaProvider area="store">
<TestComponent />
</AreaProvider>
)
const setAdminButton = getByTestId("set-area-admin")
const areaElement = getByTestId("area")
expect(areaElement).toHaveTextContent("store")
fireEvent.click(setAdminButton)
expect(areaElement).toHaveTextContent("admin")
})
test("updates displayedArea when area changes", () => {
const { getByTestId } = render(
<AreaProvider area="store">
<TestComponent />
</AreaProvider>
)
const setAdminButton = getByTestId("set-area-admin")
const displayedAreaElement = getByTestId("displayed-area")
expect(displayedAreaElement).toHaveTextContent("Store")
fireEvent.click(setAdminButton)
expect(displayedAreaElement).toHaveTextContent("Admin")
})
})
describe("useEffect behavior", () => {
test("calls setActivePath(null) when pathname changes", () => {
const { rerender } = render(
<AreaProvider area="store">
<TestComponent />
</AreaProvider>
)
mockUsePathname.mockReturnValue("/admin/test")
rerender(
<AreaProvider area="store">
<TestComponent />
</AreaProvider>
)
expect(mockSetActivePath).toHaveBeenCalledWith(null)
})
})
describe("useArea hook", () => {
test("throws error when used outside provider", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
expect(() => {
render(<TestComponent />)
}).toThrow("useAreaProvider must be used inside an AreaProvider")
consoleSpy.mockRestore()
})
test("returns area, prevArea, displayedArea, and setArea", () => {
const { getByTestId } = render(
<AreaProvider area="store">
<TestComponent />
</AreaProvider>
)
expect(getByTestId("area")).toBeInTheDocument()
expect(getByTestId("prev-area")).toBeInTheDocument()
expect(getByTestId("displayed-area")).toBeInTheDocument()
expect(getByTestId("set-area-store")).toBeInTheDocument()
expect(getByTestId("set-area-admin")).toBeInTheDocument()
})
})
})

View File

@@ -0,0 +1,363 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render, waitFor } from "@testing-library/react"
import { OpenAPI, Sidebar } from "types"
// mock data
const mockShownSidebar: Sidebar.Sidebar = {
sidebar_id: "test-sidebar",
title: "Test Sidebar",
items: [],
}
const mockBaseSpecs: OpenAPI.ExpandedDocument = {
openapi: "3.0.0",
info: {
title: "Test API",
version: "1.0.0",
},
tags: [
{
name: "TestTag",
},
],
expandedTags: {
"testtag": {
"get": {
summary: "Test Operation",
description: "Test Operation Description",
parameters: [],
},
},
},
paths: {
"/test-path": {
get: {
operationId: "test-operation",
summary: "Test Operation",
description: "Test Operation Description",
"x-authenticated": false,
"x-codeSamples": [],
parameters: [],
responses: {
"200": {
description: "OK",
content: {}
}
},
requestBody: {
content: {}
}
},
},
},
components: {
securitySchemes: {
"test-security": {
type: "http",
scheme: "bearer",
},
},
},
}
// Mock functions
const mockPush = vi.fn()
const mockReplace = vi.fn()
const mockUseRouter = vi.fn(() => ({
push: mockPush,
replace: mockReplace,
}))
const mockSetActivePath = vi.fn()
const mockResetItems = vi.fn()
const mockUpdateItems = vi.fn()
const mockUseSidebar = vi.fn(() => ({
activePath: "testtag",
setActivePath: mockSetActivePath,
resetItems: mockResetItems,
updateItems: mockUpdateItems,
shownSidebar: mockShownSidebar as Sidebar.Sidebar | Sidebar.SidebarItemSidebar | undefined,
}))
const mockGetSectionId = vi.fn((parts: string[]) => parts.join("-").toLowerCase())
const mockGetTagChildSidebarItems = vi.fn(() => [] as Sidebar.SidebarItem[])
vi.mock("next/navigation", () => ({
useRouter: () => mockUseRouter(),
}))
vi.mock("docs-ui", () => ({
useSidebar: () => mockUseSidebar(),
}))
vi.mock("docs-utils", () => ({
getSectionId: (parts: string[]) => mockGetSectionId(parts),
}))
vi.mock("@/utils/get-tag-child-sidebar-items", () => ({
default: () => mockGetTagChildSidebarItems(),
}))
import BaseSpecsProvider, { useBaseSpecs } from "../base-specs"
// Test component that uses the hook
const TestComponent = () => {
const { baseSpecs, getSecuritySchema } = useBaseSpecs()
return (
<div>
<div data-testid="base-specs">{baseSpecs ? "present" : "null"}</div>
<div data-testid="security-schema">
{getSecuritySchema("test-security") ? "found" : "null"}
</div>
</div>
)
}
describe("BaseSpecsProvider", () => {
beforeEach(() => {
vi.clearAllMocks()
cleanup()
window.location.hash = ""
mockGetSectionId.mockImplementation((parts: string[]) => parts.join("-").toLowerCase())
mockGetTagChildSidebarItems.mockReturnValue([])
})
describe("rendering", () => {
test("renders children", () => {
const { getByText } = render(
<BaseSpecsProvider baseSpecs={mockBaseSpecs}>
<div>Test Content</div>
</BaseSpecsProvider>
)
expect(getByText("Test Content")).toBeInTheDocument()
})
})
describe("useBaseSpecs hook", () => {
test("throws error when used outside provider", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
expect(() => {
render(<TestComponent />)
}).toThrow("useBaseSpecs must be used inside a BaseSpecsProvider")
consoleSpy.mockRestore()
})
test("returns baseSpecs and getSecuritySchema", () => {
const { getByTestId } = render(
<BaseSpecsProvider baseSpecs={mockBaseSpecs}>
<TestComponent />
</BaseSpecsProvider>
)
expect(getByTestId("base-specs")).toHaveTextContent("present")
expect(getByTestId("security-schema")).toBeInTheDocument()
})
})
describe("getSecuritySchema", () => {
test("returns security schema when it exists", () => {
const { getByTestId } = render(
<BaseSpecsProvider baseSpecs={mockBaseSpecs}>
<TestComponent />
</BaseSpecsProvider>
)
const securitySchema = getByTestId("security-schema")
expect(securitySchema).toHaveTextContent("found")
})
test("returns null when security schema does not exist", () => {
const { getByTestId } = render(
<BaseSpecsProvider baseSpecs={mockBaseSpecs}>
<TestComponent />
</BaseSpecsProvider>
)
const securitySchema = getByTestId("security-schema")
// Change to a non-existent security name
const TestComponent2 = () => {
const { getSecuritySchema } = useBaseSpecs()
return (
<div data-testid="security-schema-2">
{getSecuritySchema("non-existent") ? "found" : "null"}
</div>
)
}
const { getByTestId: getByTestId2 } = render(
<BaseSpecsProvider baseSpecs={mockBaseSpecs}>
<TestComponent2 />
</BaseSpecsProvider>
)
expect(getByTestId2("security-schema-2")).toHaveTextContent("null")
})
test("returns null when security schema is a ref", () => {
const baseSpecsWithRef: OpenAPI.ExpandedDocument = {
...mockBaseSpecs,
components: {
securitySchemes: {
"test-security": {
$ref: "#/components/securitySchemes/OtherSecurity",
},
},
},
} as OpenAPI.ExpandedDocument
const TestComponent3 = () => {
const { getSecuritySchema } = useBaseSpecs()
return (
<div data-testid="security-schema-3">
{getSecuritySchema("test-security") ? "found" : "null"}
</div>
)
}
const { getByTestId } = render(
<BaseSpecsProvider baseSpecs={baseSpecsWithRef}>
<TestComponent3 />
</BaseSpecsProvider>
)
expect(getByTestId("security-schema-3")).toHaveTextContent("null")
})
})
describe("itemsToUpdate", () => {
test("generates itemsToUpdate from baseSpecs tags", async () => {
const mockSidebar: Sidebar.Sidebar = {
sidebar_id: "test-sidebar",
title: "Test Sidebar",
items: [],
}
mockUseSidebar.mockReturnValue({
activePath: "testtag",
setActivePath: mockSetActivePath,
resetItems: mockResetItems,
updateItems: mockUpdateItems,
shownSidebar: mockSidebar,
})
mockGetTagChildSidebarItems.mockReturnValue([
{
type: "link",
path: "/test-path",
title: "Test Link",
},
])
render(
<BaseSpecsProvider baseSpecs={mockBaseSpecs}>
<div>Test</div>
</BaseSpecsProvider>
)
await waitFor(() => {
expect(mockUpdateItems).toHaveBeenCalled()
})
const updateCall = mockUpdateItems.mock.calls[0][0]
expect(updateCall.sidebar_id).toBe("test-sidebar")
expect(updateCall.items).toBeDefined()
expect(Array.isArray(updateCall.items)).toBe(true)
expect(updateCall.items.length).toBeGreaterThan(0)
})
test("does not update items when baseSpecs is undefined", () => {
render(
<BaseSpecsProvider baseSpecs={undefined}>
<div>Test</div>
</BaseSpecsProvider>
)
expect(mockUpdateItems).not.toHaveBeenCalled()
})
test("does not update items when shownSidebar is null", () => {
mockUseSidebar.mockReturnValue({
activePath: "testtag",
setActivePath: mockSetActivePath,
resetItems: mockResetItems,
updateItems: mockUpdateItems,
shownSidebar: undefined,
})
render(
<BaseSpecsProvider baseSpecs={mockBaseSpecs}>
<div>Test</div>
</BaseSpecsProvider>
)
expect(mockUpdateItems).not.toHaveBeenCalled()
})
test("includes onOpen handler that updates hash and activePath", async () => {
mockUseSidebar.mockReturnValue({
activePath: "something-else",
setActivePath: mockSetActivePath,
resetItems: mockResetItems,
updateItems: mockUpdateItems,
shownSidebar: mockShownSidebar,
})
window.location.hash = ""
render(
<BaseSpecsProvider baseSpecs={mockBaseSpecs}>
<div>Test</div>
</BaseSpecsProvider>
)
await waitFor(() => {
expect(mockUpdateItems).toHaveBeenCalled()
})
const updateCall = mockUpdateItems.mock.calls[0][0]
const item = updateCall.items[0]
expect(item.newItem.onOpen).toBeDefined()
// Call onOpen
item.newItem.onOpen()
expect(mockPush).toHaveBeenCalledWith("#testtag", { scroll: false })
expect(mockSetActivePath).toHaveBeenCalledWith("testtag")
})
test("onOpen does not update hash when it already matches", async () => {
mockUseSidebar.mockReturnValue({
activePath: "testtag",
setActivePath: mockSetActivePath,
resetItems: mockResetItems,
updateItems: mockUpdateItems,
shownSidebar: mockShownSidebar,
})
window.location.hash = "#testtag"
render(
<BaseSpecsProvider baseSpecs={mockBaseSpecs}>
<div>Test</div>
</BaseSpecsProvider>
)
await waitFor(() => {
expect(mockUpdateItems).toHaveBeenCalled()
})
const updateCall = mockUpdateItems.mock.calls[0][0]
const item = updateCall.items[0]
mockPush.mockClear()
mockSetActivePath.mockClear()
// Call onOpen
item.newItem.onOpen()
expect(mockPush).not.toHaveBeenCalled()
expect(mockSetActivePath).not.toHaveBeenCalled()
})
})
describe("cleanup", () => {
test("calls resetItems on unmount", () => {
const { unmount } = render(
<BaseSpecsProvider baseSpecs={mockBaseSpecs}>
<div>Test</div>
</BaseSpecsProvider>
)
unmount()
expect(mockResetItems).toHaveBeenCalled()
})
})
})

View File

@@ -0,0 +1,95 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, fireEvent, render } from "@testing-library/react"
import LoadingProvider, { useLoading } from "../loading"
// Test component that uses the hook
const TestComponent = () => {
const { loading, removeLoading } = useLoading()
return (
<div>
<div data-testid="loading-state">{loading.toString()}</div>
<button data-testid="remove-loading" onClick={removeLoading}>
Remove Loading
</button>
</div>
)
}
describe("LoadingProvider", () => {
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders children", () => {
const { getByText } = render(
<LoadingProvider>
<div>Test Content</div>
</LoadingProvider>
)
expect(getByText("Test Content")).toBeInTheDocument()
})
})
describe("initial loading state", () => {
test("initializes with loading false by default", () => {
const { getByTestId } = render(
<LoadingProvider>
<TestComponent />
</LoadingProvider>
)
expect(getByTestId("loading-state")).toHaveTextContent("false")
})
test("initializes with loading true when initialLoading is true", () => {
const { getByTestId } = render(
<LoadingProvider initialLoading={true}>
<TestComponent />
</LoadingProvider>
)
expect(getByTestId("loading-state")).toHaveTextContent("true")
})
})
describe("removeLoading", () => {
test("sets loading to false when removeLoading is called", () => {
const { getByTestId } = render(
<LoadingProvider initialLoading={true}>
<TestComponent />
</LoadingProvider>
)
const removeLoadingButton = getByTestId("remove-loading")
const loadingState = getByTestId("loading-state")
expect(loadingState).toHaveTextContent("true")
fireEvent.click(removeLoadingButton)
expect(loadingState).toHaveTextContent("false")
})
})
describe("useLoading hook", () => {
test("throws error when used outside provider", () => {
// Suppress console.error for this test
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
expect(() => {
render(<TestComponent />)
}).toThrow("useLoading must be used inside a LoadingProvider")
consoleSpy.mockRestore()
})
test("returns loading state and removeLoading function", () => {
const { getByTestId } = render(
<LoadingProvider>
<TestComponent />
</LoadingProvider>
)
expect(getByTestId("loading-state")).toBeInTheDocument()
expect(getByTestId("remove-loading")).toBeInTheDocument()
})
})
})

View File

@@ -0,0 +1,80 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
import { MainNavProvider } from "../main-nav"
// Mock functions
const mockGetNavDropdownItems = vi.fn((options: unknown) => [
{
title: "Test Item",
path: "/test",
},
])
vi.mock("docs-ui", () => ({
getNavDropdownItems: (options: unknown) => mockGetNavDropdownItems(options),
MainNavProvider: ({ children, navItems }: { children: React.ReactNode; navItems: unknown[] }) => (
<div data-testid="ui-main-nav-provider" data-nav-items={JSON.stringify(navItems)}>
{children}
</div>
),
}))
vi.mock("@/config", () => ({
config: {
baseUrl: "https://test.com",
},
}))
describe("MainNavProvider", () => {
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
describe("rendering", () => {
test("renders children", () => {
const { getByText } = render(
<MainNavProvider>
<div>Test Content</div>
</MainNavProvider>
)
expect(getByText("Test Content")).toBeInTheDocument()
})
test("renders UiMainNavProvider with navItems", () => {
const { getByTestId } = render(
<MainNavProvider>
<div>Test</div>
</MainNavProvider>
)
const uiProvider = getByTestId("ui-main-nav-provider")
expect(uiProvider).toBeInTheDocument()
expect(mockGetNavDropdownItems).toHaveBeenCalledWith({
basePath: "https://test.com",
})
})
})
describe("navigationDropdownItems", () => {
test("memoizes navigationDropdownItems", () => {
const { rerender } = render(
<MainNavProvider>
<div>Test</div>
</MainNavProvider>
)
const callCount = mockGetNavDropdownItems.mock.calls.length
rerender(
<MainNavProvider>
<div>Test</div>
</MainNavProvider>
)
// Should not call getNavDropdownItems again due to memoization
expect(mockGetNavDropdownItems.mock.calls.length).toBe(callCount)
})
})
})

View File

@@ -0,0 +1,206 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
import PageTitleProvider from "../page-title"
import { Sidebar } from "types"
// Mock functions
const mockUseSidebar = vi.fn(() => ({
activePath: "test-path" as string | null,
activeItem: {
type: "link",
path: "test-path",
title: "Test Item",
},
}))
const mockUseArea = vi.fn(() => ({
displayedArea: "Store",
}))
vi.mock("docs-ui", () => ({
useSidebar: () => mockUseSidebar(),
}))
vi.mock("@/providers/area", () => ({
useArea: () => mockUseArea(),
}))
describe("PageTitleProvider", () => {
beforeEach(() => {
vi.clearAllMocks()
cleanup()
document.title = ""
mockUseSidebar.mockReturnValue({
activePath: "test-path",
activeItem: {
type: "link",
path: "test-path",
title: "Test Item",
},
})
mockUseArea.mockReturnValue({
displayedArea: "Store",
})
})
describe("rendering", () => {
test("renders children", () => {
const { getByText } = render(
<PageTitleProvider>
<div>Test Content</div>
</PageTitleProvider>
)
expect(getByText("Test Content")).toBeInTheDocument()
})
})
describe("document title", () => {
test("sets title to suffix when activePath is null", () => {
mockUseSidebar.mockReturnValue({
activePath: null,
activeItem: {
type: "link",
path: "test-path",
title: "Test Item",
},
})
render(
<PageTitleProvider>
<div>Test</div>
</PageTitleProvider>
)
expect(document.title).toBe("Medusa Store API Reference")
})
test("sets title to suffix when activePath is empty string", () => {
mockUseSidebar.mockReturnValue({
activePath: "",
activeItem: {
type: "link",
path: "test-path",
title: "Test Item",
},
})
render(
<PageTitleProvider>
<div>Test</div>
</PageTitleProvider>
)
expect(document.title).toBe("Medusa Store API Reference")
})
test("sets title with activeItem title when activeItem.path matches activePath", () => {
const mockItem: Sidebar.SidebarItemLink = {
type: "link",
path: "/test-path",
title: "Test Item",
}
mockUseSidebar.mockReturnValue({
activePath: "/test-path",
activeItem: mockItem,
})
render(
<PageTitleProvider>
<div>Test</div>
</PageTitleProvider>
)
expect(document.title).toBe("Test Item - Medusa Store API Reference")
})
test("sets title with child item title when activeItem has matching child", () => {
const mockChildItem: Sidebar.SidebarItemLink = {
type: "link",
path: "/child-path",
title: "Child Item",
}
const mockItem: Sidebar.SidebarItemLink = {
type: "link",
path: "/parent-path",
title: "Parent Item",
children: [mockChildItem],
}
mockUseSidebar.mockReturnValue({
activePath: "/child-path",
activeItem: mockItem,
})
render(
<PageTitleProvider>
<div>Test</div>
</PageTitleProvider>
)
expect(document.title).toBe("Child Item - Medusa Store API Reference")
})
test("sets title to suffix when activeItem has no matching child", () => {
const mockItem: Sidebar.SidebarItemLink = {
type: "link",
path: "/parent-path",
title: "Parent Item",
children: [
{
type: "link",
path: "/other-path",
title: "Other Item",
},
],
}
mockUseSidebar.mockReturnValue({
activePath: "/child-path",
activeItem: mockItem,
})
render(
<PageTitleProvider>
<div>Test</div>
</PageTitleProvider>
)
expect(document.title).toBe("Medusa Store API Reference")
})
test("updates title when displayedArea changes", () => {
mockUseArea.mockReturnValue({
displayedArea: "Admin",
})
render(
<PageTitleProvider>
<div>Test</div>
</PageTitleProvider>
)
expect(document.title).toBe("Test Item - Medusa Admin API Reference")
})
test("updates title when activePath changes", () => {
const { rerender } = render(
<PageTitleProvider>
<div>Test</div>
</PageTitleProvider>
)
expect(document.title).toBe("Test Item - Medusa Store API Reference")
const mockItem: Sidebar.SidebarItemLink = {
type: "link",
path: "/new-path",
title: "New Item",
}
mockUseSidebar.mockReturnValue({
activePath: "/new-path",
activeItem: mockItem,
})
rerender(
<PageTitleProvider>
<div>Test</div>
</PageTitleProvider>
)
expect(document.title).toBe("New Item - Medusa Store API Reference")
})
})
})

View File

@@ -0,0 +1,162 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render } from "@testing-library/react"
import SearchProvider from "../search"
// Mock functions
const mockIsLoading = vi.fn(() => false)
const mockUsePageLoading = vi.fn(() => ({
isLoading: mockIsLoading(),
}))
const mockBasePathUrl = vi.fn((url: string) => url)
vi.mock("docs-ui", () => ({
usePageLoading: () => mockUsePageLoading(),
SearchProvider: ({
children,
algolia,
indices,
defaultIndex,
searchProps,
}: {
children: React.ReactNode
algolia: unknown
indices: unknown[]
defaultIndex: string
searchProps: unknown
}) => (
<div
data-testid="ui-search-provider"
data-algolia={JSON.stringify(algolia)}
data-indices={JSON.stringify(indices)}
data-default-index={defaultIndex}
data-search-props={JSON.stringify(searchProps)}
>
{children}
</div>
),
}))
vi.mock("@/config", () => ({
config: {
baseUrl: "https://test.com",
},
}))
vi.mock("@/utils/base-path-url", () => ({
default: (url: string) => mockBasePathUrl(url),
}))
describe("SearchProvider", () => {
beforeEach(() => {
vi.clearAllMocks()
cleanup()
// Set environment variables
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID = "test-app-id"
process.env.NEXT_PUBLIC_ALGOLIA_API_KEY = "test-api-key"
process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME = "test-api-index"
process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME = "test-docs-index"
mockIsLoading.mockReturnValue(false)
})
describe("rendering", () => {
test("renders children", () => {
const { getByText } = render(
<SearchProvider>
<div>Test Content</div>
</SearchProvider>
)
expect(getByText("Test Content")).toBeInTheDocument()
})
test("renders UiSearchProvider with correct props", () => {
const { getByTestId } = render(
<SearchProvider>
<div>Test</div>
</SearchProvider>
)
const uiProvider = getByTestId("ui-search-provider")
expect(uiProvider).toBeInTheDocument()
const algolia = JSON.parse(uiProvider.getAttribute("data-algolia") || "{}")
expect(algolia).toEqual({
appId: "test-app-id",
apiKey: "test-api-key",
mainIndexName: "test-api-index",
})
const indices = JSON.parse(uiProvider.getAttribute("data-indices") || "[]")
expect(indices).toEqual([
{
value: "test-docs-index",
title: "Docs",
},
{
value: "test-api-index",
title: "Store & Admin API",
},
])
expect(uiProvider.getAttribute("data-default-index")).toBe("test-api-index")
})
})
describe("environment variables", () => {
test("uses default values when env vars are not set", () => {
delete process.env.NEXT_PUBLIC_ALGOLIA_APP_ID
delete process.env.NEXT_PUBLIC_ALGOLIA_API_KEY
delete process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME
delete process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME
const { getByTestId } = render(
<SearchProvider>
<div>Test</div>
</SearchProvider>
)
const uiProvider = getByTestId("ui-search-provider")
const algolia = JSON.parse(uiProvider.getAttribute("data-algolia") || "{}")
expect(algolia.appId).toBe("temp")
expect(algolia.apiKey).toBe("temp")
expect(algolia.mainIndexName).toBe("temp")
})
})
describe("searchProps", () => {
test("passes isLoading from usePageLoading", () => {
mockIsLoading.mockReturnValue(true)
const { getByTestId } = render(
<SearchProvider>
<div>Test</div>
</SearchProvider>
)
const uiProvider = getByTestId("ui-search-provider")
const searchProps = JSON.parse(uiProvider.getAttribute("data-search-props") || "{}")
expect(searchProps.isLoading).toBe(true)
})
test("includes suggestions", () => {
const { getByTestId } = render(
<SearchProvider>
<div>Test</div>
</SearchProvider>
)
const uiProvider = getByTestId("ui-search-provider")
const searchProps = JSON.parse(uiProvider.getAttribute("data-search-props") || "{}")
expect(searchProps.suggestions).toBeDefined()
expect(Array.isArray(searchProps.suggestions)).toBe(true)
expect(searchProps.suggestions.length).toBeGreaterThan(0)
})
test("includes checkInternalPattern regex", () => {
const { getByTestId } = render(
<SearchProvider>
<div>Test</div>
</SearchProvider>
)
const uiProvider = getByTestId("ui-search-provider")
const searchProps = JSON.parse(uiProvider.getAttribute("data-search-props") || "{}")
expect(searchProps.checkInternalPattern).toBeDefined()
})
})
})

View File

@@ -0,0 +1,237 @@
import React from "react"
import { beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, render, waitFor } from "@testing-library/react"
import SidebarProvider from "../sidebar"
import { Sidebar } from "types"
// Mock functions
const mockIsLoading = vi.fn(() => false)
const mockSetIsLoading = vi.fn()
const mockUsePageLoading = vi.fn(() => ({
isLoading: mockIsLoading(),
setIsLoading: mockSetIsLoading,
}))
const mockScrollableElement = vi.fn(() => null as HTMLElement | null)
const mockUseScrollController = vi.fn(() => ({
scrollableElement: mockScrollableElement(),
}))
const mockPathname = vi.fn(() => "/store/test")
const mockUsePathname = vi.fn(() => mockPathname())
const mockStoreSidebar: Sidebar.Sidebar = {
sidebar_id: "store-sidebar",
title: "Store Sidebar",
items: [],
}
const mockAdminSidebar: Sidebar.Sidebar = {
sidebar_id: "admin-sidebar",
title: "Admin Sidebar",
items: [],
}
vi.mock("docs-ui", () => ({
SidebarProvider: ({
children,
isLoading,
setIsLoading,
shouldHandleHashChange,
shouldHandlePathChange,
scrollableElement,
sidebars,
persistCategoryState,
disableActiveTransition,
isSidebarStatic,
}: {
children: React.ReactNode
isLoading: boolean
setIsLoading: (value: boolean) => void
shouldHandleHashChange: boolean
shouldHandlePathChange: boolean
scrollableElement: HTMLElement | null
sidebars: Sidebar.Sidebar[]
persistCategoryState: boolean
disableActiveTransition: boolean
isSidebarStatic: boolean
}) => (
<div
data-testid="ui-sidebar-provider"
data-is-loading={isLoading.toString()}
data-should-handle-hash-change={shouldHandleHashChange.toString()}
data-should-handle-path-change={shouldHandlePathChange.toString()}
data-scrollable-element={scrollableElement ? "present" : "null"}
data-sidebars={JSON.stringify(sidebars)}
data-persist-category-state={persistCategoryState.toString()}
data-disable-active-transition={disableActiveTransition.toString()}
data-is-sidebar-static={isSidebarStatic.toString()}
>
{children}
</div>
),
usePageLoading: () => mockUsePageLoading(),
useScrollController: () => mockUseScrollController(),
}))
vi.mock("next/navigation", () => ({
usePathname: () => mockUsePathname(),
}))
vi.mock("@/config", () => ({
config: {
sidebars: [{
sidebar_id: "store-sidebar",
title: "Store Sidebar",
items: [],
}],
},
}))
// Mock dynamic imports
vi.mock("@/generated/generated-store-sidebar.mjs", () => ({
default: mockStoreSidebar,
}))
vi.mock("@/generated/generated-admin-sidebar.mjs", () => ({
default: mockAdminSidebar,
}))
describe("SidebarProvider", () => {
beforeEach(() => {
vi.clearAllMocks()
cleanup()
mockIsLoading.mockReturnValue(false)
mockScrollableElement.mockReturnValue(null)
mockPathname.mockReturnValue("/store/test")
})
describe("rendering", () => {
test("renders children", () => {
const { getByText } = render(
<SidebarProvider>
<div>Test Content</div>
</SidebarProvider>
)
expect(getByText("Test Content")).toBeInTheDocument()
})
test("renders UiSidebarProvider with correct props", async () => {
const { getByTestId } = render(
<SidebarProvider>
<div>Test</div>
</SidebarProvider>
)
await waitFor(() => {
const uiProvider = getByTestId("ui-sidebar-provider")
expect(uiProvider).toBeInTheDocument()
expect(uiProvider.getAttribute("data-is-loading")).toBe("false")
expect(uiProvider.getAttribute("data-should-handle-hash-change")).toBe("true")
expect(uiProvider.getAttribute("data-should-handle-path-change")).toBe("false")
expect(uiProvider.getAttribute("data-persist-category-state")).toBe("false")
expect(uiProvider.getAttribute("data-disable-active-transition")).toBe("false")
expect(uiProvider.getAttribute("data-is-sidebar-static")).toBe("false")
})
})
})
describe("sidebar loading", () => {
test("loads store sidebar when path starts with /store", async () => {
mockPathname.mockReturnValue("/store/test")
const { getByTestId } = render(
<SidebarProvider>
<div>Test</div>
</SidebarProvider>
)
await waitFor(() => {
const uiProvider = getByTestId("ui-sidebar-provider")
const sidebars = JSON.parse(uiProvider.getAttribute("data-sidebars") || "[]")
expect(sidebars).toEqual([mockStoreSidebar])
})
})
test("loads admin sidebar when path does not start with /store", async () => {
mockPathname.mockReturnValue("/admin/test")
const { getByTestId } = render(
<SidebarProvider>
<div>Test</div>
</SidebarProvider>
)
await waitFor(() => {
const uiProvider = getByTestId("ui-sidebar-provider")
const sidebars = JSON.parse(uiProvider.getAttribute("data-sidebars") || "[]")
expect(sidebars).toEqual([mockAdminSidebar])
})
})
test("uses config sidebars as fallback when sidebar is not loaded", () => {
const { getByTestId } = render(
<SidebarProvider>
<div>Test</div>
</SidebarProvider>
)
// Initially should use config sidebars
const uiProvider = getByTestId("ui-sidebar-provider")
const sidebars = JSON.parse(uiProvider.getAttribute("data-sidebars") || "[]")
expect(sidebars).toEqual([mockStoreSidebar])
})
})
describe("props passing", () => {
test("passes isLoading and setIsLoading to UiSidebarProvider", async () => {
mockIsLoading.mockReturnValue(true)
const { getByTestId } = render(
<SidebarProvider>
<div>Test</div>
</SidebarProvider>
)
await waitFor(() => {
const uiProvider = getByTestId("ui-sidebar-provider")
expect(uiProvider.getAttribute("data-is-loading")).toBe("true")
})
})
test("passes scrollableElement to UiSidebarProvider", async () => {
const mockElement = document.createElement("div")
mockScrollableElement.mockReturnValue(mockElement)
const { getByTestId } = render(
<SidebarProvider>
<div>Test</div>
</SidebarProvider>
)
await waitFor(() => {
const uiProvider = getByTestId("ui-sidebar-provider")
expect(uiProvider.getAttribute("data-scrollable-element")).toBe("present")
})
})
})
describe("error handling", () => {
test("handles sidebar loading errors gracefully", async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
// Mock a failing import
vi.doMock("../generated/generated-store-sidebar.mjs", () => {
throw new Error("Failed to load sidebar")
})
const { getByTestId } = render(
<SidebarProvider>
<div>Test</div>
</SidebarProvider>
)
await waitFor(() => {
const uiProvider = getByTestId("ui-sidebar-provider")
expect(uiProvider).toBeInTheDocument()
})
consoleSpy.mockRestore()
})
})
})

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import type { OpenAPI } from "types"
import { capitalize, usePrevious, useSidebar } from "docs-ui"
import { createContext, useContext, useEffect, useMemo, useState } from "react"

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import { OpenAPI } from "types"
import { ReactNode, createContext, useContext, useEffect, useMemo } from "react"
import getTagChildSidebarItems from "../utils/get-tag-child-sidebar-items"
@@ -68,7 +69,8 @@ const BaseSpecsProvider = ({ children, baseSpecs }: BaseSpecsProviderProps) => {
children: childItems,
loaded: childItems.length > 0,
onOpen: () => {
if (location.hash !== tagPathName) {
const currentHash = location.hash.replace("#", "")
if (currentHash !== tagPathName) {
router.push(`#${tagPathName}`, {
scroll: false,
})

View File

@@ -1,3 +1,4 @@
import React from "react"
import { createContext, useContext, useState } from "react"
type LoadingContextType = {

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import {
getNavDropdownItems,
MainNavProvider as UiMainNavProvider,

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import { createContext, useEffect } from "react"
import { useSidebar } from "docs-ui"
import { useArea } from "./area"
@@ -30,6 +31,8 @@ const PageTitleProvider = ({ children }: PageTitleProviderProps) => {
) as Sidebar.SidebarItemLink
if (item) {
document.title = `${item.title} - ${titleSuffix}`
} else {
document.title = titleSuffix
}
}
}

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import { usePageLoading, SearchProvider as UiSearchProvider } from "docs-ui"
import { config } from "../config"
import basePathUrl from "../utils/base-path-url"

View File

@@ -1,5 +1,6 @@
"use client"
import React from "react"
import {
SidebarProvider as UiSidebarProvider,
usePageLoading,

View File

@@ -22,6 +22,8 @@
"**/*.mjs"
],
"exclude": [
"specs"
"specs",
"**/__tests__",
"**/__mocks__"
]
}

View File

@@ -0,0 +1,16 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"include": [
"**/*.tsx",
"**/*.ts",
"**/*.js",
"src/**/*",
"**/*.mjs",
"__tests__/**/*",
"__mocks__/**/*"
],
"exclude": [
"specs"
]
}

View File

@@ -0,0 +1,18 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
import { resolve } from 'path'
export default defineConfig({
plugins: [
tsconfigPaths({
configNames: ["tsconfig.tests.json"]
}),
react()
],
test: {
environment: 'jsdom',
setupFiles: [resolve(__dirname, '../../vitest.setup.ts')],
},
})

View File

@@ -1,7 +1,7 @@
import React from "react"
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, fireEvent, render, waitFor } from "@testing-library/react"
import * as AiAssistantMocks from "@/components/AiAssistant/__mocks__"
import * as AiAssistantMocks from "../../../components/AiAssistant/__mocks__"
// Mock dependencies
const mockUseIsBrowser = vi.fn(() => ({
@@ -297,6 +297,9 @@ describe("useAiAssistant hook", () => {
// Mock grecaptcha to be available after a delay
setTimeout(() => {
if (!window) {
return
}
;(window as unknown as { grecaptcha?: unknown }).grecaptcha = {}
}, 100)

Some files were not shown because too many files have changed in this diff Show More