(
- async () => import("../../../MDXContent/Server"),
+ async () => import("@/components/MDXContent/Server"),
{
loading: () => ,
}
@@ -31,22 +32,25 @@ const SecurityDescription = ({
}: SecurityDescriptionProps) => {
return (
<>
- {securitySchema["x-displayName"] as string}
+ {securitySchema["x-displayName"] as string}
{isServer && }
{!isServer && }
-
+
Security Scheme Type:{" "}
{getSecuritySchemaTypeName(securitySchema)}
{(securitySchema.type === "http" || securitySchema.type === "apiKey") && (
-
+
{securitySchema.type === "http"
? "HTTP Authorization Scheme"
: "Cookie parameter name"}
:
{" "}
-
+
{securitySchema.type === "http"
? securitySchema.scheme
: securitySchema.name}
diff --git a/www/apps/api-reference/components/MDXComponents/Security/__tests__/index.test.tsx b/www/apps/api-reference/components/MDXComponents/Security/__tests__/index.test.tsx
new file mode 100644
index 0000000000..9b9ec32a93
--- /dev/null
+++ b/www/apps/api-reference/components/MDXComponents/Security/__tests__/index.test.tsx
@@ -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 }) => (
+ {securitySchema.type}
+ ),
+}))
+
+import Security from ".."
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+describe("rendering", () => {
+ test("does not render when specs is not provided", () => {
+ const { container } = render()
+ const securityDescriptionElements = container.querySelectorAll("[data-testid='security-description']")
+ expect(securityDescriptionElements).toHaveLength(0)
+ })
+ test("renders security information for specs", async () => {
+ const { getByTestId } = render()
+ 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()
+ const securityDescriptionElements = container.querySelectorAll("[data-testid='security-description']")
+ expect(securityDescriptionElements).toHaveLength(0)
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/MDXComponents/Security/index.tsx b/www/apps/api-reference/components/MDXComponents/Security/index.tsx
index 41104f900a..8e28c72c55 100644
--- a/www/apps/api-reference/components/MDXComponents/Security/index.tsx
+++ b/www/apps/api-reference/components/MDXComponents/Security/index.tsx
@@ -1,3 +1,4 @@
+import React from "react"
import dynamic from "next/dynamic"
import type { OpenAPI } from "types"
import type { SecurityDescriptionProps } from "./Description"
diff --git a/www/apps/api-reference/components/MDXComponents/index.tsx b/www/apps/api-reference/components/MDXComponents/index.tsx
index 7a5476cb3a..28fabe17a6 100644
--- a/www/apps/api-reference/components/MDXComponents/index.tsx
+++ b/www/apps/api-reference/components/MDXComponents/index.tsx
@@ -1,3 +1,4 @@
+import React from "react"
import type { MDXComponents } from "mdx/types"
import Security from "./Security"
import type { OpenAPI } from "types"
diff --git a/www/apps/api-reference/components/MDXContent/Client/index.tsx b/www/apps/api-reference/components/MDXContent/Client/index.tsx
index bc0642e04c..70230c66d5 100644
--- a/www/apps/api-reference/components/MDXContent/Client/index.tsx
+++ b/www/apps/api-reference/components/MDXContent/Client/index.tsx
@@ -1,5 +1,6 @@
"use client"
+import React from "react"
import { useEffect, useState } from "react"
import getCustomComponents from "../../MDXComponents"
import type { ScopeType } from "../../MDXComponents"
diff --git a/www/apps/api-reference/components/MDXContent/Server/index.tsx b/www/apps/api-reference/components/MDXContent/Server/index.tsx
index 9e4722b7bd..dcae58e6c0 100644
--- a/www/apps/api-reference/components/MDXContent/Server/index.tsx
+++ b/www/apps/api-reference/components/MDXContent/Server/index.tsx
@@ -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"
diff --git a/www/apps/api-reference/components/MethodLabel/__tests__/index.test.tsx b/www/apps/api-reference/components/MethodLabel/__tests__/index.test.tsx
new file mode 100644
index 0000000000..eb51933d94
--- /dev/null
+++ b/www/apps/api-reference/components/MethodLabel/__tests__/index.test.tsx
@@ -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
+ }) => (
+ {children}
+ ),
+ 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()
+ 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()
+ 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()
+ 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()
+ const badgeElement = getByTestId("badge")
+ expect(badgeElement).toBeInTheDocument()
+ expect(badgeElement).toHaveTextContent("Get")
+ expect(badgeElement).toHaveAttribute("data-variant", "green")
+ expect(badgeElement).toHaveClass("test-class")
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/MethodLabel/index.tsx b/www/apps/api-reference/components/MethodLabel/index.tsx
index d5b7241929..811cf2ffe5 100644
--- a/www/apps/api-reference/components/MethodLabel/index.tsx
+++ b/www/apps/api-reference/components/MethodLabel/index.tsx
@@ -1,3 +1,4 @@
+import React from "react"
import clsx from "clsx"
import { Badge, capitalize } from "docs-ui"
diff --git a/www/apps/api-reference/components/Section/Container/__tests__/index.test.tsx b/www/apps/api-reference/components/Section/Container/__tests__/index.test.tsx
new file mode 100644
index 0000000000..b7be07a8ae
--- /dev/null
+++ b/www/apps/api-reference/components/Section/Container/__tests__/index.test.tsx
@@ -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: () => Section Divider
,
+}))
+vi.mock("docs-ui", () => ({
+ WideSection: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}))
+
+import SectionContainer from ".."
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+describe("rendering", () => {
+ test("renders section container with children", () => {
+ const { getByTestId } = render(Test)
+ const wideSectionElement = getByTestId("wide-section")
+ expect(wideSectionElement).toBeInTheDocument()
+ expect(wideSectionElement).toHaveTextContent("Test")
+ })
+ test("renders section container with no top padding", () => {
+ const { getByTestId } = render(Test)
+ const sectionContainerElement = getByTestId("section-container")
+ expect(sectionContainerElement).not.toHaveClass("pt-7")
+ })
+ test("renders section container with top padding", () => {
+ const { getByTestId } = render(Test)
+ const sectionContainerElement = getByTestId("section-container")
+ expect(sectionContainerElement).toHaveClass("pt-7")
+ })
+ test("renders section container with divider", () => {
+ const { getByTestId } = render(Test)
+ const sectionDividerElement = getByTestId("section-divider")
+ expect(sectionDividerElement).toBeInTheDocument()
+ })
+ test("renders section container with no divider", () => {
+ const { container } = render(Test)
+ const sectionDividerElement = container.querySelectorAll("[data-testid='section-divider']")
+ expect(sectionDividerElement).toHaveLength(0)
+ })
+ test("renders section container with className", () => {
+ const { getByTestId } = render(Test)
+ const sectionContainerElement = getByTestId("section-container")
+ expect(sectionContainerElement).toHaveClass("test-class")
+ })
+ test("renders section container with ref", () => {
+ const ref = vi.fn()
+ render(Test)
+ expect(ref).toHaveBeenCalled()
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Section/Container/index.tsx b/www/apps/api-reference/components/Section/Container/index.tsx
index 6775e13445..b3fab4f3c6 100644
--- a/www/apps/api-reference/components/Section/Container/index.tsx
+++ b/www/apps/api-reference/components/Section/Container/index.tsx
@@ -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(
!noTopPadding && "pt-7",
className
)}
+ data-testid="section-container"
>
{children}
{!noDivider && }
diff --git a/www/apps/api-reference/components/Section/Divider/index.tsx b/www/apps/api-reference/components/Section/Divider/index.tsx
index 2119d3ac6e..5b8b3353a1 100644
--- a/www/apps/api-reference/components/Section/Divider/index.tsx
+++ b/www/apps/api-reference/components/Section/Divider/index.tsx
@@ -1,3 +1,4 @@
+import React from "react"
import clsx from "clsx"
type SectionDividerProps = {
diff --git a/www/apps/api-reference/components/Section/__tests__/index.test.tsx b/www/apps/api-reference/components/Section/__tests__/index.test.tsx
new file mode 100644
index 0000000000..325e61adca
--- /dev/null
+++ b/www/apps/api-reference/components/Section/__tests__/index.test.tsx
@@ -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()
+ 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()
+ 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()
+ expect(mockSetActivePath).not.toHaveBeenCalled()
+ expect(mockPush).not.toHaveBeenCalled()
+ })
+
+ test("disables scroll restoration when history is available", () => {
+ render()
+ expect(history.scrollRestoration).toBe("manual")
+ })
+
+ test("does not disable scroll restoration when history is not available", () => {
+ delete (window.history as any).scrollRestoration
+ render()
+ expect(history.scrollRestoration).not.toBe("manual")
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Section/index.tsx b/www/apps/api-reference/components/Section/index.tsx
index 288cf926ef..8b62554fd5 100644
--- a/www/apps/api-reference/components/Section/index.tsx
+++ b/www/apps/api-reference/components/Section/index.tsx
@@ -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"
diff --git a/www/apps/api-reference/components/Space/__tests__/index.test.tsx b/www/apps/api-reference/components/Space/__tests__/index.test.tsx
new file mode 100644
index 0000000000..01ff32c9e6
--- /dev/null
+++ b/www/apps/api-reference/components/Space/__tests__/index.test.tsx
@@ -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()
+ 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()
+ const spaceElement = getByTestId("space")
+ expect(spaceElement).toBeInTheDocument()
+ expect(spaceElement).toHaveStyle({
+ height: "1px",
+ marginTop: "9px",
+ marginBottom: "9px",
+ marginLeft: "10px",
+ marginRight: "10px",
+ })
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Space/index.tsx b/www/apps/api-reference/components/Space/index.tsx
index 527e20eb1b..f74bd4c047 100644
--- a/www/apps/api-reference/components/Space/index.tsx
+++ b/www/apps/api-reference/components/Space/index.tsx
@@ -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"
>
)
}
diff --git a/www/apps/api-reference/components/Tags/Operation/CodeSection/RequestSamples/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/CodeSection/RequestSamples/__tests__/index.test.tsx
new file mode 100644
index 0000000000..8f9a138d53
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/CodeSection/RequestSamples/__tests__/index.test.tsx
@@ -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
+ }) => (
+ {source}
+ ),
+ CodeTab: ({
+ children,
+ label,
+ value
+ }: {
+ children: React.ReactNode,
+ label: string,
+ value: string
+ }) => (
+ {children}
+ ),
+ CodeTabs: ({
+ children,
+ group
+ }: {
+ children: React.ReactNode,
+ group: string
+ }) => (
+ {children}
+ ),
+}))
+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(
+
+ )
+ 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)
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/CodeSection/RequestSamples/index.tsx b/www/apps/api-reference/components/Tags/Operation/CodeSection/RequestSamples/index.tsx
index 791d54e7cc..94929a94d2 100644
--- a/www/apps/api-reference/components/Tags/Operation/CodeSection/RequestSamples/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/CodeSection/RequestSamples/index.tsx
@@ -1,3 +1,4 @@
+import React from "react"
import type { OpenAPI } from "types"
import { CodeBlock, CodeTab, CodeTabs } from "docs-ui"
import slugify from "slugify"
diff --git a/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/Sample/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/Sample/__tests__/index.test.tsx
new file mode 100644
index 0000000000..d92d44b4e6
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/Sample/__tests__/index.test.tsx
@@ -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 }) => (
+ {source}
+ ),
+}))
+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(
+
+ )
+ 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(
+
+ )
+ expect(container).toHaveTextContent("Empty Response")
+ })
+
+ test("renders content type when content is available", () => {
+ const { container } = render(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ const selectElement = container.querySelector("select")
+ fireEvent.change(selectElement!, { target: { value: "example 2" } })
+ const codeBlockElement = getByTestId("code-block")
+ expect(codeBlockElement).toHaveTextContent("Example 2")
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/Sample/index.tsx b/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/Sample/index.tsx
index 76009b19d8..53203cbb6f 100644
--- a/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/Sample/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/Sample/index.tsx
@@ -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 (
<>
- {response.content && (
-
Content type: {Object.keys(response.content)[0]}
+ {!isEmptyResponse && (
+
+ Content type: {Object.keys(response.content)[0]}
+
)}
<>
{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)
}
diff --git a/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/__tests__/index.test.tsx
new file mode 100644
index 0000000000..2871c332b0
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/__tests__/index.test.tsx
@@ -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 }) => (
+
{children}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/CodeSection/Responses/Sample", () => ({
+ default: () =>
Sample
,
+}))
+
+import TagsOperationCodeSectionResponses from ".."
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+describe("rendering", () => {
+ test("renders response code badge", () => {
+ const { getByTestId } = render(
+
+ )
+ 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(
+
+ )
+ expect(container).toBeEmptyDOMElement()
+ })
+
+ test("renders sample component", () => {
+ const { getByTestId } = render(
+
+ )
+ const sampleElement = getByTestId("sample")
+ expect(sampleElement).toBeInTheDocument()
+ expect(sampleElement).toHaveTextContent("Sample")
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/index.tsx b/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/index.tsx
index f2f5a9847a..58de9f5a7b 100644
--- a/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/CodeSection/Responses/index.tsx
@@ -1,3 +1,4 @@
+import React from "react"
import type { OpenAPI } from "types"
import dynamic from "next/dynamic"
import type { TagsOperationCodeSectionResponsesSampleProps } from "./Sample"
diff --git a/www/apps/api-reference/components/Tags/Operation/CodeSection/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/CodeSection/__tests__/index.test.tsx
new file mode 100644
index 0000000000..7fbc05dcda
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/CodeSection/__tests__/index.test.tsx
@@ -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 }) => (
+
{method}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/CodeSection/RequestSamples", () => ({
+ default: () =>
Request Samples
,
+}))
+vi.mock("@/components/Tags/Operation/CodeSection/Responses", () => ({
+ default: () =>
Responses
,
+}))
+vi.mock("docs-ui", () => ({
+ CopyButton: ({ text }: { text: string }) => (
+
{text}
+ ),
+}))
+
+import TagsOperationCodeSection from ".."
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+describe("rendering", () => {
+ test("renders method label, request samples, and responses", async () => {
+ const { getByTestId } = render(
+
+ )
+ 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(
+
+ )
+ const requestSamplesElement = container.querySelector("[data-testid='request-samples']")
+ expect(requestSamplesElement).not.toBeInTheDocument()
+ })
+
+ test("renders code section with className", () => {
+ const { getByTestId } = render(
+
+ )
+ const codeSectionElement = getByTestId("code-section")
+ expect(codeSectionElement).toHaveClass("test-class")
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/CodeSection/index.tsx b/www/apps/api-reference/components/Tags/Operation/CodeSection/index.tsx
index a4c75eed7c..0c7964a026 100644
--- a/www/apps/api-reference/components/Tags/Operation/CodeSection/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/CodeSection/index.tsx
@@ -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 (
-
+
-
+
{endpointPath}
@@ -44,7 +51,7 @@ const TagOperationCodeSection = ({
- {operation["x-codeSamples"] && (
+ {operation["x-codeSamples"] && operation["x-codeSamples"].length > 0 && (
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/DeprecationNotice/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/DeprecationNotice/__tests__/index.test.tsx
new file mode 100644
index 0000000000..e02c9b4467
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/DeprecationNotice/__tests__/index.test.tsx
@@ -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 }) => (
+
{children}
+ ),
+ Tooltip: ({ text, children }: { text: string, children: React.ReactNode }) => (
+
{children}
+ ),
+}))
+
+import TagsOperationDescriptionSectionDeprecationNotice from ".."
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+describe("rendering", () => {
+ test("renders deprecation notice with tooltip", () => {
+ const { getByTestId } = render(
+
+ )
+ 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(
+
+ )
+ const tooltipElement = container.querySelector("[data-testid='tooltip']")
+ expect(tooltipElement).not.toBeInTheDocument()
+ })
+
+ test("renders deprecation notice with className", () => {
+ const { getByTestId } = render(
+
+ )
+ const deprecationNoticeElement = getByTestId("deprecation-notice")
+ expect(deprecationNoticeElement).toHaveClass("test-class")
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/DeprecationNotice/index.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/DeprecationNotice/index.tsx
index 9f024848e0..aace8cb756 100644
--- a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/DeprecationNotice/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/DeprecationNotice/index.tsx
@@ -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 (
-
+
{deprecationMessage && (
{getBadge()}
)}
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/__tests__/index.test.tsx
new file mode 100644
index 0000000000..9409c1a4b5
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/__tests__/index.test.tsx
@@ -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
) => (
+ {children}
+ ),
+ DetailsSummary: ({ title, subtitle }: { title: string, subtitle: React.ReactNode }) => (
+ {title}
+ ),
+ DropdownMenu: ({
+ dropdownButtonContent,
+ menuItems
+ }: { dropdownButtonContent: React.ReactNode, menuItems: MenuItem[] }) => (
+
+
{dropdownButtonContent}
+
+ {menuItems.map((item, index) => (
+
{
+ if ("action" in item) {
+ item.action()
+ }
+ }}>
+ {"title" in item && item.title}
+
+ ))}
+
+
+ ),
+ Link: ({ href, children }: { href: string, children: React.ReactNode }) => (
+ {children}
+ ),
+ MarkdownContent: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ parseEventPayload: (payload: string) => mockParseEventPayload(payload),
+ Tabs: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ TabsContent: ({ value, children }: { value: string, children: React.ReactNode }) => (
+ {children}
+ ),
+ TabsContentWrapper: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ TabsList: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ TabsTrigger: ({ value, children }: { value: string, children: React.ReactNode }) => (
+ {children}
+ ),
+ Tooltip: ({
+ text,
+ children,
+ ...props
+ }: { text: string, children: React.ReactNode } & React.HTMLAttributes) => (
+ {children}
+ ),
+ useCopy: (options: unknown) => mockUseCopied(options),
+ useGenerateSnippet: (options: unknown) => mockUseGenerateSnippet(options),
+}))
+vi.mock("@/components/Tags/Operation/Parameters", () => ({
+ default: () => Parameters
,
+}))
+vi.mock("@medusajs/icons", () => ({
+ CheckCircle: () => CheckCircle
,
+ SquareTwoStack: () => SquareTwoStack
,
+ Tag: () => Tag
,
+ Brackets: () => Brackets
,
+}))
+
+import TagsOperationDescriptionSectionEvents from ".."
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+describe("rendering", () => {
+ test("renders events", () => {
+ const { container } = render(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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)
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/index.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/index.tsx
index c1cced590c..1788227e5e 100644
--- a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/index.tsx
@@ -1,5 +1,6 @@
"use client"
+import React from "react"
import {
Badge,
DetailsSummary,
@@ -105,15 +106,27 @@ const TagsOperationDescriptionSectionEvent = ({
{event.deprecated &&
(event.deprecated_message ? (
-
- Deprecated
+
+
+ Deprecated
+
) : (
- Deprecated
+
+ Deprecated
+
))}
{event.since && (
-
- v{event.since}
+
+
+ v{event.since}
+
)}
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Parameters/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Parameters/__tests__/index.test.tsx
new file mode 100644
index 0000000000..1c4ffe1f37
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Parameters/__tests__/index.test.tsx
@@ -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) => (
+
+ {Object.values(props.schemaObject.properties).map((property) => (
+
+ {property.parameterName}
+
+ ))}
+
+ ),
+}))
+
+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(
+
+ )
+
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ const queryParameters = container.querySelector("[data-testid='query-parameters']")
+ expect(queryParameters).not.toBeInTheDocument()
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Parameters/index.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Parameters/index.tsx
index e7aae8126b..6e7daf4244 100644
--- a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Parameters/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Parameters/index.tsx
@@ -1,3 +1,4 @@
+import React from "react"
import type { OpenAPI } from "types"
import TagOperationParameters from "../../Parameters"
@@ -60,6 +61,7 @@ const TagsOperationDescriptionSectionParameters = ({
>
)}
@@ -71,6 +73,7 @@ const TagsOperationDescriptionSectionParameters = ({
>
)}
@@ -82,6 +85,7 @@ const TagsOperationDescriptionSectionParameters = ({
>
)}
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/RequestBody/index.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/RequestBody/index.tsx
index 10dacd2bb8..e5ba6765d6 100644
--- a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/RequestBody/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/RequestBody/index.tsx
@@ -1,3 +1,4 @@
+import React from "react"
import type { OpenAPI } from "types"
import TagOperationParameters from "../../Parameters"
import { DetailsSummary } from "docs-ui"
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Responses/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Responses/__tests__/index.test.tsx
new file mode 100644
index 0000000000..28a658206b
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Responses/__tests__/index.test.tsx
@@ -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) => (
+
+ {Object.entries(props.schemaObject.properties).map(([key, property]) => (
+
+ {key}
+
+ ))}
+
+ ),
+}))
+vi.mock("docs-ui", () => ({
+ Badge: ({ variant, children }: { variant: string, children: React.ReactNode }) => (
+
{children}
+ ),
+ DetailsSummary: ({
+ title,
+ badge,
+ ...props }: { title: string, [key: string]: any }) => (
+
+ {title}
+ {badge}
+
+ ),
+ Details: ({
+ children,
+ summaryElm,
+ ...props
+ }: { children: React.ReactNode, [key: string]: any }) => (
+
{summaryElm}{children}
+ ),
+}))
+
+import TagsOperationDescriptionSectionResponses from ".."
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+describe("rendering", () => {
+ test("renders success response with content", () => {
+ const { container } = render(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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")
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Responses/index.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Responses/index.tsx
index 61aa2d41c5..52814ce288 100644
--- a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Responses/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Responses/index.tsx
@@ -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 (
- {response.content && (
+ {!isEmptyResponse && (
<>
{successCode && (
<>
@@ -34,6 +38,7 @@ const TagsOperationDescriptionSectionResponses = ({
index !== 0 && "border-t-0",
index === 0 && "border-b-0"
)}
+ data-testid="response-success"
/>
>
)}
@@ -56,6 +62,7 @@ const TagsOperationDescriptionSectionResponses = ({
}
openInitial={index === 0}
className={clsx(index > 1 && "border-t-0")}
+ data-testid="response-error"
>
)}
>
)}
- {!response.content && (
+ {isEmptyResponse && (
1 &&
"border-b-0"
)}
+ data-testid="response-empty"
/>
)}
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Security/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Security/__tests__/index.test.tsx
new file mode 100644
index 0000000000..4cc942ab8a
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Security/__tests__/index.test.tsx
@@ -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 }) => (
+
+ {text}
+
+ ),
+}))
+
+import TagsOperationDescriptionSectionSecurity from ".."
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+describe("rendering", () => {
+ test("renders security with authentication", () => {
+ const { container } = render(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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")
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Security/index.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Security/index.tsx
index 14812636cf..6dcfdf0fa2 100644
--- a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Security/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Security/index.tsx
@@ -1,3 +1,4 @@
+import React from "react"
import { useBaseSpecs } from "@/providers/base-specs"
import type { OpenAPI } from "types"
import { Card } from "docs-ui"
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/WorkflowBadge/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/WorkflowBadge/__tests__/index.test.tsx
new file mode 100644
index 0000000000..5dbc71ef79
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/WorkflowBadge/__tests__/index.test.tsx
@@ -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 }) => (
+
+ {text} {icon}
+
+ ),
+ DecisionProcessIcon: () => (
+
+ Icon
+
+ ),
+}))
+
+import TagsOperationDescriptionSectionWorkflowBadge from ".."
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+describe("rendering", () => {
+ test("renders workflow badge", () => {
+ const { container } = render(
+
+ )
+
+ 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()
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/WorkflowBadge/index.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/WorkflowBadge/index.tsx
index 8e712aaec2..10efe850b4 100644
--- a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/WorkflowBadge/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/WorkflowBadge/index.tsx
@@ -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
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/__tests__/index.test.tsx
new file mode 100644
index 0000000000..f0be924026
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/__tests__/index.test.tsx
@@ -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[] }) => (
+
{JSON.stringify(security)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/DescriptionSection/RequestBody", () => ({
+ default: ({ requestBody }: { requestBody: OpenAPI.RequestObject }) => (
+
{JSON.stringify(requestBody)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/DescriptionSection/Responses", () => ({
+ default: ({ responses }: { responses: OpenAPI.ResponsesObject }) => (
+
{JSON.stringify(responses)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/DescriptionSection/Parameters", () => ({
+ default: ({ parameters }: { parameters: OpenAPI.Parameter[] }) => (
+
{JSON.stringify(parameters)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/DescriptionSection/WorkflowBadge", () => ({
+ default: ({ workflow }: { workflow: string }) => (
+
{workflow}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/DescriptionSection/Events", () => ({
+ default: ({ events }: { events: OpenAPI.OasEvents[] }) => (
+
{JSON.stringify(events)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/DescriptionSection/DeprecationNotice", () => ({
+ default: ({ deprecationMessage }: { deprecationMessage: string }) => (
+
{deprecationMessage}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/DescriptionSection/FeatureFlagNotice", () => ({
+ default: ({ featureFlag }: { featureFlag: string }) => (
+
{featureFlag}
+ ),
+}))
+vi.mock("@/components/MDXContent/Client", () => ({
+ default: ({ content }: { content: string }) => (
+
{content}
+ ),
+}))
+vi.mock("docs-ui", () => ({
+ Badge: ({
+ variant,
+ children,
+ ...props
+ }: { variant: string, children: React.ReactNode, [key: string]: unknown }) => (
+
{children}
+ ),
+ Link: ({
+ href,
+ children,
+ ...props
+ }: { href: string, children: React.ReactNode, [key: string]: unknown }) => (
+
{children}
+ ),
+ FeatureFlagNotice: ({ featureFlag }: { featureFlag: string }) => (
+
{featureFlag}
+ ),
+ H2: ({ children }: { children: React.ReactNode }) => (
+
{children}
+ ),
+ Tooltip: ({ children }: { children: React.ReactNode }) => (
+
{children}
+ ),
+ MarkdownContent: ({ children }: { children: React.ReactNode }) => (
+
{children}
+ ),
+}))
+vi.mock("@/components/Feedback", () => ({
+ Feedback: ({ question }: { question: string }) => (
+
{question}
+ ),
+}))
+
+import TagsOperationDescriptionSection from ".."
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+describe("rendering", () => {
+ test("renders operation summary, description, feedback, responses (default)", async () => {
+ const { container } = render(
)
+
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ 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(
)
+ const eventsElement = container.querySelector("[data-testid='events']")
+ expect(eventsElement).not.toBeInTheDocument()
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/index.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/index.tsx
index c18fb8ff78..0c87c36e3e 100644
--- a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/index.tsx
@@ -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 = ({
-
+
v{operation["x-since"]}
@@ -95,7 +96,11 @@ const TagsOperationDescriptionSection = ({
}
clickable={true}
>
-
+
{badge.text}
@@ -116,6 +121,7 @@ const TagsOperationDescriptionSection = ({
href={operation.externalDocs.url}
target="_blank"
variant="content"
+ data-testid="related-guide-link"
>
{operation.externalDocs.description || "Read More"}
@@ -133,16 +139,17 @@ const TagsOperationDescriptionSection = ({
security={operation.security}
/>
)}
- {operation.parameters && (
+ {operation.parameters && operation.parameters.length > 0 && (
)}
- {operation.requestBody && (
-
- )}
+ {operation.requestBody?.content !== undefined &&
+ Object.keys(operation.requestBody.content).length > 0 && (
+
+ )}
diff --git a/www/apps/api-reference/components/Tags/Operation/FeatureFlagNotice/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/FeatureFlagNotice/__tests__/index.test.tsx
new file mode 100644
index 0000000000..d3d9203c2b
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/FeatureFlagNotice/__tests__/index.test.tsx
@@ -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) => (
+ {children}
+ ),
+ Link: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ Tooltip: ({ tooltipChildren, children }: TooltipProps) => (
+
+
{tooltipChildren}
+
{children}
+
+ ),
+}))
+
+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()
+ 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()
+ 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()
+ 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()
+ const badgeElement = container.querySelector("[data-testid='badge']")
+ expect(badgeElement).toBeInTheDocument()
+ expect(badgeElement).toHaveClass("bg-red-500")
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/FeatureFlagNotice/index.tsx b/www/apps/api-reference/components/Tags/Operation/FeatureFlagNotice/index.tsx
index f3c41fa014..70a833e788 100644
--- a/www/apps/api-reference/components/Tags/Operation/FeatureFlagNotice/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/FeatureFlagNotice/index.tsx
@@ -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 (
- To use this {type}, make sure to
-
+
+ To use this {type}, make sure to
text.charAt(0).toUpperCase() + text.slice(1))
+
+// mock components
+vi.mock("docs-ui", () => ({
+ InlineCode: ({ children }: InlineCodeProps) => (
+ {children}
+ ),
+ Link: (props: React.HTMLAttributes) => (
+
+ ),
+ capitalize: (text: string) => mockCapitalize(text),
+}))
+vi.mock("@/components/MDXContent/Client", () => ({
+ default: ({ content }: { content: string }) => (
+ {content}
+ ),
+}))
+
+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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ const relatedGuideElement = container.querySelector("[data-testid='related-guide']")
+ expect(relatedGuideElement).not.toBeInTheDocument()
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Description/index.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Description/index.tsx
index 48ce3eaf48..3dbd633c38 100644
--- a/www/apps/api-reference/components/Tags/Operation/Parameters/Description/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Description/index.tsx
@@ -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(
async () => (await import("docs-ui")).InlineCode
) as React.FC
-type TagOperationParametersDescriptionProps = {
+export type TagOperationParametersDescriptionProps = {
schema: OpenAPI.SchemaObject
}
@@ -19,7 +20,7 @@ const TagOperationParametersDescription = ({
return (
{schema.default !== undefined && (
-
+
Default:{" "}
{JSON.stringify(schema.default)}
@@ -27,7 +28,7 @@ const TagOperationParametersDescription = ({
)}
{schema.enum && (
-
+
Enum:{" "}
{schema.enum.map((value, index) => (
@@ -38,7 +39,7 @@ const TagOperationParametersDescription = ({
)}
{schema.example !== undefined && (
-
+
Example:{" "}
{JSON.stringify(schema.example)}
@@ -57,7 +58,7 @@ const TagOperationParametersDescription = ({
>
)}
{schema.externalDocs && (
-
+
Related guide:{" "}
({
+ Badge: ({ variant, children, className }: BadgeProps) => (
+ {children}
+ ),
+ ExpandableNotice: ({ type, link }: ExpandableNoticeProps) => (
+
+ Expandable Notice
+
+ ),
+ FeatureFlagNotice: ({ featureFlag, type }: FeatureFlagNoticeProps) => (
+
+ Feature Flag Notice
+
+ ),
+}))
+
+import TagOperationParametersName from ".."
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+describe("rendering", () => {
+ test("renders name", () => {
+ const { container } = render(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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, with format", () => {
+ const defaultSchema: OpenAPI.SchemaObject = {
+ type: "string",
+ properties: {},
+ format: "date-time",
+ nullable: true,
+ }
+ const { container } = render(
+
+ )
+ const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
+ expect(typeDescriptionElement).toBeInTheDocument()
+ expect(typeDescriptionElement).toHaveTextContent("string or null")
+ })
+
+ test("formats description for default type schema without type and no nullable, no format", () => {
+ const defaultSchema: OpenAPI.SchemaObject = {
+ properties: {}
+ }
+ const { container } = render(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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, with format", () => {
+ const defaultSchema: OpenAPI.SchemaObject = {
+ properties: {},
+ format: "date-time",
+ nullable: true,
+ }
+ const { container } = render(
+
+ )
+ const typeDescriptionElement = container.querySelector("[data-testid='type-description']")
+ expect(typeDescriptionElement).toBeInTheDocument()
+ expect(typeDescriptionElement).toHaveTextContent("any or null")
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Name/index.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Name/index.tsx
index 4eba3f1d22..5ffaee335d 100644
--- a/www/apps/api-reference/components/Tags/Operation/Parameters/Name/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Name/index.tsx
@@ -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) => (
{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 (
- {name}
-
+
+ {name}
+
+
{typeDescription}
{schema.deprecated && (
@@ -84,7 +90,10 @@ const TagOperationParametersName = ({
)}
{!isRequired && (
-
+
optional
)}
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Nested/index.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Nested/index.tsx
index ba79ebf6f9..ce0336f016 100644
--- a/www/apps/api-reference/components/Tags/Operation/Parameters/Nested/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Nested/index.tsx
@@ -1,3 +1,4 @@
+import React from "react"
import clsx from "clsx"
export type TagsOperationParametersNestedProps =
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Section/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Section/__tests__/index.test.tsx
new file mode 100644
index 0000000000..383b10e886
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Section/__tests__/index.test.tsx
@@ -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) => (
+ {JSON.stringify(schemaObject)}
+ ),
+}))
+vi.mock("docs-ui", () => ({
+ Loading: () => Loading...
,
+}))
+
+import TagsOperationParametersSection from ".."
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+describe("rendering", () => {
+ test("renders parameters", async () => {
+ const { container } = render()
+ 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(
+
+ )
+ 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()
+ const headerElement = container.querySelector("[data-testid='header']")
+ expect(headerElement).not.toBeInTheDocument()
+ })
+
+ test("renders content type when content type is provided", () => {
+ const { container } = render()
+ 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()
+ const contentTypeElement = container.querySelector("[data-testid='content-type']")
+ expect(contentTypeElement).not.toBeInTheDocument()
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Section/index.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Section/index.tsx
index 4f648e62fd..fd7168ec43 100644
--- a/www/apps/api-reference/components/Tags/Operation/Parameters/Section/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Section/index.tsx
@@ -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 && (
{header}
)}
{contentType && (
-
+
Content type: {contentType}
)}
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Array/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Array/__tests__/index.test.tsx
new file mode 100644
index 0000000000..ea5ce5ff2f
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Array/__tests__/index.test.tsx
@@ -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) => (
+ {JSON.stringify(schemaObject)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/Parameters/Types/Default", () => ({
+ default: ({ schema }: TagOperationParametersDefaultProps) => (
+ {JSON.stringify(schema)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/Parameters/Nested", () => ({
+ default: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}))
+vi.mock("docs-ui", () => ({
+ Loading: () => Loading...
,
+ Details: ({ children, summaryElm }: { children: React.ReactNode, summaryElm: React.ReactNode }) => (
+
+
{summaryElm}
+
{children}
+
+ ),
+}))
+
+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(
+
+ )
+ 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()
+ 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(
+
+ )
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ const nestedElement = container.querySelector("[data-testid='nested']")
+ expect(nestedElement).toBeInTheDocument()
+ expect(nestedElement).toHaveTextContent(JSON.stringify(modifiedSchema.items))
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Array/index.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Array/index.tsx
index 82bf25e002..b9e4253114 100644
--- a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Array/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Array/index.tsx
@@ -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(
- async () => import("../Default"),
+ async () => import("@/components/Tags/Operation/Parameters/Types/Default"),
{
loading: () => ,
}
) as React.FC
const TagOperationParameters = dynamic(
- async () => import("../.."),
+ async () => import("@/components/Tags/Operation/Parameters"),
{
loading: () => ,
}
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Default/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Default/__tests__/index.test.tsx
new file mode 100644
index 0000000000..e1fc6fb1b8
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Default/__tests__/index.test.tsx
@@ -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) => (
+ {JSON.stringify(schema)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/Parameters/Name", () => ({
+ default: ({ name, isRequired, schema }: TagOperationParametersNameProps) => (
+ {name} {isRequired ? "required" : "optional"} {JSON.stringify(schema)}
+ ),
+}))
+
+import TagsOperationParametersDefault from ".."
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+describe("rendering", () => {
+ test("renders description", async () => {
+ const { container } = render()
+ 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()
+ 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()
+ 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()
+ const nameElement = container.querySelector("[data-testid='name']")
+ expect(nameElement).not.toBeInTheDocument()
+ })
+
+ test("add expandable class when expandable is true", () => {
+ const { container } = render()
+ 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()
+ 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()
+ const element = container.querySelector("[data-testid='default']")
+ expect(element).toHaveClass("test-class")
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Default/index.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Default/index.tsx
index e094cd6680..92bbe78ca4 100644
--- a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Default/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Default/index.tsx
@@ -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 && (
({
+ default: ({ schema }: TagOperationParametersDefaultProps) => (
+ {JSON.stringify(schema)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/Parameters/Nested", () => ({
+ default: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}))
+vi.mock("docs-ui", () => ({
+ Loading: () => Loading...
,
+ Details: ({ children, summaryElm }: { children: React.ReactNode, summaryElm: React.ReactNode }) => (
+
+
{summaryElm}
+
{children}
+
+ ),
+}))
+vi.mock("@/components/Tags/Operation/Parameters", () => ({
+ default: ({ schemaObject, isRequired, isExpanded }: TagOperationParametersProps) => (
+
+ {JSON.stringify(schemaObject)} {isRequired ? "required" : "optional"}
+
+ ),
+}))
+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()
+ expect(container).toBeEmptyDOMElement()
+ })
+
+ test("does not render when schema type is undefined", () => {
+ const modifiedSchema: OpenAPI.SchemaObject = {
+ type: undefined,
+ properties: {},
+ }
+ const { container } = render()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ await waitFor(() => {
+ const parametersElement = container.querySelector("[data-testid='parameters']")
+ expect(parametersElement).toBeInTheDocument()
+ expect(parametersElement).toHaveTextContent("optional")
+ })
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Object/index.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Object/index.tsx
index 8391f194bf..957c51d676 100644
--- a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Object/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Object/index.tsx
@@ -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(
- async () => import("../.."),
+ async () => import("@/components/Tags/Operation/Parameters"),
{
loading: () => ,
}
@@ -18,7 +19,7 @@ const TagOperationParameters = dynamic(
const TagsOperationParametersNested =
dynamic(
- async () => import("../../Nested"),
+ async () => import("@/components/Tags/Operation/Parameters/Nested"),
{
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
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/OneOf/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/OneOf/__tests__/index.test.tsx
new file mode 100644
index 0000000000..1910ef7359
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/OneOf/__tests__/index.test.tsx
@@ -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) => (
+ {JSON.stringify(schema)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/Parameters/Nested", () => ({
+ default: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/Parameters", () => ({
+ default: ({ schemaObject }: TagOperationParametersProps) => (
+ {JSON.stringify(schemaObject)}
+ ),
+}))
+vi.mock("docs-ui", () => ({
+ Details: ({ children, summaryElm }: { children: React.ReactNode, summaryElm: React.ReactNode }) => (
+
+ {summaryElm}
+ {children}
+
+ ),
+ Loading: () => Loading...
,
+}))
+
+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()
+ expect(container).toBeEmptyDOMElement()
+ })
+
+ test("renders one of type schema not nested by default", async () => {
+ const { container } = render()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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]))
+ })
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/OneOf/index.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/OneOf/index.tsx
index 3b201eade7..bc22271b77 100644
--- a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/OneOf/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/OneOf/index.tsx
@@ -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(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)}
@@ -91,14 +85,10 @@ const TagOperationParamatersOneOf = ({
- {schema.oneOf && (
- <>
-
- >
- )}
+
>
)
}
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Union/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Union/__tests__/index.test.tsx
new file mode 100644
index 0000000000..21739e7b0c
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Union/__tests__/index.test.tsx
@@ -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) => (
+ {JSON.stringify(schema)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/Parameters/Types/Default", () => ({
+ default: ({ schema }: TagOperationParametersDefaultProps) => (
+ {JSON.stringify(schema)}
+ ),
+}))
+
+vi.mock("docs-ui", () => ({
+ Loading: () => Loading...
,
+}))
+
+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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ await waitFor(() => {
+ const objectElement = container.querySelector("[data-testid='object']")
+ expect(objectElement).toBeInTheDocument()
+ expect(objectElement).toHaveAttribute("data-description", "test-description")
+ })
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Union/index.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Union/index.tsx
index 0521e27d88..e04377645b 100644
--- a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Union/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Union/index.tsx
@@ -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(
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
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/__tests__/index.test.tsx
new file mode 100644
index 0000000000..1596aa7000
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/__tests__/index.test.tsx
@@ -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) => (
+ {JSON.stringify(schema)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/Parameters/Types/OneOf", () => ({
+ default: ({ schema, isRequired }: TagOperationParamatersOneOfProps) => (
+ {JSON.stringify(schema)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/Parameters/Types/Array", () => ({
+ default: ({ schema, isRequired, name }: TagOperationParametersArrayProps) => (
+ {JSON.stringify(schema)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/Parameters/Types/Object", () => ({
+ default: ({ schema, isRequired, name }: TagOperationParametersObjectProps) => (
+ {JSON.stringify(schema)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/Parameters/Types/Default", () => ({
+ default: ({ schema, isRequired, name }: TagOperationParametersDefaultProps) => (
+ {JSON.stringify(schema)}
+ ),
+}))
+vi.mock("@/utils/check-required", () => ({
+ default: (schema: OpenAPI.SchemaObject) => mockCheckRequired(schema),
+}))
+vi.mock("docs-utils", () => ({
+ Loading: () => Loading...
,
+}))
+
+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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ await waitFor(() => {
+ const defaultElement = container.querySelector("[data-testid='default']")
+ expect(defaultElement).toBeInTheDocument()
+ expect(defaultElement).toHaveAttribute("data-name", "test-name")
+ })
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/index.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/index.tsx
index 33dac38847..64b7dc1cfe 100644
--- a/www/apps/api-reference/components/Tags/Operation/Parameters/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/Parameters/index.tsx
@@ -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 {getElement()}
+ return (
+
+ {getElement()}
+
+ )
}
export default TagOperationParameters
diff --git a/www/apps/api-reference/components/Tags/Operation/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Operation/__tests__/index.test.tsx
new file mode 100644
index 0000000000..0ca075fd7b
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Operation/__tests__/index.test.tsx
@@ -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 (
+
+ {children}
+
+
+ )
+ },
+}))
+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 }) => (
+ {JSON.stringify(operation)}
+ ),
+}))
+vi.mock("@/components/Tags/Operation/DescriptionSection", () => ({
+ default: ({ operation}: { operation: OpenAPI.Operation }) => (
+ {JSON.stringify(operation)}
+ ),
+}))
+vi.mock("@/layouts/Divided", () => ({
+ default: ({ mainContent, codeContent }: { mainContent: React.ReactNode, codeContent: React.ReactNode }) => (
+
+
{mainContent}
+
{codeContent}
+
+ ),
+}))
+vi.mock("@/providers/loading", () => ({
+ useLoading: () => mockUseLoading(),
+}))
+vi.mock("@/utils/check-element-in-viewport", () => ({
+ default: () => mockCheckElementInViewport(),
+}))
+vi.mock("@/components/DividedLoading", () => ({
+ default: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}))
+vi.mock("@/components/Section/Container", () => ({
+ default: React.forwardRef(
+ ({ children, className }, ref) => (
+
+ {children}
+
+ )
+ ),
+}))
+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(
+
+ )
+ const inViewElement = getByTestId("in-view")
+ expect(inViewElement).toBeInTheDocument()
+ })
+
+ test("renders SectionContainer", () => {
+ const { getByTestId } = render(
+
+ )
+ const sectionContainerElement = getByTestId("section-container")
+ expect(sectionContainerElement).toBeInTheDocument()
+ })
+
+ test("renders with className", () => {
+ const { getByTestId } = render(
+
+ )
+ const sectionContainerElement = getByTestId("section-container")
+ expect(sectionContainerElement).toHaveClass("test-class")
+ })
+
+ test("renders loading component when show is false", () => {
+ const { getByTestId } = render(
+
+ )
+ const loadingElement = getByTestId("divided-loading")
+ expect(loadingElement).toBeInTheDocument()
+ })
+
+ test("renders loading component initially", () => {
+ const { getByTestId } = render(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ expect(mockGetSectionId).toHaveBeenCalledWith(["test-operation"])
+ })
+})
+
+describe("hash matching and scrolling", () => {
+ test("removes loading when nodeRef is set", async () => {
+ render(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+ 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(
+
+ )
+
+ 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", "")
+ })
+ })
+})
+
diff --git a/www/apps/api-reference/components/Tags/Operation/index.tsx b/www/apps/api-reference/components/Tags/Operation/index.tsx
index 90bd9edabf..879f8a521d 100644
--- a/www/apps/api-reference/components/Tags/Operation/index.tsx
+++ b/www/apps/api-reference/components/Tags/Operation/index.tsx
@@ -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"
>
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("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) => Loading...
,
+}))
+vi.mock("@/utils/sort-operations-utils", () => ({
+ compareOperations: (options: unknown) => mockCompareOperations(options),
+}))
+vi.mock("@/components/Tags/Operation", () => ({
+ default: (props: TagOperationProps) => (
+ Operation
+ ),
+}))
+
+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(
+
+ )
+ 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(
+
+ )
+ const dividedLoadingElement = container.querySelector("[data-testid='divided-loading']")
+ expect(dividedLoadingElement).not.toBeInTheDocument()
+ })
+
+ test("renders operations", () => {
+ const { container } = render(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ expect(mockAddItems).not.toHaveBeenCalled()
+ })
+
+ test("doesn't add items to sidebar when paths is not set", () => {
+ mockUseSidebar.mockReturnValue({
+ shownSidebar: mockSidebar,
+ addItems: mockAddItems,
+ })
+ render(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ expect(mockAddItems).not.toHaveBeenCalled()
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Paths/index.tsx b/www/apps/api-reference/components/Tags/Paths/index.tsx
index 45b0df6001..c105fa89fd 100644
--- a/www/apps/api-reference/components/Tags/Paths/index.tsx
+++ b/www/apps/api-reference/components/Tags/Paths/index.tsx
@@ -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(
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])
diff --git a/www/apps/api-reference/components/Tags/Section/RoutesSummary/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Section/RoutesSummary/__tests__/index.test.tsx
new file mode 100644
index 0000000000..df07df754f
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Section/RoutesSummary/__tests__/index.test.tsx
@@ -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(
+
+ )
+ expect(container).toBeEmptyDOMElement()
+ })
+
+ test("renders operation", () => {
+ const { container } = render(
+
+ )
+ 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(
+
+ )
+ 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")
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Section/RoutesSummary/index.tsx b/www/apps/api-reference/components/Tags/Section/RoutesSummary/index.tsx
index dee3b0dfba..b61403b937 100644
--- a/www/apps/api-reference/components/Tags/Section/RoutesSummary/index.tsx
+++ b/www/apps/api-reference/components/Tags/Section/RoutesSummary/index.tsx
@@ -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) => {
{endpointPath}
diff --git a/www/apps/api-reference/components/Tags/Section/Schema/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Section/Schema/__tests__/index.test.tsx
new file mode 100644
index 0000000000..b024651f58
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Section/Schema/__tests__/index.test.tsx
@@ -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 (
+
+ {children}
+
+
+ )
+ },
+}))
+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
+ }) => (
+
+ {source}
+
+ ),
+ Note: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ Link: ({
+ href,
+ children,
+ variant,
+ }: {
+ href: string
+ children: React.ReactNode
+ variant?: string
+ }) => (
+
+ {children}
+
+ ),
+}))
+vi.mock("@/components/Tags/Operation/Parameters", () => ({
+ default: ({
+ schemaObject,
+ topLevel,
+ }: {
+ schemaObject: OpenAPI.SchemaObject
+ topLevel?: boolean
+ }) => (
+
+ {JSON.stringify(schemaObject)}
+
+ ),
+}))
+vi.mock("@/layouts/Divided", () => ({
+ default: ({
+ mainContent,
+ codeContent,
+ }: {
+ mainContent: React.ReactNode
+ codeContent: React.ReactNode
+ }) => (
+
+
{mainContent}
+
{codeContent}
+
+ ),
+}))
+vi.mock("@/components/Section/Container", () => ({
+ default: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}))
+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(
+
+ )
+ const inViewElement = getByTestId("in-view")
+ expect(inViewElement).toBeInTheDocument()
+ expect(inViewElement).toHaveAttribute("id", "mock-schema-slug")
+ })
+
+ test("renders SectionContainer", () => {
+ const { getByTestId } = render(
+
+ )
+ const sectionContainerElement = getByTestId("section-container")
+ expect(sectionContainerElement).toBeInTheDocument()
+ })
+
+ test("renders DividedLayout with mainContent and codeContent", () => {
+ const { getByTestId } = render(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ const noteElement = getByTestId("note")
+ expect(noteElement).toBeInTheDocument()
+ expect(noteElement).toHaveTextContent("Admin")
+ })
+
+ test("renders Link to Commerce Modules Documentation", () => {
+ const { getByTestId } = render(
+
+ )
+ 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(
+
+ )
+ const fieldsHeading = container.querySelector("h4")
+ expect(fieldsHeading).toBeInTheDocument()
+ expect(fieldsHeading).toHaveTextContent("Fields")
+ })
+
+ test("renders TagOperationParameters with schema and topLevel prop", () => {
+ const { getByTestId } = render(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ const codeBlockElement = queryByTestId("code-block")
+ expect(codeBlockElement).not.toBeInTheDocument()
+ })
+
+ test("renders CodeBlock with formatted name as title", async () => {
+ mockSingular.mockReturnValue("mockTagNam")
+ const { getByTestId } = render(
+
+ )
+ 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()
+ expect(mockSingular).toHaveBeenCalledWith("products")
+ })
+
+ test("removes spaces from formatted name", () => {
+ mockSingular.mockReturnValue("test tag")
+ const { container } = render(
+
+ )
+ 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()
+ 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()
+
+ 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()
+
+ 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()
+
+ 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()
+
+ 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()
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ 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(
+
+ )
+
+ const inViewElement = getByTestId("in-view")
+ expect(inViewElement).toBeInTheDocument()
+ expect(inViewElement).toHaveAttribute("data-root", "DIV")
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/Section/Schema/index.tsx b/www/apps/api-reference/components/Tags/Section/Schema/index.tsx
index 38571de0bb..f5b028af8f 100644
--- a/www/apps/api-reference/components/Tags/Section/Schema/index.tsx
+++ b/www/apps/api-reference/components/Tags/Section/Schema/index.tsx
@@ -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(" ", ""),
diff --git a/www/apps/api-reference/components/Tags/Section/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/Section/__tests__/index.test.tsx
new file mode 100644
index 0000000000..0f8556812b
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/Section/__tests__/index.test.tsx
@@ -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 (
+
+ {children}
+
+
+ )
+ },
+}))
+vi.mock("docs-ui", () => ({
+ isElmWindow: () => mockIsElmWindow(),
+ useIsBrowser: () => mockUseIsBrowser(),
+ useScrollController: () => mockUseScrollController(),
+ useSidebar: () => mockUseSidebar(),
+ H2: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ Link: ({
+ href,
+ children,
+ target,
+ variant,
+ }: {
+ href: string
+ children: React.ReactNode
+ target?: string
+ variant?: string
+ }) => (
+
+ {children}
+
+ ),
+ Loading: () => Loading...
,
+ swrFetcher: () => mockSwrFetcher(),
+}))
+vi.mock("@/providers/area", () => ({
+ useArea: () => mockUseArea(),
+}))
+vi.mock("@/components/Section/Container", () => ({
+ default: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}))
+vi.mock("@/layouts/Divided", () => ({
+ default: ({
+ mainContent,
+ codeContent,
+ }: {
+ mainContent: React.ReactNode
+ codeContent: React.ReactNode
+ }) => (
+
+
{mainContent}
+
{codeContent}
+
+ ),
+}))
+vi.mock("@/components/Tags/Section/Schema", () => ({
+ default: ({
+ schema,
+ tagName,
+ }: {
+ schema: OpenAPI.SchemaObject
+ tagName: string
+ }) => (
+
+ {JSON.stringify(schema)}
+
+ ),
+}))
+vi.mock("@/components/Tags/Paths", () => ({
+ default: ({
+ tag,
+ paths,
+ }: {
+ tag: OpenAPI.TagObject
+ paths: OpenAPI.PathsObject
+ }) => (
+
+ {JSON.stringify(paths)}
+
+ ),
+}))
+vi.mock("@/components/Tags/Section/RoutesSummary", () => ({
+ RoutesSummary: ({
+ tagName,
+ paths,
+ }: {
+ tagName: string
+ paths: OpenAPI.PathsObject
+ }) => (
+
+ {JSON.stringify(paths)}
+
+ ),
+}))
+vi.mock("@/components/Section/Divider", () => ({
+ default: ({ className }: { className?: string }) => (
+
+ ),
+}))
+vi.mock("@/components/Feedback", () => ({
+ Feedback: ({
+ question,
+ extraData,
+ }: {
+ question: string
+ extraData: { section: string }
+ }) => (
+
+ {question}
+
+ ),
+}))
+vi.mock("@/providers/loading", () => ({
+ default: ({
+ children,
+ initialLoading,
+ }: {
+ children: React.ReactNode
+ initialLoading?: boolean
+ }) => (
+
+ {children}
+
+ ),
+}))
+vi.mock("@/components/MDXContent/Client", () => ({
+ default: ({
+ content,
+ scope,
+ }: {
+ content: string
+ scope: { addToSidebar: boolean }
+ }) => (
+
+ {content}
+
+ ),
+}))
+vi.mock("@/components/Section/Divider", () => ({
+ default: ({ className }: { className?: string }) => (
+
+ ),
+}))
+vi.mock("@/components/Section", () => ({
+ default: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}))
+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()
+ 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()
+ const sectionContainerElement = getByTestId("section-container")
+ expect(sectionContainerElement).toBeInTheDocument()
+ })
+
+ test("renders DividedLayout with mainContent and codeContent", () => {
+ const { getByTestId } = render()
+ 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()
+ const h2Element = getByTestId("h2")
+ expect(h2Element).toBeInTheDocument()
+ expect(h2Element).toHaveTextContent(mockTag.name)
+ })
+
+ test("renders MDXContentClient when tag has description", async () => {
+ const { getByTestId } = render()
+ 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(
+
+ )
+ const mdxContentElement = queryByTestId("mdx-content-client")
+ expect(mdxContentElement).not.toBeInTheDocument()
+ })
+
+ test("renders external docs link when tag has externalDocs", () => {
+ const { getByTestId } = render(
+
+ )
+ 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(
+
+ )
+ const linkElement = getByTestId("link")
+ expect(linkElement).toHaveTextContent("Read More")
+ })
+
+ test("does not render external docs link when tag has no externalDocs", () => {
+ const { queryByTestId } = render()
+ const linkElement = queryByTestId("link")
+ expect(linkElement).not.toBeInTheDocument()
+ })
+
+ test("renders Feedback component", () => {
+ const { getByTestId } = render()
+ 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()
+ 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()
+ 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(
+
+ )
+ 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()
+ expect(mockGetSectionId).toHaveBeenCalledWith([mockTag.name])
+ })
+})
+
+describe("useSWR hooks", () => {
+ test("does not fetch schema data when loadData is false", () => {
+ render()
+ 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(
+
+ )
+ 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()
+ 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()
+ 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(
+
+ )
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+
+ 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()
+
+ 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()
+ // 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()
+
+ expect(mockScrollToTop).not.toHaveBeenCalled()
+ })
+
+ test("does not scroll when activePath is null", () => {
+ mockUseSidebar.mockReturnValue({
+ activePath: null,
+ setActivePath: mockSetActivePath,
+ })
+
+ render()
+
+ 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()
+
+ expect(mockScrollToTop).not.toHaveBeenCalled()
+ })
+})
+
+describe("InView onChange behavior", () => {
+ test("sets loadData to true when in view", () => {
+ const { getByTestId, queryByTestId } = render(
+
+ )
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ const inViewElement = getByTestId("in-view")
+ expect(inViewElement).toBeInTheDocument()
+ expect(inViewElement).toHaveAttribute("data-root", "DIV")
+ })
+})
+
diff --git a/www/apps/api-reference/components/Tags/Section/index.tsx b/www/apps/api-reference/components/Tags/Section/index.tsx
index 657acc63c8..1c7c3ae279 100644
--- a/www/apps/api-reference/components/Tags/Section/index.tsx
+++ b/www/apps/api-reference/components/Tags/Section/index.tsx
@@ -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
const Section = dynamic(
- async () => import("../../Section")
+ async () => import("@/components/Section")
) as React.FC
const MDXContentClient = dynamic(
- async () => import("../../MDXContent/Client"),
+ async () => import("@/components/MDXContent/Client"),
{
loading: () => ,
}
diff --git a/www/apps/api-reference/components/Tags/__tests__/index.test.tsx b/www/apps/api-reference/components/Tags/__tests__/index.test.tsx
new file mode 100644
index 0000000000..83b1f36866
--- /dev/null
+++ b/www/apps/api-reference/components/Tags/__tests__/index.test.tsx
@@ -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 }) => (
+ {tag.name}
+ ),
+}))
+vi.mock("react", async () => {
+ const actual = await vi.importActual("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()
+ expect(container).toBeEmptyDOMElement()
+ })
+
+ test("renders tags when tags is defined", async () => {
+ const { container } = render()
+ await waitFor(() => {
+ const tagSections = container.querySelectorAll("[data-testid='tag-section']")
+ expect(tagSections).toHaveLength(mockTags.length)
+ })
+ })
+})
\ No newline at end of file
diff --git a/www/apps/api-reference/components/Tags/index.tsx b/www/apps/api-reference/components/Tags/index.tsx
index a1a2002839..84698eab1d 100644
--- a/www/apps/api-reference/components/Tags/index.tsx
+++ b/www/apps/api-reference/components/Tags/index.tsx
@@ -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(
async () => import("./Section")
diff --git a/www/apps/api-reference/eslint.config.mjs b/www/apps/api-reference/eslint.config.mjs
index 722b100ce3..ad8117f072 100644
--- a/www/apps/api-reference/eslint.config.mjs
+++ b/www/apps/api-reference/eslint.config.mjs
@@ -29,6 +29,8 @@ export default [
"**/public",
"**/.eslintrc.js",
"**/generated",
+ "**/__tests__",
+ "**/__mocks__",
],
},
...compat.extends(
diff --git a/www/apps/api-reference/package.json b/www/apps/api-reference/package.json
index 645f366acf..021d41deeb 100644
--- a/www/apps/api-reference/package.json
+++ b/www/apps/api-reference/package.json
@@ -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"
diff --git a/www/apps/api-reference/providers/__tests__/area.test.tsx b/www/apps/api-reference/providers/__tests__/area.test.tsx
new file mode 100644
index 0000000000..aa3e6e0a5b
--- /dev/null
+++ b/www/apps/api-reference/providers/__tests__/area.test.tsx
@@ -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 (
+
+
{area}
+
{prevArea || "undefined"}
+
{displayedArea}
+
+
+
+ )
+}
+
+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(
+
+ Test Content
+
+ )
+ expect(getByText("Test Content")).toBeInTheDocument()
+ })
+ })
+
+ describe("initial state", () => {
+ test("initializes with passed area", () => {
+ const { getByTestId } = render(
+
+
+
+ )
+ expect(getByTestId("area")).toHaveTextContent("store")
+ })
+
+ test("displays capitalized area", () => {
+ const { getByTestId } = render(
+
+
+
+ )
+ expect(getByTestId("displayed-area")).toHaveTextContent("Store")
+ expect(mockCapitalize).toHaveBeenCalledWith("store")
+ })
+
+ test("prevArea is undefined initially", () => {
+ const { getByTestId } = render(
+
+
+
+ )
+ expect(getByTestId("prev-area")).toHaveTextContent("undefined")
+ })
+ })
+
+ describe("setArea", () => {
+ test("updates area when setArea is called", () => {
+ const { getByTestId } = render(
+
+
+
+ )
+ 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(
+
+
+
+ )
+ 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(
+
+
+
+ )
+
+ mockUsePathname.mockReturnValue("/admin/test")
+ rerender(
+
+
+
+ )
+
+ expect(mockSetActivePath).toHaveBeenCalledWith(null)
+ })
+ })
+
+ describe("useArea hook", () => {
+ test("throws error when used outside provider", () => {
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ expect(() => {
+ render()
+ }).toThrow("useAreaProvider must be used inside an AreaProvider")
+
+ consoleSpy.mockRestore()
+ })
+
+ test("returns area, prevArea, displayedArea, and setArea", () => {
+ const { getByTestId } = render(
+
+
+
+ )
+ 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()
+ })
+ })
+})
+
diff --git a/www/apps/api-reference/providers/__tests__/base-specs.test.tsx b/www/apps/api-reference/providers/__tests__/base-specs.test.tsx
new file mode 100644
index 0000000000..db0a7eb84f
--- /dev/null
+++ b/www/apps/api-reference/providers/__tests__/base-specs.test.tsx
@@ -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 (
+
+
{baseSpecs ? "present" : "null"}
+
+ {getSecuritySchema("test-security") ? "found" : "null"}
+
+
+ )
+}
+
+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(
+
+ Test Content
+
+ )
+ expect(getByText("Test Content")).toBeInTheDocument()
+ })
+ })
+
+ describe("useBaseSpecs hook", () => {
+ test("throws error when used outside provider", () => {
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ expect(() => {
+ render()
+ }).toThrow("useBaseSpecs must be used inside a BaseSpecsProvider")
+
+ consoleSpy.mockRestore()
+ })
+
+ test("returns baseSpecs and getSecuritySchema", () => {
+ const { getByTestId } = render(
+
+
+
+ )
+ expect(getByTestId("base-specs")).toHaveTextContent("present")
+ expect(getByTestId("security-schema")).toBeInTheDocument()
+ })
+ })
+
+ describe("getSecuritySchema", () => {
+ test("returns security schema when it exists", () => {
+ const { getByTestId } = render(
+
+
+
+ )
+ const securitySchema = getByTestId("security-schema")
+ expect(securitySchema).toHaveTextContent("found")
+ })
+
+ test("returns null when security schema does not exist", () => {
+ const { getByTestId } = render(
+
+
+
+ )
+ const securitySchema = getByTestId("security-schema")
+ // Change to a non-existent security name
+ const TestComponent2 = () => {
+ const { getSecuritySchema } = useBaseSpecs()
+ return (
+
+ {getSecuritySchema("non-existent") ? "found" : "null"}
+
+ )
+ }
+ const { getByTestId: getByTestId2 } = render(
+
+
+
+ )
+ 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 (
+
+ {getSecuritySchema("test-security") ? "found" : "null"}
+
+ )
+ }
+
+ const { getByTestId } = render(
+
+
+
+ )
+ 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(
+
+ Test
+
+ )
+
+ 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(
+
+ Test
+
+ )
+
+ 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(
+
+ Test
+
+ )
+
+ 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(
+
+ Test
+
+ )
+
+ 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(
+
+ Test
+
+ )
+
+ 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(
+
+ Test
+
+ )
+
+ unmount()
+ expect(mockResetItems).toHaveBeenCalled()
+ })
+ })
+})
+
diff --git a/www/apps/api-reference/providers/__tests__/loading.test.tsx b/www/apps/api-reference/providers/__tests__/loading.test.tsx
new file mode 100644
index 0000000000..bbb4e302d3
--- /dev/null
+++ b/www/apps/api-reference/providers/__tests__/loading.test.tsx
@@ -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 (
+
+
{loading.toString()}
+
+
+ )
+}
+
+describe("LoadingProvider", () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+ })
+
+ describe("rendering", () => {
+ test("renders children", () => {
+ const { getByText } = render(
+
+ Test Content
+
+ )
+ expect(getByText("Test Content")).toBeInTheDocument()
+ })
+ })
+
+ describe("initial loading state", () => {
+ test("initializes with loading false by default", () => {
+ const { getByTestId } = render(
+
+
+
+ )
+ expect(getByTestId("loading-state")).toHaveTextContent("false")
+ })
+
+ test("initializes with loading true when initialLoading is true", () => {
+ const { getByTestId } = render(
+
+
+
+ )
+ expect(getByTestId("loading-state")).toHaveTextContent("true")
+ })
+ })
+
+ describe("removeLoading", () => {
+ test("sets loading to false when removeLoading is called", () => {
+ const { getByTestId } = render(
+
+
+
+ )
+ 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()
+ }).toThrow("useLoading must be used inside a LoadingProvider")
+
+ consoleSpy.mockRestore()
+ })
+
+ test("returns loading state and removeLoading function", () => {
+ const { getByTestId } = render(
+
+
+
+ )
+ expect(getByTestId("loading-state")).toBeInTheDocument()
+ expect(getByTestId("remove-loading")).toBeInTheDocument()
+ })
+ })
+})
+
diff --git a/www/apps/api-reference/providers/__tests__/main-nav.test.tsx b/www/apps/api-reference/providers/__tests__/main-nav.test.tsx
new file mode 100644
index 0000000000..71ef4da7f9
--- /dev/null
+++ b/www/apps/api-reference/providers/__tests__/main-nav.test.tsx
@@ -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[] }) => (
+
+ {children}
+
+ ),
+}))
+
+vi.mock("@/config", () => ({
+ config: {
+ baseUrl: "https://test.com",
+ },
+}))
+
+describe("MainNavProvider", () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+ })
+
+ describe("rendering", () => {
+ test("renders children", () => {
+ const { getByText } = render(
+
+ Test Content
+
+ )
+ expect(getByText("Test Content")).toBeInTheDocument()
+ })
+
+ test("renders UiMainNavProvider with navItems", () => {
+ const { getByTestId } = render(
+
+ Test
+
+ )
+ 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(
+
+ Test
+
+ )
+
+ const callCount = mockGetNavDropdownItems.mock.calls.length
+
+ rerender(
+
+ Test
+
+ )
+
+ // Should not call getNavDropdownItems again due to memoization
+ expect(mockGetNavDropdownItems.mock.calls.length).toBe(callCount)
+ })
+ })
+})
+
diff --git a/www/apps/api-reference/providers/__tests__/page-title.test.tsx b/www/apps/api-reference/providers/__tests__/page-title.test.tsx
new file mode 100644
index 0000000000..24afe513c7
--- /dev/null
+++ b/www/apps/api-reference/providers/__tests__/page-title.test.tsx
@@ -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(
+
+ Test Content
+
+ )
+ 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(
+
+ Test
+
+ )
+ 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(
+
+ Test
+
+ )
+ 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(
+
+ Test
+
+ )
+
+ 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(
+
+ Test
+
+ )
+
+ 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(
+
+ Test
+
+ )
+
+ expect(document.title).toBe("Medusa Store API Reference")
+ })
+
+ test("updates title when displayedArea changes", () => {
+ mockUseArea.mockReturnValue({
+ displayedArea: "Admin",
+ })
+ render(
+
+ Test
+
+ )
+ expect(document.title).toBe("Test Item - Medusa Admin API Reference")
+ })
+
+ test("updates title when activePath changes", () => {
+ const { rerender } = render(
+
+ Test
+
+ )
+
+ 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(
+
+ Test
+
+ )
+
+ expect(document.title).toBe("New Item - Medusa Store API Reference")
+ })
+ })
+})
+
diff --git a/www/apps/api-reference/providers/__tests__/search.test.tsx b/www/apps/api-reference/providers/__tests__/search.test.tsx
new file mode 100644
index 0000000000..7b287132f5
--- /dev/null
+++ b/www/apps/api-reference/providers/__tests__/search.test.tsx
@@ -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
+ }) => (
+
+ {children}
+
+ ),
+}))
+
+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(
+
+ Test Content
+
+ )
+ expect(getByText("Test Content")).toBeInTheDocument()
+ })
+
+ test("renders UiSearchProvider with correct props", () => {
+ const { getByTestId } = render(
+
+ Test
+
+ )
+ 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(
+
+ Test
+
+ )
+ 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(
+
+ Test
+
+ )
+ 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(
+
+ Test
+
+ )
+ 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(
+
+ Test
+
+ )
+ const uiProvider = getByTestId("ui-search-provider")
+ const searchProps = JSON.parse(uiProvider.getAttribute("data-search-props") || "{}")
+ expect(searchProps.checkInternalPattern).toBeDefined()
+ })
+ })
+})
+
diff --git a/www/apps/api-reference/providers/__tests__/sidebar.test.tsx b/www/apps/api-reference/providers/__tests__/sidebar.test.tsx
new file mode 100644
index 0000000000..5a02630d0c
--- /dev/null
+++ b/www/apps/api-reference/providers/__tests__/sidebar.test.tsx
@@ -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
+ }) => (
+
+ {children}
+
+ ),
+ 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(
+
+ Test Content
+
+ )
+ expect(getByText("Test Content")).toBeInTheDocument()
+ })
+
+ test("renders UiSidebarProvider with correct props", async () => {
+ const { getByTestId } = render(
+
+ Test
+
+ )
+
+ 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(
+
+ Test
+
+ )
+
+ 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(
+
+ Test
+
+ )
+
+ 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(
+
+ Test
+
+ )
+
+ // 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(
+
+ Test
+
+ )
+
+ 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(
+
+ Test
+
+ )
+
+ 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(
+
+ Test
+
+ )
+
+ await waitFor(() => {
+ const uiProvider = getByTestId("ui-sidebar-provider")
+ expect(uiProvider).toBeInTheDocument()
+ })
+
+ consoleSpy.mockRestore()
+ })
+ })
+})
+
diff --git a/www/apps/api-reference/providers/area.tsx b/www/apps/api-reference/providers/area.tsx
index 3ad9b5f624..c63415bc8f 100644
--- a/www/apps/api-reference/providers/area.tsx
+++ b/www/apps/api-reference/providers/area.tsx
@@ -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"
diff --git a/www/apps/api-reference/providers/base-specs.tsx b/www/apps/api-reference/providers/base-specs.tsx
index 65ba96e887..189f0b6b80 100644
--- a/www/apps/api-reference/providers/base-specs.tsx
+++ b/www/apps/api-reference/providers/base-specs.tsx
@@ -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,
})
diff --git a/www/apps/api-reference/providers/loading.tsx b/www/apps/api-reference/providers/loading.tsx
index 15ff51313c..1882614216 100644
--- a/www/apps/api-reference/providers/loading.tsx
+++ b/www/apps/api-reference/providers/loading.tsx
@@ -1,3 +1,4 @@
+import React from "react"
import { createContext, useContext, useState } from "react"
type LoadingContextType = {
diff --git a/www/apps/api-reference/providers/main-nav.tsx b/www/apps/api-reference/providers/main-nav.tsx
index cdb5a18040..43bd1619fb 100644
--- a/www/apps/api-reference/providers/main-nav.tsx
+++ b/www/apps/api-reference/providers/main-nav.tsx
@@ -1,5 +1,6 @@
"use client"
+import React from "react"
import {
getNavDropdownItems,
MainNavProvider as UiMainNavProvider,
diff --git a/www/apps/api-reference/providers/page-title.tsx b/www/apps/api-reference/providers/page-title.tsx
index 5b40e0200a..9b0ef69b3f 100644
--- a/www/apps/api-reference/providers/page-title.tsx
+++ b/www/apps/api-reference/providers/page-title.tsx
@@ -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
}
}
}
diff --git a/www/apps/api-reference/providers/search.tsx b/www/apps/api-reference/providers/search.tsx
index a642be45a5..985ae7347d 100644
--- a/www/apps/api-reference/providers/search.tsx
+++ b/www/apps/api-reference/providers/search.tsx
@@ -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"
diff --git a/www/apps/api-reference/providers/sidebar.tsx b/www/apps/api-reference/providers/sidebar.tsx
index 42319d26fb..de6775a369 100644
--- a/www/apps/api-reference/providers/sidebar.tsx
+++ b/www/apps/api-reference/providers/sidebar.tsx
@@ -1,5 +1,6 @@
"use client"
+import React from "react"
import {
SidebarProvider as UiSidebarProvider,
usePageLoading,
diff --git a/www/apps/api-reference/tsconfig.json b/www/apps/api-reference/tsconfig.json
index 48bd244a7f..46ad9c0284 100644
--- a/www/apps/api-reference/tsconfig.json
+++ b/www/apps/api-reference/tsconfig.json
@@ -22,6 +22,8 @@
"**/*.mjs"
],
"exclude": [
- "specs"
+ "specs",
+ "**/__tests__",
+ "**/__mocks__"
]
}
diff --git a/www/apps/api-reference/tsconfig.tests.json b/www/apps/api-reference/tsconfig.tests.json
new file mode 100644
index 0000000000..5baf29e6f9
--- /dev/null
+++ b/www/apps/api-reference/tsconfig.tests.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./tsconfig.json",
+ "include": [
+ "**/*.tsx",
+ "**/*.ts",
+ "**/*.js",
+ "src/**/*",
+ "**/*.mjs",
+ "__tests__/**/*",
+ "__mocks__/**/*"
+ ],
+ "exclude": [
+ "specs"
+ ]
+}
\ No newline at end of file
diff --git a/www/apps/api-reference/vitest.config.mts b/www/apps/api-reference/vitest.config.mts
new file mode 100644
index 0000000000..8c8c5f61a3
--- /dev/null
+++ b/www/apps/api-reference/vitest.config.mts
@@ -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')],
+ },
+})
+
diff --git a/www/packages/docs-ui/src/providers/AiAssistant/__tests__/index.test.tsx b/www/packages/docs-ui/src/providers/AiAssistant/__tests__/index.test.tsx
index 3acea61de2..1cb94ad937 100644
--- a/www/packages/docs-ui/src/providers/AiAssistant/__tests__/index.test.tsx
+++ b/www/packages/docs-ui/src/providers/AiAssistant/__tests__/index.test.tsx
@@ -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)
diff --git a/www/turbo.json b/www/turbo.json
index 7b2ee9a1a8..a01871240a 100644
--- a/www/turbo.json
+++ b/www/turbo.json
@@ -28,7 +28,9 @@
},
"lint": { },
"lint:content": { },
- "test": { },
+ "test": {
+ "dependsOn": ["^build"]
+ },
"watch": { },
"dev:monorepo": {
"dependsOn": [
diff --git a/www/yarn.lock b/www/yarn.lock
index 1b10693c64..fe09a9062b 100644
--- a/www/yarn.lock
+++ b/www/yarn.lock
@@ -6364,6 +6364,8 @@ __metadata:
tailwindcss: 3.3.3
types: "*"
typescript: 5.1.6
+ vite-tsconfig-paths: ^5.1.4
+ vitest: ^2.1.8
yaml: ^2.3.1
languageName: unknown
linkType: soft