docs: added support for tests in www workspace (#14415)
* tests for components (WIP) * finished adding tests to components * added tests for providers * add test command to doc tests * fix imports * exclude test files * remove import * add vitest as dev dependency * fix build error * ignore test files from eslint * fix test from docs-ui
This commit is contained in:
4
.github/workflows/docs-test.yml
vendored
4
.github/workflows/docs-test.yml
vendored
@@ -34,6 +34,10 @@ jobs:
|
||||
working-directory: www
|
||||
run: yarn install
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: www
|
||||
run: yarn test
|
||||
|
||||
- name: Build Doc Apps
|
||||
working-directory: www
|
||||
run: yarn build
|
||||
|
||||
@@ -1,171 +1,204 @@
|
||||
import prettier from "eslint-plugin-prettier/recommended";
|
||||
import globals from "globals";
|
||||
import babelParser from "@babel/eslint-parser";
|
||||
import typescriptEslintEslintPlugin from "@typescript-eslint/eslint-plugin";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import prettier from "eslint-plugin-prettier/recommended"
|
||||
import globals from "globals"
|
||||
import babelParser from "@babel/eslint-parser"
|
||||
import typescriptEslintEslintPlugin from "@typescript-eslint/eslint-plugin"
|
||||
import tsParser from "@typescript-eslint/parser"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import js from "@eslint/js"
|
||||
import { FlatCompat } from "@eslint/eslintrc"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
});
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
})
|
||||
|
||||
export default [
|
||||
prettier,
|
||||
{
|
||||
prettier,
|
||||
{
|
||||
ignores: [
|
||||
"**/dist",
|
||||
"**/build",
|
||||
"**/global-config.ts",
|
||||
"**/next.config.js",
|
||||
"**/spec",
|
||||
"**/node_modules",
|
||||
"**/public",
|
||||
"**/.eslintrc.js",
|
||||
"**/generated",
|
||||
"packages/tags/src/tags"
|
||||
"**/dist",
|
||||
"**/build",
|
||||
"**/global-config.ts",
|
||||
"**/next.config.js",
|
||||
"**/spec",
|
||||
"**/node_modules",
|
||||
"**/public",
|
||||
"**/.eslintrc.js",
|
||||
"**/generated",
|
||||
"packages/tags/src/tags",
|
||||
"**/__tests__",
|
||||
"**/__mocks__",
|
||||
],
|
||||
}, ...compat.extends(
|
||||
},
|
||||
...compat.extends(
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
), {
|
||||
|
||||
"plugin:react/jsx-runtime"
|
||||
),
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
...globals.browser,
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
...globals.browser,
|
||||
},
|
||||
|
||||
parser: babelParser,
|
||||
ecmaVersion: 13,
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
requireConfigFile: false,
|
||||
|
||||
ecmaFeatures: {
|
||||
experimentalDecorators: true,
|
||||
jsx: true,
|
||||
modules: true,
|
||||
},
|
||||
|
||||
parser: babelParser,
|
||||
ecmaVersion: 13,
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
requireConfigFile: false,
|
||||
|
||||
ecmaFeatures: {
|
||||
experimentalDecorators: true,
|
||||
jsx: true,
|
||||
modules: true
|
||||
},
|
||||
|
||||
project: true,
|
||||
},
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
curly: ["error", "all"],
|
||||
"new-cap": "off",
|
||||
"require-jsdoc": "off",
|
||||
"no-unused-expressions": "off",
|
||||
"no-unused-vars": "off",
|
||||
camelcase: "off",
|
||||
"no-invalid-this": "off",
|
||||
curly: ["error", "all"],
|
||||
"new-cap": "off",
|
||||
"require-jsdoc": "off",
|
||||
"no-unused-expressions": "off",
|
||||
"no-unused-vars": "off",
|
||||
camelcase: "off",
|
||||
"no-invalid-this": "off",
|
||||
|
||||
"max-len": ["error", {
|
||||
code: 80,
|
||||
ignoreStrings: true,
|
||||
ignoreRegExpLiterals: true,
|
||||
ignoreComments: true,
|
||||
ignoreTrailingComments: true,
|
||||
ignoreUrls: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
}],
|
||||
"max-len": [
|
||||
"error",
|
||||
{
|
||||
code: 80,
|
||||
ignoreStrings: true,
|
||||
ignoreRegExpLiterals: true,
|
||||
ignoreComments: true,
|
||||
ignoreTrailingComments: true,
|
||||
ignoreUrls: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
},
|
||||
],
|
||||
|
||||
semi: ["error", "never"],
|
||||
semi: ["error", "never"],
|
||||
|
||||
quotes: ["error", "double", {
|
||||
allowTemplateLiterals: true,
|
||||
}],
|
||||
quotes: [
|
||||
"error",
|
||||
"double",
|
||||
{
|
||||
allowTemplateLiterals: true,
|
||||
},
|
||||
],
|
||||
|
||||
"comma-dangle": ["error", {
|
||||
arrays: "always-multiline",
|
||||
objects: "always-multiline",
|
||||
imports: "always-multiline",
|
||||
exports: "always-multiline",
|
||||
functions: "never",
|
||||
}],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
{
|
||||
arrays: "always-multiline",
|
||||
objects: "always-multiline",
|
||||
imports: "always-multiline",
|
||||
exports: "always-multiline",
|
||||
functions: "never",
|
||||
},
|
||||
],
|
||||
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"arrow-parens": ["error", "always"],
|
||||
"linebreak-style": 0,
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"arrow-parens": ["error", "always"],
|
||||
"linebreak-style": 0,
|
||||
|
||||
"no-confusing-arrow": ["error", {
|
||||
allowParens: false,
|
||||
}],
|
||||
"no-confusing-arrow": [
|
||||
"error",
|
||||
{
|
||||
allowParens: false,
|
||||
},
|
||||
],
|
||||
|
||||
"space-before-function-paren": ["error", {
|
||||
anonymous: "always",
|
||||
named: "never",
|
||||
asyncArrow: "always",
|
||||
}],
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
{
|
||||
anonymous: "always",
|
||||
named: "never",
|
||||
asyncArrow: "always",
|
||||
},
|
||||
],
|
||||
|
||||
"space-infix-ops": "error",
|
||||
"eol-last": ["error", "always"],
|
||||
"space-infix-ops": "error",
|
||||
"eol-last": ["error", "always"],
|
||||
|
||||
"no-console": ["error", {
|
||||
allow: ["error", "warn"],
|
||||
}],
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
allow: ["error", "warn"],
|
||||
},
|
||||
],
|
||||
|
||||
"react/prop-types": "off",
|
||||
"react/prop-types": "off",
|
||||
},
|
||||
}, ...compat.extends("plugin:@typescript-eslint/recommended", "plugin:react/recommended").map(config => ({
|
||||
...config,
|
||||
files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
||||
})), {
|
||||
},
|
||||
...compat
|
||||
.extends(
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended"
|
||||
)
|
||||
.map((config) => ({
|
||||
...config,
|
||||
files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
||||
})),
|
||||
{
|
||||
files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
||||
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslintEslintPlugin,
|
||||
"@typescript-eslint": typescriptEslintEslintPlugin,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 13,
|
||||
sourceType: "module",
|
||||
parser: tsParser,
|
||||
ecmaVersion: 13,
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
"valid-jsdoc": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@/keyword-spacing": "error",
|
||||
"valid-jsdoc": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@/keyword-spacing": "error",
|
||||
|
||||
"@/space-before-function-paren": ["error", {
|
||||
anonymous: "always",
|
||||
named: "never",
|
||||
asyncArrow: "always",
|
||||
}],
|
||||
"@/space-before-function-paren": [
|
||||
"error",
|
||||
{
|
||||
anonymous: "always",
|
||||
named: "never",
|
||||
asyncArrow: "always",
|
||||
},
|
||||
],
|
||||
|
||||
"@/space-infix-ops": "error",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"@/space-infix-ops": "error",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
|
||||
"react/prop-types": "off",
|
||||
"react/prop-types": "off",
|
||||
},
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["apps/*/"],
|
||||
},
|
||||
next: {
|
||||
rootDir: ["apps/*/"],
|
||||
},
|
||||
},
|
||||
}];
|
||||
},
|
||||
]
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"lint:content": "turbo run lint:content",
|
||||
"watch": "turbo run watch",
|
||||
"prep": "turbo run prep",
|
||||
"test": "turbo run test -- run",
|
||||
"up:medusa": "yarn workspaces foreach -v --topological-dev --recursive exec yarn up @medusajs/icons @medusajs/ui @medusajs/ui-preset --exact"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -34,14 +35,21 @@
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.25.9",
|
||||
"@eslint/js": "9.13.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@types/eslint__js": "8.42.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint-config-next": "15.3.6",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-config-turbo": "2.2.3",
|
||||
"eslint-plugin-markdown": "5.1.0",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-react": "7.37.2",
|
||||
"typescript-eslint": "8.11.0"
|
||||
"jsdom": "^25.0.1",
|
||||
"typescript-eslint": "8.11.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17.0"
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"build:js:esm": "tsc --project tsconfig.esm.json && tsc-alias -p tsconfig.esm.json",
|
||||
"clean": "rimraf dist",
|
||||
"dev": "yarn build:js:cjs && yarn build:js:esm",
|
||||
"lint": "eslint src --fix"
|
||||
"lint": "eslint src --fix",
|
||||
"test": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "19.2.3",
|
||||
@@ -47,7 +48,9 @@
|
||||
"tsc-alias": "^1.8.7",
|
||||
"tsup": "^5.10.1",
|
||||
"types": "*",
|
||||
"typescript": "^5.1.6"
|
||||
"typescript": "^5.1.6",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from "react"
|
||||
import { expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import * as AiAssistantMocks from "../../../__mocks__"
|
||||
|
||||
vi.mock("@/providers/AiAssistant", () => ({
|
||||
useAiAssistant: () => AiAssistantMocks.mockUseAiAssistant(),
|
||||
}))
|
||||
|
||||
import { AiAssistantChatWindowHeader } from "../../Header"
|
||||
|
||||
test("handles chat window close", () => {
|
||||
const { container } = render(<AiAssistantChatWindowHeader />)
|
||||
const button = container.querySelector("button")
|
||||
const xMark = button?.querySelector("svg")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(xMark).toBeInTheDocument()
|
||||
expect(xMark).toHaveClass("text-medusa-fg-muted")
|
||||
expect(xMark).toHaveAttribute("height", "15")
|
||||
expect(xMark).toHaveAttribute("width", "15")
|
||||
})
|
||||
|
||||
test("calls setChatOpened(false) when button is clicked", () => {
|
||||
// Reset the mock before each test
|
||||
AiAssistantMocks.mockSetChatOpened.mockClear()
|
||||
|
||||
const { container } = render(<AiAssistantChatWindowHeader />)
|
||||
const button = container.querySelector("button")
|
||||
|
||||
expect(button).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(button!)
|
||||
|
||||
expect(AiAssistantMocks.mockSetChatOpened).toHaveBeenCalledTimes(1)
|
||||
expect(AiAssistantMocks.mockSetChatOpened).toHaveBeenCalledWith(false)
|
||||
})
|
||||
@@ -6,7 +6,7 @@ import { Tooltip } from "../../../Tooltip"
|
||||
import { Link } from "../../../Link"
|
||||
import { ShieldCheck, XMark } from "@medusajs/icons"
|
||||
import { Button } from "../../../Button"
|
||||
import { useAiAssistant } from "../../../../providers"
|
||||
import { useAiAssistant } from "../../../../providers/AiAssistant"
|
||||
|
||||
export const AiAssistantChatWindowHeader = () => {
|
||||
const { setChatOpened } = useAiAssistant()
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
import React from "react"
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import * as AiAssistantMocks from "../../../__mocks__"
|
||||
|
||||
vi.mock("@/providers/AiAssistant", () => ({
|
||||
useAiAssistant: () => AiAssistantMocks.mockUseAiAssistant(),
|
||||
}))
|
||||
vi.mock("@/providers/Analytics", () => ({
|
||||
useAnalytics: () => ({
|
||||
track: AiAssistantMocks.mockTrack,
|
||||
}),
|
||||
}))
|
||||
vi.mock("@/providers/BrowserProvider", () => ({
|
||||
useIsBrowser: () => ({
|
||||
isBrowser: true,
|
||||
}),
|
||||
}))
|
||||
vi.mock("@/hooks/use-ai-assistant-chat-navigation", () => ({
|
||||
useAiAssistantChatNavigation: () => ({
|
||||
getChatWindowElm: AiAssistantMocks.mockGetChatWindowElm,
|
||||
getInputElm: AiAssistantMocks.mockGetInputElm,
|
||||
focusInput: AiAssistantMocks.mockFocusInput,
|
||||
question: "",
|
||||
}),
|
||||
}))
|
||||
vi.mock("@kapaai/react-sdk", () => ({
|
||||
useChat: () => AiAssistantMocks.mockUseChat(),
|
||||
useDeepThinking: () => AiAssistantMocks.mockUseDeepThinking(),
|
||||
}))
|
||||
vi.mock("@/components/Tooltip", () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
import { AiAssistantChatWindowInput } from "../../Input"
|
||||
import { DocsTrackingEvents } from "../../../../.."
|
||||
|
||||
// Reset mock before each test to ensure clean state
|
||||
beforeEach(() => {
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseAiAssistantReturn
|
||||
)
|
||||
AiAssistantMocks.mockSetChatOpened.mockClear()
|
||||
AiAssistantMocks.mockTrack.mockClear()
|
||||
AiAssistantMocks.mockAddFeedback.mockClear()
|
||||
AiAssistantMocks.mockSubmitQuery.mockClear()
|
||||
AiAssistantMocks.mockStopGeneration.mockClear()
|
||||
AiAssistantMocks.mockConversation.length = 1
|
||||
AiAssistantMocks.mockUseChat.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseChatReturn
|
||||
)
|
||||
AiAssistantMocks.mockUseDeepThinking.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseDeepThinkingReturn
|
||||
)
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders the chat window input", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
const input = container.querySelector("textarea")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("")
|
||||
})
|
||||
|
||||
test("should focus input when chat is opened", () => {
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValueOnce({
|
||||
...AiAssistantMocks.defaultUseAiAssistantReturn,
|
||||
chatOpened: true,
|
||||
})
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
const input = container.querySelector("textarea")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveFocus()
|
||||
})
|
||||
})
|
||||
|
||||
describe("form submission", () => {
|
||||
test("submits the question when the form is submitted", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
const input = container.querySelector("textarea")
|
||||
expect(input).toBeInTheDocument()
|
||||
fireEvent.change(input!, { target: { value: "test" } })
|
||||
fireEvent.submit(input!)
|
||||
expect(AiAssistantMocks.mockSubmitQuery).toHaveBeenCalledWith("test")
|
||||
})
|
||||
})
|
||||
|
||||
describe("stop generation", () => {
|
||||
test("should stop generation when loading is true", () => {
|
||||
// Set loading to true for this test
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValueOnce({
|
||||
...AiAssistantMocks.defaultUseAiAssistantReturn,
|
||||
loading: true,
|
||||
})
|
||||
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
|
||||
const input = container.querySelector("textarea")
|
||||
const form = container.querySelector("form")
|
||||
const submitButton = form?.querySelector("button[type=submit]")
|
||||
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
|
||||
// Verify loading state - button should show stop icon and be enabled
|
||||
const stopIcon = submitButton?.querySelector("svg")
|
||||
expect(stopIcon).toBeInTheDocument()
|
||||
expect(submitButton).not.toBeDisabled()
|
||||
|
||||
// When form is submitted while loading, it should call stopGeneration
|
||||
AiAssistantMocks.mockStopGeneration.mockClear()
|
||||
fireEvent.submit(input!.closest("form")!)
|
||||
expect(AiAssistantMocks.mockStopGeneration).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test("should not stop generation when loading is false", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
const input = container.querySelector("textarea")
|
||||
const form = container.querySelector("form")
|
||||
const submitButton = form?.querySelector("button[type=submit]")
|
||||
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
|
||||
fireEvent.change(input!, { target: { value: "test" } })
|
||||
expect(input).toHaveValue("test")
|
||||
expect(submitButton).not.toBeDisabled()
|
||||
AiAssistantMocks.mockStopGeneration.mockClear()
|
||||
fireEvent.submit(input!.closest("form")!)
|
||||
expect(AiAssistantMocks.mockStopGeneration).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("analytics tracking", () => {
|
||||
test("should track start chat event when no conversation", () => {
|
||||
AiAssistantMocks.mockConversation.length = 0
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
const input = container.querySelector("textarea")
|
||||
expect(input).toBeInTheDocument()
|
||||
fireEvent.change(input!, { target: { value: "test" } })
|
||||
fireEvent.submit(input!.closest("form")!)
|
||||
expect(AiAssistantMocks.mockTrack).toHaveBeenCalledWith({
|
||||
event: {
|
||||
event: DocsTrackingEvents.AI_ASSISTANT_START_CHAT,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("should not track start chat event when conversation is not empty", () => {
|
||||
AiAssistantMocks.mockConversation.length = 1
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
const input = container.querySelector("textarea")
|
||||
expect(input).toBeInTheDocument()
|
||||
fireEvent.change(input!, { target: { value: "test" } })
|
||||
fireEvent.submit(input!.closest("form")!)
|
||||
expect(AiAssistantMocks.mockTrack).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("keyboard interactions", () => {
|
||||
test("should set question state to last question when arrow up is pressed and question is empty", () => {
|
||||
AiAssistantMocks.mockConversation.getLatest.mockReturnValue({
|
||||
question: "last question",
|
||||
})
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
const input = container.querySelector("textarea")
|
||||
expect(input).toBeInTheDocument()
|
||||
fireEvent.keyDown(input!, { key: "ArrowUp" })
|
||||
expect(input).toHaveValue("last question")
|
||||
})
|
||||
|
||||
test("should not set question state to last question when arrow up is pressed and question is not empty", () => {
|
||||
AiAssistantMocks.mockConversation.getLatest.mockReturnValue({
|
||||
question: "last question",
|
||||
})
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
const input = container.querySelector("textarea")
|
||||
expect(input).toBeInTheDocument()
|
||||
fireEvent.change(input!, { target: { value: "test" } })
|
||||
fireEvent.keyDown(input!, { key: "ArrowUp" })
|
||||
expect(input).toHaveValue("test")
|
||||
})
|
||||
|
||||
test("should add new line when shift + enter are pressed and question is not empty", () => {
|
||||
AiAssistantMocks.mockConversation.getLatest.mockReturnValue({
|
||||
question: "last question",
|
||||
})
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
const input = container.querySelector("textarea")
|
||||
expect(input).toBeInTheDocument()
|
||||
fireEvent.change(input!, { target: { value: "test" } })
|
||||
fireEvent.keyDown(input!, { key: "Enter", shiftKey: true })
|
||||
expect(input).toHaveValue("test\n")
|
||||
})
|
||||
})
|
||||
|
||||
describe("search query parameters", () => {
|
||||
const originalLocation = window.location
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset location mock before each test
|
||||
delete (window as unknown as { location?: Location }).location
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original location after each test
|
||||
Object.defineProperty(window, "location", {
|
||||
value: originalLocation,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
})
|
||||
|
||||
test("should set question from query parameter when query is present", () => {
|
||||
// Mock window.location with query parameter
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
...originalLocation,
|
||||
search: "?query=test%20question",
|
||||
},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
|
||||
const input = container.querySelector("textarea")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("test question")
|
||||
expect(AiAssistantMocks.mockSetChatOpened).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
test("should set question and submit when queryType is submit", () => {
|
||||
// Mock window.location with query parameter and queryType=submit
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
...originalLocation,
|
||||
search: "?query=test%20question&queryType=submit",
|
||||
},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
AiAssistantMocks.mockSubmitQuery.mockClear()
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
|
||||
const input = container.querySelector("textarea")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(AiAssistantMocks.mockSetChatOpened).toHaveBeenCalledWith(true)
|
||||
expect(AiAssistantMocks.mockSubmitQuery).toHaveBeenCalledWith(
|
||||
"test question"
|
||||
)
|
||||
})
|
||||
|
||||
test("should not set question when query parameter is not present", () => {
|
||||
// Mock window.location without query parameter
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
...originalLocation,
|
||||
search: "",
|
||||
},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
|
||||
const input = container.querySelector("textarea")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("")
|
||||
})
|
||||
|
||||
test("should not set question when isCaptchaLoaded is false", () => {
|
||||
// Mock window.location with query parameter
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
...originalLocation,
|
||||
search: "?query=test%20question",
|
||||
},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
// Set isCaptchaLoaded to false
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValueOnce({
|
||||
...AiAssistantMocks.defaultUseAiAssistantReturn,
|
||||
isCaptchaLoaded: false,
|
||||
})
|
||||
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
|
||||
const input = container.querySelector("textarea")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("")
|
||||
expect(AiAssistantMocks.mockSetChatOpened).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("deep thinking", () => {
|
||||
test("should toggle deep thinking when button is clicked", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
|
||||
const deepThinkingButton = container.querySelector("button")
|
||||
expect(deepThinkingButton).toBeInTheDocument()
|
||||
fireEvent.click(deepThinkingButton!)
|
||||
expect(AiAssistantMocks.mockToggle).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("button should be disabled when loading is true", () => {
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValueOnce({
|
||||
...AiAssistantMocks.defaultUseAiAssistantReturn,
|
||||
loading: true,
|
||||
})
|
||||
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
const deepThinkingButton = container.querySelector("button")
|
||||
expect(deepThinkingButton).toBeInTheDocument()
|
||||
expect(deepThinkingButton).toBeDisabled()
|
||||
})
|
||||
|
||||
test("button should have light bulb icon when deep thinking is inactive", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
const deepThinkingButton = container.querySelector("button")
|
||||
const icon = deepThinkingButton?.querySelector("svg")
|
||||
expect(icon).toBeInTheDocument()
|
||||
expect(icon).not.toHaveClass("text-medusa-tag-orange-icon")
|
||||
})
|
||||
|
||||
test("button should have light bulb solid icon when deep thinking is active", () => {
|
||||
AiAssistantMocks.mockUseDeepThinking.mockReturnValueOnce({
|
||||
active: true,
|
||||
toggle: AiAssistantMocks.mockToggle,
|
||||
})
|
||||
const { container } = render(
|
||||
<AiAssistantChatWindowInput
|
||||
chatWindowRef={React.createRef<HTMLDivElement>()}
|
||||
/>
|
||||
)
|
||||
const deepThinkingButton = container.querySelector("button")
|
||||
const icon = deepThinkingButton?.querySelector("svg")
|
||||
expect(icon).toBeInTheDocument()
|
||||
expect(icon).toHaveClass("text-medusa-tag-orange-icon")
|
||||
})
|
||||
})
|
||||
@@ -6,13 +6,11 @@ import {
|
||||
LightBulbSolid,
|
||||
StopCircleSolid,
|
||||
} from "@medusajs/icons"
|
||||
import {
|
||||
useAiAssistant,
|
||||
useAnalytics,
|
||||
useIsBrowser,
|
||||
} from "../../../../providers"
|
||||
import { useAiAssistant } from "@/providers/AiAssistant"
|
||||
import { useAnalytics } from "@/providers/Analytics"
|
||||
import { useIsBrowser } from "@/providers/BrowserProvider"
|
||||
import { useChat, useDeepThinking } from "@kapaai/react-sdk"
|
||||
import { useAiAssistantChatNavigation } from "../../../../hooks"
|
||||
import { useAiAssistantChatNavigation } from "../../../../hooks/use-ai-assistant-chat-navigation"
|
||||
import { Tooltip } from "../../../Tooltip"
|
||||
import { DocsTrackingEvents } from "../../../../constants"
|
||||
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import * as AiAssistantMocks from "../../__mocks__"
|
||||
import { AiAssistantThreadItemProps } from "../../ThreadItem"
|
||||
|
||||
// Mock components and hooks
|
||||
vi.mock("@/providers/AiAssistant", () => ({
|
||||
useAiAssistant: () => AiAssistantMocks.mockUseAiAssistant(),
|
||||
}))
|
||||
vi.mock("@/providers/Analytics", () => ({
|
||||
useAnalytics: () => ({
|
||||
track: AiAssistantMocks.mockTrack,
|
||||
}),
|
||||
}))
|
||||
vi.mock("@/providers/BrowserProvider", () => ({
|
||||
useIsBrowser: () => ({
|
||||
isBrowser: true,
|
||||
}),
|
||||
}))
|
||||
vi.mock("@/hooks/use-ai-assistant-chat-navigation", () => ({
|
||||
useAiAssistantChatNavigation: () => ({
|
||||
getChatWindowElm: AiAssistantMocks.mockGetChatWindowElm,
|
||||
getInputElm: AiAssistantMocks.mockGetInputElm,
|
||||
focusInput: AiAssistantMocks.mockFocusInput,
|
||||
question: "",
|
||||
}),
|
||||
}))
|
||||
vi.mock("@kapaai/react-sdk", () => ({
|
||||
useChat: () => AiAssistantMocks.mockUseChat(),
|
||||
useDeepThinking: () => AiAssistantMocks.mockUseDeepThinking(),
|
||||
}))
|
||||
vi.mock("@/components/Tooltip", () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/AiAssistant/ChatWindow/Header", () => ({
|
||||
AiAssistantChatWindowHeader: () => <div>Header</div>,
|
||||
}))
|
||||
vi.mock("@/components/AiAssistant/ChatWindow/Input", () => ({
|
||||
AiAssistantChatWindowInput: () => <div>Input</div>,
|
||||
}))
|
||||
vi.mock("@/components/AiAssistant/ChatWindow/Footer", () => ({
|
||||
AiAssistantChatWindowFooter: () => <div>Footer</div>,
|
||||
}))
|
||||
vi.mock("@/components/AiAssistant/Suggestions", () => ({
|
||||
AiAssistantSuggestions: () => <div>Suggestions</div>,
|
||||
}))
|
||||
vi.mock("@/components/AiAssistant/ThreadItem", () => ({
|
||||
AiAssistantThreadItem: ({ item }: AiAssistantThreadItemProps) => (
|
||||
<div className="thread-item">ThreadItem - type: {item.type}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
import { AiAssistantChatWindow } from "../../ChatWindow"
|
||||
|
||||
// Reset mock before each test to ensure clean state
|
||||
beforeEach(() => {
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseAiAssistantReturn
|
||||
)
|
||||
AiAssistantMocks.mockSetChatOpened.mockClear()
|
||||
AiAssistantMocks.mockTrack.mockClear()
|
||||
AiAssistantMocks.mockAddFeedback.mockClear()
|
||||
AiAssistantMocks.mockSubmitQuery.mockClear()
|
||||
AiAssistantMocks.mockStopGeneration.mockClear()
|
||||
AiAssistantMocks.mockConversation.length = 1
|
||||
AiAssistantMocks.mockUseChat.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseChatReturn
|
||||
)
|
||||
AiAssistantMocks.mockUseDeepThinking.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseDeepThinkingReturn
|
||||
)
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders chat window", () => {
|
||||
const { container } = render(<AiAssistantChatWindow />)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container).toHaveTextContent("Header")
|
||||
expect(container).toHaveTextContent("Input")
|
||||
expect(container).toHaveTextContent("Footer")
|
||||
})
|
||||
|
||||
test("chat is hidden when chatOpened is false", () => {
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValueOnce({
|
||||
...AiAssistantMocks.defaultUseAiAssistantReturn,
|
||||
chatOpened: false,
|
||||
})
|
||||
const { container } = render(<AiAssistantChatWindow />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const overlay = container.querySelector(".bg-medusa-bg-overlay")
|
||||
expect(overlay).toBeInTheDocument()
|
||||
expect(overlay).toHaveClass("hidden")
|
||||
const chatWindow = container.querySelector(".z-50.flex")
|
||||
expect(chatWindow).toBeInTheDocument()
|
||||
expect(chatWindow).toHaveClass("!fixed")
|
||||
})
|
||||
|
||||
test("chat is shown when chatOpened is true", () => {
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValueOnce({
|
||||
...AiAssistantMocks.defaultUseAiAssistantReturn,
|
||||
chatOpened: true,
|
||||
})
|
||||
const { container } = render(<AiAssistantChatWindow />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const overlay = container.querySelector(".bg-medusa-bg-overlay")
|
||||
expect(overlay).toBeInTheDocument()
|
||||
expect(overlay).toHaveClass("block")
|
||||
const chatWindow = container.querySelector(".z-50.flex")
|
||||
expect(chatWindow).toBeInTheDocument()
|
||||
expect(chatWindow).toHaveClass("!right-0")
|
||||
})
|
||||
})
|
||||
|
||||
describe("conversation", () => {
|
||||
test("show suggestions when conversation is empty", () => {
|
||||
AiAssistantMocks.mockConversation.length = 0
|
||||
const { container } = render(<AiAssistantChatWindow />)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container).toHaveTextContent("Suggestions")
|
||||
})
|
||||
|
||||
test("show thread items when conversation is not empty", () => {
|
||||
AiAssistantMocks.mockConversation.length = 2
|
||||
const { container } = render(<AiAssistantChatWindow />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const threadItems = container.querySelectorAll(".thread-item")
|
||||
expect(threadItems).toHaveLength(4)
|
||||
expect(threadItems[0]).toHaveTextContent("ThreadItem - type: question")
|
||||
expect(threadItems[1]).toHaveTextContent("ThreadItem - type: answer")
|
||||
expect(threadItems[2]).toHaveTextContent("ThreadItem - type: question")
|
||||
expect(threadItems[3]).toHaveTextContent("ThreadItem - type: answer")
|
||||
})
|
||||
|
||||
test("show error message when error is not empty", () => {
|
||||
AiAssistantMocks.mockUseChat.mockReturnValueOnce({
|
||||
...AiAssistantMocks.defaultUseChatReturn,
|
||||
error: "error",
|
||||
})
|
||||
const { container } = render(<AiAssistantChatWindow />)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container).toHaveTextContent("ThreadItem - type: error")
|
||||
})
|
||||
})
|
||||
|
||||
describe("keyboard shortcuts", () => {
|
||||
test("escape key should close chat window", () => {
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValueOnce({
|
||||
...AiAssistantMocks.defaultUseAiAssistantReturn,
|
||||
chatOpened: true,
|
||||
})
|
||||
const { container } = render(<AiAssistantChatWindow />)
|
||||
expect(container).toBeInTheDocument()
|
||||
|
||||
// Focus an element inside the chat window to satisfy the contains check
|
||||
// The chat window ref needs to contain the active element
|
||||
const chatWindow = container.querySelector(".z-50.flex") as HTMLElement
|
||||
expect(chatWindow).toBeInTheDocument()
|
||||
|
||||
// Create a focusable element inside the chat window and focus it
|
||||
const focusableElement = document.createElement("div")
|
||||
focusableElement.setAttribute("tabindex", "0")
|
||||
chatWindow.appendChild(focusableElement)
|
||||
focusableElement.focus()
|
||||
|
||||
// Fire Escape key on window (where useKeyboardShortcut listens)
|
||||
fireEvent.keyDown(window, { key: "Escape" })
|
||||
|
||||
// Verify that setChatOpened(false) was called
|
||||
expect(AiAssistantMocks.mockSetChatOpened).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("scroll", () => {
|
||||
test("show fade when scroll is not at the bottom", () => {
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValueOnce({
|
||||
...AiAssistantMocks.defaultUseAiAssistantReturn,
|
||||
chatOpened: true,
|
||||
loading: false,
|
||||
})
|
||||
const { container } = render(<AiAssistantChatWindow />)
|
||||
expect(container).toBeInTheDocument()
|
||||
|
||||
// Find the fade element
|
||||
const fade = container.querySelector(".bg-ai-assistant-bottom")
|
||||
expect(fade).toBeInTheDocument()
|
||||
|
||||
// Find the scrollable parent element (contentRef.current.parentElement)
|
||||
// This is the div with class "overflow-y-auto flex-auto px-docs_0.5 pt-docs_0.25 pb-docs_2"
|
||||
const scrollableParent = container.querySelector(
|
||||
".overflow-y-auto.flex-auto"
|
||||
) as HTMLElement
|
||||
expect(scrollableParent).toBeInTheDocument()
|
||||
|
||||
// Initially, fade should not be shown (opacity-0)
|
||||
expect(fade).toHaveClass("opacity-0")
|
||||
expect(fade).not.toHaveClass("opacity-100")
|
||||
|
||||
// Set up scroll dimensions to simulate not being at the bottom
|
||||
// The condition is: offsetHeight + scrollTop < scrollHeight - 1
|
||||
// So if scrollHeight is 1000, offsetHeight is 500, scrollTop is 0
|
||||
// Then 500 + 0 < 1000 - 1 = true, so fade should show
|
||||
Object.defineProperty(scrollableParent, "scrollHeight", {
|
||||
value: 1000,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
Object.defineProperty(scrollableParent, "offsetHeight", {
|
||||
value: 500,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
Object.defineProperty(scrollableParent, "scrollTop", {
|
||||
value: 0,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
// Fire scroll event on the parent element
|
||||
fireEvent.scroll(scrollableParent)
|
||||
|
||||
// Fade should now be shown (opacity-100)
|
||||
expect(fade).toHaveClass("opacity-100")
|
||||
})
|
||||
|
||||
test("hide fade when scroll is at the bottom", () => {
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValueOnce({
|
||||
...AiAssistantMocks.defaultUseAiAssistantReturn,
|
||||
chatOpened: true,
|
||||
loading: false,
|
||||
})
|
||||
const { container } = render(<AiAssistantChatWindow />)
|
||||
expect(container).toBeInTheDocument()
|
||||
|
||||
const fade = container.querySelector(".bg-ai-assistant-bottom")
|
||||
expect(fade).toBeInTheDocument()
|
||||
|
||||
const scrollableParent = container.querySelector(
|
||||
".overflow-y-auto.flex-auto"
|
||||
) as HTMLElement
|
||||
expect(scrollableParent).toBeInTheDocument()
|
||||
|
||||
// Set up scroll dimensions to simulate being at the bottom
|
||||
// offsetHeight + scrollTop >= scrollHeight - 1
|
||||
// So if scrollHeight is 1000, offsetHeight is 500, scrollTop is 500
|
||||
// Then 500 + 500 >= 1000 - 1 = true, so fade should hide
|
||||
Object.defineProperty(scrollableParent, "scrollHeight", {
|
||||
value: 1000,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
Object.defineProperty(scrollableParent, "offsetHeight", {
|
||||
value: 500,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
Object.defineProperty(scrollableParent, "scrollTop", {
|
||||
value: 500,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
// Fire scroll event
|
||||
fireEvent.scroll(scrollableParent)
|
||||
|
||||
// Fade should be hidden
|
||||
expect(fade).not.toHaveClass("opacity-100")
|
||||
})
|
||||
})
|
||||
@@ -8,12 +8,13 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import { useAiAssistant, useIsBrowser } from "../../../providers"
|
||||
import { useAiAssistant } from "../../../providers/AiAssistant"
|
||||
import { useIsBrowser } from "../../../providers/BrowserProvider"
|
||||
import { AiAssistantChatWindowHeader } from "./Header"
|
||||
import { AiAssistantSuggestions } from "../Suggestions"
|
||||
import { AiAssistantThreadItem } from "../ThreadItem"
|
||||
import { AiAssistantChatWindowInput } from "./Input"
|
||||
import { useKeyboardShortcut } from "../../.."
|
||||
import { useKeyboardShortcut } from "../../../hooks/use-keyboard-shortcut"
|
||||
import { AiAssistantChatWindowFooter } from "./Footer"
|
||||
import { useChat } from "@kapaai/react-sdk"
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
"use client"
|
||||
|
||||
import React, { Fragment, useCallback, useState } from "react"
|
||||
import { Badge, Button, InputText, Kbd, Tooltip, Link } from "@/components"
|
||||
import { useAiAssistant, useSearch } from "@/providers"
|
||||
import { Badge } from "../../Badge"
|
||||
import { Button } from "../../Button"
|
||||
import { InputText } from "../../Input/Text"
|
||||
import { Kbd } from "../../Kbd"
|
||||
import { Tooltip } from "../../Tooltip"
|
||||
import { Link } from "../../Link"
|
||||
import { useAiAssistant } from "@/providers/AiAssistant"
|
||||
import { useSearch } from "@/providers/Search"
|
||||
import { ArrowUturnLeft } from "@medusajs/icons"
|
||||
import clsx from "clsx"
|
||||
import { AiAssistantThreadItem } from "../ThreadItem"
|
||||
import { AiAssistantSuggestions } from "../Suggestions"
|
||||
import { useSearchNavigation } from "../../.."
|
||||
import { useSearchNavigation } from "../../../hooks/use-search-navigation"
|
||||
import { useChat } from "@kapaai/react-sdk"
|
||||
|
||||
export const AiAssistantSearchWindow = () => {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from "react"
|
||||
import { beforeAll, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import * as AiAssistantMocks from "../../__mocks__"
|
||||
|
||||
// Mock components and hooks
|
||||
vi.mock("@kapaai/react-sdk", () => ({
|
||||
useChat: () => AiAssistantMocks.mockUseChat(),
|
||||
}))
|
||||
vi.mock("@/providers/SiteConfig", () => ({
|
||||
useSiteConfig: () => ({
|
||||
config: {
|
||||
baseUrl: "https://docs.medusajs.com",
|
||||
},
|
||||
}),
|
||||
}))
|
||||
vi.mock("@/components/Link", () => ({
|
||||
Link: ({
|
||||
children,
|
||||
href,
|
||||
variant,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
href: string
|
||||
variant: "content"
|
||||
}) => (
|
||||
<a
|
||||
href={href}
|
||||
className={
|
||||
variant === "content"
|
||||
? "text-medusa-fg-content"
|
||||
: "text-medusa-fg-muted"
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/Search/Hits/GroupName", () => ({
|
||||
SearchHitGroupName: ({ name }: { name: string }) => <div>{name}</div>,
|
||||
}))
|
||||
vi.mock("@/components/Search/Suggestions/Item", () => ({
|
||||
SearchSuggestionItem: ({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick: () => void
|
||||
}) => (
|
||||
<div onClick={onClick} className="suggestion-item">
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
import { AiAssistantSuggestions } from "../index"
|
||||
|
||||
beforeAll(() => {
|
||||
AiAssistantMocks.mockUseChat.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseChatReturn
|
||||
)
|
||||
AiAssistantMocks.mockAddFeedback.mockClear()
|
||||
AiAssistantMocks.mockSubmitQuery.mockClear()
|
||||
AiAssistantMocks.mockStopGeneration.mockClear()
|
||||
AiAssistantMocks.mockConversation.length = 1
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders suggestions", () => {
|
||||
const { container } = render(<AiAssistantSuggestions />)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container).toHaveTextContent("FAQ")
|
||||
expect(container).toHaveTextContent("Recipes")
|
||||
expect(container).toHaveTextContent("What is Medusa?")
|
||||
expect(container).toHaveTextContent("How can I create a module?")
|
||||
expect(container).toHaveTextContent("How can I create a data model?")
|
||||
expect(container).toHaveTextContent("How do I create a workflow?")
|
||||
expect(container).toHaveTextContent(
|
||||
"How can I extend a data model in the Product Module?"
|
||||
)
|
||||
expect(container).toHaveTextContent(
|
||||
"How do I build a marketplace with Medusa?"
|
||||
)
|
||||
expect(container).toHaveTextContent(
|
||||
"How do I build digital products with Medusa?"
|
||||
)
|
||||
expect(container).toHaveTextContent(
|
||||
"How do I build subscription-based purchases with Medusa?"
|
||||
)
|
||||
expect(container).toHaveTextContent(
|
||||
"What other recipes are available in the Medusa documentation?"
|
||||
)
|
||||
expect(container).toHaveTextContent("Medusa MCP server")
|
||||
})
|
||||
})
|
||||
|
||||
describe("interaction", () => {
|
||||
test("clicking a suggestion item should submit the query", () => {
|
||||
const { container } = render(<AiAssistantSuggestions />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const suggestionItem = container.querySelector(".suggestion-item")
|
||||
expect(suggestionItem).toBeInTheDocument()
|
||||
fireEvent.click(suggestionItem!)
|
||||
expect(AiAssistantMocks.mockSubmitQuery).toHaveBeenCalledWith(
|
||||
suggestionItem!.textContent
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -6,7 +6,7 @@ import { SearchHitGroupName } from "../../Search/Hits/GroupName"
|
||||
import { SearchSuggestionItem } from "../../Search/Suggestions/Item"
|
||||
import { useChat } from "@kapaai/react-sdk"
|
||||
import { Link } from "../../Link"
|
||||
import { useSiteConfig } from "../../../providers"
|
||||
import { useSiteConfig } from "../../../providers/SiteConfig"
|
||||
|
||||
type AiAssistantSuggestionsProps = React.AllHTMLAttributes<HTMLDivElement>
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import { ButtonProps } from "../../../../Button"
|
||||
import { LinkProps } from "../../../../Link"
|
||||
import { BadgeProps } from "../../../../Badge"
|
||||
import { AiAssistantThreadItem } from "../../../../../providers/AiAssistant"
|
||||
import * as AiAssistantMocks from "../../../__mocks__"
|
||||
|
||||
// mock data
|
||||
const mockQuestionThreadItem: AiAssistantThreadItem = {
|
||||
type: "question",
|
||||
content: "test content",
|
||||
}
|
||||
const mockAnswerThreadItem: AiAssistantThreadItem = {
|
||||
type: "answer",
|
||||
content: "test answer",
|
||||
question_id: "123",
|
||||
}
|
||||
const mockSiteConfig = {
|
||||
config: {
|
||||
baseUrl: "https://docs.medusajs.com",
|
||||
},
|
||||
}
|
||||
|
||||
// Mock functions
|
||||
const mockHandleCopy = vi.fn()
|
||||
const defaultUseCopyReturn = {
|
||||
handleCopy: mockHandleCopy,
|
||||
isCopied: false,
|
||||
}
|
||||
const useCopyMock = vi.fn(() => defaultUseCopyReturn)
|
||||
|
||||
// Mock components and hooks
|
||||
vi.mock("@kapaai/react-sdk", () => ({
|
||||
useChat: () => AiAssistantMocks.mockUseChat(),
|
||||
}))
|
||||
vi.mock("@/providers/SiteConfig", () => ({
|
||||
useSiteConfig: () => mockSiteConfig,
|
||||
}))
|
||||
vi.mock("@/components/Badge", () => ({
|
||||
Badge: (props: BadgeProps) => <div {...props} />,
|
||||
}))
|
||||
vi.mock("@/components/Button", () => ({
|
||||
Button: (props: ButtonProps) => <button {...props} />,
|
||||
}))
|
||||
vi.mock("@/components/Link", () => ({
|
||||
Link: (props: LinkProps) => <a {...props} />,
|
||||
}))
|
||||
vi.mock("@/hooks/use-copy", () => ({
|
||||
useCopy: () => useCopyMock(),
|
||||
}))
|
||||
|
||||
import { AiAssistantThreadItemActions } from "../../Actions"
|
||||
|
||||
beforeEach(() => {
|
||||
AiAssistantMocks.mockUseChat.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseChatReturn
|
||||
)
|
||||
AiAssistantMocks.mockAddFeedback.mockClear()
|
||||
AiAssistantMocks.mockSubmitQuery.mockClear()
|
||||
AiAssistantMocks.mockStopGeneration.mockClear()
|
||||
AiAssistantMocks.mockConversation.length = 1
|
||||
mockHandleCopy.mockClear()
|
||||
useCopyMock.mockReturnValue(defaultUseCopyReturn)
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders question thread item actions", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItemActions item={mockQuestionThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const wrapper = container.querySelector("div")
|
||||
expect(wrapper).toHaveClass("justify-end")
|
||||
const linkCopyButton = container.querySelector(
|
||||
"[data-testid='link-copy-button']"
|
||||
)
|
||||
expect(linkCopyButton).toBeInTheDocument()
|
||||
})
|
||||
test("renders answer thread item actions", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItemActions item={mockAnswerThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const wrapper = container.querySelector("div")
|
||||
expect(wrapper).toHaveClass("justify-between")
|
||||
const answerCopyButton = container.querySelector(
|
||||
"[data-testid='answer-copy-button']"
|
||||
)
|
||||
expect(answerCopyButton).toBeInTheDocument()
|
||||
const upvoteButton = container.querySelector(
|
||||
"[data-testid='upvote-button']"
|
||||
)
|
||||
expect(upvoteButton).toBeInTheDocument()
|
||||
const downvoteButton = container.querySelector(
|
||||
"[data-testid='downvote-button']"
|
||||
)
|
||||
expect(downvoteButton).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("question interactions", () => {
|
||||
test("clicking link copy button should copy the link", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItemActions item={mockQuestionThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const linkCopyButton = container.querySelector(
|
||||
"[data-testid='link-copy-button']"
|
||||
)
|
||||
expect(linkCopyButton).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(linkCopyButton!)
|
||||
expect(mockHandleCopy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("answer interactions", () => {
|
||||
test("clicking answer copy button should copy the answer", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItemActions item={mockAnswerThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const answerCopyButton = container.querySelector(
|
||||
"[data-testid='answer-copy-button']"
|
||||
)
|
||||
expect(answerCopyButton).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(answerCopyButton!)
|
||||
expect(mockHandleCopy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
test("clicking upvote button should upvote the answer", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItemActions item={mockAnswerThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const upvoteButton = container.querySelector(
|
||||
"[data-testid='upvote-button']"
|
||||
)
|
||||
expect(upvoteButton).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(upvoteButton!)
|
||||
expect(AiAssistantMocks.mockAddFeedback).toHaveBeenCalledTimes(1)
|
||||
expect(AiAssistantMocks.mockAddFeedback).toHaveBeenCalledWith(
|
||||
mockAnswerThreadItem.question_id,
|
||||
"upvote"
|
||||
)
|
||||
})
|
||||
test("clicking downvote button should downvote the answer", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItemActions item={mockAnswerThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const downvoteButton = container.querySelector(
|
||||
"[data-testid='downvote-button']"
|
||||
)
|
||||
expect(downvoteButton).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(downvoteButton!)
|
||||
expect(AiAssistantMocks.mockAddFeedback).toHaveBeenCalledTimes(1)
|
||||
expect(AiAssistantMocks.mockAddFeedback).toHaveBeenCalledWith(
|
||||
mockAnswerThreadItem.question_id,
|
||||
"downvote"
|
||||
)
|
||||
})
|
||||
|
||||
test("downvote button should be hidden if the answer has been upvoted", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItemActions item={mockAnswerThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const downvoteButton = container.querySelector(
|
||||
"[data-testid='downvote-button']"
|
||||
)
|
||||
expect(downvoteButton).toBeInTheDocument()
|
||||
const upvoteButton = container.querySelector(
|
||||
"[data-testid='upvote-button']"
|
||||
)
|
||||
expect(upvoteButton).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(upvoteButton!)
|
||||
expect(downvoteButton).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("upvote button should be hidden if the answer has been downvoted", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItemActions item={mockAnswerThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const upvoteButton = container.querySelector(
|
||||
"[data-testid='upvote-button']"
|
||||
)
|
||||
expect(upvoteButton).toBeInTheDocument()
|
||||
const downvoteButton = container.querySelector(
|
||||
"[data-testid='downvote-button']"
|
||||
)
|
||||
expect(downvoteButton).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(downvoteButton!)
|
||||
expect(upvoteButton).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState } from "react"
|
||||
import clsx from "clsx"
|
||||
import { Badge, Button, Link, type ButtonProps } from "@/components"
|
||||
import { Badge } from "@/components/Badge"
|
||||
import { Button, type ButtonProps } from "@/components/Button"
|
||||
import { Link } from "@/components/Link"
|
||||
import {
|
||||
ThumbDown,
|
||||
ThumbUp,
|
||||
@@ -8,12 +10,10 @@ import {
|
||||
CheckCircle,
|
||||
SquareTwoStack,
|
||||
} from "@medusajs/icons"
|
||||
import {
|
||||
AiAssistantThreadItem as AiAssistantThreadItemType,
|
||||
useSiteConfig,
|
||||
} from "../../../../providers"
|
||||
import { useSiteConfig } from "../../../../providers/SiteConfig"
|
||||
import { AiAssistantThreadItem as AiAssistantThreadItemType } from "../../../../providers/AiAssistant"
|
||||
import { Reaction, useChat } from "@kapaai/react-sdk"
|
||||
import { useCopy } from "../../../../hooks"
|
||||
import { useCopy } from "../../../../hooks/use-copy"
|
||||
|
||||
export type AiAssistantThreadItemActionsProps = {
|
||||
item: AiAssistantThreadItemType
|
||||
@@ -59,7 +59,7 @@ export const AiAssistantThreadItemActions = ({
|
||||
>
|
||||
{item.type === "question" && (
|
||||
<div className="flex gap-docs_0.25 items-center text-medusa-fg-muted">
|
||||
<ActionButton onClick={handleLinkCopy}>
|
||||
<ActionButton onClick={handleLinkCopy} data-testid="link-copy-button">
|
||||
{isLinkCopied ? <CheckCircle /> : <LinkIcon />}
|
||||
</ActionButton>
|
||||
</div>
|
||||
@@ -78,12 +78,16 @@ export const AiAssistantThreadItemActions = ({
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-docs_0.25 items-center text-medusa-fg-muted">
|
||||
<ActionButton onClick={handleAnswerCopy}>
|
||||
<ActionButton
|
||||
onClick={handleAnswerCopy}
|
||||
data-testid="answer-copy-button"
|
||||
>
|
||||
{isAnswerCopied ? <CheckCircle /> : <SquareTwoStack />}
|
||||
</ActionButton>
|
||||
{(feedback === null || feedback === "upvote") && (
|
||||
<ActionButton
|
||||
onClick={async () => handleFeedback("upvote", item.question_id)}
|
||||
data-testid="upvote-button"
|
||||
className={clsx(
|
||||
feedback === "upvote" && "!text-medusa-fg-muted"
|
||||
)}
|
||||
@@ -96,6 +100,7 @@ export const AiAssistantThreadItemActions = ({
|
||||
onClick={async () =>
|
||||
handleFeedback("downvote", item.question_id)
|
||||
}
|
||||
data-testid="downvote-button"
|
||||
className={clsx(
|
||||
feedback === "downvote" && "!text-medusa-fg-muted"
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import * as AiAssistantMocks from "../../__mocks__"
|
||||
import { AiAssistantThreadItem as AiAssistantThreadItemType } from "../../../../providers/AiAssistant"
|
||||
import { CodeMdxProps } from "../../../CodeMdx"
|
||||
|
||||
// mock data
|
||||
const mockQuestionThreadItem: AiAssistantThreadItemType = {
|
||||
type: "question",
|
||||
content: "test content",
|
||||
}
|
||||
const mockEmptyAnswerThreadItem: AiAssistantThreadItemType = {
|
||||
type: "answer",
|
||||
content: "",
|
||||
}
|
||||
const mockAnswerWithQuestionThreadItem: AiAssistantThreadItemType = {
|
||||
type: "answer",
|
||||
content: "test answer",
|
||||
question_id: "123",
|
||||
}
|
||||
const mockAnswerWithoutQuestionThreadItem: AiAssistantThreadItemType = {
|
||||
type: "answer",
|
||||
content: "test answer",
|
||||
}
|
||||
const mockErrorThreadItem: AiAssistantThreadItemType = {
|
||||
type: "error",
|
||||
content: "test error",
|
||||
}
|
||||
|
||||
// Mock components and hooks
|
||||
vi.mock("@kapaai/react-sdk", () => ({
|
||||
useChat: () => AiAssistantMocks.mockUseChat(),
|
||||
}))
|
||||
vi.mock("@/components/Icons/AiAssistant", () => ({
|
||||
AiAssistantIcon: () => <span>AiAssistantIcon</span>,
|
||||
}))
|
||||
vi.mock("@/components/CodeMdx", () => ({
|
||||
CodeMdx: (props: CodeMdxProps) => <code {...props} />,
|
||||
}))
|
||||
vi.mock("@/components/MarkdownContent", () => ({
|
||||
MarkdownContent: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/MDXComponents", () => ({
|
||||
MDXComponents: () => {
|
||||
return {}
|
||||
},
|
||||
}))
|
||||
vi.mock("@/components/AiAssistant/ThreadItem/Actions", () => ({
|
||||
AiAssistantThreadItemActions: () => <div>AiAssistantThreadItemActions</div>,
|
||||
}))
|
||||
vi.mock("@/components/Loading/Dots", () => ({
|
||||
DotsLoading: () => <div>DotsLoading</div>,
|
||||
}))
|
||||
|
||||
import { AiAssistantThreadItem } from "../../ThreadItem"
|
||||
|
||||
beforeEach(() => {
|
||||
AiAssistantMocks.mockConversation.length = 1
|
||||
AiAssistantMocks.mockAddFeedback.mockClear()
|
||||
AiAssistantMocks.mockSubmitQuery.mockClear()
|
||||
AiAssistantMocks.mockStopGeneration.mockClear()
|
||||
AiAssistantMocks.mockUseChat.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseChatReturn
|
||||
)
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders question thread item", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItem item={mockQuestionThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const wrapper = container.querySelector("div")
|
||||
expect(wrapper).toHaveClass("justify-end")
|
||||
const aiAssistantIcon = container.querySelector("span")
|
||||
expect(aiAssistantIcon).not.toBeInTheDocument()
|
||||
expect(container).toHaveTextContent(mockQuestionThreadItem.content)
|
||||
expect(container).toHaveTextContent("AiAssistantThreadItemActions")
|
||||
})
|
||||
test("renders answer thread item", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItem item={mockAnswerWithQuestionThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const wrapper = container.querySelector("div")
|
||||
expect(wrapper).toHaveClass("!pr-[20px]")
|
||||
const aiAssistantIcon = container.querySelector("span")
|
||||
expect(aiAssistantIcon).toBeInTheDocument()
|
||||
expect(aiAssistantIcon).toHaveTextContent("AiAssistantIcon")
|
||||
expect(container).toHaveTextContent(
|
||||
mockAnswerWithQuestionThreadItem.content
|
||||
)
|
||||
expect(container).toHaveTextContent("AiAssistantThreadItemActions")
|
||||
})
|
||||
test("renders answer thread item without actions if answer has no question_id", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItem item={mockAnswerWithoutQuestionThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container).not.toHaveTextContent("AiAssistantThreadItemActions")
|
||||
})
|
||||
test("renders error thread item", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItem item={mockErrorThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const aiAssistantIcon = container.querySelector("span")
|
||||
expect(aiAssistantIcon).toBeInTheDocument()
|
||||
expect(aiAssistantIcon).toHaveTextContent("AiAssistantIcon")
|
||||
const span = container.querySelector("span.text-medusa-fg-error")
|
||||
expect(span).toBeInTheDocument()
|
||||
expect(span).toHaveTextContent(mockErrorThreadItem.content)
|
||||
})
|
||||
})
|
||||
|
||||
describe("loading", () => {
|
||||
test("shows loading when answer has no question_id and no content", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItem item={mockEmptyAnswerThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container).toHaveTextContent("DotsLoading")
|
||||
})
|
||||
test("hide loading when answer has question_id and no content", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItem
|
||||
item={{
|
||||
...mockAnswerWithQuestionThreadItem,
|
||||
content: "",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container).not.toHaveTextContent("DotsLoading")
|
||||
})
|
||||
test("hide loading when answer has content", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItem item={mockAnswerWithoutQuestionThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container).not.toHaveTextContent("DotsLoading")
|
||||
})
|
||||
test("hide loading when error is not empty", () => {
|
||||
const { container } = render(
|
||||
<AiAssistantThreadItem item={mockErrorThreadItem} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container).not.toHaveTextContent("DotsLoading")
|
||||
})
|
||||
})
|
||||
@@ -1,15 +1,12 @@
|
||||
import clsx from "clsx"
|
||||
import React, { useMemo } from "react"
|
||||
import {
|
||||
AiAssistantIcon,
|
||||
CodeMdx,
|
||||
CodeMdxProps,
|
||||
DotsLoading,
|
||||
MarkdownContent,
|
||||
MDXComponents,
|
||||
} from "@/components"
|
||||
import { AiAssistantIcon } from "../../Icons/AiAssistant"
|
||||
import { CodeMdx, CodeMdxProps } from "../../CodeMdx"
|
||||
import { DotsLoading } from "../../Loading/Dots"
|
||||
import { MarkdownContent } from "../../MarkdownContent"
|
||||
import { MDXComponents } from "../../MDXComponents"
|
||||
import { AiAssistantThreadItemActions } from "./Actions"
|
||||
import { AiAssistantThreadItem as AiAssistantThreadItemType } from "../../../providers"
|
||||
import { AiAssistantThreadItem as AiAssistantThreadItemType } from "../../../providers/AiAssistant"
|
||||
import { useChat } from "@kapaai/react-sdk"
|
||||
|
||||
export type AiAssistantThreadItemProps = {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import * as AiAssistantMocks from "../../__mocks__"
|
||||
import { TooltipProps } from "../../../Tooltip"
|
||||
import { ButtonProps } from "../../../Button"
|
||||
import { KbdProps } from "../../../Kbd"
|
||||
|
||||
// mock functions
|
||||
const mockSetIsSearchOpen = vi.fn()
|
||||
const mockGetOsShortcut = vi.fn(() => "Ctrl")
|
||||
const defaultUseKeyboardShortcutReturn = {
|
||||
metakey: true,
|
||||
shortcutKeys: ["i"],
|
||||
action: vi.fn(),
|
||||
checkEditing: false,
|
||||
}
|
||||
const mockUseKeyboardShortcut = vi.fn(() => defaultUseKeyboardShortcutReturn)
|
||||
|
||||
// Mock components and hooks
|
||||
vi.mock("@/components/Button", () => ({
|
||||
Button: (props: ButtonProps) => <button {...props} />,
|
||||
}))
|
||||
vi.mock("@/components/Tooltip", () => ({
|
||||
Tooltip: (props: TooltipProps) => (
|
||||
<div>
|
||||
{/* @ts-expect-error - props.render is not typed properly */}
|
||||
<div>{props.render?.()}</div>
|
||||
<div>{props.tooltipChildren}</div>
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/Kbd", () => ({
|
||||
Kbd: (props: KbdProps) => <kbd {...props} />,
|
||||
}))
|
||||
vi.mock("@/providers/AiAssistant", () => ({
|
||||
useAiAssistant: () => AiAssistantMocks.mockUseAiAssistant(),
|
||||
}))
|
||||
vi.mock("@/providers/Search", () => ({
|
||||
useSearch: () => {
|
||||
return {
|
||||
setIsOpen: mockSetIsSearchOpen,
|
||||
}
|
||||
},
|
||||
}))
|
||||
vi.mock("@/providers/SiteConfig", () => ({
|
||||
useSiteConfig: () => {
|
||||
return {
|
||||
config: {
|
||||
basePath: "https://docs.medusajs.com",
|
||||
},
|
||||
}
|
||||
},
|
||||
}))
|
||||
vi.mock("@/utils/os-browser-utils", () => ({
|
||||
getOsShortcut: () => mockGetOsShortcut(),
|
||||
}))
|
||||
vi.mock("@/hooks/use-keyboard-shortcut", () => ({
|
||||
useKeyboardShortcut: () => mockUseKeyboardShortcut(),
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
mockSetIsSearchOpen.mockClear()
|
||||
mockGetOsShortcut.mockClear()
|
||||
mockUseKeyboardShortcut.mockReturnValue(defaultUseKeyboardShortcutReturn)
|
||||
AiAssistantMocks.mockSetChatOpened.mockClear()
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseAiAssistantReturn
|
||||
)
|
||||
})
|
||||
|
||||
import { AiAssistantTriggerButton } from "../../TriggerButton"
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders the trigger button", () => {
|
||||
const { container } = render(<AiAssistantTriggerButton />)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container).toHaveTextContent("Ask AI")
|
||||
})
|
||||
test("renders the trigger button with correct OS shortcut", () => {
|
||||
const { container } = render(<AiAssistantTriggerButton />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const kbd = container.querySelectorAll("kbd")
|
||||
expect(kbd.length).toBe(2)
|
||||
expect(kbd[0]).toBeInTheDocument()
|
||||
expect(kbd[0]).toHaveTextContent("Ctrl")
|
||||
expect(kbd[1]).toBeInTheDocument()
|
||||
expect(kbd[1]).toHaveTextContent("i")
|
||||
})
|
||||
})
|
||||
|
||||
describe("interactions", () => {
|
||||
test("clicking the trigger button should toggle the chat", () => {
|
||||
const { container } = render(<AiAssistantTriggerButton />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(button!)
|
||||
expect(AiAssistantMocks.mockSetChatOpened).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -4,9 +4,11 @@ import React from "react"
|
||||
import { Button } from "../../Button"
|
||||
import { Tooltip } from "../../Tooltip"
|
||||
import { Kbd } from "../../Kbd"
|
||||
import { getOsShortcut } from "../../../utils"
|
||||
import { useAiAssistant, useSearch, useSiteConfig } from "../../../providers"
|
||||
import { getOsShortcut } from "../../../utils/os-browser-utils"
|
||||
import { useAiAssistant } from "../../../providers/AiAssistant"
|
||||
import { useKeyboardShortcut } from "../../../hooks"
|
||||
import { useSearch } from "../../../providers/Search"
|
||||
import { useSiteConfig } from "../../../providers/SiteConfig"
|
||||
import Image from "next/image"
|
||||
|
||||
const AI_ASSISTANT_ICON_ACTIVE = "/images/ai-assistent.png"
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import React from "react"
|
||||
import { vi } from "vitest"
|
||||
|
||||
// Mock functions
|
||||
const mockSetChatOpened = vi.fn()
|
||||
const mockTrack = vi.fn()
|
||||
const mockGetChatWindowElm = vi.fn()
|
||||
const mockGetInputElm = vi.fn()
|
||||
const mockFocusInput = vi.fn()
|
||||
const mockSubmitQuery = vi.fn()
|
||||
const mockGetLatest = vi.fn(() => ({
|
||||
question: "last question",
|
||||
}))
|
||||
const mockAddFeedback = vi.fn()
|
||||
|
||||
// Create a mock conversation that behaves like an array
|
||||
const createMockConversation = () => {
|
||||
const conversationItems: Array<{
|
||||
question: string
|
||||
answer: string
|
||||
id: string
|
||||
sources?: unknown[]
|
||||
isGenerationAborted?: boolean
|
||||
}> = []
|
||||
|
||||
return {
|
||||
get length() {
|
||||
return conversationItems.length
|
||||
},
|
||||
set length(value: number) {
|
||||
// When length is set, update the items array
|
||||
if (value > conversationItems.length) {
|
||||
// Add new items
|
||||
for (let i = conversationItems.length; i < value; i++) {
|
||||
conversationItems.push({
|
||||
question: `Question ${i + 1}`,
|
||||
answer: `Answer ${i + 1}`,
|
||||
id: `id-${i + 1}`,
|
||||
sources: [],
|
||||
isGenerationAborted: false,
|
||||
})
|
||||
}
|
||||
} else if (value < conversationItems.length) {
|
||||
// Remove items
|
||||
conversationItems.splice(value)
|
||||
}
|
||||
},
|
||||
getLatest: mockGetLatest,
|
||||
map(callback: (item: unknown, index: number) => React.ReactNode) {
|
||||
return conversationItems.map(callback)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const mockConversation = createMockConversation()
|
||||
// Initialize with default length
|
||||
mockConversation.length = 1
|
||||
const mockStopGeneration = vi.fn()
|
||||
const mockToggle = vi.fn()
|
||||
const defaultUseAiAssistantReturn = {
|
||||
chatOpened: true,
|
||||
inputRef: React.createRef<HTMLTextAreaElement>(),
|
||||
contentRef: React.createRef<HTMLDivElement>(),
|
||||
loading: false,
|
||||
setChatOpened: mockSetChatOpened,
|
||||
isCaptchaLoaded: true,
|
||||
}
|
||||
|
||||
const defaultUseDeepThinkingReturn = {
|
||||
active: false,
|
||||
toggle: mockToggle,
|
||||
}
|
||||
const defaultUseChatReturn = {
|
||||
conversation: mockConversation,
|
||||
error: "",
|
||||
submitQuery: mockSubmitQuery,
|
||||
stopGeneration: mockStopGeneration,
|
||||
addFeedback: mockAddFeedback,
|
||||
isGeneratingAnswer: false,
|
||||
isPreparingAnswer: false,
|
||||
}
|
||||
const mockUseAiAssistant = vi.fn(() => defaultUseAiAssistantReturn)
|
||||
const mockUseDeepThinking = vi.fn(() => defaultUseDeepThinkingReturn)
|
||||
const mockUseChat = vi.fn(() => defaultUseChatReturn)
|
||||
|
||||
export {
|
||||
mockSetChatOpened,
|
||||
mockTrack,
|
||||
mockGetChatWindowElm,
|
||||
mockGetInputElm,
|
||||
mockFocusInput,
|
||||
mockSubmitQuery,
|
||||
mockGetLatest,
|
||||
mockConversation,
|
||||
mockStopGeneration,
|
||||
mockToggle,
|
||||
mockUseAiAssistant,
|
||||
mockUseDeepThinking,
|
||||
mockUseChat,
|
||||
mockAddFeedback,
|
||||
defaultUseAiAssistantReturn,
|
||||
defaultUseDeepThinkingReturn,
|
||||
defaultUseChatReturn,
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import { ApiRunnerParamInputProps } from "../../Default"
|
||||
import { ButtonProps } from "../../../../Button"
|
||||
|
||||
// mock data
|
||||
const mockApiRunnerParamArrayInput: ApiRunnerParamInputProps = {
|
||||
paramName: "test",
|
||||
paramValue: ["test"],
|
||||
objPath: "test",
|
||||
setValue: vi.fn(),
|
||||
}
|
||||
const mockApiRunnerParamArrayInput2: ApiRunnerParamInputProps = {
|
||||
paramName: "test",
|
||||
paramValue: ["test", "test2"],
|
||||
objPath: "test",
|
||||
setValue: vi.fn(),
|
||||
}
|
||||
const mockApiRunnerNotArrayInput: ApiRunnerParamInputProps = {
|
||||
paramName: "test",
|
||||
paramValue: "test",
|
||||
objPath: "test",
|
||||
setValue: vi.fn(),
|
||||
}
|
||||
|
||||
// mock components
|
||||
vi.mock("@/components/Button", () => ({
|
||||
Button: (props: ButtonProps) => <button {...props} />,
|
||||
}))
|
||||
vi.mock("@/components/ApiRunner/ParamInputs/Default", () => ({
|
||||
ApiRunnerParamInput: () => (
|
||||
<div className="api-runner-param-input">ApiRunnerParamInput</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/utils/set-obj-value", () => ({
|
||||
setObjValue: vi.fn(),
|
||||
}))
|
||||
|
||||
import { ApiRunnerParamArrayInput } from "../../Array"
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders when param value is an array", () => {
|
||||
const { container } = render(
|
||||
<ApiRunnerParamArrayInput {...mockApiRunnerParamArrayInput} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const fieldset = container.querySelector("fieldset")
|
||||
expect(fieldset).toBeInTheDocument()
|
||||
const legend = fieldset?.querySelector("legend")
|
||||
expect(legend).toBeInTheDocument()
|
||||
expect(legend).toHaveTextContent("test Array Items")
|
||||
expect(fieldset).toHaveTextContent("ApiRunnerParamInput")
|
||||
const button = fieldset?.querySelector("button[data-testid='minus-button']")
|
||||
expect(button).not.toBeInTheDocument()
|
||||
const plusButton = fieldset?.querySelector(
|
||||
"button[data-testid='plus-button']"
|
||||
)
|
||||
expect(plusButton).toBeInTheDocument()
|
||||
const svg = plusButton?.querySelector("svg")
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
test("renders minus button when param value is an array and has more than one item", () => {
|
||||
const { container } = render(
|
||||
<ApiRunnerParamArrayInput {...mockApiRunnerParamArrayInput2} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const fieldset = container.querySelector("fieldset")
|
||||
expect(fieldset).toBeInTheDocument()
|
||||
const button = fieldset?.querySelector("button[data-testid='minus-button']")
|
||||
expect(button).toBeInTheDocument()
|
||||
const svg = button?.querySelector("svg")
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
test("doesn't render when param value is not an array", () => {
|
||||
const { container } = render(
|
||||
<ApiRunnerParamArrayInput {...mockApiRunnerNotArrayInput} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const fieldset = container.querySelector("fieldset")
|
||||
expect(fieldset).not.toBeInTheDocument()
|
||||
expect(container).toHaveTextContent("ApiRunnerParamInput")
|
||||
})
|
||||
})
|
||||
|
||||
describe("interactions", () => {
|
||||
test("clicking minus button should remove item from array", () => {
|
||||
const { container } = render(
|
||||
<ApiRunnerParamArrayInput {...mockApiRunnerParamArrayInput2} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const fieldset = container.querySelector("fieldset")
|
||||
expect(fieldset).toBeInTheDocument()
|
||||
const button = fieldset?.querySelector("button[data-testid='minus-button']")
|
||||
expect(button).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(button!)
|
||||
const apiRunnerParamInput = container.querySelectorAll(
|
||||
".api-runner-param-input"
|
||||
)
|
||||
expect(apiRunnerParamInput).toHaveLength(1)
|
||||
})
|
||||
test("clicking plus button should add item to array", () => {
|
||||
const { container } = render(
|
||||
<ApiRunnerParamArrayInput {...mockApiRunnerParamArrayInput} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const fieldset = container.querySelector("fieldset")
|
||||
expect(fieldset).toBeInTheDocument()
|
||||
const plusButton = fieldset?.querySelector(
|
||||
"button[data-testid='plus-button']"
|
||||
)
|
||||
expect(plusButton).toBeInTheDocument()
|
||||
const initialApiRunnerParamInput = container.querySelectorAll(
|
||||
".api-runner-param-input"
|
||||
)
|
||||
expect(initialApiRunnerParamInput).toHaveLength(1)
|
||||
|
||||
fireEvent.click(plusButton!)
|
||||
const updatedApiRunnerParamInput = container.querySelectorAll(
|
||||
".api-runner-param-input"
|
||||
)
|
||||
expect(updatedApiRunnerParamInput).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react"
|
||||
import { ApiRunnerParamInput, ApiRunnerParamInputProps } from "../Default"
|
||||
import clsx from "clsx"
|
||||
import setObjValue from "@/utils/set-obj-value"
|
||||
import { Button } from "../../../.."
|
||||
import { Button } from "../../../../components/Button"
|
||||
import { Minus, Plus } from "@medusajs/icons"
|
||||
|
||||
export const ApiRunnerParamArrayInput = ({
|
||||
@@ -17,7 +17,7 @@ export const ApiRunnerParamArrayInput = ({
|
||||
|
||||
useEffect(() => {
|
||||
setValue((prev: unknown) => {
|
||||
return typeof prev === "object"
|
||||
return typeof prev === "object" && !Array.isArray(prev)
|
||||
? setObjValue({
|
||||
obj: { ...prev },
|
||||
value: itemsValue,
|
||||
@@ -70,6 +70,7 @@ export const ApiRunnerParamArrayInput = ({
|
||||
setItemsValue((prev: unknown[]) => prev.splice(index, 1))
|
||||
}}
|
||||
className="mt-0.5"
|
||||
data-testid="minus-button"
|
||||
>
|
||||
<Minus />
|
||||
</Button>
|
||||
@@ -90,6 +91,7 @@ export const ApiRunnerParamArrayInput = ({
|
||||
])
|
||||
}}
|
||||
className="mt-0.5"
|
||||
data-testid="plus-button"
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import { ApiRunnerParamInputProps } from "../index"
|
||||
import { InputTextProps } from "../../../../Input/Text"
|
||||
|
||||
// mock data
|
||||
const mockApiRunnerParamInput: ApiRunnerParamInputProps = {
|
||||
paramName: "testName",
|
||||
paramValue: "testValue",
|
||||
objPath: "test",
|
||||
setValue: vi.fn(),
|
||||
}
|
||||
const mockApiRunnerParamArrayInput: ApiRunnerParamInputProps = {
|
||||
paramName: "testName",
|
||||
paramValue: ["testValue"],
|
||||
objPath: "",
|
||||
setValue: vi.fn(),
|
||||
}
|
||||
const mockApiRunnerParamObjectInput: ApiRunnerParamInputProps = {
|
||||
paramName: "",
|
||||
paramValue: { test: "testValue" },
|
||||
objPath: "",
|
||||
setValue: vi.fn(),
|
||||
}
|
||||
|
||||
// mock components
|
||||
vi.mock("@/components/Input/Text", () => ({
|
||||
InputText: (props: InputTextProps) => <input {...props} />,
|
||||
}))
|
||||
|
||||
const TestWrapper = (props: ApiRunnerParamInputProps) => {
|
||||
const [value, setValue] = React.useState(props.paramValue)
|
||||
return (
|
||||
<ApiRunnerParamInput {...props} paramValue={value} setValue={setValue} />
|
||||
)
|
||||
}
|
||||
|
||||
import { ApiRunnerParamInput } from "../index"
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders when param value is a string", () => {
|
||||
const { container } = render(
|
||||
<ApiRunnerParamInput {...mockApiRunnerParamInput} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const input = container.querySelector("input")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveAttribute("name", "testName")
|
||||
expect(input).toHaveValue("testValue")
|
||||
})
|
||||
test("renders when param value is an array", () => {
|
||||
const { container } = render(
|
||||
<ApiRunnerParamInput {...mockApiRunnerParamArrayInput} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const input = container.querySelector("input")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("testValue")
|
||||
})
|
||||
test("renders when param value is an object", () => {
|
||||
const { container } = render(
|
||||
<ApiRunnerParamInput {...mockApiRunnerParamObjectInput} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const input = container.querySelector("input")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("testValue")
|
||||
})
|
||||
})
|
||||
|
||||
describe("interactions", () => {
|
||||
test("typing input for string value should update param value", () => {
|
||||
const { container } = render(<TestWrapper {...mockApiRunnerParamInput} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const input = container.querySelector("input")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("testValue")
|
||||
|
||||
fireEvent.change(input!, { target: { value: "testValue2" } })
|
||||
expect(input).toHaveValue("testValue2")
|
||||
})
|
||||
|
||||
test("typing input for array item should update array value", () => {
|
||||
const { container } = render(
|
||||
<TestWrapper {...mockApiRunnerParamArrayInput} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const input = container.querySelector("input")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("testValue")
|
||||
|
||||
fireEvent.change(input!, { target: { value: "testValue2" } })
|
||||
expect(input).toHaveValue("testValue2")
|
||||
})
|
||||
|
||||
test("typing input for object property should update object value", () => {
|
||||
const { container } = render(
|
||||
<TestWrapper {...mockApiRunnerParamObjectInput} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const input = container.querySelector("input")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("testValue")
|
||||
|
||||
fireEvent.change(input!, { target: { value: "testValue2" } })
|
||||
expect(input).toHaveValue("testValue2")
|
||||
})
|
||||
})
|
||||
|
||||
describe("object path handling", () => {
|
||||
test("should update nested object property with objPath", () => {
|
||||
const { container } = render(
|
||||
<TestWrapper
|
||||
paramName=""
|
||||
paramValue={{
|
||||
parent: {
|
||||
child: "initialValue",
|
||||
},
|
||||
}}
|
||||
objPath=""
|
||||
setValue={vi.fn()}
|
||||
/>
|
||||
)
|
||||
// Find the input for the "child" property (nested under "parent")
|
||||
const inputs = container.querySelectorAll("input")
|
||||
const input = Array.from(inputs).find(
|
||||
(input) => input.getAttribute("name") === "child"
|
||||
)
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("initialValue")
|
||||
|
||||
fireEvent.change(input!, { target: { value: "updatedValue" } })
|
||||
expect(input).toHaveValue("updatedValue")
|
||||
})
|
||||
|
||||
test("should update deeply nested object property", () => {
|
||||
const { container } = render(
|
||||
<TestWrapper
|
||||
paramName=""
|
||||
paramValue={{
|
||||
level1: {
|
||||
level2: {
|
||||
level3: "deepValue",
|
||||
},
|
||||
},
|
||||
}}
|
||||
objPath=""
|
||||
setValue={vi.fn()}
|
||||
/>
|
||||
)
|
||||
// Find the input for the "level3" property (deeply nested)
|
||||
const inputs = container.querySelectorAll("input")
|
||||
const input = Array.from(inputs).find(
|
||||
(input) => input.getAttribute("name") === "level3"
|
||||
)
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("deepValue")
|
||||
|
||||
fireEvent.change(input!, { target: { value: "newDeepValue" } })
|
||||
expect(input).toHaveValue("newDeepValue")
|
||||
})
|
||||
|
||||
test("should update object property within array", () => {
|
||||
const { container } = render(
|
||||
<TestWrapper
|
||||
paramName="testName"
|
||||
paramValue={[{ name: "item1", value: "value1" }]}
|
||||
objPath=""
|
||||
setValue={vi.fn()}
|
||||
/>
|
||||
)
|
||||
// Find the input for the "value" property within the object in the array
|
||||
const inputs = container.querySelectorAll("input")
|
||||
expect(inputs.length).toBeGreaterThan(0)
|
||||
const valueInput = Array.from(inputs).find(
|
||||
(input) => input.getAttribute("name") === "value"
|
||||
)
|
||||
expect(valueInput).toBeInTheDocument()
|
||||
expect(valueInput).toHaveValue("value1")
|
||||
|
||||
fireEvent.change(valueInput!, { target: { value: "updatedValue1" } })
|
||||
expect(valueInput).toHaveValue("updatedValue1")
|
||||
})
|
||||
|
||||
test("should update array item with objPath", () => {
|
||||
const { container } = render(
|
||||
<TestWrapper
|
||||
paramName="items"
|
||||
paramValue={{ items: ["item1", "item2"] }}
|
||||
objPath=""
|
||||
setValue={vi.fn()}
|
||||
/>
|
||||
)
|
||||
// Find the input for the first array item (paramName will be "[0]")
|
||||
const inputs = container.querySelectorAll("input")
|
||||
expect(inputs.length).toBeGreaterThan(0)
|
||||
// The first input should be for the first array item
|
||||
const firstInput = inputs[0]
|
||||
expect(firstInput).toHaveValue("item1")
|
||||
|
||||
fireEvent.change(firstInput, { target: { value: "updatedItem1" } })
|
||||
expect(firstInput).toHaveValue("updatedItem1")
|
||||
})
|
||||
|
||||
test("should update nested object property within array", () => {
|
||||
const { container } = render(
|
||||
<TestWrapper
|
||||
paramName="data"
|
||||
paramValue={{
|
||||
data: [
|
||||
{ id: 1, name: "first" },
|
||||
{ id: 2, name: "second" },
|
||||
],
|
||||
}}
|
||||
objPath=""
|
||||
setValue={vi.fn()}
|
||||
/>
|
||||
)
|
||||
// Find the input for the name property of the first object in the array
|
||||
const inputs = container.querySelectorAll("input")
|
||||
expect(inputs.length).toBeGreaterThan(0)
|
||||
// The name input should be one of the inputs
|
||||
const nameInput = Array.from(inputs).find(
|
||||
(input) => input.getAttribute("name") === "name"
|
||||
)
|
||||
expect(nameInput).toBeInTheDocument()
|
||||
expect(nameInput).toHaveValue("first")
|
||||
|
||||
fireEvent.change(nameInput!, { target: { value: "updatedFirst" } })
|
||||
expect(nameInput).toHaveValue("updatedFirst")
|
||||
})
|
||||
|
||||
test("should handle empty objPath correctly", () => {
|
||||
const { container } = render(
|
||||
<TestWrapper
|
||||
paramName=""
|
||||
paramValue={{ root: "rootValue" }}
|
||||
objPath=""
|
||||
setValue={vi.fn()}
|
||||
/>
|
||||
)
|
||||
const input = container.querySelector("input")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("rootValue")
|
||||
|
||||
fireEvent.change(input!, { target: { value: "newRootValue" } })
|
||||
expect(input).toHaveValue("newRootValue")
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react"
|
||||
import { InputText } from "../../../.."
|
||||
import { InputText } from "../../../../components/Input/Text"
|
||||
import setObjValue from "../../../../utils/set-obj-value"
|
||||
import { ApiRunnerParamObjectInput } from "../Object"
|
||||
import { ApiRunnerParamArrayInput } from "../Array"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import { ApiRunnerParamInputProps } from "../../Default"
|
||||
|
||||
// mock data
|
||||
const mockApiRunnerParamObjectInput: ApiRunnerParamInputProps = {
|
||||
paramName: "",
|
||||
paramValue: { test: "testValue" },
|
||||
objPath: "",
|
||||
setValue: vi.fn(),
|
||||
}
|
||||
const mockApiRunnerParamObjectInput2: ApiRunnerParamInputProps = {
|
||||
paramName: "",
|
||||
paramValue: { test: "testValue", test2: "testValue2" },
|
||||
objPath: "",
|
||||
setValue: vi.fn(),
|
||||
}
|
||||
const mockApiRunnerNotObjectInput: ApiRunnerParamInputProps = {
|
||||
paramName: "testName",
|
||||
paramValue: "testValue",
|
||||
objPath: "",
|
||||
setValue: vi.fn(),
|
||||
}
|
||||
|
||||
// mock components
|
||||
vi.mock("@/components/ApiRunner/ParamInputs/Default", () => ({
|
||||
ApiRunnerParamInput: ({
|
||||
paramName,
|
||||
paramValue,
|
||||
objPath,
|
||||
}: ApiRunnerParamInputProps) => (
|
||||
<div className="api-runner-param-input">
|
||||
<span>ApiRunnerParamInput</span>
|
||||
<span>{paramName}</span>
|
||||
<span>{paramValue as string}</span>
|
||||
<span>{objPath}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
import { ApiRunnerParamObjectInput } from "../../Object"
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders when param value is an object", () => {
|
||||
const { container } = render(
|
||||
<ApiRunnerParamObjectInput {...mockApiRunnerParamObjectInput} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const fieldset = container.querySelector("fieldset")
|
||||
expect(fieldset).toBeInTheDocument()
|
||||
const apiRunnerParamInputs = fieldset?.querySelectorAll(
|
||||
".api-runner-param-input"
|
||||
)
|
||||
expect(apiRunnerParamInputs).toHaveLength(1)
|
||||
expect(apiRunnerParamInputs?.[0]).toHaveTextContent("ApiRunnerParamInput")
|
||||
expect(apiRunnerParamInputs?.[0]).toHaveTextContent("test")
|
||||
expect(apiRunnerParamInputs?.[0]).toHaveTextContent("testValue")
|
||||
})
|
||||
|
||||
test("renders when param value is an object with multiple properties", () => {
|
||||
const { container } = render(
|
||||
<ApiRunnerParamObjectInput {...mockApiRunnerParamObjectInput2} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const fieldset = container.querySelector("fieldset")
|
||||
expect(fieldset).toBeInTheDocument()
|
||||
const apiRunnerParamInputs = fieldset?.querySelectorAll(
|
||||
".api-runner-param-input"
|
||||
)
|
||||
expect(apiRunnerParamInputs).toHaveLength(2)
|
||||
expect(apiRunnerParamInputs?.[0]).toHaveTextContent("ApiRunnerParamInput")
|
||||
expect(apiRunnerParamInputs?.[0]).toHaveTextContent("test")
|
||||
expect(apiRunnerParamInputs?.[0]).toHaveTextContent("testValue")
|
||||
expect(apiRunnerParamInputs?.[1]).toHaveTextContent("ApiRunnerParamInput")
|
||||
expect(apiRunnerParamInputs?.[1]).toHaveTextContent("test2")
|
||||
expect(apiRunnerParamInputs?.[1]).toHaveTextContent("testValue2")
|
||||
})
|
||||
|
||||
test("renders when param value is not an object", () => {
|
||||
const { container } = render(
|
||||
<ApiRunnerParamObjectInput {...mockApiRunnerNotObjectInput} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container).toHaveTextContent("ApiRunnerParamInput")
|
||||
expect(container).toHaveTextContent("testName")
|
||||
expect(container).toHaveTextContent("testValue")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,233 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react"
|
||||
import { CodeBlockProps } from "../../CodeBlock"
|
||||
import { ButtonProps } from "../../Button"
|
||||
|
||||
// mock data
|
||||
const mockApiRunnerSimpleGet = {
|
||||
apiMethod: "GET" as ApiMethod,
|
||||
apiUrl: "https://api.example.com",
|
||||
}
|
||||
const mockApiRunnerGet = {
|
||||
apiMethod: "GET" as ApiMethod,
|
||||
apiUrl: "https://api.example.com",
|
||||
pathData: {
|
||||
id: "123",
|
||||
},
|
||||
queryData: {
|
||||
limit: 10,
|
||||
},
|
||||
}
|
||||
const mockApiRunnerPost = {
|
||||
apiMethod: "POST" as ApiMethod,
|
||||
apiUrl: "https://api.example.com",
|
||||
bodyData: {
|
||||
name: "John Doe",
|
||||
},
|
||||
}
|
||||
const mockApiRunnerDelete = {
|
||||
apiMethod: "DELETE" as ApiMethod,
|
||||
apiUrl: "https://api.example.com",
|
||||
pathData: {
|
||||
id: "123",
|
||||
},
|
||||
}
|
||||
|
||||
// mock functions
|
||||
let capturedOnFinish: ((message: string, statusCode: string) => void) | null =
|
||||
null
|
||||
|
||||
// mock components and hooks
|
||||
vi.mock("@/components/CodeBlock", () => ({
|
||||
CodeBlock: ({ badgeColor, badgeLabel, ...props }: CodeBlockProps) => (
|
||||
<div data-badge-color={badgeColor} data-badge-label={badgeLabel}>
|
||||
<pre {...props} />
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/Button", () => ({
|
||||
Button: (props: ButtonProps) => <button {...props} />,
|
||||
}))
|
||||
vi.mock("@/components/ApiRunner/ParamInputs", () => ({
|
||||
ApiRunnerParamInputs: () => <div>ApiRunnerParamInputs</div>,
|
||||
}))
|
||||
vi.mock("@/components/ApiRunner/FooterBackground", () => ({
|
||||
ApiRunnerFooterBackground: () => <div>ApiRunnerFooterBackground</div>,
|
||||
}))
|
||||
vi.mock("@/components/Icons/ArrowRightDown", () => ({
|
||||
ArrowRightDownIcon: () => <div>ArrowRightDownIcon</div>,
|
||||
}))
|
||||
vi.mock("@/hooks/use-request-runner", () => ({
|
||||
useRequestRunner: ({
|
||||
pushLog,
|
||||
onFinish,
|
||||
}: {
|
||||
pushLog: (...message: string[]) => void
|
||||
onFinish: (message: string, statusCode: string) => void
|
||||
replaceLog?: (message: string) => void
|
||||
}) => {
|
||||
capturedOnFinish = onFinish
|
||||
return {
|
||||
runRequest: vi.fn(() => {
|
||||
pushLog("Response data")
|
||||
onFinish("Finished running request.", "")
|
||||
}),
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
import { ApiRunner } from "../../ApiRunner"
|
||||
import { ApiMethod } from "types"
|
||||
|
||||
beforeEach(() => {
|
||||
capturedOnFinish = null
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders manual test trigger when there is data", () => {
|
||||
const { container } = render(<ApiRunner {...mockApiRunnerGet} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container).toHaveTextContent("ApiRunnerParamInputs")
|
||||
expect(container).toHaveTextContent("ApiRunnerFooterBackground")
|
||||
expect(container).toHaveTextContent("ArrowRightDownIcon")
|
||||
const submitButton = container.querySelector("button")
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
expect(submitButton).toHaveTextContent("Send Request")
|
||||
})
|
||||
test("renders code block when request is running or has been run", () => {
|
||||
const { container } = render(<ApiRunner {...mockApiRunnerGet} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const submitButton = container.querySelector("button")
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
fireEvent.click(submitButton!)
|
||||
const pre = container.querySelector("pre")
|
||||
expect(pre).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("interactions", () => {
|
||||
test("clicking submit button should trigger request", () => {
|
||||
const { container } = render(<ApiRunner {...mockApiRunnerGet} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const submitButton = container.querySelector("button")
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
fireEvent.click(submitButton!)
|
||||
// Verify the CodeBlock appears after clicking
|
||||
expect(container.querySelector("pre")).toBeInTheDocument()
|
||||
})
|
||||
test("clicking submit button should trigger POST request", () => {
|
||||
const { container } = render(<ApiRunner {...mockApiRunnerPost} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const submitButton = container.querySelector("button")
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
fireEvent.click(submitButton!)
|
||||
expect(container.querySelector("pre")).toBeInTheDocument()
|
||||
})
|
||||
test("clicking submit button should trigger DELETE request", () => {
|
||||
const { container } = render(<ApiRunner {...mockApiRunnerDelete} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const submitButton = container.querySelector("button")
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
fireEvent.click(submitButton!)
|
||||
expect(container.querySelector("pre")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("should send request automatically for simple GET request", () => {
|
||||
const { container } = render(<ApiRunner {...mockApiRunnerSimpleGet} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
// The request is automatically triggered, so we just verify the component renders
|
||||
expect(container.querySelector("pre")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("badge display", () => {
|
||||
test("should show green badge with status code for successful request (2xx)", async () => {
|
||||
const { container } = render(<ApiRunner {...mockApiRunnerGet} />)
|
||||
const submitButton = container.querySelector("button")
|
||||
fireEvent.click(submitButton!)
|
||||
|
||||
// Wait for the component to render the CodeBlock
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector("pre")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Simulate successful response by calling onFinish with 200 status code
|
||||
if (capturedOnFinish) {
|
||||
capturedOnFinish("Finished running request.", "200")
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
const codeBlock = container.querySelector("[data-badge-color]")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveAttribute("data-badge-color", "green")
|
||||
expect(codeBlock).toHaveAttribute("data-badge-label", "200")
|
||||
})
|
||||
})
|
||||
|
||||
test("should show red badge with status code for failed request (4xx)", async () => {
|
||||
const { container } = render(<ApiRunner {...mockApiRunnerGet} />)
|
||||
const submitButton = container.querySelector("button")
|
||||
fireEvent.click(submitButton!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector("pre")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Simulate failed response by calling onFinish with 404 status code
|
||||
if (capturedOnFinish) {
|
||||
capturedOnFinish("Finished running request.", "404")
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
const codeBlock = container.querySelector("[data-badge-color]")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveAttribute("data-badge-color", "red")
|
||||
expect(codeBlock).toHaveAttribute("data-badge-label", "404")
|
||||
})
|
||||
})
|
||||
|
||||
test("should show red badge with 'Failed' label when no status code", async () => {
|
||||
const { container } = render(<ApiRunner {...mockApiRunnerGet} />)
|
||||
const submitButton = container.querySelector("button")
|
||||
fireEvent.click(submitButton!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector("pre")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Simulate error with no status code (empty string)
|
||||
if (capturedOnFinish) {
|
||||
capturedOnFinish("Finished running request.", "")
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
const codeBlock = container.querySelector("[data-badge-color]")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveAttribute("data-badge-color", "red")
|
||||
expect(codeBlock).toHaveAttribute("data-badge-label", "Failed")
|
||||
})
|
||||
})
|
||||
|
||||
test("should show green badge for 201 status code", async () => {
|
||||
const { container } = render(<ApiRunner {...mockApiRunnerPost} />)
|
||||
const submitButton = container.querySelector("button")
|
||||
fireEvent.click(submitButton!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector("pre")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Simulate successful POST response with 201 status code
|
||||
if (capturedOnFinish) {
|
||||
capturedOnFinish("Finished running request.", "201")
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
const codeBlock = container.querySelector("[data-badge-color]")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveAttribute("data-badge-color", "green")
|
||||
expect(codeBlock).toHaveAttribute("data-badge-label", "201")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import React from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useRequestRunner } from "../../hooks"
|
||||
import { useRequestRunner } from "../../hooks/use-request-runner"
|
||||
import { CodeBlock } from "../CodeBlock"
|
||||
import { Button } from "../.."
|
||||
import { Button } from "@/components/Button"
|
||||
import { ApiMethod, ApiTestingOptions } from "types"
|
||||
import { ApiRunnerParamInputs } from "./ParamInputs"
|
||||
import clsx from "clsx"
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import React from "react"
|
||||
import { expect, test } from "vitest"
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import { Badge } from "../../Badge"
|
||||
|
||||
test("renders children", () => {
|
||||
render(<Badge variant="purple">Test Badge</Badge>)
|
||||
expect(screen.getByText("Test Badge")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("renders purple badge", () => {
|
||||
const { container } = render(<Badge variant="purple">Test Badge</Badge>)
|
||||
const badge = container.querySelector(".badge")
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge).toHaveClass(
|
||||
"bg-medusa-tag-purple-bg",
|
||||
"text-medusa-tag-purple-text",
|
||||
"border-medusa-tag-purple-border"
|
||||
)
|
||||
})
|
||||
|
||||
test("renders orange badge", () => {
|
||||
const { container } = render(<Badge variant="orange">Test Badge</Badge>)
|
||||
const badge = container.querySelector(".badge")
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge).toHaveClass(
|
||||
"bg-medusa-tag-orange-bg",
|
||||
"text-medusa-tag-orange-text",
|
||||
"border-medusa-tag-orange-border"
|
||||
)
|
||||
})
|
||||
|
||||
test("renders green badge", () => {
|
||||
const { container } = render(<Badge variant="green">Test Badge</Badge>)
|
||||
const badge = container.querySelector(".badge")
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge).toHaveClass(
|
||||
"bg-medusa-tag-green-bg",
|
||||
"text-medusa-tag-green-text",
|
||||
"border-medusa-tag-green-border"
|
||||
)
|
||||
})
|
||||
|
||||
test("renders blue badge", () => {
|
||||
const { container } = render(<Badge variant="blue">Test Badge</Badge>)
|
||||
const badge = container.querySelector(".badge")
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge).toHaveClass(
|
||||
"bg-medusa-tag-blue-bg",
|
||||
"text-medusa-tag-blue-text",
|
||||
"border-medusa-tag-blue-border"
|
||||
)
|
||||
})
|
||||
|
||||
test("renders red badge", () => {
|
||||
const { container } = render(<Badge variant="red">Test Badge</Badge>)
|
||||
const badge = container.querySelector(".badge")
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge).toHaveClass(
|
||||
"bg-medusa-tag-red-bg",
|
||||
"text-medusa-tag-red-text",
|
||||
"border-medusa-tag-red-border"
|
||||
)
|
||||
})
|
||||
|
||||
test("renders neutral badge", () => {
|
||||
const { container } = render(<Badge variant="neutral">Test Badge</Badge>)
|
||||
const badge = container.querySelector(".badge")
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge).toHaveClass(
|
||||
"bg-medusa-tag-neutral-bg",
|
||||
"text-medusa-tag-neutral-text",
|
||||
"border-medusa-tag-neutral-border"
|
||||
)
|
||||
})
|
||||
|
||||
test("renders code badge", () => {
|
||||
const { container } = render(<Badge variant="code">Test Badge</Badge>)
|
||||
const badge = container.querySelector(".badge")
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge).toHaveClass(
|
||||
"bg-medusa-contrast-bg-subtle",
|
||||
"text-medusa-contrast-fg-secondary",
|
||||
"border-medusa-contrast-border-bot"
|
||||
)
|
||||
})
|
||||
|
||||
test("renders shaded badge", () => {
|
||||
const { container } = render(
|
||||
<Badge variant="purple" badgeType="shaded">
|
||||
Test Badge
|
||||
</Badge>
|
||||
)
|
||||
const badge = container.querySelector(".badge")
|
||||
const shadedBgIcon = container.querySelector("svg")
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge).toHaveClass("px-[3px]", "!bg-transparent", "relative")
|
||||
expect(shadedBgIcon).toBeInTheDocument()
|
||||
expect(shadedBgIcon).toHaveClass(
|
||||
"absolute",
|
||||
"top-0",
|
||||
"left-0",
|
||||
"w-full",
|
||||
"h-full"
|
||||
)
|
||||
const rect = shadedBgIcon?.querySelector("rect")
|
||||
expect(rect).toBeInTheDocument()
|
||||
expect(rect).toHaveAttribute("fill", "var(--docs-tags-purple-border)")
|
||||
})
|
||||
|
||||
test("doesn't render shaded badge if badgeType is not shaded", () => {
|
||||
const { container } = render(<Badge variant="purple">Test Badge</Badge>)
|
||||
const badge = container.querySelector(".badge")
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge).not.toHaveClass("px-[3px]", "!bg-transparent", "relative")
|
||||
const shadedBgIcon = container.querySelector("svg")
|
||||
expect(shadedBgIcon).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("renders with correct class name", () => {
|
||||
const { container } = render(
|
||||
<Badge variant="purple" className="test-class">
|
||||
Test Badge
|
||||
</Badge>
|
||||
)
|
||||
const badge = container.querySelector(".test-class")
|
||||
expect(badge).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("renders children wrapper with correct class name", () => {
|
||||
const { container } = render(
|
||||
<Badge variant="purple" childrenWrapperClassName="test-class">
|
||||
Test Badge
|
||||
</Badge>
|
||||
)
|
||||
const childrenWrapper = container.querySelector(".test-class")
|
||||
expect(childrenWrapper).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("passes HTML attributes to the badge", () => {
|
||||
const { container } = render(
|
||||
<Badge variant="purple" data-testid="test-id">
|
||||
Test Badge
|
||||
</Badge>
|
||||
)
|
||||
const badge = container.querySelector("[data-testid='test-id']")
|
||||
expect(badge).toBeInTheDocument()
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
import { ShadedBgIcon } from "../.."
|
||||
import { ShadedBgIcon } from "../Icons/ShadedBg"
|
||||
|
||||
export type BadgeVariant =
|
||||
| "purple"
|
||||
@@ -26,6 +26,7 @@ export const Badge = ({
|
||||
badgeType = "default",
|
||||
children,
|
||||
childrenWrapperClassName,
|
||||
...props
|
||||
}: BadgeProps) => {
|
||||
return (
|
||||
<span
|
||||
@@ -52,6 +53,7 @@ export const Badge = ({
|
||||
"badge",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{badgeType === "shaded" && (
|
||||
<ShadedBgIcon
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react"
|
||||
import { Badge, Tooltip } from "../.."
|
||||
import { Badge } from "@/components/Badge"
|
||||
import { Tooltip } from "@/components/Tooltip"
|
||||
|
||||
type BetaBadgeProps = {
|
||||
text?: string
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import { BorderedIcon } from ".."
|
||||
|
||||
const TestIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
data-testid="test-icon"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M12 2L2 22h20L12 2Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const exampleDataImageUrl =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAACV0lEQVR4nO2Wz2/SURjH/72769q9u9N7757e5f3oYsQgL2p4EUNFURFFEUVxU1ExYiD6gMREjEFE4qJiEBFRERERETGQmD/O/9/H/T8O93rXm+c5zzkzZ855zpnB3Tt37gQAAAAAAABg6U8AADwA4wEAAAAASUVORK5CYII="
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders image when provided as icon", () => {
|
||||
const { container } = render(<BorderedIcon icon={exampleDataImageUrl} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const image = container.querySelector("img")
|
||||
expect(image).toBeInTheDocument()
|
||||
expect(image).toHaveAttribute("src", exampleDataImageUrl)
|
||||
expect(image).toHaveAttribute("alt", "")
|
||||
expect(image).toHaveClass("bordered-icon")
|
||||
expect(image).toHaveClass("rounded-docs_xs")
|
||||
expect(image).toHaveAttribute("width", "28")
|
||||
expect(image).toHaveAttribute("height", "28")
|
||||
})
|
||||
|
||||
test("renders icon component when provided as IconComponent", () => {
|
||||
const { container } = render(<BorderedIcon IconComponent={TestIcon} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const icon = container.querySelector("svg")
|
||||
expect(icon).toBeInTheDocument()
|
||||
expect(icon).toHaveClass("bordered-icon")
|
||||
expect(icon).toHaveClass("rounded-docs_xs")
|
||||
expect(icon).toHaveClass("text-medusa-fg-subtle")
|
||||
})
|
||||
|
||||
test("render IconComponent even when icon is provided", () => {
|
||||
const { container } = render(
|
||||
<BorderedIcon icon={exampleDataImageUrl} IconComponent={TestIcon} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const icon = container.querySelector("svg")
|
||||
expect(icon).toBeInTheDocument()
|
||||
expect(icon).toHaveClass("bordered-icon")
|
||||
expect(icon).toHaveClass("rounded-docs_xs")
|
||||
expect(icon).toHaveClass("text-medusa-fg-subtle")
|
||||
const image = container.querySelector("img")
|
||||
expect(image).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render icon with custom width and height", () => {
|
||||
const { container } = render(
|
||||
<BorderedIcon icon={exampleDataImageUrl} iconWidth={32} iconHeight={32} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const image = container.querySelector("img")
|
||||
expect(image).toBeInTheDocument()
|
||||
expect(image).toHaveAttribute("width", "32")
|
||||
expect(image).toHaveAttribute("height", "32")
|
||||
})
|
||||
|
||||
test("render icon with custom icon wrapper className", () => {
|
||||
const { container } = render(
|
||||
<BorderedIcon
|
||||
icon={exampleDataImageUrl}
|
||||
iconWrapperClassName="test-icon-wrapper"
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const span = container.querySelector("span.test-icon-wrapper")
|
||||
expect(span).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render icon with custom wrapper className", () => {
|
||||
const { container } = render(
|
||||
<BorderedIcon
|
||||
icon={exampleDataImageUrl}
|
||||
wrapperClassName="test-wrapper"
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const span = container.querySelector("span.test-wrapper")
|
||||
expect(span).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render image with custom icon className", () => {
|
||||
const { container } = render(
|
||||
<BorderedIcon icon={exampleDataImageUrl} iconClassName="test-icon" />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const image = container.querySelector("img.test-icon")
|
||||
expect(image).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render icon component with custom icon className", () => {
|
||||
const { container } = render(
|
||||
<BorderedIcon IconComponent={TestIcon} iconClassName="test-icon" />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const icon = container.querySelector("svg.test-icon")
|
||||
expect(icon).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render icon with custom icon color className", () => {
|
||||
const { container } = render(
|
||||
<BorderedIcon
|
||||
IconComponent={TestIcon}
|
||||
iconColorClassName="test-icon-color"
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const icon = container.querySelector("svg")
|
||||
expect(icon).toBeInTheDocument()
|
||||
expect(icon).toHaveClass("test-icon-color")
|
||||
})
|
||||
})
|
||||
@@ -23,6 +23,7 @@ export const BorderedIcon = ({
|
||||
wrapperClassName,
|
||||
iconWidth = 28,
|
||||
iconHeight = 28,
|
||||
...props
|
||||
}: BorderedIconProps) => {
|
||||
return (
|
||||
<span
|
||||
@@ -31,6 +32,7 @@ export const BorderedIcon = ({
|
||||
"shadow-border-base dark:shadow-border-base-dark",
|
||||
iconWrapperClassName
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className={clsx("rounded-docs_xs", wrapperClassName)}>
|
||||
{!IconComponent && (
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
|
||||
// mock data
|
||||
const sidebarHistory = ["sidebar_1", "sidebar_2", "sidebar_3"]
|
||||
const sidebars = [
|
||||
{
|
||||
sidebar_id: "sidebar_1",
|
||||
title: "sidebar_1",
|
||||
items: [
|
||||
{
|
||||
type: "link",
|
||||
title: "sidebar_1_item_1",
|
||||
link: "/sidebar_1_item_1",
|
||||
isPathHref: true,
|
||||
path: "/sidebar_1_item_1",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sidebar_id: "sidebar_2",
|
||||
title: "sidebar_2",
|
||||
items: [
|
||||
{
|
||||
type: "link",
|
||||
title: "sidebar_2_item_1",
|
||||
link: "/sidebar_2_item_1",
|
||||
isPathHref: true,
|
||||
path: "/sidebar_2_item_1",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const baseUrl = "https://example.com"
|
||||
const basePath = "/docs"
|
||||
|
||||
// mock functions
|
||||
const getSidebar = (sidebar_id: string) => {
|
||||
return sidebars.find((sidebar) => sidebar.sidebar_id === sidebar_id)
|
||||
}
|
||||
const getSidebarFirstLinkChild = (sidebar: Sidebar.Sidebar) => {
|
||||
return sidebar.items[0] as Sidebar.SidebarItemLink | undefined
|
||||
}
|
||||
const mockUseSidebar = vi.fn(() => ({
|
||||
sidebarHistory,
|
||||
getSidebarFirstLinkChild,
|
||||
getSidebar,
|
||||
}))
|
||||
|
||||
// mock components
|
||||
vi.mock("@/providers/Sidebar", () => ({
|
||||
useSidebar: () => mockUseSidebar(),
|
||||
}))
|
||||
|
||||
vi.mock("@/providers/SiteConfig", () => ({
|
||||
useSiteConfig: () => ({
|
||||
config: {
|
||||
breadcrumbOptions: {
|
||||
startItems: [{ title: "start", link: "/start" }],
|
||||
},
|
||||
baseUrl,
|
||||
basePath,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
import { Breadcrumbs } from ".."
|
||||
import { Sidebar } from "types"
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseSidebar.mockReturnValue({
|
||||
sidebarHistory,
|
||||
getSidebarFirstLinkChild,
|
||||
getSidebar,
|
||||
})
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders config start item only when no sidebar history is provided", () => {
|
||||
mockUseSidebar.mockReturnValue({
|
||||
sidebarHistory: [],
|
||||
getSidebarFirstLinkChild: () => undefined,
|
||||
getSidebar,
|
||||
})
|
||||
const { container } = render(<Breadcrumbs />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const startItem = container.querySelector("a[href='/start']")
|
||||
expect(startItem).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("renders breadcrumbs for sidebar with one history item", () => {
|
||||
mockUseSidebar.mockReturnValue({
|
||||
sidebarHistory: ["sidebar_1"],
|
||||
getSidebarFirstLinkChild,
|
||||
getSidebar,
|
||||
})
|
||||
const { container } = render(<Breadcrumbs />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const breadcrumbs = container.querySelectorAll("a")
|
||||
expect(breadcrumbs).toHaveLength(2)
|
||||
expect(breadcrumbs[0]).toHaveTextContent("start")
|
||||
expect(breadcrumbs[1]).toHaveTextContent("sidebar_1")
|
||||
expect(breadcrumbs[1]).toHaveAttribute("href", "/sidebar_1_item_1")
|
||||
})
|
||||
|
||||
test("renders breadcrumbs for sidebar with two history items", () => {
|
||||
mockUseSidebar.mockReturnValue({
|
||||
sidebarHistory: ["sidebar_1", "sidebar_2"],
|
||||
getSidebarFirstLinkChild,
|
||||
getSidebar,
|
||||
})
|
||||
const { container } = render(<Breadcrumbs />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const breadcrumbs = container.querySelectorAll("a")
|
||||
expect(breadcrumbs).toHaveLength(3)
|
||||
expect(breadcrumbs[0]).toHaveTextContent("start")
|
||||
expect(breadcrumbs[1]).toHaveTextContent("sidebar_1")
|
||||
expect(breadcrumbs[1]).toHaveAttribute("href", "/sidebar_1_item_1")
|
||||
expect(breadcrumbs[2]).toHaveTextContent("sidebar_2")
|
||||
expect(breadcrumbs[2]).toHaveAttribute("href", "/sidebar_2_item_1")
|
||||
})
|
||||
|
||||
test("renders json-ld breadcrumb list", () => {
|
||||
mockUseSidebar.mockReturnValue({
|
||||
sidebarHistory: ["sidebar_1", "sidebar_2"],
|
||||
getSidebarFirstLinkChild,
|
||||
getSidebar,
|
||||
})
|
||||
const { container } = render(<Breadcrumbs />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const jsonLd = container.querySelector("script[type='application/ld+json']")
|
||||
expect(jsonLd).toBeInTheDocument()
|
||||
expect(jsonLd).toHaveAttribute("type", "application/ld+json")
|
||||
const content = jsonLd?.textContent
|
||||
expect(content).toBeDefined()
|
||||
const parsedContent = JSON.parse(content!)
|
||||
const baseLink = `${baseUrl}${basePath}`.replace(/\/+$/, "")
|
||||
expect(parsedContent).toEqual({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 1,
|
||||
name: "start",
|
||||
item: `${baseLink}/start`,
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 2,
|
||||
name: "sidebar_1",
|
||||
item: `${baseLink}/sidebar_1_item_1`,
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 3,
|
||||
name: "sidebar_2",
|
||||
item: `${baseLink}/sidebar_2_item_1`,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,11 +3,12 @@
|
||||
import React, { useMemo } from "react"
|
||||
import clsx from "clsx"
|
||||
import Link from "next/link"
|
||||
import { useSidebar, useSiteConfig } from "../../providers"
|
||||
import { useSidebar } from "@/providers/Sidebar"
|
||||
import { useSiteConfig } from "@/providers/SiteConfig"
|
||||
import { Button } from "../Button"
|
||||
import { TriangleRightMini } from "@medusajs/icons"
|
||||
import { Sidebar } from "types"
|
||||
import { getJsonLd } from "../../utils"
|
||||
import { getJsonLd } from "@/utils/get-json-ld"
|
||||
import type { BreadcrumbList } from "schema-dts"
|
||||
|
||||
type BreadcrumbItems = {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import { Button } from "../../Button"
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders primary button", () => {
|
||||
const { container } = render(<Button variant="primary">Click me</Button>)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveClass("bg-medusa-button-inverted")
|
||||
expect(button).toHaveClass("hover:bg-medusa-button-inverted-hover")
|
||||
expect(button).toHaveClass("active:bg-medusa-button-inverted-pressed")
|
||||
expect(button).toHaveClass("focus:bg-medusa-button-inverted")
|
||||
expect(button).toHaveClass("shadow-button-inverted")
|
||||
expect(button).toHaveClass("dark:shadow-button-inverted-dark")
|
||||
expect(button).toHaveClass("dark:focus:shadow-button-inverted-focused-dark")
|
||||
expect(button).toHaveClass("disabled:bg-medusa-bg-disabled")
|
||||
expect(button).toHaveClass("disabled:shadow-button-neutral")
|
||||
expect(button).toHaveClass("disabled:shadow-button-neutral")
|
||||
})
|
||||
|
||||
test("renders secondary button", () => {
|
||||
const { container } = render(<Button variant="secondary">Click me</Button>)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveClass("bg-medusa-button-neutral")
|
||||
expect(button).toHaveClass("hover:bg-medusa-button-neutral-hover")
|
||||
expect(button).toHaveClass("active:bg-medusa-button-neutral-pressed")
|
||||
expect(button).toHaveClass("focus:bg-medusa-button-neutral")
|
||||
expect(button).toHaveClass("shadow-button-neutral")
|
||||
expect(button).toHaveClass("dark:shadow-button-neutral")
|
||||
})
|
||||
|
||||
test("renders transparent button", () => {
|
||||
const { container } = render(
|
||||
<Button variant="transparent">Click me</Button>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveClass("bg-transparent")
|
||||
expect(button).toHaveClass("shadow-none")
|
||||
expect(button).toHaveClass("border-0")
|
||||
expect(button).toHaveClass("outline-none")
|
||||
})
|
||||
|
||||
test("renders transparent clear button", () => {
|
||||
const { container } = render(
|
||||
<Button variant="transparent-clear">Click me</Button>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveClass("bg-transparent")
|
||||
expect(button).toHaveClass("shadow-none")
|
||||
expect(button).toHaveClass("border-0")
|
||||
expect(button).toHaveClass("outline-none")
|
||||
})
|
||||
|
||||
test("renders icon button", () => {
|
||||
const { container } = render(
|
||||
<Button variant="primary" buttonType="icon">
|
||||
Click me
|
||||
</Button>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveClass("!px-docs_0.25")
|
||||
})
|
||||
|
||||
test("renders button with custom className", () => {
|
||||
const { container } = render(
|
||||
<Button className="custom-class">Click me</Button>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveClass("custom-class")
|
||||
})
|
||||
|
||||
test("renders button with custom buttonRef", () => {
|
||||
const buttonRef = vi.fn()
|
||||
render(<Button buttonRef={buttonRef}>Click me</Button>)
|
||||
expect(buttonRef).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,215 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import { CardDefaultLayout } from "../index"
|
||||
import { IconProps } from "@medusajs/icons/dist/types"
|
||||
import { LinkProps } from "../../../../Link"
|
||||
import { BorderedIconProps } from "../../../../BorderedIcon"
|
||||
import { BadgeProps } from "../../../../Badge"
|
||||
|
||||
// mock components
|
||||
vi.mock("@/components/BorderedIcon", () => ({
|
||||
BorderedIcon: (props: BorderedIconProps) => (
|
||||
<div>
|
||||
BorderedIcon {props.icon} {props.IconComponent && <props.IconComponent />}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/Link", () => ({
|
||||
Link: (props: LinkProps) => <a {...props} />,
|
||||
}))
|
||||
vi.mock("@/components/Badge", () => ({
|
||||
Badge: (props: BadgeProps) => (
|
||||
<div>
|
||||
Badge {props.variant} - {props.children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders card default layout", () => {
|
||||
const { container } = render(
|
||||
<CardDefaultLayout>Click me</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const cardContent = container.querySelector("div")
|
||||
expect(cardContent).toBeInTheDocument()
|
||||
expect(cardContent).toHaveClass("flex")
|
||||
expect(cardContent).toHaveTextContent("Click me")
|
||||
})
|
||||
test("renders card default layout with icon", () => {
|
||||
const icon = (props: IconProps) => <div data-testid="icon">Icon</div>
|
||||
const { container } = render(
|
||||
<CardDefaultLayout icon={icon}>Click me</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const borderedIcon = container.querySelector("[data-testid='icon']")
|
||||
expect(borderedIcon).toBeInTheDocument()
|
||||
expect(borderedIcon).toHaveTextContent("Icon")
|
||||
})
|
||||
test("renders card default layout with image", () => {
|
||||
const image = "https://example.com/image.png"
|
||||
const { container } = render(
|
||||
<CardDefaultLayout image={image}>Click me</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const borderedIcon = container.querySelector("div")
|
||||
expect(borderedIcon).toBeInTheDocument()
|
||||
expect(borderedIcon).toHaveTextContent(image)
|
||||
})
|
||||
test("renders card default layout with title", () => {
|
||||
const title = "Title"
|
||||
const { container } = render(
|
||||
<CardDefaultLayout title={title}>Click me</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const titleElement = container.querySelector("[data-testid='title']")
|
||||
expect(titleElement).toBeInTheDocument()
|
||||
expect(titleElement).toHaveTextContent(title)
|
||||
})
|
||||
test("renders card default layout with text", () => {
|
||||
const text = "Text"
|
||||
const { container } = render(
|
||||
<CardDefaultLayout text={text}>Click me</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const textElement = container.querySelector("[data-testid='text']")
|
||||
expect(textElement).toBeInTheDocument()
|
||||
expect(textElement).toHaveTextContent(text)
|
||||
})
|
||||
test("renders card default layout with badge", () => {
|
||||
const badge = { variant: "blue", children: "Badge" } as BadgeProps
|
||||
const { container } = render(
|
||||
<CardDefaultLayout badge={badge}>Click me</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const badgeElement = container.querySelector("div")
|
||||
expect(badgeElement).toBeInTheDocument()
|
||||
expect(badgeElement).toHaveTextContent("Badge blue - Badge")
|
||||
})
|
||||
test("renders card default layout with right icon", () => {
|
||||
const rightIcon = () => <div data-testid="right-icon">RightIcon</div>
|
||||
const { container } = render(
|
||||
<CardDefaultLayout rightIcon={rightIcon}>Click me</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const rightIconElement = container.querySelector(
|
||||
"[data-testid='right-icon']"
|
||||
)
|
||||
expect(rightIconElement).toBeInTheDocument()
|
||||
expect(rightIconElement).toHaveTextContent("RightIcon")
|
||||
})
|
||||
test("renders card default layout with external href", () => {
|
||||
const href = "https://example.com"
|
||||
const { container } = render(
|
||||
<CardDefaultLayout href={href}>Click me</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const linkElement = container.querySelector("a")
|
||||
expect(linkElement).toBeInTheDocument()
|
||||
expect(linkElement).toHaveAttribute("href", href)
|
||||
const arrowUpRightOnBoxElement = container.querySelector(
|
||||
"[data-testid='external-icon']"
|
||||
)
|
||||
expect(arrowUpRightOnBoxElement).toBeInTheDocument()
|
||||
const internalIconElement = container.querySelector(
|
||||
"[data-testid='internal-icon']"
|
||||
)
|
||||
expect(internalIconElement).not.toBeInTheDocument()
|
||||
})
|
||||
test("renders card default with internal link", () => {
|
||||
const href = "/example"
|
||||
const { container } = render(
|
||||
<CardDefaultLayout href={href}>Click me</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const linkElement = container.querySelector("a")
|
||||
expect(linkElement).toBeInTheDocument()
|
||||
expect(linkElement).toHaveAttribute("href", href)
|
||||
const internalIconElement = container.querySelector(
|
||||
"[data-testid='internal-icon']"
|
||||
)
|
||||
expect(internalIconElement).toBeInTheDocument()
|
||||
const arrowUpRightOnBoxElement = container.querySelector(
|
||||
"[data-testid='external-icon']"
|
||||
)
|
||||
expect(arrowUpRightOnBoxElement).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("highlight text", () => {
|
||||
test("highlight text not provided", () => {
|
||||
const { container } = render(
|
||||
<CardDefaultLayout title="Title with highlight">
|
||||
Click me
|
||||
</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const highlightTextElement = container.querySelector(
|
||||
"[data-testid='highlight-text']"
|
||||
)
|
||||
expect(highlightTextElement).not.toBeInTheDocument()
|
||||
})
|
||||
test("highlight text in title", () => {
|
||||
const { container } = render(
|
||||
<CardDefaultLayout
|
||||
title="Title with highlight"
|
||||
highlightText={["highlight"]}
|
||||
>
|
||||
Click me
|
||||
</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const highlightTextElement = container.querySelector(
|
||||
"[data-testid='highlight-text']"
|
||||
)
|
||||
expect(highlightTextElement).toBeInTheDocument()
|
||||
expect(highlightTextElement).toHaveTextContent("highlight")
|
||||
})
|
||||
test("highlight text not in title", () => {
|
||||
const { container } = render(
|
||||
<CardDefaultLayout
|
||||
title="Title without highlight"
|
||||
highlightText={["not-highlight"]}
|
||||
>
|
||||
Click me
|
||||
</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const highlightTextElement = container.querySelector(
|
||||
"[data-testid='highlight-text']"
|
||||
)
|
||||
expect(highlightTextElement).not.toBeInTheDocument()
|
||||
})
|
||||
test("highlight text in text", () => {
|
||||
const { container } = render(
|
||||
<CardDefaultLayout
|
||||
text="Text with highlight"
|
||||
highlightText={["highlight"]}
|
||||
>
|
||||
Click me
|
||||
</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const highlightTextElement = container.querySelector(
|
||||
"[data-testid='highlight-text']"
|
||||
)
|
||||
expect(highlightTextElement).toBeInTheDocument()
|
||||
expect(highlightTextElement).toHaveTextContent("highlight")
|
||||
})
|
||||
test("highlight text not in text", () => {
|
||||
const { container } = render(
|
||||
<CardDefaultLayout
|
||||
text="Text without highlight"
|
||||
highlightText={["not-highlight"]}
|
||||
>
|
||||
Click me
|
||||
</CardDefaultLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const highlightTextElement = container.querySelector(
|
||||
"[data-testid='highlight-text']"
|
||||
)
|
||||
expect(highlightTextElement).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
import { Badge, BorderedIcon, Link } from "@/components"
|
||||
import { Badge } from "@/components/Badge"
|
||||
import { BorderedIcon } from "@/components/BorderedIcon"
|
||||
import { Link } from "@/components/Link"
|
||||
import { ArrowUpRightOnBox, TriangleRightMini } from "@medusajs/icons"
|
||||
import { CardProps } from "../../.."
|
||||
import { useIsExternalLink } from "../../../.."
|
||||
@@ -37,6 +39,7 @@ export const CardDefaultLayout = ({
|
||||
<span
|
||||
key={index}
|
||||
className="bg-medusa-tag-blue-bg px-px rounded-s-docs_xxs"
|
||||
data-testid="highlight-text"
|
||||
>
|
||||
{part}
|
||||
</span>
|
||||
@@ -77,12 +80,18 @@ export const CardDefaultLayout = ({
|
||||
className={clsx("flex flex-col flex-1 overflow-auto", contentClassName)}
|
||||
>
|
||||
{title && (
|
||||
<div className="text-small-plus text-medusa-fg-base truncate">
|
||||
<div
|
||||
className="text-small-plus text-medusa-fg-base truncate"
|
||||
data-testid="title"
|
||||
>
|
||||
{getHighlightedText(title)}
|
||||
</div>
|
||||
)}
|
||||
{text && (
|
||||
<span className="text-small-plus text-medusa-fg-subtle">
|
||||
<span
|
||||
className="text-small-plus text-medusa-fg-subtle"
|
||||
data-testid="text"
|
||||
>
|
||||
{getHighlightedText(text)}
|
||||
</span>
|
||||
)}
|
||||
@@ -91,8 +100,12 @@ export const CardDefaultLayout = ({
|
||||
{badge && <Badge {...badge} />}
|
||||
<span className="text-medusa-fg-subtle">
|
||||
{RightIconComponent && <RightIconComponent />}
|
||||
{!RightIconComponent && isExternal && <ArrowUpRightOnBox />}
|
||||
{!RightIconComponent && !isExternal && <TriangleRightMini />}
|
||||
{!RightIconComponent && isExternal && (
|
||||
<ArrowUpRightOnBox data-testid="external-icon" />
|
||||
)}
|
||||
{!RightIconComponent && !isExternal && (
|
||||
<TriangleRightMini data-testid="internal-icon" />
|
||||
)}
|
||||
</span>
|
||||
|
||||
{href && (
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import { CardLargeLayout } from "../index"
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders card large layout with title", () => {
|
||||
const title = "Title"
|
||||
const { container } = render(
|
||||
<CardLargeLayout title={title}>Click me</CardLargeLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const titleElement = container.querySelector("[data-testid='title']")
|
||||
expect(titleElement).toBeInTheDocument()
|
||||
expect(titleElement).toHaveTextContent(title)
|
||||
})
|
||||
test("renders card large layout with text", () => {
|
||||
const text = "Text"
|
||||
const { container } = render(
|
||||
<CardLargeLayout text={text}>Click me</CardLargeLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const textElement = container.querySelector("[data-testid='text']")
|
||||
expect(textElement).toBeInTheDocument()
|
||||
expect(textElement).toHaveTextContent(text)
|
||||
})
|
||||
test("renders card large layout with external href", () => {
|
||||
const href = "https://example.com"
|
||||
const { container } = render(
|
||||
<CardLargeLayout href={href}>Click me</CardLargeLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const linkElement = container.querySelector("a")
|
||||
expect(linkElement).toBeInTheDocument()
|
||||
expect(linkElement).toHaveAttribute("href", href)
|
||||
const arrowUpRightOnBoxElement = container.querySelector(
|
||||
"[data-testid='external-icon']"
|
||||
)
|
||||
expect(arrowUpRightOnBoxElement).toBeInTheDocument()
|
||||
const internalIconElement = container.querySelector(
|
||||
"[data-testid='internal-icon']"
|
||||
)
|
||||
expect(internalIconElement).not.toBeInTheDocument()
|
||||
})
|
||||
test("renders card large layout with internal href", () => {
|
||||
const href = "/example"
|
||||
const { container } = render(
|
||||
<CardLargeLayout href={href}>Click me</CardLargeLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const linkElement = container.querySelector("a")
|
||||
expect(linkElement).toBeInTheDocument()
|
||||
expect(linkElement).toHaveAttribute("href", href)
|
||||
const internalIconElement = container.querySelector(
|
||||
"[data-testid='internal-icon']"
|
||||
)
|
||||
expect(internalIconElement).toBeInTheDocument()
|
||||
const arrowUpRightOnBoxElement = container.querySelector(
|
||||
"[data-testid='external-icon']"
|
||||
)
|
||||
expect(arrowUpRightOnBoxElement).not.toBeInTheDocument()
|
||||
})
|
||||
test("renders card large layout with icon", () => {
|
||||
const icon = () => <div data-testid="icon">Icon</div>
|
||||
const { container } = render(
|
||||
<CardLargeLayout icon={icon}>Click me</CardLargeLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const iconElement = container.querySelector("[data-testid='icon']")
|
||||
expect(iconElement).toBeInTheDocument()
|
||||
})
|
||||
test("renders card large layout with image", () => {
|
||||
const image = "https://example.com/image.png"
|
||||
const { container } = render(
|
||||
<CardLargeLayout image={image}>Click me</CardLargeLayout>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const imageElement = container.querySelector("img")
|
||||
expect(imageElement).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react"
|
||||
import { CardProps } from "../.."
|
||||
import { useIsExternalLink } from "../../../.."
|
||||
import { CardProps } from "@/components/Card"
|
||||
import { useIsExternalLink } from "@/hooks/use-is-external-link"
|
||||
import clsx from "clsx"
|
||||
import { ArrowUpRightOnBox, TriangleRightMini } from "@medusajs/icons"
|
||||
import Link from "next/link"
|
||||
@@ -47,14 +47,28 @@ export const CardLargeLayout = ({
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex gap-docs_0.25 items-center text-medusa-fg-base">
|
||||
{title && <span className="text-compact-small-plus">{title}</span>}
|
||||
{href && isExternal && <ArrowUpRightOnBox />}
|
||||
{title && (
|
||||
<span className="text-compact-small-plus" data-testid="title">
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
{href && isExternal && (
|
||||
<ArrowUpRightOnBox data-testid="external-icon" />
|
||||
)}
|
||||
{href && !isExternal && (
|
||||
<TriangleRightMini className="group-hover:translate-x-docs_0.125 transition-transform" />
|
||||
<TriangleRightMini
|
||||
className="group-hover:translate-x-docs_0.125 transition-transform"
|
||||
data-testid="internal-icon"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{text && (
|
||||
<span className="text-small-plus text-medusa-fg-subtle">{text}</span>
|
||||
<span
|
||||
className="text-small-plus text-medusa-fg-subtle"
|
||||
data-testid="text"
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{href && (
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import { BorderedIconProps } from "../../../../BorderedIcon"
|
||||
import { LinkProps } from "../../../../Link"
|
||||
|
||||
// mock data
|
||||
const exampleDataImageUrl =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAACV0lEQVR4nO2Wz2/SURjH/72769q9u9N7757e5f3oYsQgL2p4EUNFURFFEUVxU1ExYiD6gMREjEFE4qJiEBFRERERETGQmD/O/9/H/T8O93rXm+c5zzkzZ855zpnB3Tt37gQAAAAAAABg6U8AADwA4wEAAAAASUVORK5CYII="
|
||||
|
||||
// mock components
|
||||
vi.mock("@/components/BorderedIcon", () => ({
|
||||
BorderedIcon: (props: BorderedIconProps) => (
|
||||
<div data-testid="bordered-icon">
|
||||
BorderedIcon {props.icon} {props.IconComponent && <props.IconComponent />}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/Link", () => ({
|
||||
Link: (props: LinkProps) => <a {...props} />,
|
||||
}))
|
||||
vi.mock("@/components/ThemeImage", () => ({
|
||||
ThemeImage: (props: ThemeImageProps) => (
|
||||
<div data-testid="theme-image" className={props.className}>
|
||||
ThemeImage {props.light} {props.dark} {props.className}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
import { CardLayoutMini } from "../../Mini"
|
||||
import { ThemeImageProps } from "../../../../ThemeImage"
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders card mini layout with title", () => {
|
||||
const title = "Title"
|
||||
const { container } = render(
|
||||
<CardLayoutMini title={title}>Click me</CardLayoutMini>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const titleElement = container.querySelector("[data-testid='title']")
|
||||
expect(titleElement).toBeInTheDocument()
|
||||
expect(titleElement).toHaveTextContent(title)
|
||||
})
|
||||
test("renders card mini layout with text", () => {
|
||||
const text = "Text"
|
||||
const { container } = render(
|
||||
<CardLayoutMini text={text}>Click me</CardLayoutMini>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const textElement = container.querySelector("[data-testid='text']")
|
||||
expect(textElement).toBeInTheDocument()
|
||||
expect(textElement).toHaveTextContent(text)
|
||||
})
|
||||
test("renders card mini layout with external href", () => {
|
||||
const href = "https://example.com"
|
||||
const { container } = render(
|
||||
<CardLayoutMini href={href}>Click me</CardLayoutMini>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const linkElement = container.querySelector("a")
|
||||
expect(linkElement).toBeInTheDocument()
|
||||
expect(linkElement).toHaveAttribute("href", href)
|
||||
})
|
||||
test("renders card mini layout with internal href", () => {
|
||||
const href = "/example"
|
||||
const { container } = render(
|
||||
<CardLayoutMini href={href}>Click me</CardLayoutMini>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const linkElement = container.querySelector("a")
|
||||
expect(linkElement).toBeInTheDocument()
|
||||
expect(linkElement).toHaveAttribute("href", href)
|
||||
})
|
||||
test("renders card mini layout with icon", () => {
|
||||
const icon = () => <div data-testid="icon">Icon</div>
|
||||
const { container } = render(
|
||||
<CardLayoutMini icon={icon}>Click me</CardLayoutMini>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const iconElement = container.querySelector("[data-testid='icon']")
|
||||
expect(iconElement).toBeInTheDocument()
|
||||
})
|
||||
test("renders card mini layout with image", () => {
|
||||
const { container } = render(
|
||||
<CardLayoutMini image={exampleDataImageUrl}>Click me</CardLayoutMini>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const imageElement = container.querySelector("img")
|
||||
expect(imageElement).toBeInTheDocument()
|
||||
expect(imageElement).toHaveAttribute("src", exampleDataImageUrl)
|
||||
})
|
||||
test("renders card mini layout with theme image", () => {
|
||||
const themeImage = {
|
||||
light: exampleDataImageUrl,
|
||||
dark: exampleDataImageUrl,
|
||||
}
|
||||
const { container } = render(
|
||||
<CardLayoutMini themeImage={themeImage}>Click me</CardLayoutMini>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const themeImageElement = container.querySelector(
|
||||
"[data-testid='theme-image']"
|
||||
)
|
||||
expect(themeImageElement).toBeInTheDocument()
|
||||
expect(themeImageElement).toHaveTextContent(
|
||||
"ThemeImage " + exampleDataImageUrl + " " + exampleDataImageUrl
|
||||
)
|
||||
})
|
||||
test("renders card mini layout with closeable", () => {
|
||||
const { container } = render(
|
||||
<CardLayoutMini closeable>Click me</CardLayoutMini>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const closeButtonElement = container.querySelector(
|
||||
"[data-testid='close-button']"
|
||||
)
|
||||
expect(closeButtonElement).toBeInTheDocument()
|
||||
const closeIconElement = container.querySelector(
|
||||
"[data-testid='close-icon']"
|
||||
)
|
||||
expect(closeIconElement).toBeInTheDocument()
|
||||
})
|
||||
test("renders card mini layout with onClose", () => {
|
||||
const onClose = vi.fn()
|
||||
const { container } = render(
|
||||
<CardLayoutMini closeable onClose={onClose}>
|
||||
Click me
|
||||
</CardLayoutMini>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const closeButtonElement = container.querySelector(
|
||||
"[data-testid='close-button']"
|
||||
)
|
||||
expect(closeButtonElement).toBeInTheDocument()
|
||||
fireEvent.click(closeButtonElement!)
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
test("renders card mini layout with className", () => {
|
||||
const className = "test-class"
|
||||
const { container } = render(
|
||||
<CardLayoutMini className={className}>Click me</CardLayoutMini>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const cardElement = container.querySelector("div")
|
||||
expect(cardElement).toBeInTheDocument()
|
||||
expect(cardElement).toHaveClass(className)
|
||||
})
|
||||
test("renders card mini layout with imageDimensions", () => {
|
||||
const imageDimensions = { width: 100, height: 100 }
|
||||
const { container } = render(
|
||||
<CardLayoutMini
|
||||
imageDimensions={imageDimensions}
|
||||
image={exampleDataImageUrl}
|
||||
>
|
||||
Click me
|
||||
</CardLayoutMini>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const imageElement = container.querySelector("img")
|
||||
expect(imageElement).toBeInTheDocument()
|
||||
expect(imageElement).toHaveAttribute(
|
||||
"width",
|
||||
imageDimensions.width.toString()
|
||||
)
|
||||
expect(imageElement).toHaveAttribute(
|
||||
"height",
|
||||
imageDimensions.height.toString()
|
||||
)
|
||||
expect(imageElement).toHaveStyle({
|
||||
width: `${imageDimensions.width}px`,
|
||||
height: `${imageDimensions.height}px`,
|
||||
})
|
||||
})
|
||||
test("renders card mini layout with iconClassName and image", () => {
|
||||
const iconClassName = "test-class"
|
||||
const { container } = render(
|
||||
<CardLayoutMini image={exampleDataImageUrl} iconClassName={iconClassName}>
|
||||
Click me
|
||||
</CardLayoutMini>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const imageElement = container.querySelector("img")
|
||||
expect(imageElement).toBeInTheDocument()
|
||||
expect(imageElement).toHaveClass(iconClassName)
|
||||
})
|
||||
test("renders card mini layout with iconClassName and theme image", () => {
|
||||
const iconClassName = "test-class"
|
||||
const themeImage = {
|
||||
light: exampleDataImageUrl,
|
||||
dark: exampleDataImageUrl,
|
||||
}
|
||||
const { container } = render(
|
||||
<CardLayoutMini themeImage={themeImage} iconClassName={iconClassName}>
|
||||
Click me
|
||||
</CardLayoutMini>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const themeImageElement = container.querySelector(
|
||||
"[data-testid='theme-image']"
|
||||
)
|
||||
expect(themeImageElement).toBeInTheDocument()
|
||||
expect(themeImageElement).toHaveClass(iconClassName)
|
||||
})
|
||||
})
|
||||
@@ -2,13 +2,11 @@
|
||||
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
import { CardProps } from "../.."
|
||||
import {
|
||||
BorderedIcon,
|
||||
Button,
|
||||
ThemeImage,
|
||||
useIsExternalLink,
|
||||
} from "../../../.."
|
||||
import { CardProps } from "@/components/Card"
|
||||
import { BorderedIcon } from "@/components/BorderedIcon"
|
||||
import { Button } from "@/components/Button"
|
||||
import { ThemeImage } from "@/components/ThemeImage"
|
||||
import { useIsExternalLink } from "@/hooks/use-is-external-link"
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
import { ArrowUpRightOnBox, TriangleRightMini, XMark } from "@medusajs/icons"
|
||||
@@ -91,19 +89,29 @@ export const CardLayoutMini = ({
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
{title && (
|
||||
<span className="text-x-small-plus text-medusa-fg-base">
|
||||
<span
|
||||
className="text-x-small-plus text-medusa-fg-base"
|
||||
data-testid="title"
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
{text && (
|
||||
<span className="text-x-small-plus text-medusa-fg-subtle">
|
||||
<span
|
||||
className="text-x-small-plus text-medusa-fg-subtle"
|
||||
data-testid="text"
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!closeable && (
|
||||
<span className="text-medusa-fg-subtle">
|
||||
{isExternal ? <ArrowUpRightOnBox /> : <TriangleRightMini />}
|
||||
{isExternal ? (
|
||||
<ArrowUpRightOnBox data-testid="external-icon" />
|
||||
) : (
|
||||
<TriangleRightMini data-testid="internal-icon" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{href && (
|
||||
@@ -119,8 +127,9 @@ export const CardLayoutMini = ({
|
||||
variant="transparent-clear"
|
||||
onClick={onClose}
|
||||
className="!p-[2.5px] z-[2] hover:!bg-medusa-button-transparent-hover focus:!shadow-none focus:!bg-transparent"
|
||||
data-testid="close-button"
|
||||
>
|
||||
<XMark />
|
||||
<XMark data-testid="close-icon" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
|
||||
// mock components
|
||||
vi.mock("@/components/Badge", () => ({
|
||||
Badge: () => <div>Badge</div>,
|
||||
}))
|
||||
vi.mock("@/components/Card/Layout/Default", () => ({
|
||||
CardDefaultLayout: () => <div>CardDefaultLayout</div>,
|
||||
}))
|
||||
vi.mock("@/components/Card/Layout/Large", () => ({
|
||||
CardLargeLayout: () => <div>CardLargeLayout</div>,
|
||||
}))
|
||||
vi.mock("@/components/Card/Layout/Filler", () => ({
|
||||
CardFillerLayout: () => <div>CardFillerLayout</div>,
|
||||
}))
|
||||
vi.mock("@/components/Card/Layout/Mini", () => ({
|
||||
CardLayoutMini: () => <div>CardLayoutMini</div>,
|
||||
}))
|
||||
|
||||
import { Card } from "../../Card"
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders default card", () => {
|
||||
const { container } = render(<Card>Click me</Card>)
|
||||
expect(container).toBeInTheDocument()
|
||||
const card = container.querySelector("div")
|
||||
expect(card).toBeInTheDocument()
|
||||
expect(card).toHaveTextContent("CardDefaultLayout")
|
||||
})
|
||||
test("renders large card", () => {
|
||||
const { container } = render(<Card type="large">Click me</Card>)
|
||||
expect(container).toBeInTheDocument()
|
||||
const card = container.querySelector("div")
|
||||
expect(card).toBeInTheDocument()
|
||||
expect(card).toHaveTextContent("CardLargeLayout")
|
||||
})
|
||||
test("renders filler card", () => {
|
||||
const { container } = render(<Card type="filler">Click me</Card>)
|
||||
expect(container).toBeInTheDocument()
|
||||
const card = container.querySelector("div")
|
||||
expect(card).toBeInTheDocument()
|
||||
expect(card).toHaveTextContent("CardFillerLayout")
|
||||
})
|
||||
test("renders mini card", () => {
|
||||
const { container } = render(<Card type="mini">Click me</Card>)
|
||||
expect(container).toBeInTheDocument()
|
||||
const card = container.querySelector("div")
|
||||
expect(card).toBeInTheDocument()
|
||||
expect(card).toHaveTextContent("CardLayoutMini")
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react"
|
||||
import { BadgeProps } from "@/components"
|
||||
import { BadgeProps } from "@/components/Badge"
|
||||
import { CardDefaultLayout } from "./Layout/Default"
|
||||
import { IconProps } from "@medusajs/icons/dist/types"
|
||||
import { CardLargeLayout } from "./Layout/Large"
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import { CardProps } from "../../Card"
|
||||
|
||||
// mock components
|
||||
vi.mock("@/components/Card", () => ({
|
||||
Card: () => <div data-testid="card">Card</div>,
|
||||
}))
|
||||
|
||||
import { CardList } from "../../CardList"
|
||||
|
||||
describe("rendering", () => {
|
||||
test("render card list with one item", () => {
|
||||
const items: CardProps[] = [{ title: "Item 1" }]
|
||||
const { container } = render(<CardList items={items} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const section = container.querySelector("section")
|
||||
expect(section).toBeInTheDocument()
|
||||
expect(section).toHaveClass("grid-cols-1")
|
||||
const cards = container.querySelectorAll("[data-testid='card']")
|
||||
expect(cards).toHaveLength(items.length)
|
||||
})
|
||||
test("render card list even number of items", () => {
|
||||
const items: CardProps[] = [{ title: "Item 1" }, { title: "Item 2" }]
|
||||
const { container } = render(<CardList items={items} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const section = container.querySelector("section")
|
||||
expect(section).toBeInTheDocument()
|
||||
expect(section).toHaveClass("md:grid-cols-2 grid-cols-1")
|
||||
const cards = container.querySelectorAll("[data-testid='card']")
|
||||
expect(cards).toHaveLength(items.length)
|
||||
})
|
||||
test("render card list odd number of items", () => {
|
||||
const items: CardProps[] = [
|
||||
{ title: "Item 1" },
|
||||
{ title: "Item 2" },
|
||||
{ title: "Item 3" },
|
||||
]
|
||||
const { container } = render(<CardList items={items} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const section = container.querySelector("section")
|
||||
expect(section).toBeInTheDocument()
|
||||
expect(section).toHaveClass("lg:grid-cols-3 md:grid-col-2 grid-cols-1")
|
||||
const cards = container.querySelectorAll("[data-testid='card']")
|
||||
expect(cards).toHaveLength(items.length)
|
||||
})
|
||||
test("render card list with default items per row", () => {
|
||||
const items: CardProps[] = [
|
||||
{ title: "Item 1" },
|
||||
{ title: "Item 2" },
|
||||
{ title: "Item 3" },
|
||||
]
|
||||
const { container } = render(
|
||||
<CardList items={items} defaultItemsPerRow={2} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const section = container.querySelector("section")
|
||||
expect(section).toBeInTheDocument()
|
||||
expect(section).toHaveClass("md:grid-cols-2 grid-cols-1")
|
||||
const cards = container.querySelectorAll("[data-testid='card']")
|
||||
expect(cards).toHaveLength(items.length)
|
||||
})
|
||||
test("render card list with items per row", () => {
|
||||
const items: CardProps[] = [
|
||||
{ title: "Item 1" },
|
||||
{ title: "Item 2" },
|
||||
{ title: "Item 3" },
|
||||
]
|
||||
const { container } = render(<CardList items={items} itemsPerRow={2} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const section = container.querySelector("section")
|
||||
expect(section).toBeInTheDocument()
|
||||
expect(section).toHaveClass("md:grid-cols-2 grid-cols-1")
|
||||
const cards = container.querySelectorAll("[data-testid='card']")
|
||||
expect(cards).toHaveLength(items.length)
|
||||
})
|
||||
test("render card list with one item and default items per row", () => {
|
||||
const items: CardProps[] = [{ title: "Item 1" }]
|
||||
const { container } = render(
|
||||
<CardList items={items} defaultItemsPerRow={2} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const section = container.querySelector("section")
|
||||
expect(section).toBeInTheDocument()
|
||||
expect(section).toHaveClass("grid-cols-1")
|
||||
const cards = container.querySelectorAll("[data-testid='card']")
|
||||
expect(cards).toHaveLength(items.length)
|
||||
})
|
||||
test("render card list with items per row, ignoring default items per row", () => {
|
||||
const items: CardProps[] = [
|
||||
{ title: "Item 1" },
|
||||
{ title: "Item 2" },
|
||||
{ title: "Item 3" },
|
||||
]
|
||||
const { container } = render(
|
||||
<CardList items={items} itemsPerRow={2} defaultItemsPerRow={3} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const section = container.querySelector("section")
|
||||
expect(section).toBeInTheDocument()
|
||||
expect(section).toHaveClass("md:grid-cols-2 grid-cols-1")
|
||||
const cards = container.querySelectorAll("[data-testid='card']")
|
||||
expect(cards).toHaveLength(items.length)
|
||||
})
|
||||
test("render card list with className", () => {
|
||||
const className = "test-class"
|
||||
const items: CardProps[] = [{ title: "Item 1" }]
|
||||
const { container } = render(
|
||||
<CardList items={items} className={className} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const section = container.querySelector("section")
|
||||
expect(section).toBeInTheDocument()
|
||||
expect(section).toHaveClass(className)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react"
|
||||
import { Card, CardProps } from "@/components"
|
||||
import { Card, CardProps } from "@/components/Card"
|
||||
import clsx from "clsx"
|
||||
|
||||
type CardListProps = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useChildDocs, UseChildDocsProps } from "../.."
|
||||
import { useChildDocs, UseChildDocsProps } from "@/hooks/use-child-docs"
|
||||
|
||||
export const ChildDocs = (props: UseChildDocsProps) => {
|
||||
const { component } = useChildDocs(props)
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import * as AiAssistantMocks from "../../../../AiAssistant/__mocks__"
|
||||
|
||||
// mock components
|
||||
vi.mock("@/providers/AiAssistant", () => ({
|
||||
useAiAssistant: () => AiAssistantMocks.mockUseAiAssistant(),
|
||||
}))
|
||||
vi.mock("@/providers/SiteConfig", () => ({
|
||||
useSiteConfig: () => ({
|
||||
config: {
|
||||
basePath: "http://example.com",
|
||||
},
|
||||
}),
|
||||
}))
|
||||
vi.mock("@kapaai/react-sdk", () => ({
|
||||
useChat: () => AiAssistantMocks.mockUseChat(),
|
||||
}))
|
||||
vi.mock("@/components/Tooltip", () => ({
|
||||
Tooltip: ({
|
||||
children,
|
||||
innerClassName,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
innerClassName: string
|
||||
}) => (
|
||||
<div data-testid="tooltip">
|
||||
{children} - {innerClassName}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
import { CodeBlockAskAiAction } from "../../AskAi"
|
||||
|
||||
beforeEach(() => {
|
||||
AiAssistantMocks.mockSetChatOpened.mockClear()
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseAiAssistantReturn
|
||||
)
|
||||
AiAssistantMocks.mockSubmitQuery.mockClear()
|
||||
AiAssistantMocks.mockUseChat.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseChatReturn
|
||||
)
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("render code block ask ai action in header", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockAskAiAction
|
||||
source="console.log('Hello, world!');"
|
||||
inHeader={true}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const tooltip = container.querySelector("[data-testid='tooltip']")
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveTextContent("flex")
|
||||
const span = tooltip?.querySelector("span")
|
||||
expect(span).toBeInTheDocument()
|
||||
expect(span).toHaveClass("p-[4.5px]")
|
||||
expect(span).toHaveClass("cursor-pointer")
|
||||
const image = span?.querySelector("img")
|
||||
expect(image).toBeInTheDocument()
|
||||
expect(image).toHaveAttribute("width", "15")
|
||||
expect(image).toHaveAttribute("height", "15")
|
||||
expect(image).toHaveAttribute("alt", "Ask AI")
|
||||
})
|
||||
|
||||
test("render code block ask ai action not in header", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockAskAiAction
|
||||
source="console.log('Hello, world!');"
|
||||
inHeader={false}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const tooltip = container.querySelector("[data-testid='tooltip']")
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
const span = tooltip?.querySelector("span")
|
||||
expect(span).toBeInTheDocument()
|
||||
expect(span).toHaveClass("p-[6px]")
|
||||
})
|
||||
})
|
||||
|
||||
describe("interactions", () => {
|
||||
test("click code block ask ai action", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockAskAiAction
|
||||
source="console.log('Hello, world!');"
|
||||
inHeader={false}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const span = container.querySelector("span")
|
||||
expect(span).toBeInTheDocument()
|
||||
fireEvent.click(span!)
|
||||
expect(AiAssistantMocks.mockSetChatOpened).toHaveBeenCalledWith(true)
|
||||
expect(AiAssistantMocks.mockSubmitQuery).toHaveBeenCalledWith(
|
||||
"```tsx\nconsole.log('Hello, world!');\n```\n\nExplain the code above"
|
||||
)
|
||||
})
|
||||
test("click code block ask ai action when loading", () => {
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValue({
|
||||
...AiAssistantMocks.defaultUseAiAssistantReturn,
|
||||
loading: true,
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeBlockAskAiAction
|
||||
source="console.log('Hello, world!');"
|
||||
inHeader={false}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const span = container.querySelector("span")
|
||||
expect(span).toBeInTheDocument()
|
||||
fireEvent.click(span!)
|
||||
expect(AiAssistantMocks.mockSetChatOpened).not.toHaveBeenCalled()
|
||||
expect(AiAssistantMocks.mockSubmitQuery).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useAiAssistant, useSiteConfig } from "../../../../providers"
|
||||
import { useAiAssistant } from "../../../../providers/AiAssistant"
|
||||
import { useSiteConfig } from "../../../../providers/SiteConfig"
|
||||
import clsx from "clsx"
|
||||
import { Tooltip } from "../../../Tooltip"
|
||||
import Image from "next/image"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useState } from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import { CopyButtonProps } from "../../../../CopyButton"
|
||||
|
||||
// mock functions
|
||||
const mockTrack = vi.fn()
|
||||
|
||||
// mock components
|
||||
vi.mock("@/providers/Analytics", () => ({
|
||||
useAnalytics: () => ({
|
||||
track: mockTrack,
|
||||
}),
|
||||
}))
|
||||
vi.mock("@/components/CopyButton", () => ({
|
||||
CopyButton: ({
|
||||
text,
|
||||
buttonClassName,
|
||||
children,
|
||||
onCopy,
|
||||
}: CopyButtonProps) => {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
return (
|
||||
<div
|
||||
data-testid="copy-button"
|
||||
className={buttonClassName}
|
||||
onClick={(e) => {
|
||||
setIsCopied(true)
|
||||
onCopy?.(e)
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
{typeof children === "function" ? children({ isCopied }) : children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
import { CodeBlockCopyAction } from ".."
|
||||
|
||||
beforeEach(() => {
|
||||
mockTrack.mockClear()
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("render code block copy action not in header", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCopyAction
|
||||
source="console.log('Hello, world!');"
|
||||
inHeader={false}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const span = container.querySelector("[data-testid='copy-button']")
|
||||
expect(span).toBeInTheDocument()
|
||||
expect(span).toHaveClass("p-[6px]")
|
||||
})
|
||||
|
||||
test("render code block copy action in header", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCopyAction
|
||||
source="console.log('Hello, world!');"
|
||||
inHeader={true}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const span = container.querySelector("[data-testid='copy-button']")
|
||||
expect(span).toBeInTheDocument()
|
||||
expect(span).toHaveClass("p-[4.5px]")
|
||||
})
|
||||
})
|
||||
|
||||
describe("interactions", () => {
|
||||
test("click code block copy action", async () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCopyAction
|
||||
source="console.log('Hello, world!');"
|
||||
inHeader={false}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const span = container.querySelector("[data-testid='copy-button']")
|
||||
expect(span).toBeInTheDocument()
|
||||
fireEvent.click(span!)
|
||||
const copiedIcon = container.querySelector("[data-testid='copied-icon']")
|
||||
expect(copiedIcon).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { CopyButton, DocsTrackingEvents, useAnalytics } from "../../../.."
|
||||
import { CopyButton } from "../../../CopyButton"
|
||||
import { DocsTrackingEvents } from "../../../../constants"
|
||||
import { useAnalytics } from "../../../../providers/Analytics"
|
||||
import clsx from "clsx"
|
||||
import { CheckMini, SquareTwoStack } from "@medusajs/icons"
|
||||
|
||||
@@ -18,12 +20,14 @@ export const CodeBlockCopyAction = ({
|
||||
const { track } = useAnalytics()
|
||||
|
||||
useEffect(() => {
|
||||
if (copied) {
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 1000)
|
||||
if (!copied) {
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 1000)
|
||||
|
||||
track({
|
||||
event: {
|
||||
event: DocsTrackingEvents.CODE_BLOCK_COPY,
|
||||
@@ -50,8 +54,15 @@ export const CodeBlockCopyAction = ({
|
||||
)}
|
||||
onCopy={() => setCopied(true)}
|
||||
>
|
||||
{!copied && <SquareTwoStack className={clsx(iconClassName)} />}
|
||||
{copied && <CheckMini className={clsx(iconClassName)} />}
|
||||
{!copied && (
|
||||
<SquareTwoStack
|
||||
className={clsx(iconClassName)}
|
||||
data-testid="not-copied-icon"
|
||||
/>
|
||||
)}
|
||||
{copied && (
|
||||
<CheckMini className={clsx(iconClassName)} data-testid="copied-icon" />
|
||||
)}
|
||||
</CopyButton>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { Link, Tooltip } from "@/components"
|
||||
import { Link } from "@/components/Link"
|
||||
import { Tooltip } from "@/components/Tooltip"
|
||||
import { ExclamationCircle, PlaySolid } from "@medusajs/icons"
|
||||
import { GITHUB_ISSUES_LINK } from "@/constants"
|
||||
import { CodeBlockCopyAction } from "./Copy"
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
|
||||
// mock functions
|
||||
const mockSetCollapsed = vi.fn()
|
||||
|
||||
import { CodeBlockCollapsibleButton } from "../../Button"
|
||||
|
||||
describe("render", () => {
|
||||
test("render collapsible button start and collapsed", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleButton
|
||||
type="start"
|
||||
collapsed={true}
|
||||
setCollapsed={mockSetCollapsed}
|
||||
expandButtonLabel="Show imports"
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector(
|
||||
"[data-testid='collapsible-button-start']"
|
||||
)
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveTextContent("Show imports")
|
||||
})
|
||||
test("render collapsible button end and collapsed", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleButton
|
||||
type="end"
|
||||
collapsed={true}
|
||||
setCollapsed={mockSetCollapsed}
|
||||
expandButtonLabel="Show imports"
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector(
|
||||
"[data-testid='collapsible-button-end']"
|
||||
)
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveTextContent("Show imports")
|
||||
})
|
||||
test("render when not collapsed", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleButton
|
||||
type="start"
|
||||
collapsed={false}
|
||||
setCollapsed={mockSetCollapsed}
|
||||
expandButtonLabel="Show imports"
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector(
|
||||
"[data-testid='collapsible-button-start']"
|
||||
)
|
||||
expect(button).not.toBeInTheDocument()
|
||||
const buttonEnd = container.querySelector(
|
||||
"[data-testid='collapsible-button-end']"
|
||||
)
|
||||
expect(buttonEnd).not.toBeInTheDocument()
|
||||
})
|
||||
test("render with type start and className", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleButton
|
||||
type="start"
|
||||
collapsed={true}
|
||||
setCollapsed={mockSetCollapsed}
|
||||
expandButtonLabel="Show imports"
|
||||
className="bg-red-500"
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector(
|
||||
"[data-testid='collapsible-button-start']"
|
||||
)
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveClass("bg-red-500")
|
||||
})
|
||||
test("render with type end and className", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleButton
|
||||
type="end"
|
||||
collapsed={true}
|
||||
setCollapsed={mockSetCollapsed}
|
||||
expandButtonLabel="Show imports"
|
||||
className="bg-red-500"
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector(
|
||||
"[data-testid='collapsible-button-end']"
|
||||
)
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveClass("bg-red-500")
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { CollapsibleReturn } from "../../../../hooks"
|
||||
import { CollapsibleReturn } from "../../../../hooks/use-collapsible"
|
||||
import { Button } from "@medusajs/ui"
|
||||
|
||||
export type CodeBlockCollapsibleButtonProps = {
|
||||
@@ -33,6 +33,7 @@ export const CodeBlockCollapsibleButton = ({
|
||||
type === "start" && "rounded-t-docs_DEFAULT rounded-b-none",
|
||||
className
|
||||
)}
|
||||
data-testid="collapsible-button-start"
|
||||
onClick={() => setCollapsed(false)}
|
||||
>
|
||||
{expandButtonLabel}
|
||||
@@ -47,6 +48,7 @@ export const CodeBlockCollapsibleButton = ({
|
||||
"rounded-t-none rounded-b-docs_DEFAULT",
|
||||
className
|
||||
)}
|
||||
data-testid="collapsible-button-end"
|
||||
onClick={() => setCollapsed(false)}
|
||||
>
|
||||
{expandButtonLabel}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
|
||||
import { CodeBlockCollapsibleFade } from "../../Fade"
|
||||
|
||||
describe("render", () => {
|
||||
test("render collapsible fade start and collapsed", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleFade type="start" collapsed={true} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const wrapper = container.querySelector("span")
|
||||
expect(wrapper).toBeInTheDocument()
|
||||
expect(wrapper).toHaveClass("w-full left-0")
|
||||
expect(wrapper).toHaveClass("top-[36px]")
|
||||
const fade = container.querySelector(
|
||||
"[data-testid='collapsible-fade-start']"
|
||||
)
|
||||
expect(fade).toBeInTheDocument()
|
||||
})
|
||||
test("render collapsible fade end and collapsed", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleFade type="end" collapsed={true} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const wrapper = container.querySelector("span")
|
||||
expect(wrapper).toBeInTheDocument()
|
||||
expect(wrapper).toHaveClass("w-full left-0")
|
||||
expect(wrapper).toHaveClass("bottom-[36px]")
|
||||
const fade = container.querySelector("[data-testid='collapsible-fade-end']")
|
||||
expect(fade).toBeInTheDocument()
|
||||
})
|
||||
test("render when not collapsed", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleFade type="start" collapsed={false} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const fade = container.querySelector(
|
||||
"[data-testid='collapsible-fade-start']"
|
||||
)
|
||||
expect(fade).not.toBeInTheDocument()
|
||||
const fadeEnd = container.querySelector(
|
||||
"[data-testid='collapsible-fade-end']"
|
||||
)
|
||||
expect(fadeEnd).not.toBeInTheDocument()
|
||||
})
|
||||
test("render with type start and hasHeader", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleFade
|
||||
type="start"
|
||||
collapsed={true}
|
||||
hasHeader={true}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const wrapper = container.querySelector("span")
|
||||
expect(wrapper).toBeInTheDocument()
|
||||
expect(wrapper).toHaveClass("left-[6px] w-[calc(100%-12px)]")
|
||||
expect(wrapper).toHaveClass("top-[44px]")
|
||||
})
|
||||
test("render with type end and hasHeader", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleFade type="end" collapsed={true} hasHeader={true} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const wrapper = container.querySelector("span")
|
||||
expect(wrapper).toBeInTheDocument()
|
||||
expect(wrapper).toHaveClass("left-[6px] w-[calc(100%-12px)]")
|
||||
expect(wrapper).toHaveClass("bottom-[44px]")
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { CollapsibleReturn } from "../../../../hooks"
|
||||
import { CollapsibleReturn } from "../../../../hooks/use-collapsible"
|
||||
|
||||
export type CodeBlockCollapsibleFadeProps = {
|
||||
type: "start" | "end"
|
||||
@@ -38,6 +38,7 @@ export const CodeBlockCollapsibleFade = ({
|
||||
"w-full h-[56px]",
|
||||
"bg-code-fade-bottom-to-top dark:bg-code-fade-bottom-to-top-dark"
|
||||
)}
|
||||
data-testid="collapsible-fade-end"
|
||||
/>
|
||||
)}
|
||||
{type === "start" && (
|
||||
@@ -46,6 +47,7 @@ export const CodeBlockCollapsibleFade = ({
|
||||
"w-full h-[56px]",
|
||||
"bg-code-fade-top-to-bottom dark:bg-code-fade-top-to-bottom-dark"
|
||||
)}
|
||||
data-testid="collapsible-fade-start"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
|
||||
import { CodeBlockCollapsibleLines } from "../../Lines"
|
||||
|
||||
describe("render", () => {
|
||||
test("render with children, type start, and collapsed", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleLines type="start" collapsed={true}>
|
||||
<div>Hello</div>
|
||||
<div>World</div>
|
||||
<div>!</div>
|
||||
<div>...</div>
|
||||
</CodeBlockCollapsibleLines>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container.childElementCount).toBe(2)
|
||||
expect(container.firstChild).toHaveTextContent("!")
|
||||
expect(container.lastChild).toHaveTextContent("...")
|
||||
})
|
||||
test("render with children, type end, and collapsed", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleLines type="end" collapsed={true}>
|
||||
<div>Hello</div>
|
||||
<div>World</div>
|
||||
<div>!</div>
|
||||
<div>...</div>
|
||||
</CodeBlockCollapsibleLines>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container.childElementCount).toBe(2)
|
||||
expect(container.firstChild).toHaveTextContent("Hello")
|
||||
expect(container.lastChild).toHaveTextContent("World")
|
||||
})
|
||||
test("render with children, type start, and not collapsed", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleLines type="start" collapsed={false}>
|
||||
<div>Hello</div>
|
||||
<div>World</div>
|
||||
<div>!</div>
|
||||
<div>...</div>
|
||||
</CodeBlockCollapsibleLines>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container.childElementCount).toBe(4)
|
||||
expect(container.firstChild).toHaveTextContent("Hello")
|
||||
expect(container.lastChild).toHaveTextContent("...")
|
||||
})
|
||||
test("render with children, type end, and not collapsed", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockCollapsibleLines type="end" collapsed={false}>
|
||||
<div>Hello</div>
|
||||
<div>World</div>
|
||||
<div>!</div>
|
||||
<div>...</div>
|
||||
</CodeBlockCollapsibleLines>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container.childElementCount).toBe(4)
|
||||
expect(container.firstChild).toHaveTextContent("Hello")
|
||||
expect(container.lastChild).toHaveTextContent("...")
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react"
|
||||
import { CollapsibleReturn } from "../../../../hooks"
|
||||
import { CollapsibleReturn } from "../../../../hooks/use-collapsible"
|
||||
|
||||
export type CodeBlockCollapsibleLinesProps = {
|
||||
children: React.ReactNode
|
||||
type: "start" | "end"
|
||||
} & Omit<CollapsibleReturn, "setCollapsed">
|
||||
} & Pick<CollapsibleReturn, "collapsed">
|
||||
|
||||
export const CodeBlockCollapsibleLines = ({
|
||||
children,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
|
||||
// mock data
|
||||
const mockColorMode = "light"
|
||||
|
||||
// mock functions
|
||||
const mockUseColorMode = vi.fn(() => ({
|
||||
colorMode: mockColorMode,
|
||||
}))
|
||||
|
||||
// mock components
|
||||
vi.mock("@/providers/ColorMode", () => ({
|
||||
useColorMode: () => mockUseColorMode(),
|
||||
}))
|
||||
|
||||
import { CodeBlockHeaderWrapper } from "../index"
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseColorMode.mockReturnValue({
|
||||
colorMode: mockColorMode,
|
||||
})
|
||||
})
|
||||
|
||||
describe("render", () => {
|
||||
test("render with children, blockStyle loud, and colorMode light", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockHeaderWrapper blockStyle="loud">Hello</CodeBlockHeaderWrapper>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const wrapper = container.querySelector("div")
|
||||
expect(wrapper).toBeInTheDocument()
|
||||
expect(wrapper).toHaveClass("bg-medusa-contrast-bg-base")
|
||||
expect(wrapper).not.toHaveClass("bg-medusa-bg-component")
|
||||
expect(wrapper).not.toHaveClass("border-medusa-border-base")
|
||||
})
|
||||
|
||||
test("render with children, blockStyle subtle, and colorMode light", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockHeaderWrapper blockStyle="subtle">Hello</CodeBlockHeaderWrapper>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const wrapper = container.querySelector("div")
|
||||
expect(wrapper).toBeInTheDocument()
|
||||
expect(wrapper).toHaveClass("bg-medusa-bg-component")
|
||||
expect(wrapper).not.toHaveClass("bg-medusa-code-bg-header")
|
||||
expect(wrapper).not.toHaveClass("bg-medusa-contrast-bg-base")
|
||||
expect(wrapper).toHaveClass("border-medusa-border-base")
|
||||
expect(wrapper).not.toHaveClass("border-medusa-code-border")
|
||||
})
|
||||
|
||||
test("render with children, blockStyle subtle, and colorMode dark", () => {
|
||||
mockUseColorMode.mockReturnValue({
|
||||
colorMode: "dark",
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeBlockHeaderWrapper blockStyle="subtle">Hello</CodeBlockHeaderWrapper>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const wrapper = container.querySelector("div")
|
||||
expect(wrapper).toBeInTheDocument()
|
||||
expect(wrapper).toHaveClass("bg-medusa-code-bg-header")
|
||||
expect(wrapper).not.toHaveClass("bg-medusa-bg-component")
|
||||
expect(wrapper).not.toHaveClass("bg-medusa-contrast-bg-base")
|
||||
expect(wrapper).not.toHaveClass("border-medusa-border-base")
|
||||
expect(wrapper).toHaveClass("border-medusa-code-border")
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,9 @@
|
||||
import clsx from "clsx"
|
||||
import React, { useMemo } from "react"
|
||||
import { useColorMode } from "../../../../providers"
|
||||
import { CodeBlockStyle } from "../../../.."
|
||||
import { useColorMode } from "../../../../providers/ColorMode"
|
||||
import { CodeBlockStyle } from "../../../CodeBlock"
|
||||
|
||||
type CodeBlockHeaderWrapperProps = {
|
||||
export type CodeBlockHeaderWrapperProps = {
|
||||
blockStyle?: CodeBlockStyle
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import { BadgeProps } from "../../../Badge"
|
||||
|
||||
// mock data
|
||||
const mockColorMode = "light"
|
||||
const mockActionsProps: CodeBlockActionsProps = {
|
||||
source: "console.log('Hello, world!')",
|
||||
inHeader: true,
|
||||
isCollapsed: false,
|
||||
}
|
||||
|
||||
// mock functions
|
||||
const mockUseColorMode = vi.fn(() => ({
|
||||
colorMode: mockColorMode,
|
||||
}))
|
||||
|
||||
// mock components
|
||||
vi.mock("@/providers/ColorMode", () => ({
|
||||
useColorMode: () => mockUseColorMode(),
|
||||
}))
|
||||
vi.mock("@/components/Badge", () => ({
|
||||
Badge: ({ children, variant }: BadgeProps) => (
|
||||
<div>
|
||||
<span data-testid="badge-variant">{variant}</span>
|
||||
<span data-testid="badge-children">{children}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/CodeBlock/Actions", () => ({
|
||||
CodeBlockActions: () => <div data-testid="code-block-actions">Actions</div>,
|
||||
}))
|
||||
vi.mock("@/components/CodeBlock/Header/Wrapper", () => ({
|
||||
CodeBlockHeaderWrapper: ({
|
||||
children,
|
||||
blockStyle,
|
||||
}: CodeBlockHeaderWrapperProps) => (
|
||||
<div data-testid="code-block-header-wrapper">
|
||||
<span data-testid="code-block-header-wrapper-blockStyle">
|
||||
{blockStyle}
|
||||
</span>
|
||||
<span data-testid="code-block-header-wrapper-children">{children}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
import { CodeBlockHeader } from "../index"
|
||||
import { CodeBlockHeaderWrapperProps } from "../Wrapper"
|
||||
import { CodeBlockActionsProps } from "../../Actions"
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseColorMode.mockReturnValue({
|
||||
colorMode: mockColorMode,
|
||||
})
|
||||
})
|
||||
|
||||
describe("render", () => {
|
||||
test("default render", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockHeader actionsProps={mockActionsProps} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const header = container.querySelector("[data-testid='code-block-header']")
|
||||
expect(header).toBeInTheDocument()
|
||||
const actions = container.querySelector(
|
||||
"[data-testid='code-block-actions']"
|
||||
)
|
||||
expect(actions).toBeInTheDocument()
|
||||
expect(actions).toHaveTextContent("Actions")
|
||||
})
|
||||
|
||||
test("render with title and loud block style", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockHeader
|
||||
actionsProps={mockActionsProps}
|
||||
title="Title"
|
||||
blockStyle="loud"
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const header = container.querySelector("[data-testid='code-block-header']")
|
||||
expect(header).toBeInTheDocument()
|
||||
const title = header?.querySelector(
|
||||
"[data-testid='code-block-header-title']"
|
||||
)
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(title).toHaveTextContent("Title")
|
||||
expect(title).toHaveClass("text-medusa-contrast-fg-secondary")
|
||||
expect(title).not.toHaveClass("text-medusa-fg-subtle")
|
||||
const codeBlockHeaderWrapperBlockStyle = container.querySelector(
|
||||
"[data-testid='code-block-header-wrapper-blockStyle']"
|
||||
)
|
||||
expect(codeBlockHeaderWrapperBlockStyle).toBeInTheDocument()
|
||||
expect(codeBlockHeaderWrapperBlockStyle).toHaveTextContent("loud")
|
||||
})
|
||||
|
||||
test("render with title and subtle block style", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockHeader
|
||||
actionsProps={mockActionsProps}
|
||||
title="Title"
|
||||
blockStyle="subtle"
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const header = container.querySelector("[data-testid='code-block-header']")
|
||||
expect(header).toBeInTheDocument()
|
||||
const title = header?.querySelector(
|
||||
"[data-testid='code-block-header-title']"
|
||||
)
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(title).toHaveTextContent("Title")
|
||||
expect(title).toHaveClass("text-medusa-fg-subtle")
|
||||
expect(title).not.toHaveClass("text-medusa-contrast-fg-secondary")
|
||||
const codeBlockHeaderWrapperBlockStyle = container.querySelector(
|
||||
"[data-testid='code-block-header-wrapper-blockStyle']"
|
||||
)
|
||||
expect(codeBlockHeaderWrapperBlockStyle).toBeInTheDocument()
|
||||
expect(codeBlockHeaderWrapperBlockStyle).toHaveTextContent("subtle")
|
||||
})
|
||||
|
||||
test("render with title and subtle block style and colorMode dark", () => {
|
||||
mockUseColorMode.mockReturnValue({
|
||||
colorMode: "dark",
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeBlockHeader
|
||||
actionsProps={mockActionsProps}
|
||||
title="Title"
|
||||
blockStyle="subtle"
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const header = container.querySelector("[data-testid='code-block-header']")
|
||||
expect(header).toBeInTheDocument()
|
||||
const title = header?.querySelector(
|
||||
"[data-testid='code-block-header-title']"
|
||||
)
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(title).toHaveTextContent("Title")
|
||||
expect(title).toHaveClass("text-medusa-contrast-fg-secondary")
|
||||
expect(title).not.toHaveClass("text-medusa-fg-subtle")
|
||||
const codeBlockHeaderWrapperBlockStyle = container.querySelector(
|
||||
"[data-testid='code-block-header-wrapper-blockStyle']"
|
||||
)
|
||||
expect(codeBlockHeaderWrapperBlockStyle).toBeInTheDocument()
|
||||
expect(codeBlockHeaderWrapperBlockStyle).toHaveTextContent("subtle")
|
||||
})
|
||||
|
||||
test("render with badge label", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockHeader actionsProps={mockActionsProps} badgeLabel="Badge" />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const header = container.querySelector("[data-testid='code-block-header']")
|
||||
expect(header).toBeInTheDocument()
|
||||
const badge = header?.querySelector("[data-testid='badge-children']")
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge).toHaveTextContent("Badge")
|
||||
const badgeVariant = header?.querySelector("[data-testid='badge-variant']")
|
||||
expect(badgeVariant).toBeInTheDocument()
|
||||
expect(badgeVariant).toHaveTextContent("code")
|
||||
})
|
||||
|
||||
test("render with badge label and color", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockHeader
|
||||
actionsProps={mockActionsProps}
|
||||
badgeLabel="Badge"
|
||||
badgeColor="blue"
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const header = container.querySelector("[data-testid='code-block-header']")
|
||||
expect(header).toBeInTheDocument()
|
||||
const badge = header?.querySelector("[data-testid='badge-children']")
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge).toHaveTextContent("Badge")
|
||||
const badgeVariant = header?.querySelector("[data-testid='badge-variant']")
|
||||
expect(badgeVariant).toBeInTheDocument()
|
||||
expect(badgeVariant).toHaveTextContent("blue")
|
||||
})
|
||||
|
||||
test("render with hideActions", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockHeader actionsProps={mockActionsProps} hideActions={true} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const header = container.querySelector("[data-testid='code-block-header']")
|
||||
expect(header).toBeInTheDocument()
|
||||
const actions = container.querySelector(
|
||||
"[data-testid='code-block-actions']"
|
||||
)
|
||||
expect(actions).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import React, { useMemo } from "react"
|
||||
import clsx from "clsx"
|
||||
import { CodeBlockStyle } from ".."
|
||||
import { useColorMode } from "@/providers"
|
||||
import { Badge, BadgeVariant } from "@/components"
|
||||
import { CodeBlockStyle } from "../../CodeBlock"
|
||||
import { useColorMode } from "@/providers/ColorMode"
|
||||
import { Badge, BadgeVariant } from "@/components/Badge"
|
||||
import { CodeBlockActions, CodeBlockActionsProps } from "../Actions"
|
||||
import { CodeBlockHeaderWrapper } from "./Wrapper"
|
||||
|
||||
@@ -13,7 +13,7 @@ export type CodeBlockHeaderMeta = {
|
||||
badgeColor?: BadgeVariant
|
||||
}
|
||||
|
||||
type CodeBlockHeaderProps = {
|
||||
export type CodeBlockHeaderProps = {
|
||||
title?: string
|
||||
blockStyle?: CodeBlockStyle
|
||||
actionsProps: CodeBlockActionsProps
|
||||
@@ -44,14 +44,20 @@ export const CodeBlockHeader = ({
|
||||
|
||||
return (
|
||||
<CodeBlockHeaderWrapper blockStyle={blockStyle}>
|
||||
<div className={clsx("flex-1", "flex gap-docs_0.75 items-start")}>
|
||||
<div
|
||||
className={clsx("flex-1", "flex gap-docs_0.75 items-start")}
|
||||
data-testid="code-block-header"
|
||||
>
|
||||
{badgeLabel && (
|
||||
<Badge variant={badgeColor || "code"} className="font-base">
|
||||
{badgeLabel}
|
||||
</Badge>
|
||||
)}
|
||||
{title && (
|
||||
<div className={clsx("text-compact-x-small font-base", titleColor)}>
|
||||
<div
|
||||
className={clsx("text-compact-x-small font-base", titleColor)}
|
||||
data-testid="code-block-header-title"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import { MarkdownContentProps } from "../../../MarkdownContent"
|
||||
import { TooltipProps } from "../../../Tooltip"
|
||||
|
||||
// mock data
|
||||
const mockLine: Token[] = [
|
||||
{
|
||||
types: ["variable"],
|
||||
content: "console",
|
||||
},
|
||||
{
|
||||
types: ["punctuation"],
|
||||
content: ".",
|
||||
},
|
||||
{
|
||||
types: ["function"],
|
||||
content: "log",
|
||||
},
|
||||
{
|
||||
types: ["delimiter"],
|
||||
content: "(",
|
||||
},
|
||||
{
|
||||
types: ["string"],
|
||||
content: "'",
|
||||
},
|
||||
{
|
||||
types: ["string"],
|
||||
content: "Hello, world!",
|
||||
},
|
||||
{
|
||||
types: ["string"],
|
||||
content: "'",
|
||||
},
|
||||
{
|
||||
types: ["delimiter"],
|
||||
content: ")",
|
||||
},
|
||||
]
|
||||
|
||||
const mockHighlights: Highlight[] = [
|
||||
{
|
||||
line: 1,
|
||||
text: "console",
|
||||
},
|
||||
]
|
||||
const mockHighlightsInvalidText: Highlight[] = [
|
||||
{
|
||||
line: 1,
|
||||
text: "John",
|
||||
},
|
||||
]
|
||||
const mockHighlighWithTooltipText: Highlight[] = [
|
||||
{
|
||||
line: 1,
|
||||
text: "console",
|
||||
tooltipText: "This is a tooltip text",
|
||||
},
|
||||
]
|
||||
|
||||
// mock functions
|
||||
const mockGetLineProps = ({ line, key }: { line: Token[]; key?: number }) => ({
|
||||
className: "text-red-500",
|
||||
})
|
||||
const mockGetTokenProps = ({ token, key }: { token: Token; key?: number }) => ({
|
||||
className: "text-red-500",
|
||||
children: token.content,
|
||||
})
|
||||
|
||||
// mock components
|
||||
vi.mock("@/components/MarkdownContent", () => ({
|
||||
MarkdownContent: ({ children }: MarkdownContentProps) => (
|
||||
<div data-testid="markdown-content">{children}</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/Tooltip", () => ({
|
||||
Tooltip: ({ children, text, render }: TooltipProps) => (
|
||||
<div data-testid="tooltip">
|
||||
{/* @ts-expect-error - render is not typed properly */}
|
||||
<span data-testid="tooltip-children">{children || render?.()}</span>
|
||||
<span data-testid="tooltip-text">{text}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
import { CodeBlockLine } from "../index"
|
||||
import { Token } from "prism-react-renderer"
|
||||
import { Highlight } from "../.."
|
||||
|
||||
describe("render", () => {
|
||||
test("render without highlights", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockLine
|
||||
line={mockLine}
|
||||
highlights={[]}
|
||||
lineNumber={1}
|
||||
showLineNumber={true}
|
||||
lineNumberColorClassName="text-red-500"
|
||||
lineNumberBgClassName="bg-red-500"
|
||||
isTerminal={false}
|
||||
getLineProps={mockGetLineProps}
|
||||
getTokenProps={mockGetTokenProps}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlockLine = container.querySelector(
|
||||
"[data-testid='code-block-line']"
|
||||
)
|
||||
expect(codeBlockLine).toBeInTheDocument()
|
||||
expect(codeBlockLine).toHaveClass("text-red-500")
|
||||
expect(codeBlockLine).not.toHaveClass("bg-medusa-alpha-white-alpha-6")
|
||||
const lineNumber = codeBlockLine?.querySelector(
|
||||
"[data-testid='line-number']"
|
||||
)
|
||||
expect(lineNumber).toBeInTheDocument()
|
||||
expect(lineNumber).toHaveTextContent("2")
|
||||
expect(lineNumber).toHaveClass("text-red-500")
|
||||
expect(lineNumber).toHaveClass("bg-red-500")
|
||||
const codeBlockLineTokens = codeBlockLine?.querySelector(
|
||||
"[data-testid='code-block-line-tokens']"
|
||||
)
|
||||
expect(codeBlockLineTokens).toBeInTheDocument()
|
||||
expect(codeBlockLineTokens).not.toHaveClass("relative")
|
||||
expect(codeBlockLineTokens).toHaveTextContent(
|
||||
"console.log('Hello, world!')"
|
||||
)
|
||||
const codeBlockLineToken = codeBlockLineTokens?.querySelectorAll(
|
||||
"[data-testid='code-block-line-token']"
|
||||
)
|
||||
expect(codeBlockLineToken).toHaveLength(mockLine.length)
|
||||
expect(codeBlockLineToken![0]).toBeInTheDocument()
|
||||
expect(codeBlockLineToken![0]).toHaveClass("text-red-500")
|
||||
expect(codeBlockLineToken![0]).toHaveTextContent("console")
|
||||
expect(codeBlockLineToken![0]).not.toHaveClass("relative")
|
||||
expect(codeBlockLineToken![1]).toBeInTheDocument()
|
||||
expect(codeBlockLineToken![1]).toHaveClass("text-red-500")
|
||||
expect(codeBlockLineToken![1]).toHaveTextContent(".")
|
||||
expect(codeBlockLineToken![1]).not.toHaveClass("relative")
|
||||
expect(codeBlockLineToken![2]).toBeInTheDocument()
|
||||
expect(codeBlockLineToken![2]).toHaveClass("text-red-500")
|
||||
expect(codeBlockLineToken![2]).toHaveTextContent("log")
|
||||
expect(codeBlockLineToken![2]).not.toHaveClass("relative")
|
||||
expect(codeBlockLineToken![3]).toBeInTheDocument()
|
||||
expect(codeBlockLineToken![3]).toHaveClass("text-red-500")
|
||||
expect(codeBlockLineToken![3]).toHaveTextContent("(")
|
||||
expect(codeBlockLineToken![3]).not.toHaveClass("relative")
|
||||
expect(codeBlockLineToken![4]).toBeInTheDocument()
|
||||
expect(codeBlockLineToken![4]).toHaveClass("text-red-500")
|
||||
expect(codeBlockLineToken![4]).toHaveTextContent("'")
|
||||
expect(codeBlockLineToken![4]).not.toHaveClass("relative")
|
||||
expect(codeBlockLineToken![5]).toBeInTheDocument()
|
||||
expect(codeBlockLineToken![5]).toHaveClass("text-red-500")
|
||||
expect(codeBlockLineToken![5]).toHaveTextContent("Hello, world!")
|
||||
expect(codeBlockLineToken![5]).not.toHaveClass("relative")
|
||||
expect(codeBlockLineToken![6]).toBeInTheDocument()
|
||||
expect(codeBlockLineToken![6]).toHaveClass("text-red-500")
|
||||
expect(codeBlockLineToken![6]).toHaveTextContent("'")
|
||||
expect(codeBlockLineToken![6]).not.toHaveClass("relative")
|
||||
expect(codeBlockLineToken![7]).toBeInTheDocument()
|
||||
expect(codeBlockLineToken![7]).toHaveClass("text-red-500")
|
||||
expect(codeBlockLineToken![7]).toHaveTextContent(")")
|
||||
expect(codeBlockLineToken![7]).not.toHaveClass("relative")
|
||||
})
|
||||
|
||||
test("render with highlights", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockLine
|
||||
line={mockLine}
|
||||
highlights={mockHighlights}
|
||||
lineNumber={1}
|
||||
showLineNumber={true}
|
||||
lineNumberColorClassName="text-red-500"
|
||||
lineNumberBgClassName="bg-red-500"
|
||||
isTerminal={false}
|
||||
getLineProps={mockGetLineProps}
|
||||
getTokenProps={mockGetTokenProps}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlockLine = container.querySelector(
|
||||
"[data-testid='code-block-line']"
|
||||
)
|
||||
expect(codeBlockLine).toBeInTheDocument()
|
||||
expect(codeBlockLine).toHaveClass("text-red-500")
|
||||
// since token is highlighted, the line will not be highlighted
|
||||
expect(codeBlockLine).not.toHaveClass("bg-medusa-alpha-white-alpha-6")
|
||||
const lineTokens = codeBlockLine?.querySelectorAll(
|
||||
"[data-testid='code-block-line-tokens']"
|
||||
)
|
||||
expect(lineTokens).toHaveLength(2)
|
||||
expect(lineTokens![0]).toHaveClass("relative")
|
||||
expect(lineTokens![1]).not.toHaveClass("relative")
|
||||
expect(lineTokens![0]).toHaveTextContent("console")
|
||||
expect(lineTokens![1]).toHaveTextContent(".log('Hello, world!')")
|
||||
const lineHighlight = lineTokens![0].querySelector(
|
||||
"[data-testid='code-block-line-highlight']"
|
||||
)
|
||||
expect(lineHighlight).toBeInTheDocument()
|
||||
expect(lineHighlight).not.toHaveClass(
|
||||
"animate-fast animate-growWidth animation-fill-forwards"
|
||||
)
|
||||
expect(lineHighlight).toHaveClass("w-full")
|
||||
const highlightedLineTokens = lineTokens![0].querySelectorAll(
|
||||
"[data-testid='code-block-line-token']"
|
||||
)
|
||||
expect(highlightedLineTokens).toHaveLength(1)
|
||||
// highlighted text
|
||||
expect(highlightedLineTokens![0]).toHaveTextContent("console")
|
||||
expect(highlightedLineTokens![0]).toHaveClass("relative z-[1]")
|
||||
// not highlighted text
|
||||
const notHighlightedLineTokens = lineTokens![1].querySelectorAll(
|
||||
"[data-testid='code-block-line-token']"
|
||||
)
|
||||
expect(notHighlightedLineTokens).toHaveLength(mockLine.length - 1)
|
||||
expect(notHighlightedLineTokens![0]).toHaveTextContent(".")
|
||||
expect(notHighlightedLineTokens![0]).not.toHaveClass("relative z-[1]")
|
||||
expect(notHighlightedLineTokens![1]).toHaveTextContent("log")
|
||||
expect(notHighlightedLineTokens![1]).not.toHaveClass("relative z-[1]")
|
||||
expect(notHighlightedLineTokens![2]).toHaveTextContent("(")
|
||||
expect(notHighlightedLineTokens![2]).not.toHaveClass("relative z-[1]")
|
||||
expect(notHighlightedLineTokens![3]).toHaveTextContent("'")
|
||||
expect(notHighlightedLineTokens![3]).not.toHaveClass("relative z-[1]")
|
||||
expect(notHighlightedLineTokens![4]).toHaveTextContent("Hello, world!")
|
||||
expect(notHighlightedLineTokens![4]).not.toHaveClass("relative z-[1]")
|
||||
expect(notHighlightedLineTokens![5]).toHaveTextContent("'")
|
||||
expect(notHighlightedLineTokens![5]).not.toHaveClass("relative z-[1]")
|
||||
expect(notHighlightedLineTokens![6]).toHaveTextContent(")")
|
||||
expect(notHighlightedLineTokens![6]).not.toHaveClass("relative z-[1]")
|
||||
})
|
||||
|
||||
test("render with highlights and tooltip text", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockLine
|
||||
line={mockLine}
|
||||
highlights={mockHighlighWithTooltipText}
|
||||
lineNumber={1}
|
||||
showLineNumber={true}
|
||||
lineNumberColorClassName="text-red-500"
|
||||
lineNumberBgClassName="bg-red-500"
|
||||
isTerminal={false}
|
||||
getLineProps={mockGetLineProps}
|
||||
getTokenProps={mockGetTokenProps}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlockLine = container.querySelector(
|
||||
"[data-testid='code-block-line']"
|
||||
)
|
||||
expect(codeBlockLine).toBeInTheDocument()
|
||||
const tooltip = container.querySelector("[data-testid='tooltip']")
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveTextContent("This is a tooltip text")
|
||||
const tooltipChildren = tooltip?.querySelector(
|
||||
"[data-testid='tooltip-children']"
|
||||
)
|
||||
expect(tooltipChildren).toBeInTheDocument()
|
||||
expect(tooltipChildren).toHaveTextContent("console")
|
||||
})
|
||||
|
||||
test("render with highlights and animateTokenHighlights", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockLine
|
||||
line={mockLine}
|
||||
highlights={mockHighlights}
|
||||
lineNumber={1}
|
||||
showLineNumber={true}
|
||||
lineNumberColorClassName="text-red-500"
|
||||
lineNumberBgClassName="bg-red-500"
|
||||
isTerminal={false}
|
||||
getLineProps={mockGetLineProps}
|
||||
getTokenProps={mockGetTokenProps}
|
||||
animateTokenHighlights={true}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlockLine = container.querySelector(
|
||||
"[data-testid='code-block-line']"
|
||||
)
|
||||
expect(codeBlockLine).toBeInTheDocument()
|
||||
const codeBlockLineTokens = codeBlockLine?.querySelectorAll(
|
||||
"[data-testid='code-block-line-tokens']"
|
||||
)
|
||||
expect(codeBlockLineTokens).toHaveLength(2)
|
||||
expect(codeBlockLineTokens![0]).toHaveClass("relative")
|
||||
expect(codeBlockLineTokens![1]).not.toHaveClass("relative")
|
||||
expect(codeBlockLineTokens![0]).toHaveTextContent("console")
|
||||
expect(codeBlockLineTokens![1]).toHaveTextContent(".log('Hello, world!')")
|
||||
const lineHighlight = codeBlockLineTokens![0].querySelector(
|
||||
"[data-testid='code-block-line-highlight']"
|
||||
)
|
||||
expect(lineHighlight).toBeInTheDocument()
|
||||
expect(lineHighlight).toHaveClass(
|
||||
"animate-fast animate-growWidth animation-fill-forwards"
|
||||
)
|
||||
expect(lineHighlight).not.toHaveClass("w-full")
|
||||
})
|
||||
|
||||
test("render with invalid highlight text", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockLine
|
||||
line={mockLine}
|
||||
highlights={mockHighlightsInvalidText}
|
||||
lineNumber={1}
|
||||
showLineNumber={true}
|
||||
lineNumberColorClassName="text-red-500"
|
||||
lineNumberBgClassName="bg-red-500"
|
||||
isTerminal={false}
|
||||
getLineProps={mockGetLineProps}
|
||||
getTokenProps={mockGetTokenProps}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlockLine = container.querySelector(
|
||||
"[data-testid='code-block-line']"
|
||||
)
|
||||
expect(codeBlockLine).toBeInTheDocument()
|
||||
// line will be highlighted, but not the tokens
|
||||
expect(codeBlockLine).toHaveClass("bg-medusa-alpha-white-alpha-6")
|
||||
const highlights = codeBlockLine?.querySelectorAll(
|
||||
"[data-testid='code-block-line-highlight']"
|
||||
)
|
||||
expect(highlights).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("render with isTerminal", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockLine
|
||||
line={mockLine}
|
||||
highlights={mockHighlights}
|
||||
lineNumber={1}
|
||||
showLineNumber={true}
|
||||
lineNumberColorClassName="text-red-500"
|
||||
lineNumberBgClassName="bg-red-500"
|
||||
isTerminal={true}
|
||||
getLineProps={mockGetLineProps}
|
||||
getTokenProps={mockGetTokenProps}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlockLine = container.querySelector(
|
||||
"[data-testid='code-block-line']"
|
||||
)
|
||||
expect(codeBlockLine).toBeInTheDocument()
|
||||
const lineNumber = codeBlockLine?.querySelector(
|
||||
"[data-testid='line-number']"
|
||||
)
|
||||
expect(lineNumber).toBeInTheDocument()
|
||||
expect(lineNumber).toHaveTextContent("❯")
|
||||
})
|
||||
|
||||
test("render with false showLineNumber", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockLine
|
||||
line={mockLine}
|
||||
highlights={mockHighlights}
|
||||
lineNumber={1}
|
||||
showLineNumber={false}
|
||||
lineNumberColorClassName="text-red-500"
|
||||
lineNumberBgClassName="bg-red-500"
|
||||
isTerminal={false}
|
||||
getLineProps={mockGetLineProps}
|
||||
getTokenProps={mockGetTokenProps}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlockLine = container.querySelector(
|
||||
"[data-testid='code-block-line']"
|
||||
)
|
||||
expect(codeBlockLine).toBeInTheDocument()
|
||||
const lineNumber = codeBlockLine?.querySelector(
|
||||
"[data-testid='line-number']"
|
||||
)
|
||||
expect(lineNumber).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with false showLineNumber and isTerminal", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockLine
|
||||
line={mockLine}
|
||||
highlights={mockHighlights}
|
||||
lineNumber={1}
|
||||
showLineNumber={false}
|
||||
lineNumberColorClassName="text-red-500"
|
||||
lineNumberBgClassName="bg-red-500"
|
||||
isTerminal={true}
|
||||
getLineProps={mockGetLineProps}
|
||||
getTokenProps={mockGetTokenProps}
|
||||
/>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlockLine = container.querySelector(
|
||||
"[data-testid='code-block-line']"
|
||||
)
|
||||
expect(codeBlockLine).toBeInTheDocument()
|
||||
const lineNumber = codeBlockLine?.querySelector(
|
||||
"[data-testid='line-number']"
|
||||
)
|
||||
expect(lineNumber).toBeInTheDocument()
|
||||
expect(lineNumber).toHaveTextContent("❯")
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,17 @@
|
||||
import React, { useMemo } from "react"
|
||||
import { Highlight } from ".."
|
||||
import { Highlight } from "../../CodeBlock"
|
||||
import { RenderProps, Token } from "prism-react-renderer"
|
||||
import clsx from "clsx"
|
||||
import { MarkdownContent, Tooltip } from "@/components"
|
||||
import { Tooltip } from "@/components/Tooltip"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
const MarkdownContent = dynamic(
|
||||
async () =>
|
||||
import("@/components/MarkdownContent").then((mod) => mod.MarkdownContent),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
)
|
||||
|
||||
type HighlightedTokens = {
|
||||
start: number
|
||||
@@ -215,7 +224,10 @@ export const CodeBlockLine = ({
|
||||
isTokenHighlighted: boolean
|
||||
offset: number
|
||||
}) => (
|
||||
<span className={clsx(isTokenHighlighted && "relative")}>
|
||||
<span
|
||||
className={clsx(isTokenHighlighted && "relative")}
|
||||
data-testid="code-block-line-tokens"
|
||||
>
|
||||
{isTokenHighlighted && (
|
||||
<span
|
||||
className={clsx(
|
||||
@@ -227,6 +239,7 @@ export const CodeBlockLine = ({
|
||||
"lg:bg-medusa-alpha-white-alpha-6 lg:border lg:border-medusa-alpha-white-alpha-12",
|
||||
"lg:rounded-docs_xs scale-x-[1.05]"
|
||||
)}
|
||||
data-testid="code-block-line-highlight"
|
||||
/>
|
||||
)}
|
||||
{tokens.map((token, key) => {
|
||||
@@ -242,6 +255,7 @@ export const CodeBlockLine = ({
|
||||
tokenClassName,
|
||||
isTokenHighlighted && "relative z-[1]"
|
||||
)}
|
||||
data-testid="code-block-line-token"
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
@@ -263,6 +277,7 @@ export const CodeBlockLine = ({
|
||||
isHighlightedLine && "bg-medusa-alpha-white-alpha-6",
|
||||
lineProps.className
|
||||
)}
|
||||
data-testid="code-block-line"
|
||||
>
|
||||
{(showLineNumber || isTerminal) && (
|
||||
<span
|
||||
@@ -272,6 +287,7 @@ export const CodeBlockLine = ({
|
||||
lineNumberColorClassName,
|
||||
lineNumberBgClassName
|
||||
)}
|
||||
data-testid="line-number"
|
||||
>
|
||||
{isTerminal ? "❯" : showLineNumber ? lineNumber + 1 : ""}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import { Token } from "prism-react-renderer"
|
||||
|
||||
// mock data
|
||||
const mockSource = "console.log('Hello, world!')"
|
||||
const mockMultiLineSource = `console.log('Line 1')
|
||||
console.log('Line 2')
|
||||
console.log('Line 3')`
|
||||
const mockCurlSource = "curl -X GET https://api.example.com/data"
|
||||
const mockColorMode = "light"
|
||||
const mockUseColorMode = vi.fn(() => ({
|
||||
colorMode: mockColorMode,
|
||||
}))
|
||||
const mockTrack = vi.fn()
|
||||
|
||||
// mock components
|
||||
const ApiRunnerMock = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
{ apiMethod: string; apiUrl: string }
|
||||
>((props, ref) => (
|
||||
<div data-testid="api-runner" ref={ref}>
|
||||
ApiRunner
|
||||
</div>
|
||||
))
|
||||
ApiRunnerMock.displayName = "ApiRunner"
|
||||
|
||||
vi.mock("@/components/ApiRunner", () => ({
|
||||
ApiRunner: () => ApiRunnerMock,
|
||||
}))
|
||||
vi.mock("@/providers/Analytics", () => ({
|
||||
useAnalytics: () => ({
|
||||
track: mockTrack,
|
||||
}),
|
||||
}))
|
||||
vi.mock("@/providers/ColorMode", () => ({
|
||||
useColorMode: () => mockUseColorMode(),
|
||||
}))
|
||||
vi.mock("@/components/CodeBlock/Line", () => ({
|
||||
CodeBlockLine: ({ line }: { line: Token[] }) => (
|
||||
<div data-testid="code-block-line">
|
||||
{line.map((token) => token.content).join("")}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/CodeBlock/Header", () => ({
|
||||
CodeBlockHeader: ({ title }: CodeBlockHeaderProps) => (
|
||||
<div data-testid="code-block-header">{title}</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/CodeBlock/Actions", () => ({
|
||||
CodeBlockActions: () => <div data-testid="code-block-actions">Actions</div>,
|
||||
}))
|
||||
vi.mock("@/components/CodeBlock/Collapsible/Button", () => ({
|
||||
CodeBlockCollapsibleButton: () => (
|
||||
<div data-testid="code-block-collapsible-button">CollapsibleButton</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/CodeBlock/Collapsible/Fade", () => ({
|
||||
CodeBlockCollapsibleFade: () => (
|
||||
<div data-testid="code-block-collapsible-fade">CollapsibleFade</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/CodeBlock/Inline", () => ({
|
||||
CodeBlockInline: () => <div data-testid="code-block-inline">Inline</div>,
|
||||
}))
|
||||
|
||||
import { CodeBlock } from "../../CodeBlock"
|
||||
import { CodeBlockHeaderProps } from "../Header"
|
||||
import { DocsTrackingEvents } from "../../../constants"
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseColorMode.mockReturnValue({
|
||||
colorMode: mockColorMode,
|
||||
})
|
||||
mockTrack.mockClear()
|
||||
})
|
||||
|
||||
describe("render", () => {
|
||||
test("render with source", () => {
|
||||
const { container } = render(<CodeBlock source={mockSource} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveTextContent(mockSource)
|
||||
const codeBlockHeader = container.querySelector(
|
||||
"[data-testid='code-block-header']"
|
||||
)
|
||||
expect(codeBlockHeader).toBeInTheDocument()
|
||||
expect(codeBlockHeader).toHaveTextContent("Code")
|
||||
})
|
||||
|
||||
test("render with lang not bash", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} lang="javascript" />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveTextContent(mockSource)
|
||||
const codeBlockHeader = container.querySelector(
|
||||
"[data-testid='code-block-header']"
|
||||
)
|
||||
expect(codeBlockHeader).toBeInTheDocument()
|
||||
expect(codeBlockHeader).toHaveTextContent("Code")
|
||||
})
|
||||
|
||||
test("render with lang bash", () => {
|
||||
const { container } = render(<CodeBlock source={mockSource} lang="bash" />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveTextContent(mockSource)
|
||||
const codeBlockHeader = container.querySelector(
|
||||
"[data-testid='code-block-header']"
|
||||
)
|
||||
expect(codeBlockHeader).toBeInTheDocument()
|
||||
expect(codeBlockHeader).toHaveTextContent("Terminal")
|
||||
})
|
||||
|
||||
test("render with lang bash and source is curl", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockCurlSource} lang="bash" />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveTextContent(mockCurlSource)
|
||||
const codeBlockHeader = container.querySelector(
|
||||
"[data-testid='code-block-header']"
|
||||
)
|
||||
expect(codeBlockHeader).toBeInTheDocument()
|
||||
expect(codeBlockHeader).toHaveTextContent("Code")
|
||||
})
|
||||
|
||||
test("render with isTerminal", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} isTerminal={true} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveTextContent(mockSource)
|
||||
const codeBlockHeader = container.querySelector(
|
||||
"[data-testid='code-block-header']"
|
||||
)
|
||||
expect(codeBlockHeader).toBeInTheDocument()
|
||||
expect(codeBlockHeader).toHaveTextContent("Terminal")
|
||||
})
|
||||
|
||||
test("render with isTermina false", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} isTerminal={false} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveTextContent(mockSource)
|
||||
const codeBlockHeader = container.querySelector(
|
||||
"[data-testid='code-block-header']"
|
||||
)
|
||||
expect(codeBlockHeader).toBeInTheDocument()
|
||||
expect(codeBlockHeader).toHaveTextContent("Code")
|
||||
})
|
||||
|
||||
test("render with hasTabs", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} hasTabs={true} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveTextContent(mockSource)
|
||||
const codeBlockHeader = container.querySelector(
|
||||
"[data-testid='code-block-header']"
|
||||
)
|
||||
expect(codeBlockHeader).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with title prop", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} title="Custom Title" />
|
||||
)
|
||||
const codeBlockHeader = container.querySelector(
|
||||
"[data-testid='code-block-header']"
|
||||
)
|
||||
expect(codeBlockHeader).toBeInTheDocument()
|
||||
expect(codeBlockHeader).toHaveTextContent("Custom Title")
|
||||
})
|
||||
|
||||
test("render with forceNoTitle", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} forceNoTitle={true} />
|
||||
)
|
||||
const codeBlockHeader = container.querySelector(
|
||||
"[data-testid='code-block-header']"
|
||||
)
|
||||
expect(codeBlockHeader).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with blockStyle inline", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} blockStyle="inline" />
|
||||
)
|
||||
const inlineBlock = container.querySelector(
|
||||
"[data-testid='code-block-inline']"
|
||||
)
|
||||
expect(inlineBlock).toBeInTheDocument()
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with blockStyle subtle", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} blockStyle="subtle" />
|
||||
)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with children when source is empty", () => {
|
||||
const { container } = render(<CodeBlock source="">test children</CodeBlock>)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveTextContent("test children")
|
||||
})
|
||||
|
||||
test("render with empty source returns empty fragment", () => {
|
||||
const { container } = render(<CodeBlock source="" />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
test("render with lang json converts to plain", () => {
|
||||
const { container } = render(<CodeBlock source={mockSource} lang="json" />)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with className prop", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} className="custom-class" />
|
||||
)
|
||||
const codeBlock = container.querySelector(
|
||||
"[data-testid='code-block-inner']"
|
||||
)
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveClass("custom-class")
|
||||
})
|
||||
|
||||
test("render with wrapperClassName prop", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} wrapperClassName="wrapper-class" />
|
||||
)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toHaveClass("wrapper-class")
|
||||
})
|
||||
|
||||
test("render with innerClassName prop", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} innerClassName="inner-class" />
|
||||
)
|
||||
const innerCode = container.querySelector(".inner-class")
|
||||
expect(innerCode).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with style prop", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} style={{ marginTop: "10px" }} />
|
||||
)
|
||||
const codeBlock = container.querySelector(".code-block-elm")
|
||||
expect(codeBlock).toHaveStyle({ marginTop: "10px" })
|
||||
})
|
||||
|
||||
test("render with collapsed prop", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockMultiLineSource} collapsed={true} />
|
||||
)
|
||||
const codeBlock = container.querySelector(".code-block-elm")
|
||||
expect(codeBlock).toHaveClass("max-h-[400px]")
|
||||
})
|
||||
|
||||
test("render with noLineNumbers prop", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockMultiLineSource} noLineNumbers={true} />
|
||||
)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with highlights prop", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock
|
||||
source={mockMultiLineSource}
|
||||
highlights={[["1", "highlight text", "tooltip"]]}
|
||||
/>
|
||||
)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with overrideColors prop", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock
|
||||
source={mockSource}
|
||||
overrideColors={{
|
||||
bg: "bg-red-500",
|
||||
innerBg: "bg-blue-500",
|
||||
border: "border-green-500",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
const codeBlock = container.querySelector(".code-block-elm")
|
||||
expect(codeBlock).toHaveClass("bg-red-500", "border-green-500")
|
||||
})
|
||||
|
||||
test("render with dark color mode", () => {
|
||||
mockUseColorMode.mockReturnValueOnce({
|
||||
colorMode: "dark",
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} blockStyle="subtle" />
|
||||
)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("api testing", () => {
|
||||
test("render with apiTesting prop shows ApiRunner when toggled", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock
|
||||
source={mockSource}
|
||||
apiTesting={true}
|
||||
testApiMethod="GET"
|
||||
testApiUrl="https://api.example.com"
|
||||
/>
|
||||
)
|
||||
const apiRunner = container.querySelector("[data-testid='api-runner']")
|
||||
// ApiRunner is initially hidden (showTesting starts as false)
|
||||
expect(apiRunner).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with apiTesting but missing required props does not show ApiRunner", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} apiTesting={true} />
|
||||
)
|
||||
const apiRunner = container.querySelector("[data-testid='api-runner']")
|
||||
expect(apiRunner).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("collapsible lines", () => {
|
||||
test("render with collapsibleLines prop at start", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock
|
||||
source={mockMultiLineSource}
|
||||
collapsibleLines="1-2"
|
||||
expandButtonLabel="Show more"
|
||||
/>
|
||||
)
|
||||
const collapsibleButton = container.querySelector(
|
||||
"[data-testid='code-block-collapsible-button']"
|
||||
)
|
||||
const collapsibleFade = container.querySelector(
|
||||
"[data-testid='code-block-collapsible-fade']"
|
||||
)
|
||||
expect(collapsibleButton).toBeInTheDocument()
|
||||
expect(collapsibleFade).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with collapsibleLines prop at end", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockMultiLineSource} collapsibleLines="2-3" />
|
||||
)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("actions", () => {
|
||||
test("render with noCopy prop hides copy action", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} noCopy={true} />
|
||||
)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with noReport prop hides report action", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} noReport={true} />
|
||||
)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with noAskAi prop hides ask AI action", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock source={mockSource} noAskAi={true} />
|
||||
)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render with all action flags disabled hides actions", () => {
|
||||
const { container } = render(
|
||||
<CodeBlock
|
||||
source={mockSource}
|
||||
noCopy={true}
|
||||
noReport={true}
|
||||
noAskAi={true}
|
||||
/>
|
||||
)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("interaction", () => {
|
||||
test("tracks copy event when code is copied", () => {
|
||||
const { container } = render(<CodeBlock source={mockSource} />)
|
||||
const pre = container.querySelector("pre")
|
||||
expect(pre).toBeInTheDocument()
|
||||
|
||||
fireEvent.copy(pre!)
|
||||
expect(mockTrack).toHaveBeenCalledTimes(1)
|
||||
expect(mockTrack).toHaveBeenCalledWith({
|
||||
event: {
|
||||
event: DocsTrackingEvents.CODE_BLOCK_COPY,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("single line vs multi-line", () => {
|
||||
test("render single line code", () => {
|
||||
const { container } = render(<CodeBlock source={mockSource} />)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("render multi-line code", () => {
|
||||
const { container } = render(<CodeBlock source={mockMultiLineSource} />)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
const lines = container.querySelectorAll("[data-testid='code-block-line']")
|
||||
expect(lines.length).toBeGreaterThan(1)
|
||||
})
|
||||
})
|
||||
@@ -3,15 +3,17 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react"
|
||||
import clsx from "clsx"
|
||||
import { Highlight, HighlightProps, themes, Token } from "prism-react-renderer"
|
||||
import { ApiRunner } from "@/components"
|
||||
import { useAnalytics, useColorMode } from "@/providers"
|
||||
import { ApiRunner } from "@/components/ApiRunner"
|
||||
import { useAnalytics } from "@/providers/Analytics"
|
||||
import { useColorMode } from "@/providers/ColorMode"
|
||||
import { CodeBlockHeader, CodeBlockHeaderMeta } from "./Header"
|
||||
import { CodeBlockLine } from "./Line"
|
||||
import { ApiAuthType, ApiDataOptions, ApiMethod } from "types"
|
||||
// @ts-expect-error can't install the types package because it doesn't support React v19
|
||||
import { CSSTransition } from "react-transition-group"
|
||||
import { DocsTrackingEvents, useCollapsibleCodeLines } from "../.."
|
||||
import { HighlightProps as CollapsibleHighlightProps } from "@/hooks"
|
||||
import { DocsTrackingEvents } from "@/constants"
|
||||
import { useCollapsibleCodeLines } from "@/hooks/use-collapsible-code-lines"
|
||||
import { HighlightProps as CollapsibleHighlightProps } from "@/hooks/use-collapsible-code-lines"
|
||||
import { CodeBlockActions, CodeBlockActionsProps } from "./Actions"
|
||||
import { CodeBlockCollapsibleButton } from "./Collapsible/Button"
|
||||
import { CodeBlockCollapsibleFade } from "./Collapsible/Fade"
|
||||
@@ -367,6 +369,7 @@ export const CodeBlock = ({
|
||||
"code-block-highlight-light",
|
||||
wrapperClassName
|
||||
)}
|
||||
data-testid="code-block"
|
||||
>
|
||||
{codeTitle && (
|
||||
<CodeBlockHeader
|
||||
@@ -393,6 +396,7 @@ export const CodeBlock = ({
|
||||
className
|
||||
)}
|
||||
style={style}
|
||||
data-testid="code-block-inner"
|
||||
>
|
||||
<Highlight
|
||||
theme={codeTheme}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import { CodeBlockProps } from "../../CodeBlock"
|
||||
import { InlineCodeProps } from "../../InlineCode"
|
||||
|
||||
// mock data
|
||||
const mockSource = "console.log('Hello, world!')"
|
||||
|
||||
// mock components
|
||||
vi.mock("@/components/CodeBlock", () => ({
|
||||
CodeBlock: ({ source, ...codeBlockProps }: CodeBlockProps) => (
|
||||
<div data-testid="code-block">
|
||||
<div data-testid="code-block-source">{source}</div>
|
||||
<div data-testid="code-block-props">
|
||||
{JSON.stringify(codeBlockProps as Record<string, unknown>)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/InlineCode", () => ({
|
||||
InlineCode: ({ children, ...inlineCodeProps }: InlineCodeProps) => (
|
||||
<div data-testid="inline-code">
|
||||
<div data-testid="inline-code-children">{children}</div>
|
||||
<div data-testid="inline-code-props">
|
||||
{JSON.stringify(inlineCodeProps)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/MermaidDiagram", () => ({
|
||||
MermaidDiagram: ({ diagramContent }: { diagramContent: string }) => (
|
||||
<div data-testid="mermaid-diagram">{diagramContent}</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/Npm2YarnCode", () => ({
|
||||
Npm2YarnCode: ({ npmCode }: { npmCode: string }) => (
|
||||
<div data-testid="npm2yarn-code">{npmCode}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
import { CodeMdx } from "../../CodeMdx"
|
||||
|
||||
describe("render", () => {
|
||||
test("renders without children", () => {
|
||||
const { container } = render(<CodeMdx />)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
test("renders with children", () => {
|
||||
const { container } = render(<CodeMdx>{mockSource}</CodeMdx>)
|
||||
expect(container).toBeInTheDocument()
|
||||
const inlineCodeChildren = container.querySelector(
|
||||
"[data-testid='inline-code-children']"
|
||||
)
|
||||
expect(inlineCodeChildren).toBeInTheDocument()
|
||||
expect(inlineCodeChildren).toHaveTextContent(mockSource)
|
||||
})
|
||||
|
||||
test("renders with className", () => {
|
||||
const { container } = render(
|
||||
<CodeMdx className="language-javascript">{mockSource}</CodeMdx>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
const codeBlockSource = container.querySelector(
|
||||
"[data-testid='code-block-source']"
|
||||
)
|
||||
expect(codeBlockSource).toBeInTheDocument()
|
||||
expect(codeBlockSource).toHaveTextContent(mockSource)
|
||||
})
|
||||
|
||||
test("renders with npm2yarn", () => {
|
||||
const { container } = render(
|
||||
<CodeMdx npm2yarn={true} className="language-bash">
|
||||
{mockSource}
|
||||
</CodeMdx>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const npm2yarnCode = container.querySelector(
|
||||
"[data-testid='npm2yarn-code']"
|
||||
)
|
||||
expect(npm2yarnCode).toBeInTheDocument()
|
||||
expect(npm2yarnCode).toHaveTextContent(mockSource)
|
||||
})
|
||||
|
||||
test("renders with mermaid", () => {
|
||||
const { container } = render(
|
||||
<CodeMdx className="language-mermaid">{mockSource}</CodeMdx>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const mermaidDiagram = container.querySelector(
|
||||
"[data-testid='mermaid-diagram']"
|
||||
)
|
||||
expect(mermaidDiagram).toBeInTheDocument()
|
||||
expect(mermaidDiagram).toHaveTextContent(mockSource)
|
||||
})
|
||||
|
||||
test("renders with codeBlockProps", () => {
|
||||
const codeBlockProps = { className: "language-javascript" }
|
||||
const { container } = render(
|
||||
<CodeMdx codeBlockProps={codeBlockProps} className="language-javascript">
|
||||
{mockSource}
|
||||
</CodeMdx>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const codeBlockPropsElement = container.querySelector(
|
||||
"[data-testid='code-block-props']"
|
||||
)
|
||||
expect(codeBlockPropsElement).toBeInTheDocument()
|
||||
const parsedContent = JSON.parse(codeBlockPropsElement!.textContent || "{}")
|
||||
expect(parsedContent).toEqual({
|
||||
...codeBlockProps,
|
||||
lang: "javascript",
|
||||
})
|
||||
})
|
||||
|
||||
test("renders with inlineCodeProps", () => {
|
||||
const inlineCodeProps: Partial<InlineCodeProps> = { variant: "grey-bg" }
|
||||
const { container } = render(
|
||||
<CodeMdx inlineCodeProps={inlineCodeProps}>{mockSource}</CodeMdx>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const inlineCodePropsElement = container.querySelector(
|
||||
"[data-testid='inline-code-props']"
|
||||
)
|
||||
expect(inlineCodePropsElement).toBeInTheDocument()
|
||||
expect(inlineCodePropsElement).toHaveTextContent(
|
||||
JSON.stringify(inlineCodeProps)
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -3,10 +3,9 @@ import {
|
||||
CodeBlock,
|
||||
CodeBlockMetaFields,
|
||||
CodeBlockProps,
|
||||
InlineCode,
|
||||
InlineCodeProps,
|
||||
MermaidDiagram,
|
||||
} from "@/components"
|
||||
} from "@/components/CodeBlock"
|
||||
import { InlineCode, InlineCodeProps } from "@/components/InlineCode"
|
||||
import { MermaidDiagram } from "@/components/MermaidDiagram"
|
||||
import { Npm2YarnCode } from "../Npm2YarnCode"
|
||||
|
||||
export type CodeMdxProps = {
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
|
||||
// mock data
|
||||
const mockLabel = "Code Tab"
|
||||
const mockValue = "code-tab"
|
||||
const mockColorMode = "light"
|
||||
|
||||
// mock functions
|
||||
const mockChangeSelectedTab = vi.fn()
|
||||
const mockPushRef = vi.fn()
|
||||
const mockUseColorMode = vi.fn(() => ({
|
||||
colorMode: mockColorMode,
|
||||
}))
|
||||
const mockUseScrollPositionBlocker = vi.fn(() => ({
|
||||
blockElementScrollPositionUntilNextRender: vi.fn(),
|
||||
}))
|
||||
|
||||
// mock components and hooks
|
||||
vi.mock("@/providers/ColorMode", () => ({
|
||||
useColorMode: () => mockUseColorMode(),
|
||||
}))
|
||||
vi.mock("@/hooks/use-scroll-utils", () => ({
|
||||
useScrollPositionBlocker: () => mockUseScrollPositionBlocker(),
|
||||
}))
|
||||
|
||||
import { CodeTab } from "../../Item"
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseColorMode.mockReturnValue({
|
||||
colorMode: mockColorMode,
|
||||
})
|
||||
mockUseScrollPositionBlocker.mockReturnValue({
|
||||
blockElementScrollPositionUntilNextRender: vi.fn(),
|
||||
})
|
||||
})
|
||||
|
||||
describe("render", () => {
|
||||
test("default render (loud blockStyle, not selected, light color mode)", () => {
|
||||
const { container } = render(
|
||||
<CodeTab label={mockLabel} value={mockValue}>
|
||||
<div data-testid="children">Children</div>
|
||||
</CodeTab>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveTextContent(mockLabel)
|
||||
expect(button).toHaveAttribute("aria-selected", "false")
|
||||
expect(button).toHaveAttribute("role", "tab")
|
||||
expect(button).toHaveClass("text-medusa-contrast-fg-secondary")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-primary")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-border-base")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-code-border")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-primary")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-border-base")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-code-border")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-primary")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-border-base")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-code-border")
|
||||
})
|
||||
|
||||
test("render with selected (loud blockStyle, light color mode)", () => {
|
||||
const { container } = render(
|
||||
<CodeTab label={mockLabel} value={mockValue} isSelected={true}>
|
||||
<div data-testid="children">Children</div>
|
||||
</CodeTab>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveTextContent(mockLabel)
|
||||
expect(button).toHaveAttribute("aria-selected", "true")
|
||||
expect(button).toHaveAttribute("role", "tab")
|
||||
expect(button).toHaveClass("text-medusa-contrast-fg-primary")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-secondary")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-border-base")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-code-border")
|
||||
})
|
||||
|
||||
test("render with subtle blockStyle (not selected, light color mode)", () => {
|
||||
const { container } = render(
|
||||
<CodeTab label={mockLabel} value={mockValue} blockStyle="subtle">
|
||||
<div data-testid="children">Children</div>
|
||||
</CodeTab>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveTextContent(mockLabel)
|
||||
expect(button).toHaveAttribute("aria-selected", "false")
|
||||
expect(button).toHaveAttribute("role", "tab")
|
||||
expect(button).toHaveClass("text-medusa-fg-subtle hover:bg-medusa-bg-base")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-secondary")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-code-border")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-primary")
|
||||
expect(button).not.toHaveClass("hover:bg-medusa-code-bg-base")
|
||||
})
|
||||
|
||||
test("render with subtle blockStyle and dark color mode", () => {
|
||||
mockUseColorMode.mockReturnValue({
|
||||
colorMode: "dark",
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeTab label={mockLabel} value={mockValue} blockStyle="subtle">
|
||||
<div data-testid="children">Children</div>
|
||||
</CodeTab>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveTextContent(mockLabel)
|
||||
expect(button).toHaveAttribute("aria-selected", "false")
|
||||
expect(button).toHaveAttribute("role", "tab")
|
||||
expect(button).toHaveClass("text-medusa-contrast-fg-secondary")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-primary")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-border-base")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-code-border")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-primary")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-border-base")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-code-border")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-primary")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-border-base")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-code-border")
|
||||
})
|
||||
|
||||
test("render with subtle blockStyle and selected (light color mode)", () => {
|
||||
const { container } = render(
|
||||
<CodeTab
|
||||
label={mockLabel}
|
||||
value={mockValue}
|
||||
blockStyle="subtle"
|
||||
isSelected={true}
|
||||
>
|
||||
<div data-testid="children">Children</div>
|
||||
</CodeTab>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveTextContent(mockLabel)
|
||||
expect(button).toHaveAttribute("aria-selected", "true")
|
||||
expect(button).toHaveAttribute("role", "tab")
|
||||
expect(button).toHaveClass(
|
||||
"xs:border-medusa-border-base text-medusa-contrast-fg-primary"
|
||||
)
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-secondary")
|
||||
expect(button).not.toHaveClass("hover:bg-medusa-code-bg-base")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-secondary")
|
||||
})
|
||||
|
||||
test("render with subtle blockStyle and dark color mode and selected", () => {
|
||||
mockUseColorMode.mockReturnValue({
|
||||
colorMode: "dark",
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeTab
|
||||
label={mockLabel}
|
||||
value={mockValue}
|
||||
blockStyle="subtle"
|
||||
isSelected={true}
|
||||
>
|
||||
<div data-testid="children">Children</div>
|
||||
</CodeTab>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveTextContent(mockLabel)
|
||||
expect(button).toHaveAttribute("aria-selected", "true")
|
||||
expect(button).toHaveAttribute("role", "tab")
|
||||
expect(button).toHaveClass(
|
||||
"xs:border-medusa-code-border text-medusa-contrast-fg-primary"
|
||||
)
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-secondary")
|
||||
expect(button).not.toHaveClass("hover:bg-medusa-code-bg-base")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-secondary")
|
||||
})
|
||||
|
||||
test("render with loud blockStyle and not selected and dark color mode", () => {
|
||||
mockUseColorMode.mockReturnValue({
|
||||
colorMode: "dark",
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeTab label={mockLabel} value={mockValue} blockStyle="loud">
|
||||
<div data-testid="children">Children</div>
|
||||
</CodeTab>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button).toHaveTextContent(mockLabel)
|
||||
expect(button).toHaveAttribute("aria-selected", "false")
|
||||
expect(button).toHaveAttribute("role", "tab")
|
||||
expect(button).toHaveClass("text-medusa-contrast-fg-secondary")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-primary")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-border-base")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-code-border")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-primary")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-border-base")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-code-border")
|
||||
expect(button).not.toHaveClass("text-medusa-contrast-fg-primary")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-border-base")
|
||||
expect(button).not.toHaveClass("xs:border-medusa-code-border")
|
||||
})
|
||||
|
||||
test("render with pushRef", () => {
|
||||
const { container } = render(
|
||||
<CodeTab label={mockLabel} value={mockValue} pushRef={mockPushRef}>
|
||||
<div data-testid="children">Children</div>
|
||||
</CodeTab>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(mockPushRef).toHaveBeenCalledWith(button)
|
||||
})
|
||||
})
|
||||
|
||||
describe("interactions", () => {
|
||||
test("click on tab without changeSelectedTab", () => {
|
||||
const { container } = render(
|
||||
<CodeTab label={mockLabel} value={mockValue}>
|
||||
<div data-testid="children">Children</div>
|
||||
</CodeTab>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(button!)
|
||||
expect(
|
||||
mockUseScrollPositionBlocker().blockElementScrollPositionUntilNextRender
|
||||
).toHaveBeenCalled()
|
||||
expect(mockChangeSelectedTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("click on tab with changeSelectedTab", () => {
|
||||
const { container } = render(
|
||||
<CodeTab
|
||||
label={mockLabel}
|
||||
value={mockValue}
|
||||
changeSelectedTab={mockChangeSelectedTab}
|
||||
>
|
||||
<div data-testid="children">Children</div>
|
||||
</CodeTab>
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const button = container.querySelector("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
fireEvent.click(button!)
|
||||
expect(
|
||||
mockUseScrollPositionBlocker().blockElementScrollPositionUntilNextRender
|
||||
).toHaveBeenCalled()
|
||||
expect(mockChangeSelectedTab).toHaveBeenCalledWith({
|
||||
label: mockLabel,
|
||||
value: mockValue,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { BaseTabType, useScrollPositionBlocker } from "@/hooks"
|
||||
import { useColorMode } from "@/providers"
|
||||
import { useColorMode } from "@/providers/ColorMode"
|
||||
import clsx from "clsx"
|
||||
import { BaseTabType } from "../../../hooks/use-tabs"
|
||||
import { useScrollPositionBlocker } from "../../../hooks/use-scroll-utils"
|
||||
|
||||
type CodeTabProps = BaseTabType & {
|
||||
// Children are handled in MDX to allow for code blocks and inline code to be rendered
|
||||
children: React.ReactNode
|
||||
isSelected?: boolean
|
||||
blockStyle?: string
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import { CodeBlock } from "@/components/CodeBlock"
|
||||
|
||||
// mock data
|
||||
const mockColorMode = "light"
|
||||
const mockUseColorMode = vi.fn(() => ({
|
||||
colorMode: mockColorMode,
|
||||
}))
|
||||
|
||||
// mock hooks
|
||||
const mockSelectedTab = {
|
||||
label: "Tab 1",
|
||||
value: "tab1",
|
||||
codeProps: { source: "code1" },
|
||||
}
|
||||
const mockChangeSelectedTab = vi.fn()
|
||||
const mockUseTabs = vi.fn(() => ({
|
||||
selectedTab: mockSelectedTab,
|
||||
changeSelectedTab: mockChangeSelectedTab,
|
||||
}))
|
||||
|
||||
// mock components
|
||||
vi.mock("@/providers/ColorMode", () => ({
|
||||
useColorMode: () => mockUseColorMode(),
|
||||
}))
|
||||
|
||||
vi.mock("@/hooks/use-tabs", () => ({
|
||||
useTabs: () => mockUseTabs(),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/Badge", () => ({
|
||||
Badge: ({
|
||||
children,
|
||||
variant,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
variant?: string
|
||||
}) => (
|
||||
<div data-testid="badge" data-variant={variant}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/CodeBlock", () => ({
|
||||
CodeBlock: ({ source, hasTabs }: { source: string; hasTabs?: boolean }) => (
|
||||
<div data-testid="code-block" data-source={source} data-has-tabs={hasTabs}>
|
||||
{source}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/CodeBlock/Actions", () => ({
|
||||
CodeBlockActions: ({ source }: { source: string }) => (
|
||||
<div data-testid="code-block-actions" data-source={source}>
|
||||
Actions
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("../Item", () => ({
|
||||
CodeTab: ({
|
||||
label,
|
||||
value,
|
||||
isSelected,
|
||||
blockStyle,
|
||||
changeSelectedTab,
|
||||
pushRef,
|
||||
children,
|
||||
}: {
|
||||
label: string
|
||||
value: string
|
||||
isSelected?: boolean
|
||||
blockStyle?: string
|
||||
changeSelectedTab?: (tab: { label: string; value: string }) => void
|
||||
pushRef?: (tabButton: HTMLButtonElement | null) => void
|
||||
children: React.ReactNode
|
||||
}) => (
|
||||
<li>
|
||||
<button
|
||||
data-testid={`code-tab-${value}`}
|
||||
data-selected={isSelected}
|
||||
data-block-style={blockStyle}
|
||||
ref={(tabButton) => pushRef?.(tabButton)}
|
||||
onClick={() => changeSelectedTab?.({ label, value })}
|
||||
aria-selected={isSelected}
|
||||
role="tab"
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
{children}
|
||||
</li>
|
||||
),
|
||||
}))
|
||||
|
||||
import { CodeTab } from "../Item"
|
||||
import { CodeTabs } from "../../CodeTabs"
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseColorMode.mockReturnValue({
|
||||
colorMode: mockColorMode,
|
||||
})
|
||||
mockUseTabs.mockReturnValue({
|
||||
selectedTab: mockSelectedTab,
|
||||
changeSelectedTab: mockChangeSelectedTab,
|
||||
})
|
||||
mockChangeSelectedTab.mockClear()
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders with default props", () => {
|
||||
const { container } = render(
|
||||
<CodeTabs>
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="console.log('test')" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const codeTabs = container.querySelector("[data-testid='code-tabs']")
|
||||
expect(codeTabs).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("renders with className", () => {
|
||||
const { container } = render(
|
||||
<CodeTabs className="custom-class">
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="console.log('test')" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const wrapper = container.querySelector("[data-testid='code-tabs']")
|
||||
expect(wrapper).toHaveClass("custom-class")
|
||||
})
|
||||
|
||||
test("renders with blockStyle loud", () => {
|
||||
const { container } = render(
|
||||
<CodeTabs blockStyle="loud">
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="console.log('test')" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const wrapper = container.querySelector("[data-testid='code-tabs']")
|
||||
expect(wrapper).toHaveClass("bg-medusa-contrast-bg-base")
|
||||
expect(wrapper).toHaveClass(
|
||||
"shadow-elevation-code-block dark:shadow-elevation-code-block-dark"
|
||||
)
|
||||
})
|
||||
|
||||
test("renders with blockStyle subtle (light color mode)", () => {
|
||||
const { container } = render(
|
||||
<CodeTabs blockStyle="subtle">
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="console.log('test')" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const wrapper = container.querySelector("[data-testid='code-tabs']")
|
||||
expect(wrapper).toHaveClass("bg-medusa-bg-component")
|
||||
expect(wrapper).toHaveClass("shadow-none")
|
||||
})
|
||||
|
||||
test("renders with blockStyle subtle (dark color mode)", () => {
|
||||
mockUseColorMode.mockReturnValueOnce({
|
||||
colorMode: "dark",
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeTabs blockStyle="subtle">
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="console.log('test')" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const wrapper = container.querySelector("[data-testid='code-tabs']")
|
||||
expect(wrapper).toHaveClass("bg-medusa-code-bg-header")
|
||||
expect(wrapper).toHaveClass("shadow-none")
|
||||
})
|
||||
|
||||
test("renders multiple tabs", () => {
|
||||
const { container } = render(
|
||||
<CodeTabs>
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="console.log('test1')" />
|
||||
</CodeTab>
|
||||
<CodeTab label="Tab 2" value="tab2">
|
||||
<CodeBlock source="console.log('test2')" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const buttons = container.querySelectorAll("button[role='tab']")
|
||||
expect(buttons.length).toBe(2)
|
||||
expect(buttons[0]).toHaveTextContent("Tab 1")
|
||||
expect(buttons[1]).toHaveTextContent("Tab 2")
|
||||
})
|
||||
|
||||
test("renders selected tab code block", () => {
|
||||
mockUseTabs.mockReturnValueOnce({
|
||||
selectedTab: {
|
||||
label: "Tab 1",
|
||||
value: "tab1",
|
||||
codeProps: { source: "code1" },
|
||||
codeBlock: <CodeBlock source="code1" />,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
changeSelectedTab: mockChangeSelectedTab,
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeTabs>
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="code1" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const codeBlock = container.querySelector("[data-testid='code-block']")
|
||||
expect(codeBlock).toBeInTheDocument()
|
||||
expect(codeBlock).toHaveAttribute("data-source", "code1")
|
||||
})
|
||||
|
||||
test("renders badge when selected tab has badgeLabel", () => {
|
||||
mockUseTabs.mockReturnValueOnce({
|
||||
selectedTab: {
|
||||
label: "Tab 1",
|
||||
value: "tab1",
|
||||
codeProps: {
|
||||
source: "code1",
|
||||
badgeLabel: "New",
|
||||
badgeColor: "green",
|
||||
},
|
||||
codeBlock: (
|
||||
<CodeBlock source="code1" badgeLabel="New" badgeColor="green" />
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
changeSelectedTab: mockChangeSelectedTab,
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeTabs>
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="code1" badgeLabel="New" badgeColor="green" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const badge = container.querySelector("[data-testid='badge']")
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge).toHaveAttribute("data-variant", "green")
|
||||
expect(badge).toHaveTextContent("New")
|
||||
})
|
||||
|
||||
test("does not render badge when selected tab has no badgeLabel", () => {
|
||||
mockUseTabs.mockReturnValueOnce({
|
||||
selectedTab: {
|
||||
label: "Tab 1",
|
||||
value: "tab1",
|
||||
codeProps: { source: "code1" },
|
||||
codeBlock: <CodeBlock source="code1" />,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
changeSelectedTab: mockChangeSelectedTab,
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeTabs>
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="code1" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const badge = container.querySelector("[data-testid='badge']")
|
||||
expect(badge).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("renders code block actions when selected tab exists", () => {
|
||||
mockUseTabs.mockReturnValueOnce({
|
||||
selectedTab: {
|
||||
label: "Tab 1",
|
||||
value: "tab1",
|
||||
codeProps: { source: "code1" },
|
||||
codeBlock: <CodeBlock source="code1" />,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
changeSelectedTab: mockChangeSelectedTab,
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeTabs>
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="code1" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const actions = container.querySelector(
|
||||
"[data-testid='code-block-actions']"
|
||||
)
|
||||
expect(actions).toBeInTheDocument()
|
||||
expect(actions).toHaveAttribute("data-source", "code1")
|
||||
})
|
||||
|
||||
test("does not render code block actions when no selected tab", () => {
|
||||
mockUseTabs.mockReturnValueOnce({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
selectedTab: null as any,
|
||||
changeSelectedTab: mockChangeSelectedTab,
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeTabs>
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="code1" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const actions = container.querySelector(
|
||||
"[data-testid='code-block-actions']"
|
||||
)
|
||||
expect(actions).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("renders tab selector span", () => {
|
||||
const { container } = render(
|
||||
<CodeTabs>
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="console.log('test')" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const selector = container.querySelector("span.xs\\:absolute")
|
||||
expect(selector).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("filters out invalid children", () => {
|
||||
const { container } = render(
|
||||
<CodeTabs>
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="code1" />
|
||||
</CodeTab>
|
||||
{null}
|
||||
<div>Invalid child</div>
|
||||
<CodeTab label="Tab 2" value="tab2">
|
||||
<CodeBlock source="code2" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const buttons = container.querySelectorAll("button[role='tab']")
|
||||
expect(buttons.length).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("tab selection", () => {
|
||||
test("calls changeSelectedTab when tab is clicked", () => {
|
||||
const { container } = render(
|
||||
<CodeTabs>
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="code1" />
|
||||
</CodeTab>
|
||||
<CodeTab label="Tab 2" value="tab2">
|
||||
<CodeBlock source="code2" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const buttons = container.querySelectorAll("button[role='tab']")
|
||||
fireEvent.click(buttons[1]!)
|
||||
expect(mockChangeSelectedTab).toHaveBeenCalledWith({
|
||||
label: "Tab 2",
|
||||
value: "tab2",
|
||||
})
|
||||
})
|
||||
|
||||
test("marks first tab as selected when no selectedTab", () => {
|
||||
mockUseTabs.mockReturnValueOnce({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
selectedTab: null as any,
|
||||
changeSelectedTab: mockChangeSelectedTab,
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeTabs>
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="code1" />
|
||||
</CodeTab>
|
||||
<CodeTab label="Tab 2" value="tab2">
|
||||
<CodeBlock source="code2" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const buttons = container.querySelectorAll("button[role='tab']")
|
||||
expect(buttons[0]).toHaveAttribute("aria-selected", "true")
|
||||
expect(buttons[1]).toHaveAttribute("aria-selected", "false")
|
||||
})
|
||||
|
||||
test("marks correct tab as selected based on selectedTab value", () => {
|
||||
mockUseTabs.mockReturnValueOnce({
|
||||
selectedTab: {
|
||||
label: "Tab 2",
|
||||
value: "tab2",
|
||||
codeProps: { source: "code2" },
|
||||
codeBlock: <CodeBlock source="code2" />,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
changeSelectedTab: mockChangeSelectedTab,
|
||||
})
|
||||
const { container } = render(
|
||||
<CodeTabs>
|
||||
<CodeTab label="Tab 1" value="tab1">
|
||||
<CodeBlock source="code1" />
|
||||
</CodeTab>
|
||||
<CodeTab label="Tab 2" value="tab2">
|
||||
<CodeBlock source="code2" />
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
)
|
||||
const buttons = container.querySelectorAll("button[role='tab']")
|
||||
expect(buttons[0]).toHaveAttribute("aria-selected", "false")
|
||||
expect(buttons[1]).toHaveAttribute("aria-selected", "true")
|
||||
})
|
||||
})
|
||||
@@ -1,17 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import React, { Children, useCallback, useEffect, useMemo, useRef } from "react"
|
||||
import {
|
||||
Badge,
|
||||
BaseTabType,
|
||||
CodeBlockProps,
|
||||
CodeBlockStyle,
|
||||
useColorMode,
|
||||
useTabs,
|
||||
} from "../.."
|
||||
import { Badge } from "@/components/Badge"
|
||||
import { CodeBlockProps, CodeBlockStyle } from "@/components/CodeBlock"
|
||||
import { useColorMode } from "@/providers/ColorMode"
|
||||
import clsx from "clsx"
|
||||
import { CodeBlockActions, CodeBlockActionsProps } from "../CodeBlock/Actions"
|
||||
import { CodeBlockHeaderWrapper } from "../CodeBlock/Header/Wrapper"
|
||||
import { BaseTabType, useTabs } from "../../hooks/use-tabs"
|
||||
|
||||
type CodeTab = BaseTabType & {
|
||||
codeProps: CodeBlockProps
|
||||
@@ -262,6 +258,7 @@ export const CodeTabs = ({
|
||||
boxShadow,
|
||||
className
|
||||
)}
|
||||
data-testid="code-tabs"
|
||||
>
|
||||
<CodeBlockHeaderWrapper blockStyle={blockStyle} ref={codeTabsWrapperRef}>
|
||||
<span
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import * as AiAssistantMocks from "../../../AiAssistant/__mocks__"
|
||||
|
||||
// mock functions
|
||||
const mockUseSiteConfig = vi.fn(() => ({
|
||||
config: {
|
||||
baseUrl: "https://docs.medusajs.com",
|
||||
basePath: "",
|
||||
},
|
||||
}))
|
||||
const mockUsePathname = vi.fn(() => "")
|
||||
|
||||
// mock components
|
||||
vi.mock("@/providers/AiAssistant", () => ({
|
||||
useAiAssistant: () => AiAssistantMocks.mockUseAiAssistant(),
|
||||
}))
|
||||
vi.mock("@/providers/SiteConfig", () => ({
|
||||
useSiteConfig: () => mockUseSiteConfig(),
|
||||
}))
|
||||
vi.mock("@kapaai/react-sdk", () => ({
|
||||
useChat: () => AiAssistantMocks.mockUseChat(),
|
||||
}))
|
||||
vi.mock("next/navigation", () => ({
|
||||
usePathname: () => mockUsePathname(),
|
||||
}))
|
||||
|
||||
import { ContentMenuActions } from "../../Actions"
|
||||
|
||||
beforeEach(() => {
|
||||
AiAssistantMocks.mockSetChatOpened.mockClear()
|
||||
AiAssistantMocks.mockUseAiAssistant.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseAiAssistantReturn
|
||||
)
|
||||
AiAssistantMocks.mockSubmitQuery.mockClear()
|
||||
AiAssistantMocks.mockUseChat.mockReturnValue(
|
||||
AiAssistantMocks.defaultUseChatReturn
|
||||
)
|
||||
})
|
||||
|
||||
describe("render", () => {
|
||||
test("render action menu", () => {
|
||||
const { container } = render(<ContentMenuActions />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const markdownLink = container.querySelector(
|
||||
"a[data-testid='markdown-link']"
|
||||
)
|
||||
expect(markdownLink).toBeInTheDocument()
|
||||
expect(markdownLink).toHaveAttribute(
|
||||
"href",
|
||||
"https://docs.medusajs.com/index.html.md"
|
||||
)
|
||||
expect(markdownLink).toHaveTextContent("View as Markdown")
|
||||
const aiAssistantButton = container.querySelector(
|
||||
"button[data-testid='ai-assistant-button']"
|
||||
)
|
||||
expect(aiAssistantButton).toBeInTheDocument()
|
||||
expect(aiAssistantButton).toHaveTextContent("Explain with AI Assistant")
|
||||
})
|
||||
})
|
||||
|
||||
describe("interactions", () => {
|
||||
test("handle ai assistant click", () => {
|
||||
const { container } = render(<ContentMenuActions />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const aiAssistantButton = container.querySelector(
|
||||
"button[data-testid='ai-assistant-button']"
|
||||
)
|
||||
expect(aiAssistantButton).toBeInTheDocument()
|
||||
fireEvent.click(aiAssistantButton!)
|
||||
expect(AiAssistantMocks.mockSetChatOpened).toHaveBeenCalledWith(true)
|
||||
expect(AiAssistantMocks.mockSubmitQuery).toHaveBeenCalledWith(
|
||||
"Explain the page https://docs.medusajs.com"
|
||||
)
|
||||
})
|
||||
|
||||
test("handle ai assistant click when loading", () => {
|
||||
AiAssistantMocks.mockUseChat.mockReturnValue({
|
||||
...AiAssistantMocks.defaultUseChatReturn,
|
||||
isGeneratingAnswer: true,
|
||||
})
|
||||
const { container } = render(<ContentMenuActions />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const aiAssistantButton = container.querySelector(
|
||||
"button[data-testid='ai-assistant-button']"
|
||||
)
|
||||
expect(aiAssistantButton).toBeInTheDocument()
|
||||
fireEvent.click(aiAssistantButton!)
|
||||
expect(AiAssistantMocks.mockSetChatOpened).not.toHaveBeenCalled()
|
||||
expect(AiAssistantMocks.mockSubmitQuery).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -3,7 +3,8 @@
|
||||
import Link from "next/link"
|
||||
import React, { useMemo } from "react"
|
||||
import { MarkdownIcon } from "../../Icons/Markdown"
|
||||
import { useAiAssistant, useSiteConfig } from "../../../providers"
|
||||
import { useSiteConfig } from "@/providers/SiteConfig"
|
||||
import { useAiAssistant } from "@/providers/AiAssistant"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { BroomSparkle } from "@medusajs/icons"
|
||||
import { useChat } from "@kapaai/react-sdk"
|
||||
@@ -34,6 +35,7 @@ export const ContentMenuActions = () => {
|
||||
<Link
|
||||
className="flex items-center gap-docs_0.5 text-medusa-fg-subtle text-x-small-plus hover:text-medusa-fg-base"
|
||||
href={`${pageUrl}/index.html.md`}
|
||||
data-testid="markdown-link"
|
||||
>
|
||||
<MarkdownIcon width={15} height={15} />
|
||||
View as Markdown
|
||||
@@ -41,6 +43,7 @@ export const ContentMenuActions = () => {
|
||||
<button
|
||||
className="appearance-none p-0 flex items-center gap-docs_0.5 text-medusa-fg-subtle text-x-small-plus hover:text-medusa-fg-base"
|
||||
onClick={handleAiAssistantClick}
|
||||
data-testid="ai-assistant-button"
|
||||
>
|
||||
<BroomSparkle width={15} height={15} />
|
||||
Explain with AI Assistant
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
|
||||
// mock data
|
||||
const mockConfig = {
|
||||
baseUrl: "https://docs.medusajs.com",
|
||||
basePath: "",
|
||||
}
|
||||
const defaultUseSiteConfigReturn = {
|
||||
frontmatter: {
|
||||
products: ["product", "cart"],
|
||||
},
|
||||
config: mockConfig,
|
||||
}
|
||||
const productProduct = products.find((p) => p.name === "product")
|
||||
const cartProduct = products.find((p) => p.name === "cart")
|
||||
|
||||
// mock functions
|
||||
const mockUseSiteConfig = vi.fn(() => defaultUseSiteConfigReturn)
|
||||
|
||||
// mock components
|
||||
vi.mock("@/providers/SiteConfig", () => ({
|
||||
useSiteConfig: () => mockUseSiteConfig(),
|
||||
}))
|
||||
vi.mock("@/components/BorderedIcon", () => ({
|
||||
BorderedIcon: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="bordered-icon">{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
import { ContentMenuProducts } from "../../Products"
|
||||
import { products } from "../../../../constants"
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseSiteConfig.mockReturnValue(defaultUseSiteConfigReturn)
|
||||
})
|
||||
|
||||
describe("render", () => {
|
||||
test("render product menu", () => {
|
||||
const { container } = render(<ContentMenuProducts />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const productLinks = container.querySelectorAll(
|
||||
"a[data-testid='product-link']"
|
||||
)
|
||||
expect(productLinks).toHaveLength(2)
|
||||
expect(productLinks[0]).toBeInTheDocument()
|
||||
expect(productLinks[0]).toHaveAttribute(
|
||||
"href",
|
||||
`https://docs.medusajs.com${cartProduct?.path}`
|
||||
)
|
||||
expect(productLinks[0]).toHaveTextContent(cartProduct!.title)
|
||||
|
||||
expect(productLinks[1]).toBeInTheDocument()
|
||||
expect(productLinks[1]).toHaveAttribute(
|
||||
"href",
|
||||
`https://docs.medusajs.com${productProduct?.path}`
|
||||
)
|
||||
expect(productLinks[1]).toHaveTextContent(productProduct!.title)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import React, { useMemo } from "react"
|
||||
import { useSiteConfig } from "../../../providers"
|
||||
import { useSiteConfig } from "@/providers/SiteConfig"
|
||||
import { products } from "../../../constants"
|
||||
import { Product } from "types"
|
||||
import { BorderedIcon } from "../../BorderedIcon"
|
||||
@@ -43,6 +43,7 @@ export const ContentMenuProducts = () => {
|
||||
key={index}
|
||||
href={getProductUrl(product)}
|
||||
className="flex gap-docs_0.5 items-center group"
|
||||
data-testid="product-link"
|
||||
>
|
||||
<BorderedIcon
|
||||
wrapperClassName={clsx("bg-medusa-bg-base")}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react"
|
||||
import { ToCItem } from "types"
|
||||
|
||||
// mock data
|
||||
const mockToc: ToCItem[] = [
|
||||
{
|
||||
title: "Overview",
|
||||
id: "overview",
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
title: "Getting Started",
|
||||
id: "getting-started",
|
||||
level: 2,
|
||||
},
|
||||
]
|
||||
const mockTocWithChildren: ToCItem[] = [
|
||||
{
|
||||
title: "Overview",
|
||||
id: "overview",
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
title: "Getting Started",
|
||||
id: "getting-started",
|
||||
level: 2,
|
||||
children: [
|
||||
{
|
||||
title: "Installation",
|
||||
id: "installation",
|
||||
level: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
let currentToc: ToCItem[] | null = mockToc as ToCItem[] | null
|
||||
let currentFrontmatter = {
|
||||
generate_toc: false,
|
||||
}
|
||||
const mockHeading = document.createElement("h2")
|
||||
mockHeading.id = "overview"
|
||||
mockHeading.textContent = "Overview"
|
||||
const mockUseActiveOnScroll = vi.fn(() => ({
|
||||
items: [
|
||||
{
|
||||
heading: mockHeading,
|
||||
},
|
||||
],
|
||||
activeItemId: "overview",
|
||||
}))
|
||||
|
||||
// mock functions
|
||||
const mockSetToc = vi.fn((toc: ToCItem[] | null) => {
|
||||
currentToc = toc
|
||||
})
|
||||
|
||||
// Create a getter function that always returns the latest values
|
||||
const getUseSiteConfigReturn = () => ({
|
||||
toc: currentToc,
|
||||
frontmatter: currentFrontmatter,
|
||||
setToc: mockSetToc,
|
||||
})
|
||||
|
||||
const mockUseSiteConfig = vi.fn(() => getUseSiteConfigReturn())
|
||||
const mockScrollToElement = vi.fn()
|
||||
const mockUseScrollController = vi.fn(() => ({
|
||||
scrollToElement: mockScrollToElement,
|
||||
}))
|
||||
|
||||
// mock components
|
||||
vi.mock("@/providers/SiteConfig", () => ({
|
||||
useSiteConfig: () => mockUseSiteConfig(),
|
||||
}))
|
||||
vi.mock("@/hooks/use-scroll-utils", () => ({
|
||||
useScrollController: () => mockUseScrollController(),
|
||||
}))
|
||||
vi.mock("@/hooks/use-active-on-scroll", () => ({
|
||||
useActiveOnScroll: () => mockUseActiveOnScroll(),
|
||||
}))
|
||||
vi.mock("@/components/Loading", () => ({
|
||||
Loading: () => <div data-testid="loading">Loading...</div>,
|
||||
}))
|
||||
|
||||
import { ContentMenuToc } from "../../Toc"
|
||||
|
||||
beforeEach(() => {
|
||||
// scrollIntoView is not available in the testing environment, so we need to mock it
|
||||
window.HTMLElement.prototype.scrollIntoView = function () {}
|
||||
|
||||
// Reset toc and frontmatter to default values
|
||||
currentToc = mockToc as ToCItem[] | null
|
||||
currentFrontmatter = {
|
||||
generate_toc: false,
|
||||
}
|
||||
// Always use the getter function to return fresh values
|
||||
mockUseSiteConfig.mockImplementation(() => getUseSiteConfigReturn())
|
||||
mockSetToc.mockClear()
|
||||
mockUseScrollController.mockClear()
|
||||
mockUseActiveOnScroll.mockClear()
|
||||
})
|
||||
|
||||
describe("render", () => {
|
||||
test("render toc", () => {
|
||||
const { container } = render(<ContentMenuToc />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const tocList = container.querySelector("[data-testid='toc-list']")
|
||||
expect(tocList).toBeInTheDocument()
|
||||
const tocItems = container.querySelectorAll("[data-testid='toc-item']")
|
||||
expect(tocItems).toHaveLength(2)
|
||||
expect(tocItems[0]).toBeInTheDocument()
|
||||
expect(tocItems[0]).toHaveTextContent("Overview")
|
||||
const tocItemLink0 = tocItems[0].querySelector("a")
|
||||
expect(tocItemLink0).toBeInTheDocument()
|
||||
expect(tocItemLink0).toHaveAttribute("href", "#overview")
|
||||
expect(tocItems[1]).toBeInTheDocument()
|
||||
expect(tocItems[1]).toHaveTextContent("Getting Started")
|
||||
const tocItemLink1 = tocItems[1].querySelector("a")
|
||||
expect(tocItemLink1).toBeInTheDocument()
|
||||
expect(tocItemLink1).toHaveAttribute("href", "#getting-started")
|
||||
})
|
||||
|
||||
test("render toc with children", () => {
|
||||
mockUseSiteConfig.mockReturnValue({
|
||||
...getUseSiteConfigReturn(),
|
||||
toc: mockTocWithChildren,
|
||||
})
|
||||
const { container } = render(<ContentMenuToc />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const tocLists = container.querySelectorAll("[data-testid='toc-list']")
|
||||
expect(tocLists).toHaveLength(2)
|
||||
expect(tocLists[0]).toBeInTheDocument()
|
||||
const firstTocItems = tocLists[0].querySelectorAll(
|
||||
"[data-testid='toc-item']"
|
||||
)
|
||||
// the nested item will be included in the first toc list
|
||||
expect(firstTocItems).toHaveLength(3)
|
||||
expect(firstTocItems[0]).toBeInTheDocument()
|
||||
expect(firstTocItems[0]).toHaveTextContent("Overview")
|
||||
const firstTocItemLink0 = firstTocItems[0].querySelector("a")
|
||||
expect(firstTocItemLink0).toBeInTheDocument()
|
||||
expect(firstTocItemLink0).toHaveAttribute("href", "#overview")
|
||||
expect(firstTocItems[1]).toBeInTheDocument()
|
||||
expect(firstTocItems[1]).toHaveTextContent("Getting Started")
|
||||
const firstTocItemLink1 = firstTocItems[1].querySelector("a")
|
||||
expect(firstTocItemLink1).toBeInTheDocument()
|
||||
expect(firstTocItemLink1).toHaveAttribute("href", "#getting-started")
|
||||
const secondTocItems = tocLists[1].querySelectorAll(
|
||||
"[data-testid='toc-item']"
|
||||
)
|
||||
expect(secondTocItems).toHaveLength(1)
|
||||
expect(secondTocItems[0]).toBeInTheDocument()
|
||||
expect(secondTocItems[0]).toHaveTextContent("Installation")
|
||||
const secondTocItemLink0 = secondTocItems[0].querySelector("a")
|
||||
expect(secondTocItemLink0).toBeInTheDocument()
|
||||
expect(secondTocItemLink0).toHaveAttribute("href", "#installation")
|
||||
})
|
||||
|
||||
test("render toc with no items", () => {
|
||||
currentToc = []
|
||||
const { container } = render(<ContentMenuToc />)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(container.childElementCount).toBe(0)
|
||||
})
|
||||
|
||||
test("render toc with empty items", () => {
|
||||
currentToc = null
|
||||
const { container } = render(<ContentMenuToc />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const emptyTocItems = container.querySelector(
|
||||
"[data-testid='empty-toc-items']"
|
||||
)
|
||||
expect(emptyTocItems).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("toc generation", () => {
|
||||
test("generate toc", async () => {
|
||||
currentToc = null
|
||||
currentFrontmatter = {
|
||||
generate_toc: true,
|
||||
}
|
||||
const { container, rerender } = render(<ContentMenuToc />)
|
||||
expect(container).toBeInTheDocument()
|
||||
|
||||
// Wait for useEffect to run and call setToc
|
||||
await waitFor(() => {
|
||||
expect(mockSetToc).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// After setToc is called, currentToc should be updated
|
||||
// Rerender to see the updated toc
|
||||
rerender(<ContentMenuToc />)
|
||||
|
||||
const tocList = container.querySelector("[data-testid='toc-list']")
|
||||
expect(tocList).toBeInTheDocument()
|
||||
const tocItems = container.querySelectorAll("[data-testid='toc-item']")
|
||||
expect(tocItems.length).toBeGreaterThan(0)
|
||||
if (tocItems.length > 0) {
|
||||
expect(tocItems[0]).toBeInTheDocument()
|
||||
expect(tocItems[0]).toHaveTextContent("Overview")
|
||||
const tocItemLink0 = tocItems[0].querySelector("a")
|
||||
expect(tocItemLink0).toBeInTheDocument()
|
||||
expect(tocItemLink0).toHaveAttribute("href", "#overview")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("toc scrolling", () => {
|
||||
test("scroll to toc item", () => {
|
||||
currentToc = mockToc as ToCItem[] | null
|
||||
const { container } = render(<ContentMenuToc />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const tocItems = container.querySelectorAll("[data-testid='toc-item']")
|
||||
expect(tocItems).toHaveLength(2)
|
||||
const tocItemLink0 = tocItems[0].querySelector("a")
|
||||
expect(tocItemLink0).toBeInTheDocument()
|
||||
fireEvent.click(tocItemLink0!)
|
||||
expect(mockScrollToElement).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
import React, { useEffect } from "react"
|
||||
import { ToCItem, ToCItemUi } from "types"
|
||||
import { useScrollController } from "../../../hooks/use-scroll-utils"
|
||||
import {
|
||||
ActiveOnScrollItem,
|
||||
useActiveOnScroll,
|
||||
useScrollController,
|
||||
} from "../../../hooks"
|
||||
} from "../../../hooks/use-active-on-scroll"
|
||||
import clsx from "clsx"
|
||||
import Link from "next/link"
|
||||
import { useSiteConfig } from "../../../providers"
|
||||
import { useSiteConfig } from "../../../providers/SiteConfig"
|
||||
import { Loading } from "../../Loading"
|
||||
|
||||
export const ContentMenuToc = () => {
|
||||
@@ -92,7 +92,7 @@ type TocListProps = {
|
||||
|
||||
const TocList = ({ items, activeItemId, className }: TocListProps) => {
|
||||
return (
|
||||
<ul className={className}>
|
||||
<ul className={className} data-testid="toc-list">
|
||||
{items.map((item) => (
|
||||
<TocItem item={item} key={item.id} activeItemId={activeItemId} />
|
||||
))}
|
||||
@@ -108,7 +108,7 @@ type TocItemProps = {
|
||||
const TocItem = ({ item, activeItemId }: TocItemProps) => {
|
||||
const { scrollToElement } = useScrollController()
|
||||
return (
|
||||
<li className="w-full pt-docs_0.5 toc-item">
|
||||
<li className="w-full pt-docs_0.5 toc-item" data-testid="toc-item">
|
||||
<Link
|
||||
href={`#${item.id}`}
|
||||
className={clsx(
|
||||
@@ -143,7 +143,7 @@ const TocItem = ({ item, activeItemId }: TocItemProps) => {
|
||||
|
||||
const EmptyTocItems = () => {
|
||||
return (
|
||||
<div className="animate-pulse">
|
||||
<div className="animate-pulse" data-testid="empty-toc-items">
|
||||
<Loading count={5} className="pt-docs_0.5 px-docs_0.75 !my-0" />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react"
|
||||
|
||||
// mock data
|
||||
const mockVersion = {
|
||||
number: "2.0.0",
|
||||
releaseUrl: "https://github.com/example/releases/v2.0.0",
|
||||
hide: false,
|
||||
bannerImage: {
|
||||
light: "/banner-light.png",
|
||||
dark: "/banner-dark.png",
|
||||
},
|
||||
}
|
||||
|
||||
const defaultUseSiteConfigReturn = {
|
||||
config: {
|
||||
version: mockVersion,
|
||||
},
|
||||
setConfig: vi.fn(),
|
||||
frontmatter: {},
|
||||
setFrontmatter: vi.fn(),
|
||||
toc: null,
|
||||
setToc: vi.fn(),
|
||||
}
|
||||
|
||||
// mock functions
|
||||
const mockUseSiteConfig = vi.fn(() => defaultUseSiteConfigReturn)
|
||||
const mockUseIsBrowser = vi.fn(() => ({
|
||||
isBrowser: true,
|
||||
}))
|
||||
|
||||
// mock components
|
||||
vi.mock("@/providers/SiteConfig", () => ({
|
||||
useSiteConfig: () => mockUseSiteConfig(),
|
||||
}))
|
||||
|
||||
vi.mock("@/providers/BrowserProvider", () => ({
|
||||
useIsBrowser: () => mockUseIsBrowser(),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/Card", () => ({
|
||||
Card: ({
|
||||
title,
|
||||
text,
|
||||
closeable,
|
||||
onClose,
|
||||
href,
|
||||
hrefProps,
|
||||
themeImage,
|
||||
imageDimensions,
|
||||
className,
|
||||
iconClassName,
|
||||
cardRef,
|
||||
}: {
|
||||
title?: string
|
||||
text?: string
|
||||
closeable?: boolean
|
||||
onClose?: () => void
|
||||
href?: string
|
||||
hrefProps?: Record<string, unknown>
|
||||
themeImage?: { light: string; dark: string }
|
||||
imageDimensions?: { width: number; height: number }
|
||||
className?: string
|
||||
iconClassName?: string
|
||||
cardRef?: React.Ref<HTMLDivElement>
|
||||
}) => {
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
React.useImperativeHandle(cardRef, () => ref.current as HTMLDivElement)
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="version-card"
|
||||
ref={ref}
|
||||
className={className}
|
||||
data-title={title}
|
||||
data-text={text}
|
||||
data-href={href}
|
||||
data-closeable={closeable}
|
||||
data-theme-image-light={themeImage?.light}
|
||||
data-theme-image-dark={themeImage?.dark}
|
||||
data-image-width={imageDimensions?.width}
|
||||
data-image-height={imageDimensions?.height}
|
||||
data-icon-class-name={iconClassName}
|
||||
>
|
||||
{title && <div data-testid="card-title">{title}</div>}
|
||||
{text && <div data-testid="card-text">{text}</div>}
|
||||
{closeable && onClose && (
|
||||
<button data-testid="card-close" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
)}
|
||||
{href && (
|
||||
<a
|
||||
data-testid="card-link"
|
||||
href={href}
|
||||
{...(hrefProps as Record<string, unknown>)}
|
||||
>
|
||||
Link
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
import { ContentMenuVersion, LOCAL_STORAGE_KEY } from "../../Version"
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear localStorage before each test
|
||||
localStorage.clear()
|
||||
mockUseSiteConfig.mockReturnValue(defaultUseSiteConfigReturn)
|
||||
mockUseIsBrowser.mockReturnValue({
|
||||
isBrowser: true,
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders card when version is new (not in localStorage)", async () => {
|
||||
const { container } = render(<ContentMenuVersion />)
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(card).toHaveClass("animate animate-fadeInDown")
|
||||
})
|
||||
})
|
||||
|
||||
test("does not render card when version matches localStorage", async () => {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, mockVersion.number)
|
||||
const { container } = render(<ContentMenuVersion />)
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(card).not.toHaveClass("animate animate-fadeInDown")
|
||||
})
|
||||
})
|
||||
|
||||
test("does not check localStorage when isBrowser is false", async () => {
|
||||
mockUseIsBrowser.mockReturnValue({
|
||||
isBrowser: false,
|
||||
})
|
||||
const { container } = render(<ContentMenuVersion />)
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(card).not.toHaveClass("animate animate-fadeInDown")
|
||||
})
|
||||
})
|
||||
|
||||
test("renders card with correct props", async () => {
|
||||
const { container } = render(<ContentMenuVersion />)
|
||||
await waitFor(() => {
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toHaveAttribute("data-title", "New version")
|
||||
expect(card).toHaveAttribute("data-text", `v${mockVersion.number} details`)
|
||||
expect(card).toHaveAttribute("data-href", mockVersion.releaseUrl)
|
||||
expect(card).toHaveAttribute("data-closeable", "true")
|
||||
expect(card).toHaveAttribute(
|
||||
"data-theme-image-light",
|
||||
mockVersion.bannerImage.light
|
||||
)
|
||||
expect(card).toHaveAttribute(
|
||||
"data-theme-image-dark",
|
||||
mockVersion.bannerImage.dark
|
||||
)
|
||||
expect(card).toHaveAttribute("data-image-width", "64")
|
||||
expect(card).toHaveAttribute("data-image-height", "40")
|
||||
})
|
||||
|
||||
test("renders card link with correct href props", async () => {
|
||||
const { container } = render(<ContentMenuVersion />)
|
||||
await waitFor(() => {
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const link = container.querySelector("[data-testid='card-link']")
|
||||
expect(link).toBeInTheDocument()
|
||||
expect(link).toHaveAttribute("href", mockVersion.releaseUrl)
|
||||
expect(link).toHaveAttribute("target", "_blank")
|
||||
expect(link).toHaveAttribute("rel", "noopener noreferrer")
|
||||
})
|
||||
|
||||
test("renders card with correct className", async () => {
|
||||
const { container } = render(<ContentMenuVersion />)
|
||||
await waitFor(() => {
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toHaveClass("!border-0")
|
||||
expect(card).toHaveClass("!bg-medusa-bg-component")
|
||||
expect(card).toHaveClass("hover:!bg-medusa-bg-component-hover")
|
||||
expect(card).toHaveClass("animation-fill-forwards")
|
||||
expect(card).toHaveClass("opacity-0")
|
||||
})
|
||||
|
||||
test("renders card with correct iconClassName", async () => {
|
||||
const { container } = render(<ContentMenuVersion />)
|
||||
await waitFor(() => {
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toHaveAttribute(
|
||||
"data-icon-class-name",
|
||||
"!shadow-none border-[0.5px] border-medusa-alphas-alpha-250"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("interactions", () => {
|
||||
test("closes card and saves version to localStorage when close button is clicked", () => {
|
||||
const { container } = render(<ContentMenuVersion />)
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toBeInTheDocument()
|
||||
|
||||
const closeButton = container.querySelector("[data-testid='card-close']")
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(closeButton!)
|
||||
expect(localStorage.getItem(LOCAL_STORAGE_KEY)).toBe(mockVersion.number)
|
||||
})
|
||||
|
||||
test("does not close card if showNewVersion is false", async () => {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, mockVersion.number)
|
||||
const { container } = render(<ContentMenuVersion />)
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(card).not.toHaveClass("animate animate-fadeInDown")
|
||||
})
|
||||
|
||||
// Card should not have close button visible when hidden
|
||||
const closeButton = container.querySelector("[data-testid='card-close']")
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
fireEvent.click(closeButton!)
|
||||
expect(localStorage.getItem(LOCAL_STORAGE_KEY)).toBe(mockVersion.number)
|
||||
})
|
||||
})
|
||||
|
||||
describe("animations", () => {
|
||||
test("does not add animation classes when version.hide is true", async () => {
|
||||
mockUseSiteConfig.mockReturnValue({
|
||||
...defaultUseSiteConfigReturn,
|
||||
config: {
|
||||
version: {
|
||||
...mockVersion,
|
||||
hide: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
const { container } = render(<ContentMenuVersion />)
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
// Animation classes should not be added when hide is true
|
||||
expect(card).not.toHaveClass("animate")
|
||||
expect(card).not.toHaveClass("animate-fadeInDown")
|
||||
})
|
||||
})
|
||||
|
||||
test("does not add animation classes when showNewVersion is false", async () => {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, mockVersion.number)
|
||||
const { container } = render(<ContentMenuVersion />)
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
// Animation classes should not be added when showNewVersion is false
|
||||
expect(card).not.toHaveClass("animate")
|
||||
expect(card).not.toHaveClass("animate-fadeInDown")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("version configuration", () => {
|
||||
test("renders with different version number", async () => {
|
||||
const newVersion = {
|
||||
...mockVersion,
|
||||
number: "3.0.0",
|
||||
}
|
||||
mockUseSiteConfig.mockReturnValue({
|
||||
...defaultUseSiteConfigReturn,
|
||||
config: {
|
||||
version: newVersion,
|
||||
},
|
||||
})
|
||||
const { container } = render(<ContentMenuVersion />)
|
||||
const card = container.querySelector("[data-testid='version-card']")
|
||||
expect(card).toBeInTheDocument()
|
||||
expect(card).toHaveAttribute("data-text", `v${newVersion.number} details`)
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { Card } from "../../Card"
|
||||
import { useIsBrowser, useSiteConfig } from "../../../providers"
|
||||
import { useIsBrowser } from "../../../providers/BrowserProvider"
|
||||
import { useSiteConfig } from "../../../providers/SiteConfig"
|
||||
import clsx from "clsx"
|
||||
|
||||
const LOCAL_STORAGE_KEY = "last-version"
|
||||
export const LOCAL_STORAGE_KEY = "last-version"
|
||||
|
||||
export const ContentMenuVersion = () => {
|
||||
const {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ContentMenuVersion } from "./Version"
|
||||
import { ContentMenuToc } from "./Toc"
|
||||
import { ContentMenuActions } from "./Actions"
|
||||
import { ContentMenuProducts } from "./Products"
|
||||
import { useLayout } from "../../providers"
|
||||
import { useLayout } from "../../providers/Layout"
|
||||
|
||||
export const ContentMenu = () => {
|
||||
const { showCollapsedNavbar } = useLayout()
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react"
|
||||
import { TooltipProps } from "../../Tooltip"
|
||||
|
||||
// mock functions
|
||||
const mockHandleCopy = vi.fn()
|
||||
const defaultUseCopyReturn = {
|
||||
isCopied: false,
|
||||
handleCopy: mockHandleCopy,
|
||||
}
|
||||
const mockUseCopy = vi.fn(() => defaultUseCopyReturn)
|
||||
const mockOnCopy = vi.fn()
|
||||
// mock components
|
||||
vi.mock("@/components/Tooltip", () => ({
|
||||
Tooltip: ({
|
||||
children,
|
||||
text,
|
||||
tooltipClassName,
|
||||
innerClassName,
|
||||
}: TooltipProps) => (
|
||||
<div
|
||||
data-testid="tooltip"
|
||||
data-text={text}
|
||||
data-tooltip-class-name={tooltipClassName}
|
||||
data-inner-class-name={innerClassName}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/hooks/use-copy", () => ({
|
||||
useCopy: () => mockUseCopy(),
|
||||
}))
|
||||
|
||||
import { CopyButton } from "../index"
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("render", () => {
|
||||
test("renders copy button", () => {
|
||||
const { container } = render(<CopyButton text="Copy to Clipboard" />)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
const tooltip = container.querySelector("[data-testid='tooltip']")
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute("data-text", "Copy to Clipboard")
|
||||
})
|
||||
|
||||
test("renders copy button with tooltip text", () => {
|
||||
const { container } = render(
|
||||
<CopyButton text="Copy to Clipboard" tooltipText="Custom Copy" />
|
||||
)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
const tooltip = container.querySelector("[data-testid='tooltip']")
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute("data-text", "Custom Copy")
|
||||
})
|
||||
|
||||
test("renders copy button with tooltip class name", () => {
|
||||
const { container } = render(
|
||||
<CopyButton text="Copy to Clipboard" tooltipClassName="custom-tooltip" />
|
||||
)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
const tooltip = container.querySelector("[data-testid='tooltip']")
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute("data-tooltip-class-name", "custom-tooltip")
|
||||
})
|
||||
|
||||
test("renders copy button with tooltip inner class name", () => {
|
||||
const { container } = render(
|
||||
<CopyButton
|
||||
text="Copy to Clipboard"
|
||||
tooltipInnerClassName="custom-tooltip-inner"
|
||||
/>
|
||||
)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
const tooltip = container.querySelector("[data-testid='tooltip']")
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute(
|
||||
"data-inner-class-name",
|
||||
"custom-tooltip-inner"
|
||||
)
|
||||
})
|
||||
|
||||
test("renders copy button with button class name", () => {
|
||||
const { container } = render(
|
||||
<CopyButton text="Copy to Clipboard" buttonClassName="custom-button" />
|
||||
)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
expect(copyButton).toHaveClass("custom-button")
|
||||
})
|
||||
|
||||
test("renders copy button with handleTouch", () => {
|
||||
const { container } = render(
|
||||
<CopyButton text="Copy to Clipboard" handleTouch />
|
||||
)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
const tooltip = container.querySelector("[data-testid='tooltip']")
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute("data-tooltip-class-name", "!block")
|
||||
})
|
||||
|
||||
test("tooltip text changes to 'Copied!' when isCopied is true", () => {
|
||||
mockUseCopy.mockReturnValue({
|
||||
...defaultUseCopyReturn,
|
||||
isCopied: true,
|
||||
})
|
||||
const { container } = render(<CopyButton text="Copy to Clipboard" />)
|
||||
const tooltip = container.querySelector("[data-testid='tooltip']")
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute("data-text", "Copied!")
|
||||
})
|
||||
})
|
||||
|
||||
describe("interactions", () => {
|
||||
test("calls handleCopy when copy button is clicked", () => {
|
||||
const { container } = render(<CopyButton text="Copy to Clipboard" />)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
fireEvent.click(copyButton!)
|
||||
expect(mockHandleCopy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("calls handleCopy when copy button is clicked with handleTouch", () => {
|
||||
const { container } = render(
|
||||
<CopyButton text="Copy to Clipboard" handleTouch />
|
||||
)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
fireEvent.click(copyButton!)
|
||||
expect(mockHandleCopy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("doesn't call handleCopy when copy button is touched then clicked, with handleTouch", async () => {
|
||||
const { container } = render(
|
||||
<CopyButton text="Copy to Clipboard" handleTouch />
|
||||
)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
fireEvent.touchEnd(copyButton!)
|
||||
fireEvent.click(copyButton!)
|
||||
expect(mockHandleCopy).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
test("doesn't call handleCopy when button is touched and handleTouch is true and touchCount is 0", () => {
|
||||
const { container } = render(
|
||||
<CopyButton text="Copy to Clipboard" handleTouch />
|
||||
)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
fireEvent.touchEnd(copyButton!)
|
||||
expect(mockHandleCopy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("calls handleCopy when button is touched and handleTouch is true and touchCount is 1", () => {
|
||||
const { container } = render(
|
||||
<CopyButton text="Copy to Clipboard" handleTouch />
|
||||
)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
fireEvent.touchEnd(copyButton!)
|
||||
expect(mockHandleCopy).not.toHaveBeenCalled()
|
||||
fireEvent.touchEnd(copyButton!)
|
||||
expect(mockHandleCopy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("calls onCopy when copy button is clicked", () => {
|
||||
const { container } = render(
|
||||
<CopyButton text="Copy to Clipboard" onCopy={mockOnCopy} />
|
||||
)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
fireEvent.click(copyButton!)
|
||||
expect(mockOnCopy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("calls onCopy when button is touched and handleTouch is true and touchCount is 1", () => {
|
||||
const { container } = render(
|
||||
<CopyButton text="Copy to Clipboard" handleTouch onCopy={mockOnCopy} />
|
||||
)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
fireEvent.touchEnd(copyButton!)
|
||||
expect(mockOnCopy).not.toHaveBeenCalled()
|
||||
fireEvent.touchEnd(copyButton!)
|
||||
expect(mockOnCopy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import React, { useState } from "react"
|
||||
import clsx from "clsx"
|
||||
import { Tooltip } from "@/components"
|
||||
import { useCopy } from "../../hooks"
|
||||
import { Tooltip } from "@/components/Tooltip"
|
||||
import { useCopy } from "../../hooks/use-copy"
|
||||
|
||||
export type CopyButtonChildFn = (props: {
|
||||
isCopied: boolean
|
||||
@@ -68,6 +68,7 @@ export const CopyButton = ({
|
||||
setTouchCount(0)
|
||||
}
|
||||
}}
|
||||
data-testid="copy-button"
|
||||
>
|
||||
{typeof children === "function" ? children({ isCopied }) : children}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import { CopyButtonProps } from "../../CopyButton"
|
||||
import { UseGenerateSnippet } from "../../../hooks/use-generate-snippet"
|
||||
|
||||
// mock data
|
||||
const mockUseGenerateSnippetProps: UseGenerateSnippet = {
|
||||
type: "subscriber",
|
||||
options: {
|
||||
event: "order.placed",
|
||||
payload: {
|
||||
id: "order_123",
|
||||
},
|
||||
},
|
||||
}
|
||||
const defaultUseGenerateSnippetReturn = {
|
||||
snippet: "const snippet = 'snippet'",
|
||||
}
|
||||
|
||||
// mock functions
|
||||
const mockUseGenerateSnippet = vi.fn(() => defaultUseGenerateSnippetReturn)
|
||||
|
||||
// mock components
|
||||
vi.mock("@/hooks/use-generate-snippet", () => ({
|
||||
useGenerateSnippet: () => mockUseGenerateSnippet(),
|
||||
}))
|
||||
vi.mock("@/components/CopyButton", () => ({
|
||||
CopyButton: ({ children, text }: CopyButtonProps) => (
|
||||
<div data-testid="copy-button" data-text={text}>
|
||||
{typeof children === "function"
|
||||
? children({ isCopied: false })
|
||||
: children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
import { CopyGeneratedSnippetButton } from "../../CopyGeneratedSnippetButton"
|
||||
|
||||
describe("render", () => {
|
||||
test("renders copy generated snippet button", () => {
|
||||
const { container } = render(
|
||||
<CopyGeneratedSnippetButton {...mockUseGenerateSnippetProps} />
|
||||
)
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
expect(copyButton).toHaveAttribute(
|
||||
"data-text",
|
||||
defaultUseGenerateSnippetReturn.snippet
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { CopyButton, useGenerateSnippet, UseGenerateSnippet } from "../.."
|
||||
import { CopyButton } from "@/components/CopyButton"
|
||||
import {
|
||||
useGenerateSnippet,
|
||||
UseGenerateSnippet,
|
||||
} from "@/hooks/use-generate-snippet"
|
||||
import { SquareTwoStack, CheckCircle } from "@medusajs/icons"
|
||||
|
||||
export type CopyGeneratedSnippetButtonProps = UseGenerateSnippet & {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import { DetailsSummary } from "../index"
|
||||
|
||||
describe("render", () => {
|
||||
test("renders details summary with children", () => {
|
||||
const { container } = render(<DetailsSummary>Test</DetailsSummary>)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
const title = container.querySelector(
|
||||
"[data-testid='details-summary-title']"
|
||||
)
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(title).toHaveTextContent("Test")
|
||||
})
|
||||
|
||||
test("renders details summary with title", () => {
|
||||
const { container } = render(<DetailsSummary title="Test" />)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
const title = container.querySelector(
|
||||
"[data-testid='details-summary-title']"
|
||||
)
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(title).toHaveTextContent("Test")
|
||||
})
|
||||
|
||||
test("renders details summary with subtitle", () => {
|
||||
const { container } = render(<DetailsSummary subtitle="Test" />)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
const subtitle = container.querySelector(
|
||||
"[data-testid='details-summary-subtitle']"
|
||||
)
|
||||
expect(subtitle).toBeInTheDocument()
|
||||
expect(subtitle).toHaveTextContent("Test")
|
||||
})
|
||||
|
||||
test("renders details summary with badge", () => {
|
||||
const badge = <div data-testid="test-badge">Test Badge</div>
|
||||
const { container } = render(<DetailsSummary badge={badge} />)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
const extra = container.querySelector(
|
||||
"[data-testid='details-summary-extra']"
|
||||
)
|
||||
expect(extra).toBeInTheDocument()
|
||||
const badgeElement = extra?.querySelector("[data-testid='test-badge']")
|
||||
expect(badgeElement).toBeInTheDocument()
|
||||
expect(badgeElement).toHaveTextContent("Test Badge")
|
||||
})
|
||||
|
||||
test("renders details summary with expandable", () => {
|
||||
const { container } = render(<DetailsSummary expandable />)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
expect(summary).toHaveClass("cursor-pointer")
|
||||
expect(summary).toHaveClass("gap-0.5")
|
||||
const extra = container.querySelector(
|
||||
"[data-testid='details-summary-extra']"
|
||||
)
|
||||
expect(extra).toBeInTheDocument()
|
||||
const icon = extra?.querySelector("svg")
|
||||
expect(icon).toBeInTheDocument()
|
||||
expect(icon).toHaveClass("transition-transform")
|
||||
expect(icon).not.toHaveClass("rotate-45")
|
||||
})
|
||||
|
||||
test("renders details summary with hideExpandableIcon", () => {
|
||||
const { container } = render(<DetailsSummary hideExpandableIcon />)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
const extra = container.querySelector(
|
||||
"[data-testid='details-summary-extra']"
|
||||
)
|
||||
expect(extra).toBeInTheDocument()
|
||||
const icon = extra?.querySelector("svg")
|
||||
expect(icon).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("renders details summary with className", () => {
|
||||
const { container } = render(<DetailsSummary className="test-class" />)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
expect(summary).toHaveClass("test-class")
|
||||
})
|
||||
|
||||
test("renders details summary with titleClassName", () => {
|
||||
const { container } = render(
|
||||
<DetailsSummary titleClassName="test-title-class" />
|
||||
)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
const title = container.querySelector(
|
||||
"[data-testid='details-summary-title']"
|
||||
)
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(title).toHaveClass("test-title-class")
|
||||
})
|
||||
|
||||
test("renders details summary with summaryRef", () => {
|
||||
const summaryRef = vi.fn()
|
||||
const { container } = render(<DetailsSummary summaryRef={summaryRef} />)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
expect(summaryRef).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("renders details summary with rest props", () => {
|
||||
const { container } = render(<DetailsSummary data-testid="test-summary" />)
|
||||
const summary = container.querySelector("[data-testid='test-summary']")
|
||||
expect(summary).toBeInTheDocument()
|
||||
expect(summary).toHaveAttribute("data-testid", "test-summary")
|
||||
})
|
||||
|
||||
test("renders with expandable and open", () => {
|
||||
const { container } = render(<DetailsSummary expandable open />)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
expect(summary).toHaveClass("cursor-pointer")
|
||||
expect(summary).toHaveClass("gap-0.5")
|
||||
const extra = container.querySelector(
|
||||
"[data-testid='details-summary-extra']"
|
||||
)
|
||||
expect(extra).toBeInTheDocument()
|
||||
const icon = extra?.querySelector("svg")
|
||||
expect(icon).toBeInTheDocument()
|
||||
expect(icon).toHaveClass("transition-transform")
|
||||
expect(icon).toHaveClass("rotate-45")
|
||||
})
|
||||
})
|
||||
@@ -47,17 +47,21 @@ export const DetailsSummary = ({
|
||||
"text-compact-medium-plus text-medusa-fg-base",
|
||||
titleClassName
|
||||
)}
|
||||
data-testid="details-summary-title"
|
||||
>
|
||||
{title || children}
|
||||
</span>
|
||||
{subtitle && (
|
||||
<span className="text-compact-medium text-medusa-fg-subtle mt-0.5">
|
||||
<span
|
||||
className="text-compact-medium text-medusa-fg-subtle mt-0.5"
|
||||
data-testid="details-summary-subtitle"
|
||||
>
|
||||
{subtitle}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{(badge || expandable) && (
|
||||
<span className="flex gap-docs_0.5">
|
||||
<span className="flex gap-docs_0.5" data-testid="details-summary-extra">
|
||||
{badge}
|
||||
{expandable && !hideExpandableIcon && (
|
||||
<PlusMini
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render } from "@testing-library/react"
|
||||
import { CollapsibleProps } from "../../../hooks/use-collapsible"
|
||||
|
||||
// mock hooks
|
||||
const mockSetCollapsed = vi.fn()
|
||||
const mockGetCollapsibleElms = vi.fn(
|
||||
(children: React.ReactNode): React.ReactNode => children
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const mockUseCollapsible = vi.fn((_options: CollapsibleProps) => ({
|
||||
getCollapsibleElms: mockGetCollapsibleElms,
|
||||
setCollapsed: mockSetCollapsed,
|
||||
}))
|
||||
|
||||
// mock components
|
||||
vi.mock("@/hooks/use-collapsible", () => ({
|
||||
useCollapsible: (options: CollapsibleProps) => mockUseCollapsible(options),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/Loading", () => ({
|
||||
Loading: ({ className }: { className?: string }) => (
|
||||
<div data-testid="loading" className={className}>
|
||||
Loading...
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/Details/Summary", () => ({
|
||||
DetailsSummary: ({ title, onClick }: DetailsSummaryProps) => (
|
||||
<summary data-testid="details-summary-title" onClick={onClick}>
|
||||
{title}
|
||||
</summary>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Suspense to avoid act warnings - render children directly
|
||||
vi.mock("react", async () => {
|
||||
const actual = await vi.importActual<typeof React>("react")
|
||||
return {
|
||||
...actual,
|
||||
Suspense: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
}
|
||||
})
|
||||
|
||||
import { Details } from "../../Details"
|
||||
import { DetailsSummary, DetailsSummaryProps } from "../Summary"
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseCollapsible.mockClear()
|
||||
mockUseCollapsible.mockReturnValue({
|
||||
getCollapsibleElms: mockGetCollapsibleElms,
|
||||
setCollapsed: mockSetCollapsed,
|
||||
})
|
||||
mockSetCollapsed.mockClear()
|
||||
mockGetCollapsibleElms.mockImplementation(
|
||||
(children: React.ReactNode): React.ReactNode => children
|
||||
)
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders details element", () => {
|
||||
const { container } = render(
|
||||
<Details>
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const details = container.querySelector("details")
|
||||
expect(details).toBeInTheDocument()
|
||||
expect(details).toHaveTextContent("Content")
|
||||
})
|
||||
|
||||
test("renders with summaryContent", () => {
|
||||
const { container } = render(
|
||||
<Details summaryContent="Summary Title">
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
const title = container.querySelector(
|
||||
"[data-testid='details-summary-title']"
|
||||
)
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(title).toHaveTextContent("Summary Title")
|
||||
})
|
||||
|
||||
test("renders with summaryElm", () => {
|
||||
const summaryElm = <DetailsSummary title="Custom Summary" />
|
||||
const { container } = render(
|
||||
<Details summaryElm={summaryElm}>
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
const title = container.querySelector(
|
||||
"[data-testid='details-summary-title']"
|
||||
)
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(title).toHaveTextContent("Custom Summary")
|
||||
})
|
||||
|
||||
test("renders with className", () => {
|
||||
const { container } = render(
|
||||
<Details className="custom-class">
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const details = container.querySelector("details")
|
||||
expect(details).toHaveClass("custom-class")
|
||||
})
|
||||
})
|
||||
|
||||
describe("initial state", () => {
|
||||
test("renders closed by default when openInitial is false", () => {
|
||||
const { container } = render(
|
||||
<Details openInitial={false}>
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const details = container.querySelector("details")
|
||||
expect(details).not.toHaveAttribute("open")
|
||||
})
|
||||
|
||||
test("renders open when openInitial is true", () => {
|
||||
const { container } = render(
|
||||
<Details openInitial={true}>
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const details = container.querySelector("details")
|
||||
expect(details).toHaveAttribute("open")
|
||||
})
|
||||
|
||||
test("passes correct initialValue to useCollapsible when openInitial is false", () => {
|
||||
render(
|
||||
<Details openInitial={false}>
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
expect(mockUseCollapsible).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialValue: true, // !openInitial = !false = true
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test("passes correct initialValue to useCollapsible when openInitial is true", () => {
|
||||
render(
|
||||
<Details openInitial={true}>
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
expect(mockUseCollapsible).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialValue: false, // !openInitial = !true = false
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("height animation", () => {
|
||||
test("passes heightAnimation to useCollapsible when true", () => {
|
||||
render(
|
||||
<Details heightAnimation={true}>
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
expect(mockUseCollapsible).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
heightAnimation: true,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test("passes heightAnimation to useCollapsible when false", () => {
|
||||
render(
|
||||
<Details heightAnimation={false}>
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
expect(mockUseCollapsible).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
heightAnimation: false,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("interactions", () => {
|
||||
test("toggles open state when summary is clicked", () => {
|
||||
const { container } = render(
|
||||
<Details summaryContent="Summary">
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const details = container.querySelector("details")
|
||||
const summary = container.querySelector("summary")
|
||||
expect(details).not.toHaveAttribute("open")
|
||||
|
||||
fireEvent.click(summary!)
|
||||
expect(details).toHaveAttribute("open")
|
||||
expect(mockSetCollapsed).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
test("closes when summary is clicked and already open", () => {
|
||||
mockUseCollapsible.mockReturnValue({
|
||||
getCollapsibleElms: mockGetCollapsibleElms,
|
||||
setCollapsed: mockSetCollapsed,
|
||||
})
|
||||
const { container } = render(
|
||||
<Details summaryContent="Summary" openInitial={true}>
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const details = container.querySelector("details")
|
||||
const summary = container.querySelector("summary")
|
||||
expect(details).toHaveAttribute("open")
|
||||
|
||||
fireEvent.click(summary!)
|
||||
expect(mockSetCollapsed).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
test("prevents default on details click", () => {
|
||||
const { container } = render(
|
||||
<Details summaryContent="Summary">
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const details = container.querySelector("details")
|
||||
const clickEvent = new MouseEvent("click", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
const preventDefaultSpy = vi.spyOn(clickEvent, "preventDefault")
|
||||
|
||||
fireEvent(details!, clickEvent)
|
||||
expect(preventDefaultSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("stops propagation on toggle event", () => {
|
||||
const { container } = render(
|
||||
<Details summaryContent="Summary">
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const details = container.querySelector("details")
|
||||
const toggleEvent = new Event("toggle", { bubbles: true, cancelable: true })
|
||||
const stopPropagationSpy = vi.spyOn(toggleEvent, "stopPropagation")
|
||||
|
||||
fireEvent(details!, toggleEvent)
|
||||
expect(stopPropagationSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("navigates to link when link is clicked in summary", () => {
|
||||
const originalLocation = window.location
|
||||
delete (window as unknown as { location?: Location }).location
|
||||
Object.defineProperty(window, "location", {
|
||||
value: { href: "http://localhost" },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
const { container } = render(
|
||||
<Details summaryContent={<a href="/test">Link</a>}>
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const link = container.querySelector("a")
|
||||
expect(link).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(link!)
|
||||
expect(window.location.href).toBe("/test")
|
||||
|
||||
Object.defineProperty(window, "location", {
|
||||
value: originalLocation,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
})
|
||||
|
||||
test("does not toggle when code element is clicked", () => {
|
||||
const { container } = render(
|
||||
<Details summaryContent={<code>Code</code>}>
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const details = container.querySelector("details")
|
||||
const code = container.querySelector("code")
|
||||
expect(details).not.toHaveAttribute("open")
|
||||
|
||||
mockSetCollapsed.mockClear()
|
||||
fireEvent.click(code!)
|
||||
// Should not toggle when code is clicked
|
||||
expect(mockSetCollapsed).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("summary element cloning", () => {
|
||||
test("clones summaryElm with open and onClick props", () => {
|
||||
const summaryElm = <DetailsSummary title="Custom Summary" />
|
||||
const { container } = render(
|
||||
<Details summaryElm={summaryElm} openInitial={true}>
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
// The cloned element should receive open prop
|
||||
const title = container.querySelector(
|
||||
"[data-testid='details-summary-title']"
|
||||
)
|
||||
expect(title).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("clones summaryElm with onClick handler", () => {
|
||||
const summaryElm = <DetailsSummary title="Custom Summary" />
|
||||
const { container } = render(
|
||||
<Details summaryElm={summaryElm}>
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
const summary = container.querySelector("summary")
|
||||
expect(summary).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(summary!)
|
||||
expect(mockSetCollapsed).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("collapsible integration", () => {
|
||||
test("calls onClose callback when collapsed", () => {
|
||||
mockUseCollapsible.mockReturnValue({
|
||||
getCollapsibleElms: mockGetCollapsibleElms,
|
||||
setCollapsed: mockSetCollapsed,
|
||||
})
|
||||
|
||||
render(
|
||||
<Details summaryContent="Summary">
|
||||
<div>Content</div>
|
||||
</Details>
|
||||
)
|
||||
|
||||
expect(mockUseCollapsible).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onClose: expect.any(Function),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import React, { Suspense, cloneElement, useRef, useState } from "react"
|
||||
import { Loading } from "@/components"
|
||||
import { Loading } from "@/components/Loading"
|
||||
import clsx from "clsx"
|
||||
import { DetailsSummary, DetailsSummaryProps } from "./Summary"
|
||||
import { useCollapsible } from "../../hooks"
|
||||
import { useCollapsible } from "../../hooks/use-collapsible"
|
||||
|
||||
export type DetailsProps = {
|
||||
openInitial?: boolean
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
import { Details, MarkdownContent } from "@/components"
|
||||
import { Details } from "@/components/Details"
|
||||
import { MarkdownContent } from "@/components/MarkdownContent"
|
||||
|
||||
type TroubleshootingSection = {
|
||||
title: string
|
||||
|
||||
@@ -6,14 +6,19 @@ import React from "react"
|
||||
export type DottedSeparatorProps = {
|
||||
wrapperClassName?: string
|
||||
className?: string
|
||||
"data-testid"?: string
|
||||
}
|
||||
|
||||
export const DottedSeparator = ({
|
||||
className,
|
||||
wrapperClassName,
|
||||
"data-testid": testId,
|
||||
}: DottedSeparatorProps) => {
|
||||
return (
|
||||
<div className={clsx("px-docs_0.75 my-docs_0.75", wrapperClassName)}>
|
||||
<div
|
||||
className={clsx("px-docs_0.75 my-docs_0.75", wrapperClassName)}
|
||||
data-testid={testId}
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
"block w-full h-px relative bg-border-dotted",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
|
||||
// mock components
|
||||
vi.mock("@/components/EditDate", () => ({
|
||||
EditDate: ({ date }: { date: string }) => (
|
||||
<div data-testid="edit-date">{date}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
import { EditButton } from "../../EditButton"
|
||||
|
||||
describe("render", () => {
|
||||
test("renders edit button", () => {
|
||||
const { container } = render(<EditButton filePath="/test.md" />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const editButton = container.querySelector("[data-testid='edit-button']")
|
||||
expect(editButton).toBeInTheDocument()
|
||||
expect(editButton).toHaveTextContent("Edit this page")
|
||||
expect(editButton).toHaveAttribute(
|
||||
"href",
|
||||
"https://github.com/medusajs/medusa/edit/develop/test.md"
|
||||
)
|
||||
})
|
||||
|
||||
test("renders edit button with edit date", () => {
|
||||
const { container } = render(
|
||||
<EditButton filePath="/test.md" editDate="2021-01-01" />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const editDate = container.querySelector("[data-testid='edit-date']")
|
||||
expect(editDate).toBeInTheDocument()
|
||||
expect(editDate).toHaveTextContent("2021-01-01")
|
||||
})
|
||||
})
|
||||
@@ -21,6 +21,7 @@ export const EditButton = ({ filePath, editDate }: EditButtonProps) => {
|
||||
"text-medusa-fg-subtle hover:text-medusa-fg-base",
|
||||
"text-compact-small-plus"
|
||||
)}
|
||||
data-testid="edit-button"
|
||||
>
|
||||
<span>Edit this page</span>
|
||||
<ArrowUpRightOnBox />
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import { EditDate } from "../../EditDate"
|
||||
|
||||
// mock data
|
||||
const today = new Date()
|
||||
|
||||
describe("render", () => {
|
||||
test("renders edit date", () => {
|
||||
const { container } = render(
|
||||
<EditDate date={`${today.getFullYear()}-01-01`} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
const editDate = container.querySelector("[data-testid='edit-date']")
|
||||
expect(editDate).toBeInTheDocument()
|
||||
expect(editDate).toHaveTextContent("Edited Jan 1")
|
||||
})
|
||||
|
||||
test("renders edit date with different year", () => {
|
||||
const lastYear = today.getFullYear() - 1
|
||||
const { container } = render(<EditDate date={`${lastYear}-01-01`} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const editDate = container.querySelector("[data-testid='edit-date']")
|
||||
expect(editDate).toBeInTheDocument()
|
||||
expect(editDate).toHaveTextContent(`Edited Jan 1, ${lastYear}`)
|
||||
})
|
||||
|
||||
test("doesn't render edit date if date is invalid", () => {
|
||||
const { container } = render(<EditDate date="invalid-date" />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const editDate = container.querySelector("[data-testid='edit-date']")
|
||||
expect(editDate).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -7,8 +7,11 @@ type EditDateProps = {
|
||||
}
|
||||
|
||||
export const EditDate = ({ date }: EditDateProps) => {
|
||||
const today = new Date(date)
|
||||
const today = new Date()
|
||||
const dateObj = new Date(date)
|
||||
if (isNaN(dateObj.getTime())) {
|
||||
return <></>
|
||||
}
|
||||
const formattedDate = dateObj.toString()
|
||||
const dateMatch = DATE_REGEX.exec(formattedDate)
|
||||
|
||||
@@ -18,7 +21,7 @@ export const EditDate = ({ date }: EditDateProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="text-compact-small-plus">
|
||||
<span className="text-compact-small-plus" data-testid="edit-date">
|
||||
Edited {dateMatch.groups.month} {dateObj.getDate()}
|
||||
{dateObj.getFullYear() !== today.getFullYear()
|
||||
? `, ${dateObj.getFullYear()}`
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { render, waitFor } from "@testing-library/react"
|
||||
import { GitHubSearchItem } from "../../.."
|
||||
|
||||
// mock data
|
||||
const mockGitHubSearchItem: GitHubSearchItem = {
|
||||
url: "https://github.com/medusajs/medusa/issues/123",
|
||||
html_url: "https://github.com/medusajs/medusa/issues/123",
|
||||
title: "Test Issue",
|
||||
}
|
||||
|
||||
// mock hooks
|
||||
const mockSearchGitHub = vi.fn(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async (_input: RequestInfo | URL, _init?: RequestInit | undefined) => {
|
||||
const response = new Response(
|
||||
JSON.stringify({ items: [mockGitHubSearchItem] })
|
||||
)
|
||||
// Mock the json() method to return the parsed data
|
||||
response.json = vi.fn().mockResolvedValue({ items: [mockGitHubSearchItem] })
|
||||
return Promise.resolve(response)
|
||||
}
|
||||
)
|
||||
|
||||
// mock components
|
||||
vi.mock("@/components/Link", () => ({
|
||||
Link: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href}>{children}</a>
|
||||
),
|
||||
}))
|
||||
vi.mock("@/components/MDXComponents", () => ({
|
||||
MDXComponents: {
|
||||
ul: (props: React.HTMLAttributes<HTMLUListElement>) => <ul {...props} />,
|
||||
li: (props: React.HTMLAttributes<HTMLLIElement>) => <li {...props} />,
|
||||
},
|
||||
}))
|
||||
|
||||
// helper function
|
||||
const getGitHubSearchUrl = (query: string) =>
|
||||
`https://api.github.com/search/issues?q=${encodeURIComponent(`${query} repo:medusajs/medusa is:closed is:issue`)}&sort=updated&per_page=3&advanced_search=true`
|
||||
|
||||
import { Solutions } from "../../Solutions"
|
||||
|
||||
beforeEach(() => {
|
||||
window.fetch = mockSearchGitHub
|
||||
mockSearchGitHub.mockClear()
|
||||
})
|
||||
|
||||
describe("render", () => {
|
||||
test("doesn't render solutions if feedback is true (positive)", () => {
|
||||
const { container } = render(<Solutions feedback={true} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
const solutionItems = container.querySelectorAll(
|
||||
"[data-testid='solution-item']"
|
||||
)
|
||||
expect(solutionItems).toHaveLength(0)
|
||||
expect(mockSearchGitHub).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("renders solutions if feedback is false using document title", async () => {
|
||||
document.title = "Test Page"
|
||||
const { container } = render(<Solutions feedback={false} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(mockSearchGitHub).toHaveBeenCalledWith(
|
||||
getGitHubSearchUrl("Test Page"),
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
const solutionItems = container.querySelectorAll(
|
||||
"[data-testid='solution-item']"
|
||||
)
|
||||
expect(solutionItems).toHaveLength(1)
|
||||
expect(solutionItems[0]).toBeInTheDocument()
|
||||
expect(solutionItems[0]).toHaveTextContent("Test Issue")
|
||||
})
|
||||
|
||||
test("renders solutions if feedback is false using message", async () => {
|
||||
const { container } = render(
|
||||
<Solutions feedback={false} message="Test Message" />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(mockSearchGitHub).toHaveBeenCalledWith(
|
||||
getGitHubSearchUrl("Test Message"),
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
const solutionItems = container.querySelectorAll(
|
||||
"[data-testid='solution-item']"
|
||||
)
|
||||
expect(solutionItems).toHaveLength(1)
|
||||
expect(solutionItems[0]).toBeInTheDocument()
|
||||
expect(solutionItems[0]).toHaveTextContent("Test Issue")
|
||||
})
|
||||
|
||||
test("uses first 256 characters of message if feedback is false and it is longer than 256 characters", () => {
|
||||
const longMessage = "a".repeat(257)
|
||||
const shortMessage = longMessage.substring(0, 256)
|
||||
const { container } = render(
|
||||
<Solutions feedback={false} message={shortMessage} />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
expect(mockSearchGitHub).toHaveBeenCalledWith(
|
||||
getGitHubSearchUrl(shortMessage),
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("gets query from document title if feedback is false and no results are provided for message", async () => {
|
||||
document.title = "Test Page"
|
||||
mockSearchGitHub.mockImplementation(async () => {
|
||||
const response = new Response(JSON.stringify({ items: [] }))
|
||||
response.json = vi.fn().mockResolvedValue({ items: [] })
|
||||
return response
|
||||
})
|
||||
const { container } = render(
|
||||
<Solutions feedback={false} message="Test Message" />
|
||||
)
|
||||
expect(container).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(mockSearchGitHub).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
const solutionItems = container.querySelectorAll(
|
||||
"[data-testid='solution-item']"
|
||||
)
|
||||
expect(solutionItems).toHaveLength(0)
|
||||
expect(mockSearchGitHub).toHaveBeenCalledWith(
|
||||
getGitHubSearchUrl("Test Message"),
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
}
|
||||
)
|
||||
expect(mockSearchGitHub).toHaveBeenCalledWith(
|
||||
getGitHubSearchUrl("Test Page"),
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { Link, MDXComponents } from "@/components"
|
||||
import { Link } from "@/components/Link"
|
||||
import { MDXComponents } from "@/components/MDXComponents"
|
||||
|
||||
export type SolutionsProps = {
|
||||
feedback: boolean
|
||||
@@ -75,7 +76,7 @@ export const Solutions = ({ feedback, message }: SolutionsProps) => {
|
||||
</span>
|
||||
<Ul>
|
||||
{possibleSolutions.map((solution) => (
|
||||
<Li key={solution.url}>
|
||||
<Li key={solution.url} data-testid="solution-item">
|
||||
<Link
|
||||
href={solution.html_url}
|
||||
target="_blank"
|
||||
|
||||
@@ -0,0 +1,596 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react"
|
||||
import { GITHUB_ISSUES_LINK } from "../../../constants"
|
||||
import { ButtonProps } from "../../Button"
|
||||
import { LabelProps } from "../../Label"
|
||||
import { RadioItemProps } from "../../RadioItem"
|
||||
|
||||
// mock data
|
||||
const defaultUseSiteConfigReturn = {
|
||||
config: {
|
||||
reportIssueLink: GITHUB_ISSUES_LINK as string | undefined,
|
||||
},
|
||||
}
|
||||
|
||||
// mock functions
|
||||
const mockTrack = vi.fn(({ event }: { event: { callback?: () => void } }) => {
|
||||
// Call callback immediately to simulate synchronous behavior
|
||||
event.callback?.()
|
||||
})
|
||||
const mockUseSiteConfig = vi.fn(() => defaultUseSiteConfigReturn)
|
||||
|
||||
// mock components
|
||||
vi.mock("@/providers/Analytics", () => ({
|
||||
useAnalytics: () => ({
|
||||
track: mockTrack,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock("@/providers/SiteConfig", () => ({
|
||||
useSiteConfig: () => mockUseSiteConfig(),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/Button", () => ({
|
||||
Button: (props: ButtonProps) => <button {...props} />,
|
||||
}))
|
||||
|
||||
vi.mock("@/components/TextArea", () => ({
|
||||
TextArea: ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
rows,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
placeholder?: string
|
||||
rows?: number
|
||||
}) => (
|
||||
<textarea
|
||||
data-testid="textarea"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
rows={rows}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/Label", () => ({
|
||||
Label: (props: LabelProps) => <label {...props} />,
|
||||
}))
|
||||
|
||||
vi.mock("@/components/DottedSeparator", () => ({
|
||||
DottedSeparator: ({ wrapperClassName }: { wrapperClassName?: string }) => (
|
||||
<div data-testid="dotted-separator" className={wrapperClassName} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/RadioItem", () => ({
|
||||
RadioItem: (props: RadioItemProps) => <input {...props} />,
|
||||
}))
|
||||
|
||||
vi.mock("@/components/Feedback/Solutions", () => ({
|
||||
Solutions: ({
|
||||
message,
|
||||
feedback,
|
||||
}: {
|
||||
message: string
|
||||
feedback: boolean
|
||||
}) => (
|
||||
<div
|
||||
data-testid="solutions"
|
||||
data-message={message}
|
||||
data-feedback={feedback}
|
||||
>
|
||||
Solutions
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("react-transition-group", () => ({
|
||||
CSSTransition: ({ children }: { children: React.ReactNode }) => (
|
||||
<>{children}</>
|
||||
),
|
||||
SwitchTransition: ({ children }: { children: React.ReactNode }) => (
|
||||
<>{children}</>
|
||||
),
|
||||
}))
|
||||
|
||||
import { Feedback } from "../../Feedback"
|
||||
|
||||
beforeEach(() => {
|
||||
mockTrack.mockClear()
|
||||
mockUseSiteConfig.mockImplementation(() => defaultUseSiteConfigReturn)
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("default render", () => {
|
||||
const { container } = render(<Feedback event="test-event" />)
|
||||
const positiveButton = container.querySelector(
|
||||
"button[data-testid='positive-button']"
|
||||
)
|
||||
expect(positiveButton).toBeInTheDocument()
|
||||
expect(positiveButton).toHaveTextContent("It was helpful")
|
||||
const negativeButton = container.querySelector(
|
||||
"button[data-testid='negative-button']"
|
||||
)
|
||||
expect(negativeButton).toBeInTheDocument()
|
||||
expect(negativeButton).toHaveTextContent("It wasn't helpful")
|
||||
const label = container.querySelector("label[data-testid='question-label']")
|
||||
expect(label).toBeInTheDocument()
|
||||
expect(label).toHaveTextContent("Was this page helpful?")
|
||||
|
||||
const reportIssueButton = container.querySelector(
|
||||
"button[data-testid='report-issue-button']"
|
||||
)
|
||||
expect(reportIssueButton).toBeInTheDocument()
|
||||
expect(reportIssueButton).toHaveTextContent("Report Issue")
|
||||
const link = reportIssueButton?.querySelector("a")
|
||||
expect(link).toBeInTheDocument()
|
||||
expect(link).toHaveAttribute("href", GITHUB_ISSUES_LINK)
|
||||
const separators = container.querySelectorAll(
|
||||
"[data-testid='dotted-separator']"
|
||||
)
|
||||
expect(separators).toHaveLength(2)
|
||||
})
|
||||
|
||||
test("renders custom question", () => {
|
||||
const { container } = render(
|
||||
<Feedback event="test-event" question="Custom question?" />
|
||||
)
|
||||
const label = container.querySelector("label[data-testid='question-label']")
|
||||
expect(label).toBeInTheDocument()
|
||||
expect(label).toHaveTextContent("Custom question?")
|
||||
})
|
||||
|
||||
test("renders custom positive button text", () => {
|
||||
const { container } = render(
|
||||
<Feedback event="test-event" positiveBtn="Yes!" />
|
||||
)
|
||||
const buttons = container.querySelector(
|
||||
"button[data-testid='positive-button']"
|
||||
)
|
||||
expect(buttons).toBeInTheDocument()
|
||||
expect(buttons).toHaveTextContent("Yes!")
|
||||
})
|
||||
|
||||
test("renders custom negative button text", () => {
|
||||
const { container } = render(
|
||||
<Feedback event="test-event" negativeBtn="No!" />
|
||||
)
|
||||
const buttons = container.querySelector(
|
||||
"button[data-testid='negative-button']"
|
||||
)
|
||||
expect(buttons).toBeInTheDocument()
|
||||
expect(buttons).toHaveTextContent("No!")
|
||||
})
|
||||
|
||||
test("renders report issue button when initReportLink is provided", () => {
|
||||
const customReportLink = "https://custom-link.com"
|
||||
const { container } = render(
|
||||
<Feedback event="test-event" reportLink={customReportLink} />
|
||||
)
|
||||
const reportIssueButton = container.querySelector(
|
||||
"button[data-testid='report-issue-button']"
|
||||
)
|
||||
expect(reportIssueButton).toBeInTheDocument()
|
||||
expect(reportIssueButton).toHaveTextContent("Report Issue")
|
||||
const link = reportIssueButton?.querySelector("a")
|
||||
expect(link).toBeInTheDocument()
|
||||
expect(link).toHaveAttribute("href", customReportLink)
|
||||
})
|
||||
|
||||
test("does not render report issue button when no reportLink is available", () => {
|
||||
// Override the mock to return undefined for reportIssueLink
|
||||
// Use mockImplementation (not Once) to ensure it applies to all calls during render
|
||||
// const originalImplementation = mockUseSiteConfig.getMockImplementation()
|
||||
mockUseSiteConfig.mockImplementation(() => ({
|
||||
config: {
|
||||
reportIssueLink: undefined,
|
||||
},
|
||||
}))
|
||||
|
||||
const { container } = render(<Feedback event="test-event" />)
|
||||
const reportIssueButton = container.querySelector(
|
||||
"button[data-testid='report-issue-button']"
|
||||
)
|
||||
expect(reportIssueButton).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test("does not render dotted separator when showDottedSeparator is false", () => {
|
||||
const { container } = render(
|
||||
<Feedback event="test-event" showDottedSeparator={false} />
|
||||
)
|
||||
const separators = container.querySelectorAll(
|
||||
"[data-testid='dotted-separator']"
|
||||
)
|
||||
expect(separators).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("applies custom className", () => {
|
||||
const { container } = render(
|
||||
<Feedback event="test-event" className="custom-class" />
|
||||
)
|
||||
const wrapper = container.firstChild
|
||||
expect(wrapper).toHaveClass("custom-class")
|
||||
})
|
||||
|
||||
test("shows custom submit button text", () => {
|
||||
const { container } = render(
|
||||
<Feedback event="test-event" submitBtn="Send" />
|
||||
)
|
||||
const positiveButton = container.querySelector(
|
||||
"button[data-testid='positive-button']"
|
||||
)
|
||||
fireEvent.click(positiveButton!)
|
||||
|
||||
const submitButton = container.querySelector(
|
||||
"button[data-testid='submit-button']"
|
||||
)
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
expect(submitButton).toHaveTextContent("Send")
|
||||
})
|
||||
|
||||
test("shows custom positive question", () => {
|
||||
const { container } = render(
|
||||
<Feedback event="test-event" positiveQuestion="What did you like most?" />
|
||||
)
|
||||
const positiveButton = container.querySelector(
|
||||
"button[data-testid='positive-button']"
|
||||
)
|
||||
fireEvent.click(positiveButton!)
|
||||
|
||||
const label = container.querySelector(
|
||||
"label[data-testid='submit-question-label']"
|
||||
)
|
||||
expect(label).toBeInTheDocument()
|
||||
expect(label).toHaveTextContent("What did you like most?")
|
||||
})
|
||||
|
||||
test("shows custom negative question", () => {
|
||||
const { container } = render(
|
||||
<Feedback event="test-event" negativeQuestion="What went wrong?" />
|
||||
)
|
||||
const negativeButton = container.querySelector(
|
||||
"button[data-testid='negative-button']"
|
||||
)
|
||||
fireEvent.click(negativeButton!)
|
||||
|
||||
const label = container.querySelector(
|
||||
"label[data-testid='submit-question-label']"
|
||||
)
|
||||
expect(label).toBeInTheDocument()
|
||||
expect(label).toHaveTextContent("What went wrong?")
|
||||
})
|
||||
})
|
||||
|
||||
describe("positive feedback flow", () => {
|
||||
test("shows form when positive button is clicked", () => {
|
||||
const { container } = render(<Feedback event="test-event" />)
|
||||
const positiveButton = container.querySelector(
|
||||
"button[data-testid='positive-button']"
|
||||
)
|
||||
fireEvent.click(positiveButton!)
|
||||
|
||||
const label = container.querySelector(
|
||||
"label[data-testid='submit-question-label']"
|
||||
)
|
||||
expect(label).toBeInTheDocument()
|
||||
expect(label).toHaveTextContent("What did you like?")
|
||||
})
|
||||
|
||||
test("shows positive feedback options", () => {
|
||||
const { container } = render(<Feedback event="test-event" />)
|
||||
const positiveButton = container.querySelector(
|
||||
"button[data-testid='positive-button']"
|
||||
)
|
||||
fireEvent.click(positiveButton!)
|
||||
|
||||
const radioOptions = [
|
||||
"Easy to understand",
|
||||
"Accurate code and text",
|
||||
"Exactly what I was looking for",
|
||||
"Ease of use",
|
||||
"Other",
|
||||
]
|
||||
|
||||
const feedbackOptions = Array.from(
|
||||
container.querySelectorAll<HTMLInputElement>(
|
||||
"[data-testid='feedback-option']"
|
||||
)
|
||||
)
|
||||
expect(feedbackOptions).toHaveLength(5)
|
||||
const optionValues = feedbackOptions.map((radio) => radio.value)
|
||||
radioOptions.forEach((option) => {
|
||||
expect(optionValues).toContain(option)
|
||||
})
|
||||
})
|
||||
|
||||
test("tracks positive feedback when button is clicked", () => {
|
||||
const { container } = render(<Feedback event="test-event" />)
|
||||
const positiveButton = container.querySelector(
|
||||
"button[data-testid='positive-button']"
|
||||
)
|
||||
expect(positiveButton).toBeInTheDocument()
|
||||
fireEvent.click(positiveButton!)
|
||||
|
||||
expect(mockTrack).toHaveBeenCalledWith({
|
||||
event: {
|
||||
event: "test-event",
|
||||
options: {
|
||||
feedback: "yes",
|
||||
message: null,
|
||||
feedbackOption: "Other",
|
||||
},
|
||||
callback: expect.any(Function),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("negative feedback flow", () => {
|
||||
test("shows form when negative button is clicked", () => {
|
||||
const { container } = render(<Feedback event="test-event" />)
|
||||
const negativeButton = container.querySelector(
|
||||
"button[data-testid='negative-button']"
|
||||
)
|
||||
expect(negativeButton).toBeInTheDocument()
|
||||
fireEvent.click(negativeButton!)
|
||||
|
||||
const label = container.querySelector(
|
||||
"label[data-testid='submit-question-label']"
|
||||
)
|
||||
expect(label).toBeInTheDocument()
|
||||
expect(label).toHaveTextContent("What was the problem?")
|
||||
})
|
||||
|
||||
test("shows negative feedback options", () => {
|
||||
const { container } = render(<Feedback event="test-event" />)
|
||||
const negativeButton = container.querySelector(
|
||||
"button[data-testid='negative-button']"
|
||||
)
|
||||
expect(negativeButton).toBeInTheDocument()
|
||||
fireEvent.click(negativeButton!)
|
||||
|
||||
const radioOptions = [
|
||||
"Difficult to understand",
|
||||
"Inaccurate code or text",
|
||||
"Didn't find what I was looking for",
|
||||
"Trouble using the documentation",
|
||||
"Other",
|
||||
]
|
||||
|
||||
const feedbackOptions = Array.from(
|
||||
container.querySelectorAll<HTMLInputElement>(
|
||||
"[data-testid='feedback-option']"
|
||||
)
|
||||
)
|
||||
expect(feedbackOptions).toHaveLength(5)
|
||||
const optionValues = feedbackOptions.map((radio) => radio.value)
|
||||
radioOptions.forEach((option) => {
|
||||
expect(optionValues).toContain(option)
|
||||
})
|
||||
})
|
||||
|
||||
test("tracks negative feedback when button is clicked", () => {
|
||||
const { container } = render(<Feedback event="test-event" />)
|
||||
const negativeButton = container.querySelector(
|
||||
"button[data-testid='negative-button']"
|
||||
)
|
||||
expect(negativeButton).toBeInTheDocument()
|
||||
fireEvent.click(negativeButton!)
|
||||
|
||||
expect(mockTrack).toHaveBeenCalledWith({
|
||||
event: {
|
||||
event: "test-event",
|
||||
options: {
|
||||
feedback: "no",
|
||||
message: null,
|
||||
feedbackOption: "Other",
|
||||
},
|
||||
callback: expect.any(Function),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("form interaction", () => {
|
||||
test("submits feedback with selected option and message", () => {
|
||||
const { container } = render(<Feedback event="test-event" />)
|
||||
const positiveButton = container.querySelector(
|
||||
"button[data-testid='positive-button']"
|
||||
)
|
||||
fireEvent.click(positiveButton!)
|
||||
|
||||
// Select an option
|
||||
const feedbackOption = container.querySelector(
|
||||
"[data-testid='feedback-option']"
|
||||
)
|
||||
fireEvent.click(feedbackOption!)
|
||||
|
||||
// Enter message
|
||||
const textarea = container.querySelector("textarea") as HTMLTextAreaElement
|
||||
fireEvent.change(textarea, { target: { value: "Great documentation!" } })
|
||||
|
||||
// Clear previous track calls
|
||||
mockTrack.mockClear()
|
||||
|
||||
// Submit
|
||||
const submitButton = container.querySelector(
|
||||
"button[data-testid='submit-button']"
|
||||
)
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
fireEvent.click(submitButton!)
|
||||
|
||||
expect(mockTrack).toHaveBeenCalledWith({
|
||||
event: expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
feedback: "yes",
|
||||
message: "Great documentation!",
|
||||
feedbackOption: (feedbackOption as HTMLInputElement)?.value,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("submission success", () => {
|
||||
test("shows thank you message after submission", async () => {
|
||||
const { container } = render(<Feedback event="test-event" />)
|
||||
const positiveButton = container.querySelector(
|
||||
"button[data-testid='positive-button']"
|
||||
)
|
||||
fireEvent.click(positiveButton!)
|
||||
|
||||
const submitButton = container.querySelector(
|
||||
"button[data-testid='submit-button']"
|
||||
)
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
fireEvent.click(submitButton!)
|
||||
|
||||
await waitFor(() => {
|
||||
const submittedMessage = container.querySelector(
|
||||
"[data-testid='submitted-message']"
|
||||
)
|
||||
expect(submittedMessage).toBeInTheDocument()
|
||||
expect(submittedMessage).toHaveTextContent(
|
||||
"Thank you for helping improve our documentation!"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("shows custom submit message", async () => {
|
||||
const { container } = render(
|
||||
<Feedback event="test-event" submitMessage="Custom thank you message!" />
|
||||
)
|
||||
const positiveButton = container.querySelector(
|
||||
"button[data-testid='positive-button']"
|
||||
)
|
||||
fireEvent.click(positiveButton!)
|
||||
|
||||
const submitButton = container.querySelector(
|
||||
"button[data-testid='submit-button']"
|
||||
)
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
fireEvent.click(submitButton!)
|
||||
|
||||
await waitFor(() => {
|
||||
const submittedMessage = container.querySelector(
|
||||
"[data-testid='submitted-message']"
|
||||
)
|
||||
expect(submittedMessage).toBeInTheDocument()
|
||||
expect(submittedMessage).toHaveTextContent("Custom thank you message!")
|
||||
})
|
||||
})
|
||||
|
||||
test("shows solutions after submission when showPossibleSolutions is true", async () => {
|
||||
const { container } = render(<Feedback event="test-event" />)
|
||||
const positiveButton = container.querySelector(
|
||||
"button[data-testid='positive-button']"
|
||||
)
|
||||
fireEvent.click(positiveButton!)
|
||||
|
||||
const textarea = container.querySelector("textarea") as HTMLTextAreaElement
|
||||
fireEvent.change(textarea, { target: { value: "Test message" } })
|
||||
|
||||
const submitButton = container.querySelector(
|
||||
"button[data-testid='submit-button']"
|
||||
)
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
fireEvent.click(submitButton!)
|
||||
|
||||
await waitFor(() => {
|
||||
const solutions = container.querySelector("[data-testid='solutions']")
|
||||
expect(solutions).toBeInTheDocument()
|
||||
expect(solutions).toHaveAttribute("data-message", "Test message")
|
||||
expect(solutions).toHaveAttribute("data-feedback", "true")
|
||||
})
|
||||
})
|
||||
|
||||
test("does not show solutions when showPossibleSolutions is false", async () => {
|
||||
const { container } = render(
|
||||
<Feedback event="test-event" showPossibleSolutions={false} />
|
||||
)
|
||||
const positiveButton = container.querySelector(
|
||||
"button[data-testid='positive-button']"
|
||||
)
|
||||
fireEvent.click(positiveButton!)
|
||||
|
||||
const submitButton = container.querySelector(
|
||||
"button[data-testid='submit-button']"
|
||||
)
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
fireEvent.click(submitButton!)
|
||||
|
||||
await waitFor(() => {
|
||||
const solutions = container.querySelector("[data-testid='solutions']")
|
||||
expect(solutions).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
test("passes negative feedback to solutions", async () => {
|
||||
const { container } = render(<Feedback event="test-event" />)
|
||||
const negativeButton = container.querySelector(
|
||||
"button[data-testid='negative-button']"
|
||||
)
|
||||
fireEvent.click(negativeButton!)
|
||||
|
||||
const textarea = container.querySelector("textarea") as HTMLTextAreaElement
|
||||
fireEvent.change(textarea, { target: { value: "Issue message" } })
|
||||
|
||||
const submitButton = container.querySelector(
|
||||
"button[data-testid='submit-button']"
|
||||
)
|
||||
expect(submitButton).toBeInTheDocument()
|
||||
fireEvent.click(submitButton!)
|
||||
|
||||
await waitFor(() => {
|
||||
const solutions = container.querySelector("[data-testid='solutions']")
|
||||
expect(solutions).toHaveAttribute("data-feedback", "false")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("extra data tracking", () => {
|
||||
test("includes extraData in tracking", () => {
|
||||
const { container } = render(
|
||||
<Feedback event="test-event" extraData={{ customField: "customValue" }} />
|
||||
)
|
||||
const positiveButton = container.querySelector(
|
||||
"button[data-testid='positive-button']"
|
||||
)
|
||||
fireEvent.click(positiveButton!)
|
||||
|
||||
expect(mockTrack).toHaveBeenCalledWith({
|
||||
event: expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
customField: "customValue",
|
||||
}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("layout", () => {
|
||||
test("applies vertical layout classes when vertical is true", () => {
|
||||
const { container } = render(<Feedback event="test-event" vertical />)
|
||||
const feedbackForm = container.querySelector(
|
||||
"[data-testid='feedback-form']"
|
||||
)
|
||||
expect(feedbackForm).toBeInTheDocument()
|
||||
expect(feedbackForm).toHaveClass("flex-col justify-center")
|
||||
})
|
||||
|
||||
test("applies horizontal layout classes when vertical is false", () => {
|
||||
const { container } = render(
|
||||
<Feedback event="test-event" vertical={false} />
|
||||
)
|
||||
const feedbackForm = container.querySelector(
|
||||
"[data-testid='feedback-form']"
|
||||
)
|
||||
expect(feedbackForm).toBeInTheDocument()
|
||||
expect(feedbackForm).toHaveClass("flex-col md:flex-row md:items-center")
|
||||
})
|
||||
})
|
||||
@@ -3,19 +3,17 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react"
|
||||
// @ts-expect-error can't install the types package because it doesn't support React v19
|
||||
import { CSSTransition, SwitchTransition } from "react-transition-group"
|
||||
import { Solutions } from "./Solutions"
|
||||
import { Solutions } from "@/components/Feedback/Solutions"
|
||||
import { ExtraData, useAnalytics } from "@/providers/Analytics"
|
||||
import clsx from "clsx"
|
||||
import {
|
||||
Button,
|
||||
TextArea,
|
||||
Label,
|
||||
DottedSeparator,
|
||||
RadioItem,
|
||||
} from "@/components"
|
||||
import { Button } from "@/components/Button"
|
||||
import { TextArea } from "@/components/TextArea"
|
||||
import { Label } from "@/components/Label"
|
||||
import { DottedSeparator } from "@/components/DottedSeparator"
|
||||
import { RadioItem } from "@/components/RadioItem"
|
||||
import { ChatBubbleLeftRight, ThumbDown, ThumbUp } from "@medusajs/icons"
|
||||
import Link from "next/link"
|
||||
import { useSiteConfig } from "../../providers"
|
||||
import { useSiteConfig } from "@/providers/SiteConfig"
|
||||
import { RadioGroup } from "@medusajs/ui"
|
||||
|
||||
export type FeedbackProps = {
|
||||
@@ -135,7 +133,10 @@ export const Feedback = ({
|
||||
return (
|
||||
<div className={clsx(className)}>
|
||||
{showDottedSeparator && (
|
||||
<DottedSeparator wrapperClassName="!px-0 !my-docs_2" />
|
||||
<DottedSeparator
|
||||
wrapperClassName="!px-0 !my-docs_2"
|
||||
data-testid="dotted-separator"
|
||||
/>
|
||||
)}
|
||||
<SwitchTransition mode="out-in">
|
||||
<CSSTransition
|
||||
@@ -165,8 +166,12 @@ export const Feedback = ({
|
||||
vertical && "flex-col justify-center"
|
||||
)}
|
||||
ref={inlineFeedbackRef}
|
||||
data-testid="feedback-form"
|
||||
>
|
||||
<Label className={"text-compact-small text-medusa-fg-base"}>
|
||||
<Label
|
||||
className={"text-compact-small text-medusa-fg-base"}
|
||||
data-testid="question-label"
|
||||
>
|
||||
{question}
|
||||
</Label>
|
||||
<div
|
||||
@@ -182,6 +187,7 @@ export const Feedback = ({
|
||||
"!px-docs_0.5 !py-docs_0.25 text-left md:text-center"
|
||||
)}
|
||||
variant="transparent-clear"
|
||||
data-testid="positive-button"
|
||||
>
|
||||
<ThumbUp className="text-medusa-fg-subtle" />
|
||||
<span className="text-medusa-fg-base text-compact-small-plus flex-1">
|
||||
@@ -195,6 +201,7 @@ export const Feedback = ({
|
||||
"!px-docs_0.5 !py-docs_0.25 text-left md:text-center"
|
||||
)}
|
||||
variant="transparent-clear"
|
||||
data-testid="negative-button"
|
||||
>
|
||||
<ThumbDown className="text-medusa-fg-subtle" />
|
||||
<span className="text-medusa-fg-base text-compact-small-plus flex-1">
|
||||
@@ -210,6 +217,7 @@ export const Feedback = ({
|
||||
"!justify-start md:!justify-center",
|
||||
"text-left md:text-center"
|
||||
)}
|
||||
data-testid="report-issue-button"
|
||||
>
|
||||
<ChatBubbleLeftRight className="text-medusa-fg-subtle" />
|
||||
<span className="text-medusa-fg-base text-compact-small-plus flex-1">
|
||||
@@ -226,7 +234,7 @@ export const Feedback = ({
|
||||
)}
|
||||
{showForm && !submittedFeedback && (
|
||||
<div className="flex flex-col gap-docs_1" ref={inlineQuestionRef}>
|
||||
<Label>
|
||||
<Label data-testid="submit-question-label">
|
||||
{positiveFeedback ? positiveQuestion : negativeQuestion}
|
||||
</Label>
|
||||
<RadioGroup className="gap-docs_0.5">
|
||||
@@ -247,6 +255,7 @@ export const Feedback = ({
|
||||
feedbackOption !== option &&
|
||||
"group-hover:bg-medusa-bg-component-hover"
|
||||
)}
|
||||
data-testid="feedback-option"
|
||||
/>
|
||||
<Label className="text-medusa-fg-base text-compact-small-plus">
|
||||
{option}
|
||||
@@ -265,6 +274,7 @@ export const Feedback = ({
|
||||
disabled={loading}
|
||||
className="w-fit"
|
||||
variant="secondary"
|
||||
data-testid="submit-button"
|
||||
>
|
||||
{submitBtn}
|
||||
</Button>
|
||||
@@ -275,6 +285,7 @@ export const Feedback = ({
|
||||
<div
|
||||
className="text-compact-large-plus flex flex-col"
|
||||
ref={inlineMessageRef}
|
||||
data-testid="submitted-message"
|
||||
>
|
||||
<span>{submitMessage}</span>
|
||||
{showPossibleSolutions && (
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react"
|
||||
import { describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
|
||||
// mock components
|
||||
vi.mock("@/components/Pagination", () => ({
|
||||
Pagination: () => <div>Pagination</div>,
|
||||
}))
|
||||
|
||||
import { Footer } from "../../Footer"
|
||||
|
||||
describe("rendering", () => {
|
||||
test("doesn't render anything by default", () => {
|
||||
const { container } = render(<Footer />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
test("renders feedback component when provided", () => {
|
||||
const { container } = render(
|
||||
<Footer feedbackComponent={<div>Feedback</div>} />
|
||||
)
|
||||
expect(container).toContainHTML("<div>Feedback</div>")
|
||||
})
|
||||
|
||||
test("renders pagination component when showPagination is true", () => {
|
||||
const { container } = render(<Footer showPagination={true} />)
|
||||
expect(container).toContainHTML("<div>Pagination</div>")
|
||||
})
|
||||
|
||||
test("renders edit component when provided", () => {
|
||||
const { container } = render(<Footer editComponent={<div>Edit</div>} />)
|
||||
expect(container).toContainHTML("<div>Edit</div>")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||
import { render } from "@testing-library/react"
|
||||
import { H2 } from "../../H2"
|
||||
import { LinkProps } from "../../../Link"
|
||||
|
||||
// mock functions
|
||||
const mockUseHeadingUrl = vi.fn(() => "https://example.com")
|
||||
const mockUseLayout = vi.fn(() => ({ showCollapsedNavbar: false }))
|
||||
|
||||
// mock components
|
||||
vi.mock("@/components/CopyButton", () => ({
|
||||
CopyButton: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="copy-button">{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("@/components/Link", () => ({
|
||||
Link: (props: LinkProps) => <a {...props} />,
|
||||
}))
|
||||
|
||||
vi.mock("@/providers/Layout", () => ({
|
||||
useLayout: () => mockUseLayout(),
|
||||
}))
|
||||
|
||||
vi.mock("@/hooks/use-heading-url", () => ({
|
||||
useHeadingUrl: () => mockUseHeadingUrl(),
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders correctly", () => {
|
||||
const { container } = render(<H2>Test</H2>)
|
||||
const h2 = container.querySelector("h2")
|
||||
expect(h2).toBeInTheDocument()
|
||||
expect(h2).toHaveTextContent("Test")
|
||||
})
|
||||
|
||||
test("renders with id", () => {
|
||||
const { container } = render(<H2 id="test-id">Test</H2>)
|
||||
const h2 = container.querySelector("h2")
|
||||
expect(h2).toBeInTheDocument()
|
||||
const copyButton = container.querySelector("[data-testid='copy-button']")
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
const link = copyButton?.querySelector("a")
|
||||
expect(link).toBeInTheDocument()
|
||||
expect(link).toHaveTextContent("#")
|
||||
expect(link).toHaveAttribute("href", "#test-id")
|
||||
})
|
||||
|
||||
test("renders with collapsed navbar and id", () => {
|
||||
mockUseLayout.mockReturnValue({ showCollapsedNavbar: true })
|
||||
const { container } = render(<H2 id="test-id">Test</H2>)
|
||||
const h2 = container.querySelector("h2")
|
||||
expect(h2).toBeInTheDocument()
|
||||
expect(h2).toHaveClass("scroll-m-docs_7")
|
||||
})
|
||||
|
||||
test("renders with non-collapsed navbar and id", () => {
|
||||
mockUseLayout.mockReturnValue({ showCollapsedNavbar: false })
|
||||
const { container } = render(<H2 id="test-id">Test</H2>)
|
||||
const h2 = container.querySelector("h2")
|
||||
expect(h2).toBeInTheDocument()
|
||||
expect(h2).toHaveClass("scroll-m-56")
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user