mirror of
https://github.com/Redocly/redoc.git
synced 2026-02-02 05:05:49 +03:00
fix: move to vitest and fix schema behaviour
This commit is contained in:
parent
10400113fb
commit
e156e9d15a
5
.changeset/real-lights-study.md
Normal file
5
.changeset/real-lights-study.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"redoc": patch
|
||||
---
|
||||
|
||||
Release 3.0.0-rc.1.
|
||||
2
.github/actions/setup-node/action.yml
vendored
2
.github/actions/setup-node/action.yml
vendored
|
|
@ -11,7 +11,7 @@ runs:
|
|||
- name: 📦 Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22.17.1
|
||||
node-version: 24.11.1
|
||||
cache: npm
|
||||
- name: ✅ Finished Node setup
|
||||
shell: bash
|
||||
|
|
|
|||
5
.github/actions/setup-playwright/action.yml
vendored
5
.github/actions/setup-playwright/action.yml
vendored
|
|
@ -13,7 +13,7 @@ runs:
|
|||
id: playwright-version
|
||||
shell: bash
|
||||
run: |
|
||||
echo "PLAYWRIGHT_VERSION=$(jq -r '.devDependencies["@playwright/test"]' package.json)" >> $GITHUB_OUTPUT
|
||||
echo "PLAYWRIGHT_VERSION=$(jq -r '.devDependencies[\"@playwright/test\"]' package.json)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
name: Check if Playwright browser is cached
|
||||
id: playwright-cache
|
||||
|
|
@ -25,8 +25,7 @@ runs:
|
|||
shell: bash
|
||||
run: |
|
||||
echo "Installing Playwright in root"
|
||||
echo "Installing @playwright/test@${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }} (no-save)"
|
||||
npm install -D @playwright/test@${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }} --no-save
|
||||
npm install @playwright/test
|
||||
npx playwright install --with-deps
|
||||
- name: ✅ Finished Playwright setup
|
||||
shell: bash
|
||||
|
|
|
|||
5
.github/workflows/e2e.yml
vendored
5
.github/workflows/e2e.yml
vendored
|
|
@ -1,8 +1,5 @@
|
|||
name: Tests e2e
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
|
|
@ -17,7 +14,7 @@ jobs:
|
|||
- name: Setup Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22.17
|
||||
node-version: 24.11.1
|
||||
cache: 'npm'
|
||||
- name: Install Dependencies
|
||||
run: npm ci --ignore-scripts --no-fund
|
||||
|
|
|
|||
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
|
|
@ -1,14 +1,9 @@
|
|||
name: Release
|
||||
|
||||
# permissions:
|
||||
# id-token: write
|
||||
# contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
|
@ -17,6 +12,10 @@ jobs:
|
|||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # Required for OIDC
|
||||
contents: write
|
||||
pull-requests: write
|
||||
outputs:
|
||||
published: ${{ steps.changesets.outputs.published }}
|
||||
publishedPackages: ${{ steps.changesets.outputs.publishedPackages }}
|
||||
|
|
@ -26,7 +25,7 @@ jobs:
|
|||
- name: Setup Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22.17
|
||||
node-version: 24.11.1
|
||||
cache: 'npm'
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
|
@ -40,11 +39,13 @@ jobs:
|
|||
title: 'chore: 🔖 release new versions'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
dockerhub:
|
||||
needs: [release]
|
||||
if: needs.release.outputs.published == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
uses: ./.github/workflows/docker.yml
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
|
|
@ -55,6 +56,8 @@ jobs:
|
|||
needs: [release]
|
||||
if: needs.release.outputs.published == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
|
@ -74,6 +77,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
needs: [release, publish-cdn]
|
||||
if: needs.release.outputs.published == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
|
|
|||
4
.github/workflows/unit-tests.yml
vendored
4
.github/workflows/unit-tests.yml
vendored
|
|
@ -4,8 +4,6 @@ on:
|
|||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-unit:
|
||||
|
|
@ -16,7 +14,7 @@ jobs:
|
|||
- name: Setup Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22.17
|
||||
node-version: 24.11.1
|
||||
cache: 'npm'
|
||||
- name: Install Dependencies
|
||||
run: npm ci --ignore-scripts --no-fund
|
||||
|
|
|
|||
3
.npmrc
Normal file
3
.npmrc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# When this package is built standalone (e.g., in CI),
|
||||
# ensure dependencies are installed locally, not hoisted to parent workspace
|
||||
shamefully-hoist=true
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
# npm i -g http-server
|
||||
# http-server -p 8000 --cors
|
||||
|
||||
FROM node:22.20.0-alpine@sha256:dbcedd8aeab47fbc0f4dd4bffa55b7c3c729a707875968d467aaaea42d6225af AS build
|
||||
FROM node:24.11.1-alpine3.22@sha256:2867d550cf9d8bb50059a0fff528741f11a84d985c732e60e19e8e75c7239c43 AS build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,8 @@ export default tseslint.config(
|
|||
devDependencies: [
|
||||
'**/playwright/**',
|
||||
'**/vite.config.ts',
|
||||
'**/setupTests.ts',
|
||||
'**/vitest.config.ts',
|
||||
'**/vitest.setup.ts',
|
||||
'**/__tests__/**/*.{ts,tsx}',
|
||||
'**/benchmarks/*.ts',
|
||||
'**/__mocks__/**/*.{ts,tsx}',
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600|Source+Code+Pro:400:700"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script type="module" src="../bundle/redoc.standalone.js"></script>
|
||||
<script type="module" src="https://cdn.redocly.com/redoc/v3.0.0-rc.1/redoc.standalone.js"></script>
|
||||
|
||||
<!--
|
||||
ReDoc doesn't change outer page styles
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
import { createRequire } from 'module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
/** @type {import('jest').Config} */
|
||||
export default {
|
||||
verbose: true,
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
|
||||
preset: 'ts-jest',
|
||||
collectCoverage: true,
|
||||
coverageProvider: 'v8',
|
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/{index,types}.ts', '!src/events/*'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/bundle/', '/__mocks__/', '/src/icons/'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 78,
|
||||
branches: 68,
|
||||
functions: 76,
|
||||
lines: 78,
|
||||
},
|
||||
},
|
||||
coverageReporters: ['json', 'lcov', 'text', 'text-summary'],
|
||||
coveragePathIgnorePatterns: [
|
||||
'\\.d\\.ts$',
|
||||
'/node_modules/',
|
||||
'__mock__',
|
||||
'/src/types/',
|
||||
'/src/icons/',
|
||||
],
|
||||
modulePathIgnorePatterns: [
|
||||
'/__mocks__/',
|
||||
'/benchmark/',
|
||||
'/coverage/',
|
||||
'src/components/__tests__/mocks/*',
|
||||
'src/models/__tests__/helpers.ts',
|
||||
'src/icons/*',
|
||||
'/bundle/',
|
||||
'lib/',
|
||||
'/playground/',
|
||||
'/playwright/',
|
||||
'/scripts/',
|
||||
],
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
isolatedModules: true,
|
||||
useESM: true,
|
||||
astTransformers: {
|
||||
before: [
|
||||
{
|
||||
path: 'ts-jest-mock-import-meta',
|
||||
options: { metaObjectReplacement: { url: '' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
tsconfig: {
|
||||
moduleResolution: 'node',
|
||||
module: 'ESNext',
|
||||
jsx: 'react-jsx',
|
||||
},
|
||||
},
|
||||
],
|
||||
'node_modules/.pnpm/@redocly.+\\.js$': [
|
||||
'ts-jest',
|
||||
{
|
||||
isolatedModules: true,
|
||||
useESM: true,
|
||||
tsconfig: {
|
||||
moduleResolution: 'node',
|
||||
module: 'ESNext',
|
||||
allowJs: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
// Custom module mapper to handle @redocly packages
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less)$': '<rootDir>/src/empty.js',
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
`/node_modules/.pnpm/(?!vscode-languageserver-types|vscode-languageserver-textdocument|@redocly)`,
|
||||
],
|
||||
prettierPath: require.resolve('prettier'),
|
||||
};
|
||||
6562
package-lock.json
generated
6562
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
76
package.json
76
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "redoc",
|
||||
"version": "3.0.0-rc.0",
|
||||
"version": "3.0.0-rc.1",
|
||||
"description": "ReDoc",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -39,19 +39,19 @@
|
|||
"build:standalone": "vite build --mode standalone",
|
||||
"build:e2e": "vite build --mode standalone-e2e",
|
||||
"minify": "node scripts/minify.js bundle",
|
||||
"unit": "jest -w 2",
|
||||
"unit:update": "jest -u",
|
||||
"unit:watch": "jest --watch",
|
||||
"unit:coverage": "jest --coverage",
|
||||
"unit:coverage:html": "jest --coverage --coverageReporters html",
|
||||
"unit": "vitest run",
|
||||
"unit:update": "vitest run -u",
|
||||
"unit:watch": "vitest",
|
||||
"unit:coverage": "vitest run --coverage",
|
||||
"unit:coverage:html": "vitest run --coverage --coverage.reporter=html",
|
||||
"e2e:prepare": "npm run build:e2e && cp -r ./bundle ./playwright/",
|
||||
"e2e:ui": "playwright test --ui",
|
||||
"e2e": "npm run e2e:prepare && npx playwright test",
|
||||
"test": "npm run lint && npm run unit && npm run e2e",
|
||||
"test": "npm run lint && npm run ts:check && npm run unit && npm run e2e",
|
||||
"lint": "NODE_OPTIONS=\"--max-old-space-size=5120\" eslint . --max-warnings=0",
|
||||
"lint:fix": "NODE_OPTIONS=\"--max-old-space-size=5120\" eslint . --fix",
|
||||
"ts:check": "tsc --noEmit --skipLibCheck",
|
||||
"ts:dts": "tsc -p tsconfig.json --emitDeclarationOnly && api-extractor run --local --verbose",
|
||||
"ts:dts": "tsc -p tsconfig.build.json --emitDeclarationOnly && api-extractor run --local --verbose",
|
||||
"clean": "rimraf bin bundle lib",
|
||||
"clear:cache": "scripts/clear-cache.sh",
|
||||
"pack:sourcemaps": "rimraf redoc.sourcemaps.tag.gz && find lib -name \"*.js.map\" | xargs tar -czvf redoc.sourcemaps.tar.gz",
|
||||
|
|
@ -62,22 +62,22 @@
|
|||
"publish-cdn": "scripts/publish-cdn.sh"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redocly/config": "^0.36.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"@redocly/config": "^0.41.0",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"styled-components": "^4.1.1 || ^5.3.11 || ^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@markdoc/markdoc": "0.5.2",
|
||||
"@redocly/config": "^0.36.2",
|
||||
"@redocly/openapi-core": "2.7.0",
|
||||
"@redocly/redoc-opentelemetry": "0.0.4",
|
||||
"@redocly/theme": "^0.59.0-next.8",
|
||||
"@redocly/config": "^0.41.0",
|
||||
"@redocly/openapi-core": "2.12.3",
|
||||
"@redocly/redoc-opentelemetry": "0.0.5",
|
||||
"@redocly/theme": "^0.61.0-next.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"dompurify": "3.2.7",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"flexsearch": "^0.8.2",
|
||||
"htmlparser2": "^10.0.0",
|
||||
"htmlparser2": "8.0.2",
|
||||
"jotai": "^2.12.5",
|
||||
"json-pointer": "^0.6.2",
|
||||
"openapi-sampler": "^1.6.2",
|
||||
|
|
@ -92,58 +92,52 @@
|
|||
"devDependencies": {
|
||||
"@changesets/cli": "2.29.7",
|
||||
"@eslint/compat": "1.4.0",
|
||||
"@eslint/js": "9.36.0",
|
||||
"@jest/globals": "29.5.0",
|
||||
"@eslint/js": "9.38.0",
|
||||
"@microsoft/api-extractor": "7.52.10",
|
||||
"@playwright/test": "1.53.2",
|
||||
"@swc/plugin-styled-components": "^9.1.0",
|
||||
"@playwright/test": "1.56.1",
|
||||
"@swc/plugin-styled-components": "^12.0.1",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/jest": "29.5.11",
|
||||
"@types/jest-when": "3.5.5",
|
||||
"@types/json-pointer": "1.0.34",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/node": "22.15.3",
|
||||
"@types/node": "22.18.13",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.4",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/styled-components": "5.1.34",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"chromium": "3.0.3",
|
||||
"@vitejs/plugin-react": "5.1.1",
|
||||
"@vitejs/plugin-react-swc": "4.2.2",
|
||||
"@vitest/coverage-v8": "^4.0.15",
|
||||
"concurrently": "9.2.0",
|
||||
"esbuild": "^0.25.5",
|
||||
"esbuild": "^0.27.0",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-check-file": "3.3.0",
|
||||
"eslint-plugin-import-x": "4.16.1",
|
||||
"eslint-plugin-jest": "29.0.1",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"globals": "16.3.0",
|
||||
"globals": "^16.5.0",
|
||||
"http-server": "14.1.1",
|
||||
"jest": "29.5.0",
|
||||
"jest-environment-jsdom": "29.5.0",
|
||||
"jest-styled-components": "7.2.0",
|
||||
"jest-when": "3.6.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"jsdom": "^25.0.1",
|
||||
"json-schema": "0.4.0",
|
||||
"license-checker": "25.0.1",
|
||||
"outdent": "0.8.0",
|
||||
"path-browserify": "1.0.1",
|
||||
"prettier": "2.8.8",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"rimraf": "5.0.7",
|
||||
"rollup-plugin-bundle-stats": "^4.20.2",
|
||||
"styled-components": "5.3.11",
|
||||
"ts-jest": "29.1.2",
|
||||
"ts-jest-mock-import-meta": "^1.3.0",
|
||||
"typescript": "5.6.2",
|
||||
"typescript-eslint": "8.45.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.46.2",
|
||||
"url": "0.11.4",
|
||||
"url-polyfill": "1.1.13",
|
||||
"util": "0.12.5",
|
||||
"vite": "^7.0.0"
|
||||
"vite": "7.2.7",
|
||||
"vitest": "^4.0.15"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { readFileSync } from 'fs';
|
||||
|
||||
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
||||
console.log(pkg.version);
|
||||
console.log(JSON.parse(readFileSync('../package.json', 'utf8')).version);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,26 @@
|
|||
import util from 'util';
|
||||
import { render } from '@testing-library/react';
|
||||
import * as Jotai from 'jotai';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { LayoutVariant } from '@redocly/config';
|
||||
|
||||
import type { OpenAPIInfo } from '../../../types';
|
||||
import type { GroupModel } from '../../../models';
|
||||
import type { OpenAPIInfo } from '../../../types/index.js';
|
||||
import type { GroupModel } from '../../../models/index.js';
|
||||
|
||||
import { ApiInfo } from '../ApiInfo';
|
||||
import { ApiInfo } from '../ApiInfo.js';
|
||||
import { TestBrowserRouter } from '../../../testProviders.js';
|
||||
|
||||
Object.defineProperty(global, 'TextEncoder', {
|
||||
value: util.TextEncoder,
|
||||
});
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: jest.fn(),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtomValue: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('ApiInfo', () => {
|
||||
it('ApiInfo renders correctly', () => {
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockReturnValue({
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue({
|
||||
parser: {
|
||||
definition: {
|
||||
info: {
|
||||
|
|
@ -36,6 +37,7 @@ describe('ApiInfo', () => {
|
|||
});
|
||||
const result = render(
|
||||
<ApiInfo
|
||||
layout={LayoutVariant.THREE_PANEL}
|
||||
item={
|
||||
{
|
||||
infoDefinition: {
|
||||
|
|
@ -49,7 +51,7 @@ describe('ApiInfo', () => {
|
|||
}
|
||||
/>,
|
||||
{
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ApiInfo ApiInfo renders correctly 1`] = `
|
||||
exports[`ApiInfo > ApiInfo renders correctly 1`] = `
|
||||
{
|
||||
"asFragment": [Function],
|
||||
"baseElement": .c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: var(--spacing-vertical) var(--panel-gap-horizontal);
|
||||
}
|
||||
|
||||
.c1:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
"baseElement": .c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
|
|
@ -35,7 +19,54 @@ exports[`ApiInfo ApiInfo renders correctly 1`] = `
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
.c0 {
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.c0 {
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
}.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: var(--spacing-vertical) var(--panel-gap-horizontal);
|
||||
}
|
||||
|
||||
.c1:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
width: 100%;
|
||||
padding-left: calc(var(--panel-gap-horizontal) * 2);
|
||||
padding-right: var(--panel-gap-horizontal);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.c1 {
|
||||
width: 100%;
|
||||
padding-top: var(--spacing-vertical);
|
||||
padding-bottom: var(--spacing-vertical);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width:1280px) {
|
||||
.c2 {
|
||||
color: var(--h1-text-color);
|
||||
margin: var(--h1-margin-top) 0 var(--h1-margin-bottom);
|
||||
font-size: var(--h1-font-size);
|
||||
|
|
@ -56,38 +87,7 @@ exports[`ApiInfo ApiInfo renders correctly 1`] = `
|
|||
}
|
||||
|
||||
@media screen and (min-width:1280px) {
|
||||
.c1 {
|
||||
width: 100%;
|
||||
padding-left: calc(var(--panel-gap-horizontal) * 2);
|
||||
padding-right: var(--panel-gap-horizontal);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.c1 {
|
||||
width: 100%;
|
||||
padding-top: var(--spacing-vertical);
|
||||
padding-bottom: var(--spacing-vertical);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width:1280px) {
|
||||
.c0 {
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.c0 {
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<body>
|
||||
<div>
|
||||
|
|
@ -105,23 +105,7 @@ exports[`ApiInfo ApiInfo renders correctly 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</body>,
|
||||
"container": .c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: var(--spacing-vertical) var(--panel-gap-horizontal);
|
||||
}
|
||||
|
||||
.c1:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
"container": .c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
|
|
@ -137,7 +121,54 @@ exports[`ApiInfo ApiInfo renders correctly 1`] = `
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
.c0 {
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.c0 {
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
}.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: var(--spacing-vertical) var(--panel-gap-horizontal);
|
||||
}
|
||||
|
||||
.c1:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
width: 100%;
|
||||
padding-left: calc(var(--panel-gap-horizontal) * 2);
|
||||
padding-right: var(--panel-gap-horizontal);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.c1 {
|
||||
width: 100%;
|
||||
padding-top: var(--spacing-vertical);
|
||||
padding-bottom: var(--spacing-vertical);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width:1280px) {
|
||||
.c2 {
|
||||
color: var(--h1-text-color);
|
||||
margin: var(--h1-margin-top) 0 var(--h1-margin-bottom);
|
||||
font-size: var(--h1-font-size);
|
||||
|
|
@ -158,38 +189,7 @@ exports[`ApiInfo ApiInfo renders correctly 1`] = `
|
|||
}
|
||||
|
||||
@media screen and (min-width:1280px) {
|
||||
.c1 {
|
||||
width: 100%;
|
||||
padding-left: calc(var(--panel-gap-horizontal) * 2);
|
||||
padding-right: var(--panel-gap-horizontal);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.c1 {
|
||||
width: 100%;
|
||||
padding-top: var(--spacing-vertical);
|
||||
padding-bottom: var(--spacing-vertical);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width:1280px) {
|
||||
.c0 {
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.c0 {
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,18 +1,23 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { getCallback } from '../../../models';
|
||||
import { CallbackOperation } from '../CallbackOperation';
|
||||
import { CallbacksList } from '../CallbacksList';
|
||||
import { CallbackSummary } from '../CallbackSummary';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import { getCallback } from '../../../models/index.js';
|
||||
import { CallbackOperation } from '../CallbackOperation.js';
|
||||
import { CallbacksList } from '../CallbacksList.js';
|
||||
import { CallbackSummary } from '../CallbackSummary.js';
|
||||
import * as simpleCallbackFixture from './fixtures/simple-callback.json';
|
||||
import type { OpenAPIDefinition } from '../../../types/index.js';
|
||||
import { TestBrowserRouter } from '../../../testProviders.js';
|
||||
|
||||
const options = normalizeOptions({});
|
||||
|
||||
describe('Components', () => {
|
||||
describe('Callbacks', () => {
|
||||
const parser = new OpenAPIParser(simpleCallbackFixture, undefined, options);
|
||||
const parser = new OpenAPIParser(
|
||||
simpleCallbackFixture as unknown as OpenAPIDefinition,
|
||||
undefined,
|
||||
options,
|
||||
);
|
||||
const callback = getCallback(
|
||||
parser,
|
||||
'Test.Callback',
|
||||
|
|
@ -23,18 +28,29 @@ describe('Components', () => {
|
|||
);
|
||||
|
||||
it('should correctly render CallbackView', () => {
|
||||
const { getByText } = render(<CallbackOperation operation={callback.operations[0]} />, {
|
||||
wrapper: BrowserRouter,
|
||||
});
|
||||
const { getByText } = render(
|
||||
<CallbackOperation
|
||||
operation={callback.operations[0]}
|
||||
onExpand={() => {}}
|
||||
selectedCallback={null}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestBrowserRouter,
|
||||
},
|
||||
);
|
||||
|
||||
expect(getByText('testCallback')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should correctly render CallbackTitle', () => {
|
||||
const { getByText } = render(
|
||||
<CallbackSummary toggle={() => {}} callback={callback.operations[0]} />,
|
||||
<CallbackSummary
|
||||
toggle={() => {}}
|
||||
callback={callback.operations[0]}
|
||||
translate={() => ''}
|
||||
/>,
|
||||
{
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -42,9 +58,12 @@ describe('Components', () => {
|
|||
});
|
||||
|
||||
it('should correctly render CallbacksList', () => {
|
||||
const { getByText } = render(<CallbacksList callbacks={[callback]} />, {
|
||||
wrapper: BrowserRouter,
|
||||
});
|
||||
const { getByText } = render(
|
||||
<CallbacksList callbacks={[callback]} onExpand={() => {}} selectedCallback={null} />,
|
||||
{
|
||||
wrapper: TestBrowserRouter,
|
||||
},
|
||||
);
|
||||
|
||||
expect(getByText('testCallback')).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,30 +1,39 @@
|
|||
import { renderHook } from '@testing-library/react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { jest, describe, beforeEach, test, expect, afterEach } from '@jest/globals';
|
||||
import { vi, type Mock, type MockedFunction } from 'vitest';
|
||||
|
||||
import { useAutoScroll } from '../useAutoScroll';
|
||||
import { useAutoScroll } from '../useAutoScroll.js';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useLocation: jest.fn(),
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useLocation: vi.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@redocly/theme', () => ({
|
||||
IS_BROWSER: true,
|
||||
useActiveSectionId: jest.fn(() => '/test'),
|
||||
vi.mock('lodash.throttle', () => ({
|
||||
default: (fn) => fn,
|
||||
}));
|
||||
|
||||
vi.mock('@redocly/theme', async () => {
|
||||
const actual = await vi.importActual('@redocly/theme');
|
||||
return {
|
||||
...actual,
|
||||
IS_BROWSER: true,
|
||||
useActiveSectionId: vi.fn(() => '/test'),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useAutoScroll', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
|
||||
(useAutoScroll as any).initialPageLoad = true;
|
||||
(useAutoScroll as unknown as { initialPageLoad: boolean }).initialPageLoad = true;
|
||||
|
||||
window.history.replaceState = jest.fn();
|
||||
window.scrollTo = jest.fn();
|
||||
window.history.replaceState = vi.fn();
|
||||
window.scrollTo = vi.fn();
|
||||
|
||||
document.getElementById = jest.fn() as jest.MockedFunction<typeof document.getElementById>;
|
||||
document.querySelector = jest.fn() as jest.MockedFunction<typeof document.querySelector>;
|
||||
document.getElementById = vi.fn() as MockedFunction<typeof document.getElementById>;
|
||||
document.querySelector = vi.fn() as MockedFunction<typeof document.querySelector>;
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { pathname: '', hash: '' },
|
||||
|
|
@ -33,7 +42,8 @@ describe('useAutoScroll', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('should processing without toLowerCase', () => {
|
||||
|
|
@ -44,7 +54,7 @@ describe('useAutoScroll', () => {
|
|||
key: '',
|
||||
search: '',
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
(useLocation as Mock).mockReturnValue(mockLocation);
|
||||
|
||||
renderHook(() => useAutoScroll('/'));
|
||||
|
||||
|
|
@ -59,7 +69,7 @@ describe('useAutoScroll', () => {
|
|||
key: '',
|
||||
search: '',
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
(useLocation as Mock).mockReturnValue(mockLocation);
|
||||
|
||||
renderHook(() => useAutoScroll('/'));
|
||||
|
||||
|
|
@ -74,7 +84,7 @@ describe('useAutoScroll', () => {
|
|||
key: '',
|
||||
search: '',
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
(useLocation as Mock).mockReturnValue(mockLocation);
|
||||
|
||||
renderHook(() => useAutoScroll('/'));
|
||||
|
||||
|
|
@ -82,7 +92,7 @@ describe('useAutoScroll', () => {
|
|||
});
|
||||
|
||||
test('should scroll element into view when found', () => {
|
||||
const mockElement = { scrollIntoView: jest.fn() };
|
||||
const mockElement = { scrollIntoView: vi.fn() };
|
||||
const mockLocation = {
|
||||
pathname: '/test',
|
||||
hash: '#TestHash',
|
||||
|
|
@ -90,8 +100,8 @@ describe('useAutoScroll', () => {
|
|||
key: '',
|
||||
search: '',
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
(document.getElementById as jest.Mock).mockReturnValue(mockElement);
|
||||
(useLocation as Mock).mockReturnValue(mockLocation);
|
||||
(document.getElementById as Mock).mockReturnValue(mockElement);
|
||||
|
||||
renderHook(() => useAutoScroll('/'));
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ const ContentComponentMap: Record<
|
|||
section: SectionItem,
|
||||
schema: (props) => <TagItem {...props} item={props.item as GroupModel} isExpanded={true} />,
|
||||
operation: OperationItem,
|
||||
mcp: (props) => <TagItem {...props} item={props.item as GroupModel} isExpanded={true} />,
|
||||
tool: (props) => <TagItem {...props} item={props.item as GroupModel} isExpanded={true} />,
|
||||
rsrc: (props) => <TagItem {...props} item={props.item as GroupModel} isExpanded={true} />,
|
||||
prompt: (props) => <TagItem {...props} item={props.item as GroupModel} isExpanded={true} />,
|
||||
root: ApiInfo,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import { renderHook } from '@testing-library/react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import type { ContentItemModel } from '../../../models';
|
||||
import type { ContentItemModel } from '../../../models/index.js';
|
||||
import type { MockedFunction } from 'vitest';
|
||||
|
||||
import { useIsExpanded } from '../useIsExpanded';
|
||||
import { useIsExpanded } from '../useIsExpanded.js';
|
||||
|
||||
// Mock react-router-dom
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useLocation: jest.fn(),
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useLocation: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockUseLocation = useLocation as jest.Mock;
|
||||
const mockUseLocation = useLocation as MockedFunction<typeof useLocation>;
|
||||
|
||||
describe('useIsExpanded', () => {
|
||||
const mockRoutingBasePath = '/docs';
|
||||
|
|
@ -20,13 +21,19 @@ describe('useIsExpanded', () => {
|
|||
});
|
||||
|
||||
it('should return false for non-tag items', () => {
|
||||
mockUseLocation.mockReturnValue({ pathname: '/docs/pets', hash: '' });
|
||||
mockUseLocation.mockReturnValue({
|
||||
pathname: '/docs/pets',
|
||||
hash: '',
|
||||
state: undefined,
|
||||
key: '',
|
||||
search: '',
|
||||
});
|
||||
|
||||
const item: ContentItemModel = {
|
||||
type: 'operation',
|
||||
href: '/pets',
|
||||
items: [],
|
||||
};
|
||||
} as unknown as ContentItemModel;
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useIsExpanded({ item, routingBasePath: mockRoutingBasePath }),
|
||||
|
|
@ -36,13 +43,19 @@ describe('useIsExpanded', () => {
|
|||
});
|
||||
|
||||
it('should return true when current path matches item href', () => {
|
||||
mockUseLocation.mockReturnValue({ pathname: '/docs/pets', hash: '' });
|
||||
mockUseLocation.mockReturnValue({
|
||||
pathname: '/docs/pets',
|
||||
hash: '',
|
||||
state: undefined,
|
||||
key: '',
|
||||
search: '',
|
||||
});
|
||||
|
||||
const item: ContentItemModel = {
|
||||
type: 'tag',
|
||||
href: '/pets',
|
||||
items: [{ type: 'operation', href: '/pets/get', items: [] } as unknown as ContentItemModel],
|
||||
};
|
||||
} as unknown as ContentItemModel;
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useIsExpanded({ item, routingBasePath: mockRoutingBasePath }),
|
||||
|
|
@ -55,6 +68,9 @@ describe('useIsExpanded', () => {
|
|||
mockUseLocation.mockReturnValue({
|
||||
pathname: '/docs',
|
||||
hash: '#tag/pets',
|
||||
state: undefined,
|
||||
key: '',
|
||||
search: '',
|
||||
});
|
||||
|
||||
const item: ContentItemModel = {
|
||||
|
|
@ -71,7 +87,13 @@ describe('useIsExpanded', () => {
|
|||
});
|
||||
|
||||
it('should handle nested items', () => {
|
||||
mockUseLocation.mockReturnValue({ pathname: '/docs/pets/create', hash: '' });
|
||||
mockUseLocation.mockReturnValue({
|
||||
pathname: '/docs/pets/create',
|
||||
hash: '',
|
||||
state: undefined,
|
||||
key: '',
|
||||
search: '',
|
||||
});
|
||||
|
||||
const item: ContentItemModel = {
|
||||
type: 'tag',
|
||||
|
|
@ -89,7 +111,7 @@ describe('useIsExpanded', () => {
|
|||
],
|
||||
} as unknown as ContentItemModel,
|
||||
],
|
||||
};
|
||||
} as unknown as ContentItemModel;
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useIsExpanded({ item, routingBasePath: mockRoutingBasePath }),
|
||||
|
|
@ -102,6 +124,9 @@ describe('useIsExpanded', () => {
|
|||
mockUseLocation.mockReturnValue({
|
||||
pathname: '/docs/user%20profile',
|
||||
hash: '',
|
||||
state: undefined,
|
||||
key: '',
|
||||
search: '',
|
||||
});
|
||||
|
||||
const item: ContentItemModel = {
|
||||
|
|
@ -121,13 +146,16 @@ describe('useIsExpanded', () => {
|
|||
mockUseLocation.mockReturnValue({
|
||||
pathname: '/docs/users',
|
||||
hash: '',
|
||||
state: undefined,
|
||||
key: '',
|
||||
search: '',
|
||||
});
|
||||
|
||||
const item: ContentItemModel = {
|
||||
type: 'tag',
|
||||
href: '/pets',
|
||||
items: [{ type: 'operation', href: '/pets/list', items: [] } as unknown as ContentItemModel],
|
||||
};
|
||||
} as unknown as ContentItemModel;
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useIsExpanded({ item, routingBasePath: mockRoutingBasePath }),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import { Items } from '../Items';
|
||||
import type { ContentItemModel } from '../../../models/index.js';
|
||||
|
||||
jest.mock('../../ContentItem', () => ({
|
||||
import { Items } from '../Items.js';
|
||||
import { TestMemoryRouter } from '../../../testProviders.js';
|
||||
|
||||
vi.mock('../../ContentItem', () => ({
|
||||
ContentItem: ({ children, item }: any) => (
|
||||
<div data-testid={`content-item-${item.id}`}>
|
||||
{item.title}
|
||||
|
|
@ -12,7 +14,7 @@ jest.mock('../../ContentItem', () => ({
|
|||
),
|
||||
}));
|
||||
|
||||
jest.mock('../VirtualList', () => ({
|
||||
vi.mock('../VirtualList', () => ({
|
||||
VirtualList: ({ items }: any) => (
|
||||
<div data-testid="virtual-list">
|
||||
{items.map((item: any) => (
|
||||
|
|
@ -25,25 +27,25 @@ jest.mock('../VirtualList', () => ({
|
|||
const mockItems = [
|
||||
{ id: '1', title: 'Item 1', type: 'group', items: [] },
|
||||
{ id: '2', title: 'Item 2', type: 'group', items: [] },
|
||||
];
|
||||
] as unknown as ContentItemModel[];
|
||||
|
||||
const mockMixedTypeItems = Array.from({ length: 16 }, (_, i) => ({
|
||||
id: `mixed-${i}`,
|
||||
title: `Item ${i}`,
|
||||
type: i % 2 === 0 ? 'operation' : 'schema',
|
||||
items: [],
|
||||
}));
|
||||
})) as unknown as ContentItemModel[];
|
||||
|
||||
const mockOperationItems = Array.from({ length: 16 }, (_, i) => ({
|
||||
id: `op-${i}`,
|
||||
title: `Operation ${i}`,
|
||||
type: 'operation',
|
||||
items: [],
|
||||
}));
|
||||
})) as unknown as ContentItemModel[];
|
||||
|
||||
describe('Items', () => {
|
||||
const renderWithRouter = (component: React.ReactElement) => {
|
||||
return render(<MemoryRouter>{component}</MemoryRouter>);
|
||||
return render(<TestMemoryRouter>{component}</TestMemoryRouter>);
|
||||
};
|
||||
|
||||
it('renders null when items array is empty', () => {
|
||||
|
|
@ -81,7 +83,7 @@ describe('Items', () => {
|
|||
type: 'group',
|
||||
items: [{ id: '1-1', title: 'Child', type: 'group', items: [] }],
|
||||
},
|
||||
];
|
||||
] as unknown as ContentItemModel[];
|
||||
|
||||
renderWithRouter(<Items items={nestedItems} routingBasePath="/docs" />);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
import { act } from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { act, type ReactElement } from 'react';
|
||||
|
||||
import type { ContentItemModel } from '../../../models/types';
|
||||
import type { ContentItemModel } from '../../../models/types.js';
|
||||
|
||||
import { VirtualList } from '../VirtualList';
|
||||
import { VirtualList } from '../VirtualList.js';
|
||||
import { TestMemoryRouter } from '../../../testProviders.js';
|
||||
|
||||
const mockItems: ContentItemModel[] = [
|
||||
{ href: '/item1', title: 'Item 1' },
|
||||
{ href: '/item2', title: 'Item 2' },
|
||||
{ href: '/item3', title: 'Item 3' },
|
||||
{ href: '/item4', title: 'Item 4' },
|
||||
{ href: '/item5', title: 'Item 5' },
|
||||
{ href: '/item1', title: 'Item 1' } as unknown as ContentItemModel,
|
||||
{ href: '/item2', title: 'Item 2' } as unknown as ContentItemModel,
|
||||
{ href: '/item3', title: 'Item 3' } as unknown as ContentItemModel,
|
||||
{ href: '/item4', title: 'Item 4' } as unknown as ContentItemModel,
|
||||
{ href: '/item5', title: 'Item 5' } as unknown as ContentItemModel,
|
||||
];
|
||||
|
||||
const renderItem = (item: ContentItemModel) => (
|
||||
<div key={item.href} data-testid={item.href.replace('/', '')}>
|
||||
{item.title}
|
||||
{typeof (item as any).title === 'string'
|
||||
? String((item as any).title)
|
||||
: String((item as any).name)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -24,27 +27,27 @@ const renderWithRouter = (
|
|||
initialPath: string,
|
||||
props: {
|
||||
items: ContentItemModel[];
|
||||
renderItem: (item: ContentItemModel) => JSX.Element;
|
||||
renderItem: (item: ContentItemModel) => ReactElement;
|
||||
routingBasePath: string;
|
||||
},
|
||||
) => {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={[initialPath]}>
|
||||
<TestMemoryRouter initialEntries={[initialPath]}>
|
||||
<Routes>
|
||||
<Route path="*" element={<VirtualList {...props} />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
</TestMemoryRouter>,
|
||||
);
|
||||
};
|
||||
|
||||
describe('VirtualList', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllTimers();
|
||||
jest.useRealTimers();
|
||||
vi.clearAllTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should render empty list when no items provided', () => {
|
||||
|
|
@ -121,7 +124,7 @@ describe('VirtualList', () => {
|
|||
expect(screen.getByTestId('item4')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('item1')).toBeInTheDocument();
|
||||
|
|
@ -151,20 +154,20 @@ describe('VirtualList', () => {
|
|||
});
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
const initialRenderCount = screen.getAllByTestId(/item/).length;
|
||||
|
||||
rerender(
|
||||
<MemoryRouter initialEntries={['/item3']}>
|
||||
<TestMemoryRouter initialEntries={['/item3']}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="*"
|
||||
element={<VirtualList items={mockItems} renderItem={renderItem} routingBasePath="/" />}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
</TestMemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getAllByTestId(/item/).length).toBe(initialRenderCount);
|
||||
|
|
@ -181,7 +184,7 @@ describe('VirtualList', () => {
|
|||
|
||||
expect(() => {
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
|
|
|||
52
src/components/Discriminator/DefaultMappingOption.tsx
Normal file
52
src/components/Discriminator/DefaultMappingOption.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import type { ReactElement } from 'react';
|
||||
|
||||
import { Tooltip } from '@redocly/theme/components/Tooltip/Tooltip';
|
||||
|
||||
import { DefaultMappingIcon } from '../../icons/DefaultMappingIcon/DefaultMappingIcon.js';
|
||||
import { useTranslate } from '../../hooks/index.js';
|
||||
import { styled } from '../../styled-components.js';
|
||||
|
||||
export interface DefaultMappingOptionProps {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function DefaultMappingOption({ label }: DefaultMappingOptionProps): ReactElement {
|
||||
const translate = useTranslate();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tip={translate(
|
||||
'openapi.discriminator.defaultMappingTooltip',
|
||||
"OpenAPI 3.2: defaultMapping used when other mappings don't match.",
|
||||
)}
|
||||
placement="bottom"
|
||||
>
|
||||
<Wrapper>
|
||||
<IconWrapper>
|
||||
<DefaultMappingIcon />
|
||||
</IconWrapper>
|
||||
<Label>{label}</Label>
|
||||
</Wrapper>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Label = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
|
@ -8,6 +8,7 @@ import { strikethroughText } from '../../utils/index.js';
|
|||
import { SchemaSelection } from '../common/SchemaSelection/SchemaSelection.js';
|
||||
import { SelectionTitle } from '../common/index.js';
|
||||
import { styled } from '../../styled-components.js';
|
||||
import { DefaultMappingOption } from './DefaultMappingOption.js';
|
||||
|
||||
interface DiscriminatorDropdownProps {
|
||||
activeOneOfIdx: number;
|
||||
|
|
@ -30,6 +31,20 @@ function DiscriminatorDropdownComponent({
|
|||
'deprecated',
|
||||
).toLowerCase()})`
|
||||
: subSchema.title;
|
||||
|
||||
if (subSchema.isDefaultMapping) {
|
||||
return {
|
||||
label: translate('openapi.discriminator.defaultMapping', 'Default mapping'),
|
||||
value: idx,
|
||||
element: (
|
||||
<DefaultMappingOption
|
||||
label={translate('openapi.discriminator.defaultMapping', 'Default mapping')}
|
||||
/>
|
||||
),
|
||||
...(parent.oneOf && parent.oneOf.length > 1 ? { divider: <Divider /> } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
value: idx,
|
||||
|
|
@ -62,3 +77,19 @@ const Wrapper = styled.div`
|
|||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
`;
|
||||
|
||||
const Divider = styled.span<{ $orientation?: 'horizontal' | 'vertical' }>`
|
||||
background-color: var(--border-color-primary);
|
||||
display: block;
|
||||
align-self: stretch;
|
||||
|
||||
[data-component-name='Dropdown/DropdownMenu'] & {
|
||||
height: 1px;
|
||||
margin: calc(var(--spacing-xxs) / 2) var(--spacing-xxs);
|
||||
}
|
||||
|
||||
[data-component-name='Segmented/Segmented'] & {
|
||||
width: 1px;
|
||||
margin: var(--spacing-xxs) calc(var(--spacing-xxs) / 2);
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import * as Jotai from 'jotai';
|
||||
|
||||
import type { OperationModel } from '../../../models';
|
||||
import type { OperationModel } from '../../../models/index.js';
|
||||
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { getSchema } from '../../../models';
|
||||
import { Schema } from '../../Schema';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import { getSchema } from '../../../models/index.js';
|
||||
import { Schema } from '../../Schema/index.js';
|
||||
import * as simpleDiscriminatorFixture from './fixtures/simple-discriminator.json';
|
||||
import * as discriminatorWithDefaultFixture from '../../../models/__tests__/fixtures/discriminator-with-default-mapping.json';
|
||||
import { TestBrowserRouter } from '../../../testProviders.js';
|
||||
|
||||
const options = normalizeOptions({});
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: jest.fn(),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtomValue: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('DiscriminatorDropdown', () => {
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockReturnValue(options);
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(options);
|
||||
const parser = new OpenAPIParser(simpleDiscriminatorFixture, undefined, options);
|
||||
const schema = getSchema({
|
||||
parser,
|
||||
|
|
@ -28,7 +29,7 @@ describe('DiscriminatorDropdown', () => {
|
|||
});
|
||||
|
||||
describe('discriminator', () => {
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockReturnValue({
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue({
|
||||
activeOneOf: {
|
||||
'#/components/schemas/Pet': 0,
|
||||
},
|
||||
|
|
@ -36,11 +37,43 @@ describe('DiscriminatorDropdown', () => {
|
|||
|
||||
it('should correctly render SchemaView', () => {
|
||||
const { getByText } = render(<Schema schema={schema} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(getByText('type')).toBeInTheDocument();
|
||||
expect(getByText('C̵a̵t̵ (deprecated)')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('discriminator with defaultMapping', () => {
|
||||
const parserWithDefault = new OpenAPIParser(
|
||||
discriminatorWithDefaultFixture,
|
||||
undefined,
|
||||
options,
|
||||
);
|
||||
const schemaWithDefault = getSchema({
|
||||
parser: parserWithDefault,
|
||||
schemaOrRef: { $ref: '#/components/schemas/Pet' },
|
||||
pointer: '#/components/schemas/Pet',
|
||||
options,
|
||||
deps: { operation: { pointer: 'defaultPointer' } as OperationModel },
|
||||
});
|
||||
|
||||
it('schema has correct oneOf structure with defaultMapping', () => {
|
||||
expect(schemaWithDefault.oneOf).toHaveLength(3);
|
||||
expect(schemaWithDefault.oneOf?.[0].title).toBe('cat');
|
||||
expect(schemaWithDefault.oneOf?.[0].isDefaultMapping).toBe(false);
|
||||
expect(schemaWithDefault.oneOf?.[1].title).toBe('dog');
|
||||
expect(schemaWithDefault.oneOf?.[1].isDefaultMapping).toBe(false);
|
||||
expect(schemaWithDefault.oneOf?.[2].title).toBe('Default mapping');
|
||||
expect(schemaWithDefault.oneOf?.[2].isDefaultMapping).toBe(true);
|
||||
});
|
||||
|
||||
it('finds correct defaultMappingIdx', () => {
|
||||
const defaultMappingIdx = schemaWithDefault.oneOf?.findIndex(
|
||||
(subSchema) => subSchema.isDefaultMapping,
|
||||
);
|
||||
expect(defaultMappingIdx).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"openapi": "2.0.0",
|
||||
"info": { "version": "1.0", "title": "Test" },
|
||||
"paths": {},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Pet": {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { render, renderHook } from '@testing-library/react';
|
||||
import util from 'util';
|
||||
|
||||
import { DownloadSpecification } from '../DownloadSpecification';
|
||||
import { useDownloadInfo } from '../useDownloadInfo';
|
||||
import { DownloadSpecification } from '../DownloadSpecification.js';
|
||||
import { useDownloadInfo } from '../useDownloadInfo.js';
|
||||
|
||||
Object.defineProperty(global, 'TextEncoder', {
|
||||
value: util.TextEncoder,
|
||||
|
|
@ -26,7 +26,7 @@ describe('DownloadSpecification', () => {
|
|||
downloadUrls,
|
||||
}),
|
||||
);
|
||||
const downloadObjects = result.current;
|
||||
const downloadObjects = result?.current ?? [];
|
||||
const { container } = render(<DownloadSpecification downloadObjects={downloadObjects} />);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
|
|
@ -52,7 +52,7 @@ describe('DownloadSpecification', () => {
|
|||
downloadUrls,
|
||||
}),
|
||||
);
|
||||
const downloadObjects = result.current;
|
||||
const downloadObjects = result?.current ?? [];
|
||||
|
||||
const { container } = render(<DownloadSpecification downloadObjects={downloadObjects} />);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,86 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`DownloadSpecification DownloadButton renders correctly with title 1`] = `
|
||||
.c5 {
|
||||
exports[`DownloadSpecification > DownloadButton renders correctly with title 1`] = `
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-align-self: flex-start;
|
||||
-ms-flex-item-align: start;
|
||||
align-self: flex-start;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--font-size-base);
|
||||
}.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: baseline;
|
||||
-webkit-box-align: baseline;
|
||||
-ms-flex-align: baseline;
|
||||
align-items: baseline;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--border-color-secondary);
|
||||
padding-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.c1:last-of-type {
|
||||
padding-bottom: 0px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
width: calc(100% - 25px);
|
||||
gap: var(--spacing-xs);
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c2 svg {
|
||||
-webkit-flex-shrink: 0;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.c2 a {
|
||||
width: 100%;
|
||||
color: var(--text-color-primary);
|
||||
word-break: break-word;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c5 path {
|
||||
.c3 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +163,15 @@ exports[`DownloadSpecification DownloadButton renders correctly with title 1`] =
|
|||
border-width: var(--button-border-width-disabled);
|
||||
}
|
||||
|
||||
.c5 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c5 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
|
@ -98,15 +181,6 @@ exports[`DownloadSpecification DownloadButton renders correctly with title 1`] =
|
|||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c3 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
|
@ -116,81 +190,7 @@ exports[`DownloadSpecification DownloadButton renders correctly with title 1`] =
|
|||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: baseline;
|
||||
-webkit-box-align: baseline;
|
||||
-ms-flex-align: baseline;
|
||||
align-items: baseline;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--border-color-secondary);
|
||||
padding-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.c1:last-of-type {
|
||||
padding-bottom: 0px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
width: calc(100% - 25px);
|
||||
gap: var(--spacing-xs);
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c2 svg {
|
||||
-webkit-flex-shrink: 0;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.c2 a {
|
||||
width: 100%;
|
||||
color: var(--text-color-primary);
|
||||
word-break: break-word;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-align-self: flex-start;
|
||||
-ms-flex-item-align: start;
|
||||
align-self: flex-start;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
|
|
@ -378,13 +378,87 @@ exports[`DownloadSpecification DownloadButton renders correctly with title 1`] =
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`DownloadSpecification DownloadButton renders correctly without title 1`] = `
|
||||
.c5 {
|
||||
exports[`DownloadSpecification > DownloadButton renders correctly without title 1`] = `
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-align-self: flex-start;
|
||||
-ms-flex-item-align: start;
|
||||
align-self: flex-start;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--font-size-base);
|
||||
}.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: baseline;
|
||||
-webkit-box-align: baseline;
|
||||
-ms-flex-align: baseline;
|
||||
align-items: baseline;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--border-color-secondary);
|
||||
padding-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.c1:last-of-type {
|
||||
padding-bottom: 0px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
width: calc(100% - 25px);
|
||||
gap: var(--spacing-xs);
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c2 svg {
|
||||
-webkit-flex-shrink: 0;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.c2 a {
|
||||
width: 100%;
|
||||
color: var(--text-color-primary);
|
||||
word-break: break-word;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c5 path {
|
||||
.c3 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
|
|
@ -467,6 +541,15 @@ exports[`DownloadSpecification DownloadButton renders correctly without title 1`
|
|||
border-width: var(--button-border-width-disabled);
|
||||
}
|
||||
|
||||
.c5 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c5 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
|
@ -476,15 +559,6 @@ exports[`DownloadSpecification DownloadButton renders correctly without title 1`
|
|||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c3 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
|
@ -494,81 +568,7 @@ exports[`DownloadSpecification DownloadButton renders correctly without title 1`
|
|||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: baseline;
|
||||
-webkit-box-align: baseline;
|
||||
-ms-flex-align: baseline;
|
||||
align-items: baseline;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--border-color-secondary);
|
||||
padding-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.c1:last-of-type {
|
||||
padding-bottom: 0px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
width: calc(100% - 25px);
|
||||
gap: var(--spacing-xs);
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c2 svg {
|
||||
-webkit-flex-shrink: 0;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.c2 a {
|
||||
width: 100%;
|
||||
color: var(--text-color-primary);
|
||||
word-break: break-word;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-align-self: flex-start;
|
||||
-ms-flex-item-align: start;
|
||||
align-self: flex-start;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,38 +1,39 @@
|
|||
import type { Mock } from 'vitest';
|
||||
import { useAtom } from 'jotai/index';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { ExpandAllButton } from '../ExpandAllButton';
|
||||
import { useTranslate, useTelemetry } from '../../../hooks';
|
||||
import { ExpandAllButton } from '../ExpandAllButton.js';
|
||||
import { useTranslate, useTelemetry } from '../../../hooks/index.js';
|
||||
|
||||
// Mock the dependencies
|
||||
jest.mock('jotai/index', () => ({
|
||||
useAtom: jest.fn(),
|
||||
atom: jest.fn(),
|
||||
vi.mock('jotai/index', () => ({
|
||||
useAtom: vi.fn(),
|
||||
atom: vi.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../hooks', () => ({
|
||||
useTranslate: jest.fn(),
|
||||
useTelemetry: jest.fn(), // Add mock for useTelemetry
|
||||
vi.mock('../../../hooks', () => ({
|
||||
useTranslate: vi.fn(),
|
||||
useTelemetry: vi.fn(), // Add mock for useTelemetry
|
||||
}));
|
||||
|
||||
jest.mock('@redocly/theme', () => ({
|
||||
vi.mock('@redocly/theme', () => ({
|
||||
Button: ({ children, ...props }) => <button {...props}>{children}</button>,
|
||||
MaximizeIcon: () => <span data-testid="maximize-icon" />,
|
||||
}));
|
||||
|
||||
describe('ExpandAllButton', () => {
|
||||
const mockSetOperationState = jest.fn();
|
||||
const mockTranslate = jest.fn((key, defaultValue) => defaultValue);
|
||||
const mockTelemetrySend = jest.fn();
|
||||
const mockSetOperationState = vi.fn();
|
||||
const mockTranslate = vi.fn((_key, defaultValue) => defaultValue);
|
||||
const mockTelemetrySend = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useAtom as jest.Mock).mockReturnValue([
|
||||
vi.clearAllMocks();
|
||||
(useAtom as Mock).mockReturnValue([
|
||||
{ request: { expandedAll: false }, response: { expandedAll: false } },
|
||||
mockSetOperationState,
|
||||
]);
|
||||
(useTranslate as jest.Mock).mockReturnValue(mockTranslate);
|
||||
(useTelemetry as jest.Mock).mockReturnValue({
|
||||
(useTranslate as Mock).mockReturnValue(mockTranslate);
|
||||
(useTelemetry as Mock).mockReturnValue({
|
||||
sendExpandCollapseAllClickedMessage: mockTelemetrySend,
|
||||
});
|
||||
});
|
||||
|
|
@ -56,7 +57,7 @@ describe('ExpandAllButton', () => {
|
|||
});
|
||||
|
||||
it('displays correct text based on expanded state', () => {
|
||||
(useAtom as jest.Mock).mockReturnValue([
|
||||
(useAtom as Mock).mockReturnValue([
|
||||
{ request: { expandedAll: true }, response: { expandedAll: false } },
|
||||
mockSetOperationState,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { render, fireEvent, screen } from '@testing-library/react';
|
||||
|
||||
import { LanguageDropdown } from '../LanguageDropdown';
|
||||
import { LanguageDropdown } from '../LanguageDropdown.js';
|
||||
|
||||
const mockSamples = [
|
||||
{ key: 'js', title: 'JS', lang: 'js' },
|
||||
|
|
@ -9,7 +9,7 @@ const mockSamples = [
|
|||
|
||||
const mockActiveTab = mockSamples[0].key;
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
const mockOnChange = vi.fn();
|
||||
|
||||
describe('LanguageDropdown', () => {
|
||||
test('LanguageDropdown renders correctly', () => {
|
||||
|
|
|
|||
|
|
@ -3,19 +3,19 @@ import * as Jotai from 'jotai/index';
|
|||
|
||||
import { html } from '@redocly/theme/markdoc/tags/html';
|
||||
|
||||
import { normalizeOptions } from '../../../services';
|
||||
import { Markdown } from '../Markdown';
|
||||
import { withTestProviders } from '../../../testProviders';
|
||||
import { normalizeOptions } from '../../../services/index.js';
|
||||
import { Markdown } from '../Markdown.js';
|
||||
import { withTestProviders } from '../../../testProviders.js';
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: jest.fn(),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtomValue: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('Components', () => {
|
||||
describe('Markdown', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
test('Markdown renders correctly', () => {
|
||||
const { getByText } = render(
|
||||
|
|
@ -34,11 +34,13 @@ describe('Components', () => {
|
|||
tags: {
|
||||
html: html.schema,
|
||||
},
|
||||
nodes: {},
|
||||
components: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockImplementation(() => {
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockImplementation(() => {
|
||||
return options;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,190 +1,60 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`processHtmlTokens should process block and inline html tokens 1`] = `
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": [
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": "Heading",
|
||||
},
|
||||
"type": "h1",
|
||||
},
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": {
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": [
|
||||
"Inline ",
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": "bold",
|
||||
},
|
||||
"type": "strong",
|
||||
},
|
||||
" text",
|
||||
],
|
||||
},
|
||||
"type": "p",
|
||||
},
|
||||
"id": "22",
|
||||
},
|
||||
"type": "div",
|
||||
},
|
||||
],
|
||||
},
|
||||
"type": "article",
|
||||
}
|
||||
exports[`processHtmlTokens > should process block and inline html tokens 1`] = `
|
||||
<article>
|
||||
<h1>
|
||||
Heading
|
||||
</h1>
|
||||
<div
|
||||
id="22"
|
||||
>
|
||||
<p>
|
||||
Inline
|
||||
<strong>
|
||||
bold
|
||||
</strong>
|
||||
text
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
|
||||
exports[`processHtmlTokens should process inline html tokens 1`] = `
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": [
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": [
|
||||
"Inline ",
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": "bold",
|
||||
},
|
||||
"type": "strong",
|
||||
},
|
||||
" text",
|
||||
],
|
||||
},
|
||||
"type": "p",
|
||||
},
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": "Heading",
|
||||
},
|
||||
"type": "h1",
|
||||
},
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": [
|
||||
"Inline ",
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": "bold",
|
||||
},
|
||||
"type": "strong",
|
||||
},
|
||||
" text",
|
||||
],
|
||||
},
|
||||
"type": "p",
|
||||
},
|
||||
],
|
||||
},
|
||||
"type": "article",
|
||||
}
|
||||
exports[`processHtmlTokens > should process inline html tokens 1`] = `
|
||||
<article>
|
||||
<p>
|
||||
Inline
|
||||
<strong>
|
||||
bold
|
||||
</strong>
|
||||
text
|
||||
</p>
|
||||
<h1>
|
||||
Heading
|
||||
</h1>
|
||||
<p>
|
||||
Inline
|
||||
<strong>
|
||||
bold
|
||||
</strong>
|
||||
text
|
||||
</p>
|
||||
</article>
|
||||
`;
|
||||
|
||||
exports[`processHtmlTokens should report unclosed errors 1`] = `
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": [
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": {
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": " inline",
|
||||
},
|
||||
"type": "span",
|
||||
},
|
||||
},
|
||||
"type": "p",
|
||||
},
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": "Heading",
|
||||
},
|
||||
"type": "h1",
|
||||
},
|
||||
{
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": {
|
||||
"$$typeof": Symbol(react.transitional.element),
|
||||
"_owner": null,
|
||||
"_store": {},
|
||||
"key": null,
|
||||
"props": {
|
||||
"children": "bold",
|
||||
},
|
||||
"type": "p",
|
||||
},
|
||||
},
|
||||
"type": "strong",
|
||||
},
|
||||
],
|
||||
},
|
||||
"type": "article",
|
||||
}
|
||||
exports[`processHtmlTokens > should report unclosed errors 1`] = `
|
||||
<article>
|
||||
<p>
|
||||
<span>
|
||||
inline
|
||||
</span>
|
||||
</p>
|
||||
<h1>
|
||||
Heading
|
||||
</h1>
|
||||
<strong>
|
||||
<p>
|
||||
bold
|
||||
</p>
|
||||
</strong>
|
||||
</article>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import React from 'react';
|
|||
|
||||
import { html } from '@redocly/theme/markdoc/tags/html';
|
||||
|
||||
import { processHtmlTokens } from '../processHtmlTokens';
|
||||
import { processHtmlTokens } from '../processHtmlTokens.js';
|
||||
|
||||
const tokenizer = new markdoc.Tokenizer({
|
||||
html: true,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import { McpOverview } from '../McpOverview';
|
||||
import { withTestProviders } from '../../../testProviders';
|
||||
import { McpOverview } from '../McpOverview.js';
|
||||
import { withTestProviders, TestMemoryRouter } from '../../../testProviders.js';
|
||||
import mcpInfo from './fixtures/mcp-info.json';
|
||||
|
||||
describe('McpOverview', () => {
|
||||
it('should render mcp overview', () => {
|
||||
render(
|
||||
withTestProviders(
|
||||
<MemoryRouter>
|
||||
<TestMemoryRouter>
|
||||
<McpOverview
|
||||
mcpInfo={mcpInfo as any}
|
||||
mcpServers={[{ url: 'https://api.example.com/mcp' }]}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
</TestMemoryRouter>,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -33,12 +31,12 @@ describe('McpOverview', () => {
|
|||
it('should render experimental capabilities', () => {
|
||||
render(
|
||||
withTestProviders(
|
||||
<MemoryRouter>
|
||||
<TestMemoryRouter>
|
||||
<McpOverview
|
||||
mcpInfo={mcpInfo as any}
|
||||
mcpServers={[{ url: 'https://api.example.com/mcp' }]}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
</TestMemoryRouter>,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -60,12 +58,12 @@ describe('McpOverview', () => {
|
|||
};
|
||||
render(
|
||||
withTestProviders(
|
||||
<MemoryRouter>
|
||||
<TestMemoryRouter>
|
||||
<McpOverview
|
||||
mcpInfo={mcpInfoWithExtras as any}
|
||||
mcpServers={[{ url: 'https://api.example.com/mcp' }]}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
</TestMemoryRouter>,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
157
src/components/McpTool/McpPrompt.tsx
Normal file
157
src/components/McpTool/McpPrompt.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { LayoutVariant } from '@redocly/config';
|
||||
|
||||
import type { McpPrompt as McpPromptType } from '../../types/open-api.js';
|
||||
|
||||
import { getMediaContent, getSecurity, type OperationModel } from '../../models/index.js';
|
||||
import { globalStoreAtom } from '../../jotai/store.js';
|
||||
import { Section, Row, SamplesMiddlePanel, SamplesPanel, CodeBlockPanel } from '../common/index.js';
|
||||
import { Schema } from '../Schema/index.js';
|
||||
import { layoutAtom } from '../../jotai/app.js';
|
||||
import { MediaTypeSamples } from '../PayloadSamples/MediaTypeSamples.js';
|
||||
import { useTranslate } from '../../hooks/index.js';
|
||||
import { Title } from '../common/OperationItemTitle.js';
|
||||
import { LinkToField } from '../common/LinkToField.js';
|
||||
import { makeDeepLink } from '../../services/index.js';
|
||||
import { Security } from '../Security/Security.js';
|
||||
import { styled } from '../../styled-components.js';
|
||||
|
||||
interface McpPromptProps {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
function McpPromptComponent({ name, id }: McpPromptProps) {
|
||||
const { parser } = useAtomValue(globalStoreAtom);
|
||||
const translate = useTranslate();
|
||||
|
||||
const prompt = useMemo((): McpPromptType | undefined => {
|
||||
const prompts = parser.definition['x-mcp']?.prompts || [];
|
||||
return prompts.find((p: McpPromptType) => p.name === name);
|
||||
}, [parser, name]);
|
||||
|
||||
const securityModel = useMemo(() => {
|
||||
if (!prompt) {
|
||||
return null;
|
||||
}
|
||||
return getSecurity(prompt.security, parser);
|
||||
}, [parser, prompt]);
|
||||
|
||||
if (!prompt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<Content
|
||||
id={id}
|
||||
type="input"
|
||||
prompt={prompt}
|
||||
title={translate('openapi.mcp.inputSchema', 'Arguments')}
|
||||
exampleTitle={translate('openapi.mcp.inputExample', 'Arguments example')}
|
||||
>
|
||||
{securityModel && <Security securities={securityModel} />}
|
||||
</Content>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
function Content({
|
||||
id,
|
||||
type,
|
||||
prompt,
|
||||
title,
|
||||
exampleTitle,
|
||||
children,
|
||||
}: {
|
||||
id: string;
|
||||
type: 'input' | 'output';
|
||||
prompt: McpPromptType;
|
||||
title: string;
|
||||
exampleTitle: string;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const { parser, options } = useAtomValue(globalStoreAtom);
|
||||
const layout = useAtomValue(layoutAtom);
|
||||
const isStacked = layout === LayoutVariant.STACKED;
|
||||
|
||||
const mediaContent = useMemo(() => {
|
||||
const parametersSchema = prompt.arguments && {
|
||||
type: 'object',
|
||||
properties: prompt.arguments.reduce((acc, argument) => {
|
||||
acc[argument.name] = {
|
||||
type: '',
|
||||
example: argument.example || 'string',
|
||||
description: argument.description,
|
||||
required: argument.required,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
required: prompt.arguments
|
||||
.filter((argument) => argument.required)
|
||||
.map((argument) => argument.name),
|
||||
};
|
||||
|
||||
if (!parametersSchema) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getMediaContent({
|
||||
parser,
|
||||
info: {
|
||||
'application/json': {
|
||||
schema: parametersSchema,
|
||||
},
|
||||
},
|
||||
isRequestType: false,
|
||||
options,
|
||||
data: {
|
||||
operation: { pointer: 'McpPrompt' } as OperationModel,
|
||||
},
|
||||
});
|
||||
}, [options, parser, prompt]);
|
||||
|
||||
const nope = useCallback(() => {}, []);
|
||||
|
||||
return (
|
||||
<McpSubRowStyled layout={layout}>
|
||||
<McpToolMiddlePanel isStacked={isStacked}>
|
||||
<div>
|
||||
{children}
|
||||
{mediaContent && (
|
||||
<>
|
||||
{' '}
|
||||
<Title>
|
||||
{id && <LinkToField to={makeDeepLink(id, type)} />}
|
||||
{title}
|
||||
</Title>
|
||||
<Schema schema={mediaContent.mediaTypes[0].schema} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</McpToolMiddlePanel>
|
||||
{mediaContent && (
|
||||
<SamplesPanel isStacked={isStacked}>
|
||||
<CodeBlockPanel className="panel-response-samples" header={exampleTitle}>
|
||||
<MediaTypeSamples mediaType={mediaContent.mediaTypes[0]} onChange={nope} />
|
||||
</CodeBlockPanel>
|
||||
</SamplesPanel>
|
||||
)}
|
||||
</McpSubRowStyled>
|
||||
);
|
||||
}
|
||||
|
||||
const McpSubRowStyled = styled(Row)`
|
||||
margin: calc(var(--spacing-unit) * 2) 0;
|
||||
|
||||
& + & {
|
||||
margin-top: calc(var(--spacing-base) * 2);
|
||||
}
|
||||
`;
|
||||
|
||||
const McpToolMiddlePanel = styled(SamplesMiddlePanel)`
|
||||
padding-left: 0;
|
||||
`;
|
||||
|
||||
export const McpPrompt = memo<McpPromptProps>(McpPromptComponent);
|
||||
151
src/components/McpTool/McpResource.tsx
Normal file
151
src/components/McpTool/McpResource.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import { memo, useMemo } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { LayoutVariant } from '@redocly/config';
|
||||
|
||||
import { CodeBlock } from '@redocly/theme/components/CodeBlock/CodeBlock';
|
||||
import { Tag } from '@redocly/theme/components/Tag/Tag';
|
||||
|
||||
import { getSecurity } from '../../models/index.js';
|
||||
import { globalStoreAtom } from '../../jotai/store.js';
|
||||
import { Section, Row, SamplesMiddlePanel, SamplesPanel, CodeBlockPanel } from '../common/index.js';
|
||||
import { layoutAtom } from '../../jotai/app.js';
|
||||
import { useTranslate } from '../../hooks/index.js';
|
||||
import { Security } from '../Security/Security.js';
|
||||
import { styled } from '../../styled-components.js';
|
||||
|
||||
interface McpResourceProps {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
function McpResourceComponent({ name }: McpResourceProps) {
|
||||
const { parser } = useAtomValue(globalStoreAtom);
|
||||
const translate = useTranslate();
|
||||
|
||||
const resource = useMemo(() => {
|
||||
const resources = parser.definition['x-mcp']?.resources || [];
|
||||
return resources.find((r) => r.name === name);
|
||||
}, [parser, name]);
|
||||
|
||||
const securityModel = useMemo(() => {
|
||||
if (!resource) {
|
||||
return null;
|
||||
}
|
||||
return getSecurity(resource.security, parser);
|
||||
}, [parser, resource]);
|
||||
|
||||
if (!resource) {
|
||||
return <div>Resource not found: {name}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<LeftPanel
|
||||
uriTitle={translate('openapi.mcp.uriTitle', 'Resource URI')}
|
||||
mimeTypeTitle={translate('openapi.mcp.mimeTypeTitle', 'Resource MIME type')}
|
||||
exampleTitle={translate('openapi.mcp.exampleTitle', 'Resource content')}
|
||||
uri={resource.uri}
|
||||
mimeType={resource.mimeType}
|
||||
content={resource.blob ?? resource.text ?? ''}
|
||||
>
|
||||
{securityModel && <Security securities={securityModel} />}
|
||||
</LeftPanel>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
function LeftPanel({
|
||||
uriTitle,
|
||||
mimeTypeTitle,
|
||||
exampleTitle,
|
||||
uri,
|
||||
mimeType,
|
||||
content,
|
||||
children,
|
||||
}: {
|
||||
uriTitle: string;
|
||||
mimeTypeTitle: string;
|
||||
exampleTitle: string;
|
||||
children?: React.ReactNode;
|
||||
uri: string;
|
||||
mimeType: string;
|
||||
content: string;
|
||||
}) {
|
||||
const layout = useAtomValue(layoutAtom);
|
||||
const isStacked = layout === LayoutVariant.STACKED;
|
||||
const [copiedField, setCopiedField] = React.useState<string | null>(null);
|
||||
|
||||
const copyToClipboard = async (text: string, field: string, event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopiedField(field);
|
||||
setTimeout(() => setCopiedField(null), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<McpSubRowStyled layout={layout}>
|
||||
<McpToolMiddlePanel isStacked={isStacked}>
|
||||
<div>
|
||||
{children}
|
||||
<RowLine>
|
||||
{uriTitle}:{' '}
|
||||
<StyledTag onClick={(e) => copyToClipboard(uri, 'uri', e)} title="Click to copy">
|
||||
{copiedField === 'uri' ? '✓ Copied!' : uri}
|
||||
</StyledTag>
|
||||
</RowLine>
|
||||
<RowLine>
|
||||
{mimeTypeTitle}:{' '}
|
||||
<StyledTag
|
||||
onClick={(e) => copyToClipboard(mimeType, 'mimeType', e)}
|
||||
title="Click to copy"
|
||||
>
|
||||
{copiedField === 'mimeType' ? '✓ Copied!' : mimeType}
|
||||
</StyledTag>
|
||||
</RowLine>
|
||||
</div>
|
||||
</McpToolMiddlePanel>
|
||||
{content && (
|
||||
<SamplesPanel isStacked={isStacked}>
|
||||
<CodeBlockPanel className="panel-response-samples" header={exampleTitle}>
|
||||
<StyledCodeBlock
|
||||
source={content}
|
||||
header={{ title: mimeType, controls: { copy: {} } }}
|
||||
/>
|
||||
</CodeBlockPanel>
|
||||
</SamplesPanel>
|
||||
)}
|
||||
</McpSubRowStyled>
|
||||
);
|
||||
}
|
||||
|
||||
const McpSubRowStyled = styled(Row)`
|
||||
margin: calc(var(--spacing-unit) * 2) 0;
|
||||
|
||||
& + & {
|
||||
margin-top: calc(var(--spacing-base) * 2);
|
||||
}
|
||||
`;
|
||||
|
||||
const McpToolMiddlePanel = styled(SamplesMiddlePanel)`
|
||||
padding-left: 0;
|
||||
`;
|
||||
|
||||
export const McpResource = memo<McpResourceProps>(McpResourceComponent);
|
||||
|
||||
const StyledTag = styled(Tag)`
|
||||
text-transform: none;
|
||||
`;
|
||||
|
||||
const StyledCodeBlock = styled(CodeBlock)`
|
||||
border: none;
|
||||
padding: 0 8px;
|
||||
`;
|
||||
|
||||
const RowLine = styled.div`
|
||||
margin: 1em 0;
|
||||
`;
|
||||
|
|
@ -19,18 +19,18 @@ import { Security } from '../Security/Security.js';
|
|||
import { styled } from '../../styled-components.js';
|
||||
|
||||
interface McpToolProps {
|
||||
toolName: string;
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
function McpToolComponent({ toolName, id }: McpToolProps) {
|
||||
function McpToolComponent({ name, id }: McpToolProps) {
|
||||
const { parser } = useAtomValue(globalStoreAtom);
|
||||
const translate = useTranslate();
|
||||
|
||||
const tool = useMemo(() => {
|
||||
const tools = parser.definition['x-mcp']?.tools || [];
|
||||
return tools.find((t: any) => t.name === toolName);
|
||||
}, [parser, toolName]);
|
||||
return tools.find((t: any) => t.name === name);
|
||||
}, [parser, name]);
|
||||
|
||||
const securityModel = useMemo(() => {
|
||||
if (!tool) {
|
||||
|
|
@ -40,7 +40,7 @@ function McpToolComponent({ toolName, id }: McpToolProps) {
|
|||
}, [parser, tool]);
|
||||
|
||||
if (!tool) {
|
||||
return <div>Tool not found: {toolName}</div>;
|
||||
return <div>Tool not found: {name}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
75
src/components/McpTool/__tests__/McpPrompt.test.tsx
Normal file
75
src/components/McpTool/__tests__/McpPrompt.test.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { McpPrompt } from '../McpPrompt.js';
|
||||
import { withTestProviders, TestMemoryRouter } from '../../../testProviders.js';
|
||||
|
||||
const spec = {
|
||||
openapi: '3.0.0',
|
||||
'x-mcp': {
|
||||
prompts: [
|
||||
{
|
||||
name: 'test-prompt',
|
||||
description: 'A test prompt',
|
||||
arguments: [
|
||||
{
|
||||
name: 'arg1',
|
||||
description: 'First argument',
|
||||
required: true,
|
||||
example: 'value1',
|
||||
},
|
||||
],
|
||||
security: [
|
||||
{
|
||||
apiKey: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
apiKey: {
|
||||
type: 'apiKey',
|
||||
in: 'header',
|
||||
name: 'X-API-Key',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('McpPrompt', () => {
|
||||
it('should render mcp prompt with arguments', () => {
|
||||
render(
|
||||
withTestProviders(
|
||||
<TestMemoryRouter>
|
||||
<McpPrompt name="test-prompt" id="test-prompt" />
|
||||
</TestMemoryRouter>,
|
||||
{
|
||||
definition: spec as any,
|
||||
options: {} as any,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(screen.getByText('Arguments')).toBeInTheDocument();
|
||||
expect(screen.getByText('arg1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Security')).toBeInTheDocument();
|
||||
expect(screen.getByText('apiKey')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return null if prompt not found', () => {
|
||||
const { container } = render(
|
||||
withTestProviders(
|
||||
<TestMemoryRouter>
|
||||
<McpPrompt name="not-found" id="not-found" />
|
||||
</TestMemoryRouter>,
|
||||
{
|
||||
definition: spec as any,
|
||||
options: {} as any,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
112
src/components/McpTool/__tests__/McpResource.test.tsx
Normal file
112
src/components/McpTool/__tests__/McpResource.test.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { McpResource } from '../McpResource.js';
|
||||
import { withTestProviders, TestMemoryRouter } from '../../../testProviders.js';
|
||||
|
||||
const spec = {
|
||||
openapi: '3.0.0',
|
||||
'x-mcp': {
|
||||
resources: [
|
||||
{
|
||||
name: 'test-resource',
|
||||
uri: 'mcp://test-resource',
|
||||
mimeType: 'application/json',
|
||||
text: '{"key": "value"}',
|
||||
security: [
|
||||
{
|
||||
apiKey: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
apiKey: {
|
||||
type: 'apiKey',
|
||||
in: 'header',
|
||||
name: 'X-API-Key',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Mock navigator.clipboard
|
||||
const mockClipboard = {
|
||||
writeText: vi.fn(),
|
||||
};
|
||||
Object.assign(navigator, {
|
||||
clipboard: mockClipboard,
|
||||
});
|
||||
|
||||
describe('McpResource', () => {
|
||||
beforeAll(() => {
|
||||
mockClipboard.writeText.mockImplementation(() => Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render mcp resource with content', () => {
|
||||
render(
|
||||
withTestProviders(
|
||||
<TestMemoryRouter>
|
||||
<McpResource name="test-resource" id="test-resource" />
|
||||
</TestMemoryRouter>,
|
||||
{
|
||||
definition: spec as any,
|
||||
options: {} as any,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(screen.getByText('Resource URI:')).toBeInTheDocument();
|
||||
expect(screen.getByText('mcp://test-resource')).toBeInTheDocument();
|
||||
expect(screen.getByText('Resource MIME type:')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('application/json').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText('Resource content')).toBeInTheDocument();
|
||||
expect(screen.getByText(/"key": "value"/)).toBeInTheDocument();
|
||||
expect(screen.getByText('Security')).toBeInTheDocument();
|
||||
expect(screen.getByText('apiKey')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a message if resource not found', () => {
|
||||
render(
|
||||
withTestProviders(
|
||||
<TestMemoryRouter>
|
||||
<McpResource name="not-found" id="not-found" />
|
||||
</TestMemoryRouter>,
|
||||
{
|
||||
definition: spec as any,
|
||||
options: {} as any,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(screen.getByText('Resource not found: not-found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should copy URI to clipboard when clicked', async () => {
|
||||
render(
|
||||
withTestProviders(
|
||||
<TestMemoryRouter>
|
||||
<McpResource name="test-resource" id="test-resource" />
|
||||
</TestMemoryRouter>,
|
||||
{
|
||||
definition: spec as any,
|
||||
options: {} as any,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const uriElement = screen.getByText('mcp://test-resource');
|
||||
fireEvent.click(uriElement);
|
||||
|
||||
expect(mockClipboard.writeText).toHaveBeenCalledWith('mcp://test-resource');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('✓ Copied!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import { McpTool } from '../McpTool';
|
||||
import { withTestProviders } from '../../../testProviders';
|
||||
import { McpTool } from '../McpTool.js';
|
||||
import { withTestProviders, TestMemoryRouter } from '../../../testProviders.js';
|
||||
import mcpData from './fixtures/mcp-tools.json';
|
||||
|
||||
const spec = {
|
||||
|
|
@ -24,9 +22,9 @@ describe('McpTool', () => {
|
|||
it('should render mcp tool with input and output schema', () => {
|
||||
render(
|
||||
withTestProviders(
|
||||
<MemoryRouter>
|
||||
<McpTool toolName="test-tool" id="test-tool" />
|
||||
</MemoryRouter>,
|
||||
<TestMemoryRouter>
|
||||
<McpTool name="test-tool" id="test-tool" />
|
||||
</TestMemoryRouter>,
|
||||
{
|
||||
definition: spec as any,
|
||||
options: {} as any,
|
||||
|
|
@ -45,9 +43,9 @@ describe('McpTool', () => {
|
|||
it('should render a message if tool not found', () => {
|
||||
render(
|
||||
withTestProviders(
|
||||
<MemoryRouter>
|
||||
<McpTool toolName="not-found" id="not-found" />
|
||||
</MemoryRouter>,
|
||||
<TestMemoryRouter>
|
||||
<McpTool name="not-found" id="not-found" />
|
||||
</TestMemoryRouter>,
|
||||
{
|
||||
definition: spec as any,
|
||||
options: {} as any,
|
||||
|
|
@ -81,9 +79,9 @@ describe('McpTool', () => {
|
|||
};
|
||||
render(
|
||||
withTestProviders(
|
||||
<MemoryRouter>
|
||||
<McpTool toolName="test-tool-no-output" id="test-tool-no-output" />
|
||||
</MemoryRouter>,
|
||||
<TestMemoryRouter>
|
||||
<McpTool name="test-tool-no-output" id="test-tool-no-output" />
|
||||
</TestMemoryRouter>,
|
||||
{
|
||||
definition: specWithoutOutput as any,
|
||||
options: {} as any,
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
export * from './McpTool.js';
|
||||
export * from './McpResource.js';
|
||||
export * from './McpPrompt.js';
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { act, render } from '@testing-library/react';
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import type { SelectProps } from '../../common';
|
||||
import type { OperationModel } from '../../../models';
|
||||
import type { SelectProps } from '../../common/index.js';
|
||||
import type { OperationModel } from '../../../models/index.js';
|
||||
|
||||
import { MediaTypesSwitch } from '../MediaTypesSwitch';
|
||||
import { getOperation } from '../../../models';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { MediaTypesSwitch } from '../MediaTypesSwitch.js';
|
||||
import { getOperation } from '../../../models/index.js';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import testDefinition from './fixtures/test-definition.json';
|
||||
import { withTestProviders } from '../../../testProviders';
|
||||
import { withTestProviders } from '../../../testProviders.js';
|
||||
|
||||
describe('Components', () => {
|
||||
describe('MediaTypesSwitch', () => {
|
||||
|
|
@ -25,6 +25,7 @@ describe('Components', () => {
|
|||
pathParameters: [],
|
||||
pathServers: [],
|
||||
isWebhook: false,
|
||||
isAdditionalOperation: false,
|
||||
...testDefinition.paths['/test'].get,
|
||||
},
|
||||
undefined,
|
||||
|
|
@ -34,8 +35,8 @@ describe('Components', () => {
|
|||
});
|
||||
|
||||
test('renders with default media type by default', () => {
|
||||
const childrenFn = jest.fn((mimeType) => <>children: {mimeType.name}</>);
|
||||
const renderSelectFn = jest.fn<ReactElement | null, [SelectProps]>(() => null);
|
||||
const childrenFn = vi.fn((mimeType) => <>children: {mimeType.name}</>);
|
||||
const renderSelectFn = vi.fn<(props: SelectProps) => ReactElement | null>(() => null);
|
||||
|
||||
const { getByText } = render(
|
||||
withTestProviders(
|
||||
|
|
@ -55,8 +56,8 @@ describe('Components', () => {
|
|||
});
|
||||
|
||||
test('withLabel renders label', () => {
|
||||
const childrenFn = jest.fn((mimeType) => <>children: {mimeType.name}</>);
|
||||
const renderSelectFn = jest.fn(() => <>dropdown</>);
|
||||
const childrenFn = vi.fn((mimeType) => <>children: {mimeType.name}</>);
|
||||
const renderSelectFn = vi.fn(() => <>dropdown</>);
|
||||
|
||||
const { getByText } = render(
|
||||
withTestProviders(
|
||||
|
|
@ -75,8 +76,10 @@ describe('Components', () => {
|
|||
});
|
||||
|
||||
test('should try to restore scroll on switch', async () => {
|
||||
const childrenFn = jest.fn((mimeType) => <>children: {mimeType.name}</>);
|
||||
const renderSelectFn = jest.fn<ReactElement | null, [SelectProps]>(() => <>dropdown</>);
|
||||
const childrenFn = vi.fn((mimeType) => <>children: {mimeType.name}</>);
|
||||
const renderSelectFn = vi.fn<(props: SelectProps) => ReactElement | null>(() => (
|
||||
<>dropdown</>
|
||||
));
|
||||
|
||||
const { findByText, container } = render(
|
||||
withTestProviders(
|
||||
|
|
@ -92,17 +95,20 @@ describe('Components', () => {
|
|||
);
|
||||
|
||||
// Create a mock implementation of window.scrollBy
|
||||
const scrollByMock = jest.fn();
|
||||
const scrollByMock = vi.fn();
|
||||
Object.defineProperty(window, 'scrollBy', { value: scrollByMock });
|
||||
|
||||
const rectMock = jest.fn(() => {
|
||||
const rectMock = vi.fn(() => {
|
||||
return (rectMock.mock.calls.length > 1 ? { y: 150 } : { y: 100 }) as unknown as DOMRect;
|
||||
});
|
||||
|
||||
jest.spyOn(container.firstElementChild, 'getBoundingClientRect').mockImplementation(rectMock);
|
||||
vi.spyOn(container?.firstElementChild as Element, 'getBoundingClientRect').mockImplementation(
|
||||
rectMock,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
renderSelectFn.mock.calls[0][0].onChange(renderSelectFn.mock.calls[0][0].options[1]);
|
||||
const selectProps = renderSelectFn.mock.calls[0][0];
|
||||
selectProps.onChange(selectProps.options[1]);
|
||||
});
|
||||
|
||||
await findByText('children: application/xml');
|
||||
|
|
@ -114,8 +120,8 @@ describe('Components', () => {
|
|||
});
|
||||
|
||||
test('should not throw and render null for empty media types', () => {
|
||||
const childrenFn = jest.fn((mimeType) => <>children: {mimeType.name}</>);
|
||||
const renderSelectFn = jest.fn(() => null);
|
||||
const childrenFn = vi.fn((mimeType) => <>children: {mimeType.name}</>);
|
||||
const renderSelectFn = vi.fn(() => null);
|
||||
|
||||
const { container } = render(
|
||||
withTestProviders(
|
||||
|
|
|
|||
|
|
@ -10,5 +10,5 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"components": { "securitySchemes": { "test": { "type": "apiKey" } } }
|
||||
"components": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import type { OperationMenuItem } from '../../../models';
|
||||
import type { OperationMenuItem } from '../../../models/index.js';
|
||||
|
||||
import { OperationItem } from '../OperationItem';
|
||||
import { normalizeOptions } from '../../../services';
|
||||
import { OperationItem } from '../OperationItem.js';
|
||||
import { normalizeOptions } from '../../../services/index.js';
|
||||
import spec from './fixtures/petstore.json';
|
||||
import definition from './fixtures/operationDefinition.json';
|
||||
import { globalStoreAtom } from '../../../jotai/store';
|
||||
import { MockIntersectionObserver } from './__mocks__/mock-intersection-observer';
|
||||
import { globalStoreAtom } from '../../../jotai/store.js';
|
||||
import { MockIntersectionObserver } from './__mocks__/mock-intersection-observer.js';
|
||||
import { TestBrowserRouter } from '../../../testProviders.js';
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: jest.fn((a) => {
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtomValue: vi.fn((a) => {
|
||||
if (a === globalStoreAtom) {
|
||||
return {
|
||||
parser: {
|
||||
|
|
@ -27,7 +27,7 @@ jest.mock('jotai', () => ({
|
|||
}
|
||||
return {};
|
||||
}),
|
||||
useAtom: jest.fn(() => {
|
||||
useAtom: vi.fn(() => {
|
||||
return [
|
||||
{
|
||||
activeOneOf: { '/paths/~1pet/post': 0 },
|
||||
|
|
@ -36,7 +36,7 @@ jest.mock('jotai', () => ({
|
|||
response: { expandedFields: {} },
|
||||
activeLanguage: 'curl',
|
||||
},
|
||||
jest.fn(),
|
||||
vi.fn(),
|
||||
];
|
||||
}),
|
||||
}));
|
||||
|
|
@ -58,7 +58,7 @@ describe('OperationItem', () => {
|
|||
}
|
||||
/>,
|
||||
{
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,74 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Overview should render correctly with contact data but without a name 1`] = `
|
||||
.c9 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
exports[`Overview > should render correctly with contact data but without a name 1`] = `
|
||||
.c0 > span {
|
||||
border-bottom: 1px solid var(--border-color-secondary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.c9 path {
|
||||
fill: currentColor;
|
||||
.c0 > span:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}.c4 {
|
||||
margin: 0;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.c6 {
|
||||
|
|
@ -89,6 +150,15 @@ exports[`Overview should render correctly with contact data but without a name 1
|
|||
border-width: var(--button-border-width-disabled);
|
||||
}
|
||||
|
||||
.c7 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c7 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
|
|
@ -97,12 +167,12 @@ exports[`Overview should render correctly with contact data but without a name 1
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
.c9 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c7 path {
|
||||
.c9 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
|
|
@ -115,77 +185,7 @@ exports[`Overview should render correctly with contact data but without a name 1
|
|||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
margin: 0;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.c0 > span {
|
||||
border-bottom: 1px solid var(--border-color-secondary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.c0 > span:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
|
|
@ -319,14 +319,75 @@ exports[`Overview should render correctly with contact data but without a name 1
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`Overview should render correctly with full data 1`] = `
|
||||
.c9 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
exports[`Overview > should render correctly with full data 1`] = `
|
||||
.c0 > span {
|
||||
border-bottom: 1px solid var(--border-color-secondary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.c9 path {
|
||||
fill: currentColor;
|
||||
.c0 > span:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}.c4 {
|
||||
margin: 0;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.c6 {
|
||||
|
|
@ -408,6 +469,15 @@ exports[`Overview should render correctly with full data 1`] = `
|
|||
border-width: var(--button-border-width-disabled);
|
||||
}
|
||||
|
||||
.c7 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c7 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
|
|
@ -416,12 +486,12 @@ exports[`Overview should render correctly with full data 1`] = `
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
.c9 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c7 path {
|
||||
.c9 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
|
|
@ -434,77 +504,7 @@ exports[`Overview should render correctly with full data 1`] = `
|
|||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
margin: 0;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.c0 > span {
|
||||
border-bottom: 1px solid var(--border-color-secondary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.c0 > span:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
|
|
@ -729,7 +729,7 @@ exports[`Overview should render correctly with full data 1`] = `
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`Overview should render correctly with partial data 1`] = `
|
||||
exports[`Overview > should render correctly with partial data 1`] = `
|
||||
.c0 > span {
|
||||
border-bottom: 1px solid var(--border-color-secondary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
|
|
@ -749,7 +749,77 @@ exports[`Overview should render correctly with partial data 1`] = `
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`Overview should render license with name and url when identifier is not present 1`] = `
|
||||
exports[`Overview > should render license with name and url when identifier is not present 1`] = `
|
||||
.c0 > span {
|
||||
border-bottom: 1px solid var(--border-color-secondary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.c0 > span:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}.c4 {
|
||||
margin: 0;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.c6 {
|
||||
width: auto;
|
||||
display: -webkit-inline-box;
|
||||
|
|
@ -838,77 +908,7 @@ exports[`Overview should render license with name and url when identifier is not
|
|||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
margin: 0;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.c0 > span {
|
||||
border-bottom: 1px solid var(--border-color-secondary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.c0 > span:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import * as Jotai from 'jotai';
|
||||
|
||||
import type { OpenAPIDefinition, OpenAPIExample } from '../../../types';
|
||||
import type { OpenAPIDefinition, OpenAPIExample } from '../../../types/index.js';
|
||||
|
||||
import { Example } from '../Example';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { getExamples } from '../../../models';
|
||||
import { Example } from '../Example.js';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import { getExamples } from '../../../models/index.js';
|
||||
import testDefinition from './fixtures/mediaTypeUrlencoded.json';
|
||||
|
||||
describe('Example component', () => {
|
||||
|
|
@ -23,7 +23,7 @@ describe('Example component', () => {
|
|||
});
|
||||
const { container } = render(
|
||||
<Jotai.Provider>
|
||||
<Example example={example} mimeType={example.mime} onCopyClick={jest.fn()} />
|
||||
<Example example={example} mimeType={example.mime} onCopyClick={vi.fn()} />
|
||||
</Jotai.Provider>,
|
||||
);
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ describe('Example component', () => {
|
|||
|
||||
const { container } = render(
|
||||
<Jotai.Provider>
|
||||
<Example example={example} mimeType={example.mime} onCopyClick={jest.fn()} />
|
||||
<Example example={example} mimeType={example.mime} onCopyClick={vi.fn()} />
|
||||
</Jotai.Provider>,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { render } from '@testing-library/react';
|
||||
|
||||
import type { OpenAPIDefinition } from '../../../types';
|
||||
import type { OperationModel } from '../../../models';
|
||||
import type { OpenAPIDefinition } from '../../../types/index.js';
|
||||
import type { OperationModel } from '../../../models/index.js';
|
||||
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { getMediaContent, getOperation } from '../../../models';
|
||||
import { withTestProviders } from '../../../testProviders';
|
||||
import { PayloadSamples } from '../PayloadSamples';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import { getMediaContent, getOperation } from '../../../models/index.js';
|
||||
import { withTestProviders } from '../../../testProviders.js';
|
||||
import { PayloadSamples } from '../PayloadSamples.js';
|
||||
import testDefinition from './fixtures/mediaTypeUrlencoded.json';
|
||||
|
||||
describe('PayloadSamples', () => {
|
||||
|
|
@ -15,7 +15,22 @@ describe('PayloadSamples', () => {
|
|||
const parser = new OpenAPIParser(testDefinition as OpenAPIDefinition, undefined, options);
|
||||
|
||||
test('should renders correctly without example to application/x-www-form-urlencoded', async () => {
|
||||
operation = getOperation(parser, testDefinition, undefined, options, 'tests');
|
||||
operation = getOperation(
|
||||
parser,
|
||||
{
|
||||
pointer: '#/paths/~1test/get',
|
||||
pathName: '/test',
|
||||
httpVerb: 'GET',
|
||||
pathParameters: [],
|
||||
pathServers: [],
|
||||
isWebhook: false,
|
||||
isAdditionalOperation: false,
|
||||
...testDefinition.paths['/test'].get,
|
||||
},
|
||||
undefined,
|
||||
options,
|
||||
'tests',
|
||||
);
|
||||
const props = {
|
||||
operation,
|
||||
content: getMediaContent({
|
||||
|
|
@ -25,7 +40,7 @@ describe('PayloadSamples', () => {
|
|||
options,
|
||||
data: { operation },
|
||||
}),
|
||||
onCopyClick: jest.fn(),
|
||||
onCopyClick: vi.fn(),
|
||||
};
|
||||
|
||||
const { container } = render(
|
||||
|
|
|
|||
|
|
@ -1,13 +1,108 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Example component render component with \`externalValue\` 1`] = `
|
||||
.c9 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
exports[`Example component > render component with \`externalValue\` 1`] = `
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.c9 path {
|
||||
fill: currentColor;
|
||||
.c0 > div {
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.c0 pre {
|
||||
contain: content;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
.c0 .code-block-header {
|
||||
border-bottom: 0;
|
||||
padding-right: var(--spacing-sm);
|
||||
}.c1 {
|
||||
border: 1px solid var(--border-color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--code-block-bg-color);
|
||||
margin: 0 0 var(--spacing-sm);
|
||||
--md-pre-margin: 0;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: grid;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
display: grid;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--code-block-controls-font-size);
|
||||
font-family: var(--code-block-controls-font-family);
|
||||
background-color: var(--code-block-controls-bg-color);
|
||||
padding: var(--code-block-controls-padding);
|
||||
border-bottom: var(--code-block-controls-border);
|
||||
line-height: var(--line-height-lg);
|
||||
min-height: var(--control-height-base);
|
||||
grid-template-columns: 1fr auto;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: auto;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
grid-template-columns: 1fr;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
text-align: right;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xxs);
|
||||
-webkit-box-pack: end;
|
||||
-webkit-justify-content: end;
|
||||
-ms-flex-pack: end;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
--button-icon-size: 16px;
|
||||
--button-icon-padding: 3px;
|
||||
--button-backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.c8 {
|
||||
|
|
@ -89,6 +184,19 @@ exports[`Example component render component with \`externalValue\` 1`] = `
|
|||
border-width: var(--button-border-width-disabled);
|
||||
}
|
||||
|
||||
.c9 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c9 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
color: var(--code-block-tokens-default-color);
|
||||
}
|
||||
|
||||
.c11.c11 {
|
||||
overflow-x: auto;
|
||||
font-family: var(--code-block-font-family);
|
||||
|
|
@ -359,11 +467,7 @@ exports[`Example component render component with \`externalValue\` 1`] = `
|
|||
content: '\\ea0c';
|
||||
}
|
||||
|
||||
pre.c10 {
|
||||
color: var(--code-block-tokens-default-color);
|
||||
}
|
||||
|
||||
.c11 .token.comment {
|
||||
pre.c11 .token.comment {
|
||||
color: var(--code-block-tokens-comment-color);
|
||||
}
|
||||
|
||||
|
|
@ -487,115 +591,11 @@ pre.c10 {
|
|||
color: var(--code-block-tokens-function-color);
|
||||
}
|
||||
|
||||
.c5 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
display: grid;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--code-block-controls-font-size);
|
||||
font-family: var(--code-block-controls-font-family);
|
||||
background-color: var(--code-block-controls-bg-color);
|
||||
padding: var(--code-block-controls-padding);
|
||||
border-bottom: var(--code-block-controls-border);
|
||||
line-height: var(--line-height-lg);
|
||||
min-height: var(--control-height-base);
|
||||
grid-template-columns: 1fr auto;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: auto;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
grid-template-columns: 1fr;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
text-align: right;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xxs);
|
||||
-webkit-box-pack: end;
|
||||
-webkit-justify-content: end;
|
||||
-ms-flex-pack: end;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
--button-icon-size: 16px;
|
||||
--button-icon-padding: 3px;
|
||||
--button-backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: grid;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
border: 1px solid var(--border-color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--code-block-bg-color);
|
||||
margin: 0 0 var(--spacing-sm);
|
||||
--md-pre-margin: 0;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.c0 > div {
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.c0 pre {
|
||||
contain: content;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
.c0 .code-block-header {
|
||||
border-bottom: 0;
|
||||
padding-right: var(--spacing-sm);
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
|
|
@ -672,14 +672,89 @@ pre.c10 {
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`Example component render component without error when \`externalValue\` is pdf file 1`] = `
|
||||
.c9 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
exports[`Example component > render component without error when \`externalValue\` is pdf file 1`] = `
|
||||
.c0 {
|
||||
border: 1px solid var(--border-color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--code-block-bg-color);
|
||||
margin: 0 0 var(--spacing-sm);
|
||||
--md-pre-margin: 0;
|
||||
}
|
||||
|
||||
.c9 path {
|
||||
fill: currentColor;
|
||||
.c1 {
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.c1 .code-block-header {
|
||||
border-bottom: 0;
|
||||
padding-right: var(--spacing-sm);
|
||||
}.c2 {
|
||||
display: grid;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
display: grid;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--code-block-controls-font-size);
|
||||
font-family: var(--code-block-controls-font-family);
|
||||
background-color: var(--code-block-controls-bg-color);
|
||||
padding: var(--code-block-controls-padding);
|
||||
border-bottom: var(--code-block-controls-border);
|
||||
line-height: var(--line-height-lg);
|
||||
min-height: var(--control-height-base);
|
||||
grid-template-columns: 1fr auto;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: auto;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
grid-template-columns: 1fr;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
text-align: right;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xxs);
|
||||
-webkit-box-pack: end;
|
||||
-webkit-justify-content: end;
|
||||
-ms-flex-pack: end;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
--button-icon-size: 16px;
|
||||
--button-icon-padding: 3px;
|
||||
--button-backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.c8 {
|
||||
|
|
@ -761,6 +836,19 @@ exports[`Example component render component without error when \`externalValue\`
|
|||
border-width: var(--button-border-width-disabled);
|
||||
}
|
||||
|
||||
.c9 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c9 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
color: var(--code-block-tokens-default-color);
|
||||
}
|
||||
|
||||
.c11.c11 {
|
||||
overflow-x: auto;
|
||||
font-family: var(--code-block-font-family);
|
||||
|
|
@ -1031,11 +1119,7 @@ exports[`Example component render component without error when \`externalValue\`
|
|||
content: '\\ea0c';
|
||||
}
|
||||
|
||||
pre.c10 {
|
||||
color: var(--code-block-tokens-default-color);
|
||||
}
|
||||
|
||||
.c11 .token.comment {
|
||||
pre.c11 .token.comment {
|
||||
color: var(--code-block-tokens-comment-color);
|
||||
}
|
||||
|
||||
|
|
@ -1159,91 +1243,7 @@ pre.c10 {
|
|||
color: var(--code-block-tokens-function-color);
|
||||
}
|
||||
|
||||
.c5 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
display: grid;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--code-block-controls-font-size);
|
||||
font-family: var(--code-block-controls-font-family);
|
||||
background-color: var(--code-block-controls-bg-color);
|
||||
padding: var(--code-block-controls-padding);
|
||||
border-bottom: var(--code-block-controls-border);
|
||||
line-height: var(--line-height-lg);
|
||||
min-height: var(--control-height-base);
|
||||
grid-template-columns: 1fr auto;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: auto;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
grid-template-columns: 1fr;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
text-align: right;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xxs);
|
||||
-webkit-box-pack: end;
|
||||
-webkit-justify-content: end;
|
||||
-ms-flex-pack: end;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
--button-icon-size: 16px;
|
||||
--button-icon-padding: 3px;
|
||||
--button-backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: grid;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
border: 1px solid var(--border-color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--code-block-bg-color);
|
||||
margin: 0 0 var(--spacing-sm);
|
||||
--md-pre-margin: 0;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.c1 .code-block-header {
|
||||
border-bottom: 0;
|
||||
padding-right: var(--spacing-sm);
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@ function FieldDetailsComponent({
|
|||
|
||||
const rawDefault = field.in === 'header'; // having quotes around header field default values is confusing and inappropriate
|
||||
|
||||
const shouldHideDescription = schema.oneOfType || schema.rawSchema?.allOf?.[0];
|
||||
|
||||
const renderedExamples = useMemo(() => {
|
||||
if (example !== undefined || examples !== undefined) {
|
||||
if (examples !== undefined) {
|
||||
|
|
@ -95,7 +93,7 @@ function FieldDetailsComponent({
|
|||
{schema.isComplex && (
|
||||
<RecursiveLabel>{translate('openapi.complex', 'Complex')}</RecursiveLabel>
|
||||
)}
|
||||
{!shouldHideDescription && <StyledDescription source={description} />}
|
||||
<StyledDescription source={description} />
|
||||
<FieldDetail
|
||||
raw={rawDefault}
|
||||
type="default"
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import * as Jotai from 'jotai';
|
||||
|
||||
import { Extensions } from '../Extensions';
|
||||
import { normalizeOptions } from '../../../services';
|
||||
import { Extensions } from '../Extensions.js';
|
||||
import { normalizeOptions } from '../../../services/index.js';
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: jest.fn(),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtomValue: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('Components', () => {
|
||||
describe('Extensions', () => {
|
||||
it('Extensions label renders correctly', () => {
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockReturnValue(
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(
|
||||
normalizeOptions({
|
||||
showExtensions: true,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/react';
|
||||
|
||||
import { FieldDetail } from '../FieldDetail';
|
||||
import { FieldDetail } from '../FieldDetail.js';
|
||||
|
||||
describe('FieldDetail', () => {
|
||||
const wrapper = (label: string, value?: string) =>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,31 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import * as Jotai from 'jotai';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import type { OperationModel } from '../../../models';
|
||||
import type { OperationModel } from '../../../models/index.js';
|
||||
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { FieldDetails } from '../FieldDetails';
|
||||
import { getField } from '../../../models';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import { FieldDetails } from '../FieldDetails.js';
|
||||
import { getField } from '../../../models/index.js';
|
||||
import spec from './fixtures/fields.json';
|
||||
import { TestBrowserRouter } from '../../../testProviders.js';
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: jest.fn(),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtomValue: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('FieldDetails', () => {
|
||||
const options = normalizeOptions({});
|
||||
const parser = new OpenAPIParser(spec, undefined, options);
|
||||
const parser = new OpenAPIParser(
|
||||
spec as unknown as ConstructorParameters<typeof OpenAPIParser>[0],
|
||||
undefined,
|
||||
options,
|
||||
);
|
||||
const deps = { operation: { pointer: 'defaultPointer' } as OperationModel };
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockReturnValue(options);
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(options);
|
||||
|
||||
it('should render FieldDetails with enum', async () => {
|
||||
options.showAccessMode = true;
|
||||
(options as unknown as Record<string, unknown>).showAccessMode = true;
|
||||
const field = getField(
|
||||
parser,
|
||||
{
|
||||
|
|
@ -39,7 +43,7 @@ describe('FieldDetails', () => {
|
|||
options,
|
||||
deps,
|
||||
);
|
||||
render(<FieldDetails field={field} />, { wrapper: BrowserRouter });
|
||||
render(<FieldDetails field={field} />, { wrapper: TestBrowserRouter });
|
||||
|
||||
expect(screen.getByText('Enum')).toBeInTheDocument();
|
||||
expect(screen.getByText('"foo"')).toBeInTheDocument();
|
||||
|
|
@ -64,7 +68,7 @@ describe('FieldDetails', () => {
|
|||
options,
|
||||
deps,
|
||||
);
|
||||
render(<FieldDetails field={field} />, { wrapper: BrowserRouter });
|
||||
render(<FieldDetails field={field} />, { wrapper: TestBrowserRouter });
|
||||
|
||||
expect(screen.getByText('Enum Value')).toBeInTheDocument();
|
||||
expect(screen.getByText('foo')).toBeInTheDocument();
|
||||
|
|
@ -74,9 +78,9 @@ describe('FieldDetails', () => {
|
|||
});
|
||||
|
||||
it('should render FieldDetails with enum and x-enumDescription with hiding value', async () => {
|
||||
jest
|
||||
.spyOn(Jotai, 'useAtomValue')
|
||||
.mockReturnValue(normalizeOptions({ maxDisplayedEnumValues: 1 }));
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(
|
||||
normalizeOptions({ maxDisplayedEnumValues: 1 }),
|
||||
);
|
||||
const field = getField(
|
||||
parser,
|
||||
{
|
||||
|
|
@ -94,7 +98,7 @@ describe('FieldDetails', () => {
|
|||
options,
|
||||
deps,
|
||||
);
|
||||
render(<FieldDetails field={field} />, { wrapper: BrowserRouter });
|
||||
render(<FieldDetails field={field} />, { wrapper: TestBrowserRouter });
|
||||
|
||||
expect(screen.getByText('Value')).toBeInTheDocument();
|
||||
expect(screen.getByText('foo')).toBeInTheDocument();
|
||||
|
|
@ -120,7 +124,7 @@ describe('FieldDetails', () => {
|
|||
deps,
|
||||
);
|
||||
|
||||
render(<FieldDetails field={field} />, { wrapper: BrowserRouter });
|
||||
render(<FieldDetails field={field} />, { wrapper: TestBrowserRouter });
|
||||
|
||||
const descriptions = screen.queryAllByText('This is a test anyOf schema with description');
|
||||
expect(descriptions).toHaveLength(0);
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
import { render, fireEvent, screen, act } from '@testing-library/react';
|
||||
import * as Jotai from 'jotai';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import type { OperationModel } from '../../../models';
|
||||
import type { OperationModel } from '../../../models/index.js';
|
||||
|
||||
import { PropertyDetails } from '../PropertyDetails';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { getField } from '../../../models';
|
||||
import { PropertyDetails } from '../PropertyDetails.js';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import { getField } from '../../../models/index.js';
|
||||
import spec from './fixtures/fields.json';
|
||||
import { TestBrowserRouter } from '../../../testProviders.js';
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: jest.fn(),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtomValue: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('Components', () => {
|
||||
const options = normalizeOptions({});
|
||||
const parser = new OpenAPIParser(spec, undefined, options);
|
||||
const parser = new OpenAPIParser(
|
||||
spec as unknown as ConstructorParameters<typeof OpenAPIParser>[0],
|
||||
undefined,
|
||||
options,
|
||||
);
|
||||
const deps = { operation: { pointer: 'defaultPointer' } as OperationModel };
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockReturnValue(options);
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(options);
|
||||
|
||||
it('should render FieldDetails without duplicate content', () => {
|
||||
const field = getField(
|
||||
|
|
@ -31,7 +35,7 @@ describe('Components', () => {
|
|||
deps,
|
||||
);
|
||||
render(<PropertyDetails field={field} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
const occurrences = screen.getAllByText('any');
|
||||
|
||||
|
|
@ -49,23 +53,20 @@ describe('Components', () => {
|
|||
deps,
|
||||
);
|
||||
|
||||
const { getAllByText, getByRole, queryByText, getByText } = render(
|
||||
<PropertyDetails field={field} />,
|
||||
{
|
||||
wrapper: BrowserRouter,
|
||||
},
|
||||
);
|
||||
const { getAllByText, queryByText, getByText } = render(<PropertyDetails field={field} />, {
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(queryByText('Show 2 properties')).not.toBeNull();
|
||||
act(() => {
|
||||
queryByText('Show 2 properties').click();
|
||||
queryByText('Show 2 properties')?.click();
|
||||
});
|
||||
expect(getByText('status')).toBeInTheDocument();
|
||||
expect(getByText('name')).toBeInTheDocument();
|
||||
expect(getAllByText('string').length).toBe(2);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByRole('button'));
|
||||
fireEvent.click(getByText('-'));
|
||||
});
|
||||
|
||||
expect(getAllByText('Show 2 properties').length).toBe(1);
|
||||
|
|
@ -84,7 +85,7 @@ describe('Components', () => {
|
|||
);
|
||||
|
||||
const { getByText } = render(<PropertyDetails field={field} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
expect(getByText('Array of strings')).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -101,17 +102,14 @@ describe('Components', () => {
|
|||
deps,
|
||||
);
|
||||
|
||||
const { getByText, getByRole, container } = render(
|
||||
<PropertyDetails field={field} level={2} />,
|
||||
{
|
||||
wrapper: BrowserRouter,
|
||||
},
|
||||
);
|
||||
const { getByText, container } = render(<PropertyDetails field={field} level={2} />, {
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
expect(getByText('Array of objects')).toBeInTheDocument();
|
||||
expect(getByText('Show 2 array properties')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByRole('button'));
|
||||
fireEvent.click(getByText('Show 2 array properties'));
|
||||
});
|
||||
expect(getByText('status')).toBeInTheDocument();
|
||||
expect(getByText('name')).toBeInTheDocument();
|
||||
|
|
@ -131,16 +129,13 @@ describe('Components', () => {
|
|||
deps,
|
||||
);
|
||||
|
||||
const { getByText, getByRole, container } = render(
|
||||
<PropertyDetails field={field} level={2} />,
|
||||
{
|
||||
wrapper: BrowserRouter,
|
||||
},
|
||||
);
|
||||
const { getByText, container } = render(<PropertyDetails field={field} level={2} />, {
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
expect(getByText('Show 2 properties')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByRole('button'));
|
||||
fireEvent.click(getByText('Show 2 properties'));
|
||||
});
|
||||
expect(getByText('status')).toBeInTheDocument();
|
||||
expect(getByText('name')).toBeInTheDocument();
|
||||
|
|
@ -160,16 +155,13 @@ describe('Components', () => {
|
|||
deps,
|
||||
);
|
||||
|
||||
const { getByText, getByRole, container } = render(
|
||||
<PropertyDetails field={field} level={2} />,
|
||||
{
|
||||
wrapper: BrowserRouter,
|
||||
},
|
||||
);
|
||||
const { getByText, container } = render(<PropertyDetails field={field} level={2} />, {
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
expect(getByText('Show 2 properties')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByRole('button'));
|
||||
fireEvent.click(getByText('Show 2 properties'));
|
||||
});
|
||||
expect(getByText('status')).toBeInTheDocument();
|
||||
expect(getByText('name')).toBeInTheDocument();
|
||||
|
|
@ -188,7 +180,7 @@ describe('Components', () => {
|
|||
deps,
|
||||
);
|
||||
const { container } = render(<PropertyDetails field={field} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
expect(container).toHaveTextContent(/read-only/);
|
||||
});
|
||||
|
|
@ -203,7 +195,7 @@ describe('Components', () => {
|
|||
deps,
|
||||
);
|
||||
const { container } = render(<PropertyDetails field={field} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(container).toHaveTextContent(/write-only/);
|
||||
|
|
@ -211,7 +203,7 @@ describe('Components', () => {
|
|||
|
||||
describe('PropertyDetails', () => {
|
||||
it('PropertyDetails with read-only should renders correctly', () => {
|
||||
options.showAccessMode = true;
|
||||
(options as unknown as Record<string, unknown>).showAccessMode = true;
|
||||
const field = getField(
|
||||
parser,
|
||||
{
|
||||
|
|
@ -222,13 +214,13 @@ describe('Components', () => {
|
|||
deps,
|
||||
);
|
||||
const { container } = render(<PropertyDetails field={field} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
expect(container).toHaveTextContent(/read-only/);
|
||||
});
|
||||
|
||||
it('Field with write-only should renders correctly', () => {
|
||||
options.showAccessMode = true;
|
||||
(options as unknown as Record<string, unknown>).showAccessMode = true;
|
||||
const field = getField(
|
||||
parser,
|
||||
{
|
||||
|
|
@ -239,14 +231,14 @@ describe('Components', () => {
|
|||
deps,
|
||||
);
|
||||
const { container } = render(<PropertyDetails field={field} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(container).toHaveTextContent(/write-only/);
|
||||
});
|
||||
|
||||
it('Fields with read-only and write-only in content should renders correctly', async () => {
|
||||
options.showAccessMode = true;
|
||||
(options as unknown as Record<string, unknown>).showAccessMode = true;
|
||||
const field = getField(
|
||||
parser,
|
||||
{
|
||||
|
|
@ -265,9 +257,9 @@ describe('Components', () => {
|
|||
deps,
|
||||
);
|
||||
const { container } = render(<PropertyDetails field={field} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
fireEvent.click(await screen.findByRole('button'));
|
||||
fireEvent.click(await screen.getByText('Show 2 properties'));
|
||||
expect(container).toHaveTextContent(/read-only/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { cycleColorsByLevel } from '../cycleColorsByLevel';
|
||||
import { cycleColorsByLevel } from '../cycleColorsByLevel.js';
|
||||
|
||||
describe('fieldColorByLevel', () => {
|
||||
it('returns the correct color for a given level', () => {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"version": "1.0",
|
||||
"title": "Foo"
|
||||
},
|
||||
"paths": {},
|
||||
"components": {
|
||||
"primitiveSchema": {
|
||||
"schema": {
|
||||
|
|
|
|||
|
|
@ -127,7 +127,11 @@ export const StoreProvider = memo(
|
|||
store.set(globalStoreAtom, globalStoreValue);
|
||||
|
||||
store.sub(appStore, () => undefined); // force mount so init data is read from storage
|
||||
store.set(layoutAtom, globalStoreValue.options?.layout || LayoutVariant.THREE_PANEL); // set layout atom to update appStore atom with correct layout value
|
||||
// Set layout from withState (portal) or options (local), with correct priority
|
||||
store.set(
|
||||
layoutAtom,
|
||||
withState?.layout || globalStoreValue.options?.layout || LayoutVariant.THREE_PANEL,
|
||||
); // set layout atom to update appStore atom with correct layout value
|
||||
store.set(routerAtom, router || 'hash');
|
||||
store.set(disableTelemetryAtom, disableTelemetry || false);
|
||||
store.set(telemetryAtom, { data: undefined, status: 'initializing' });
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { render } from '@testing-library/react';
|
||||
|
||||
import type { OpenAPIDefinition } from '../../../types';
|
||||
import type { OpenAPIDefinition } from '../../../types/index.js';
|
||||
|
||||
import { StoreProvider } from '../Providers';
|
||||
import { StoreProvider } from '../Providers.js';
|
||||
|
||||
describe('ProStoreProvider', () => {
|
||||
test('StoreProvider should run onInit hook', async () => {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,42 @@
|
|||
import type { Mock } from 'vitest';
|
||||
import { parseYaml } from '@redocly/openapi-core';
|
||||
import { readFileSync } from 'fs';
|
||||
import { render } from '@testing-library/react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { useSetAtom } from 'jotai';
|
||||
|
||||
import type { ReactNode, ComponentType } from 'react';
|
||||
import type { RedocConfig } from '@redocly/config';
|
||||
import type { OpenAPIDefinition } from '../../../types';
|
||||
import type { OpenAPIDefinition } from '../../../types/index.js';
|
||||
import { useTelemetry } from '../../../hooks/index.js';
|
||||
|
||||
import { useTelemetry } from '../../../hooks';
|
||||
vi.mock('../../../hooks/useTelemetry');
|
||||
|
||||
jest.mock('../../../hooks/useTelemetry');
|
||||
vi.mock('@redocly/theme/core/openapi', async () => {
|
||||
const actual = await vi.importActual('@redocly/theme/core/openapi');
|
||||
return {
|
||||
...actual,
|
||||
getOperationColor: vi.fn(() => '#007bff'),
|
||||
SearchSessionProvider: ({ children }: { children: ReactNode }) => {
|
||||
const mockContextValue = {
|
||||
searchSessionId: 'test-session-id',
|
||||
refreshSearchSessionId: vi.fn(),
|
||||
};
|
||||
const Provider = (actual as { SearchSessionContext: { Provider: ComponentType<any> } })
|
||||
.SearchSessionContext.Provider;
|
||||
return <Provider value={mockContextValue}>{children}</Provider>;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import { RedoclyOpenAPIDocs } from '../RedoclyOpenAPIDocs';
|
||||
vi.mock('jotai', async () => {
|
||||
const originalJotai = await vi.importActual('jotai');
|
||||
return {
|
||||
...(originalJotai as Record<string, unknown>),
|
||||
useSetAtom: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import { RedoclyOpenAPIDocs } from '../RedoclyOpenAPIDocs.js';
|
||||
|
||||
const contents = readFileSync('./playground/openapi/petstore.yaml', 'utf8');
|
||||
const definition = parseYaml(contents) as OpenAPIDefinition;
|
||||
|
|
@ -17,22 +44,25 @@ const definition = parseYaml(contents) as OpenAPIDefinition;
|
|||
describe('RedoclyOpenAPIDocs', () => {
|
||||
const options = {} as RedocConfig;
|
||||
const store = { definition, options };
|
||||
window.scrollTo = jest.fn();
|
||||
window.scrollTo = vi.fn();
|
||||
|
||||
const mockTelemetrySend = jest.fn() as Mock;
|
||||
const mockTelemetrySend = vi.fn();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useTelemetry as jest.Mock).mockImplementation(() => ({
|
||||
sendTestEvent: mockTelemetrySend,
|
||||
vi.clearAllMocks();
|
||||
(useTelemetry as Mock).mockImplementation(() => ({
|
||||
sendViewedMessage: mockTelemetrySend,
|
||||
}));
|
||||
(useSetAtom as Mock).mockReturnValue(vi.fn());
|
||||
});
|
||||
|
||||
describe('onLoaded callback', () => {
|
||||
it('should call callback on mount', () => {
|
||||
const onLoaded = jest.fn();
|
||||
it('should call callback on mount', async () => {
|
||||
const onLoaded = vi.fn();
|
||||
render(<RedoclyOpenAPIDocs store={store} onLoaded={onLoaded} />);
|
||||
|
||||
expect(onLoaded).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => {
|
||||
expect(onLoaded).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { RenderHook } from '../RenderHook';
|
||||
import { RenderHook } from '../RenderHook.js';
|
||||
|
||||
describe('RenderHook', () => {
|
||||
it('should render correctly', () => {
|
||||
|
|
|
|||
|
|
@ -18,17 +18,24 @@ function ExampleComponent({ mediaType, mediaContent, renderSample }: ExampleProp
|
|||
const telemetry = useTelemetry();
|
||||
const pointer = mediaType.operation.pointer;
|
||||
const [operation, setOperation] = useAtom(operationStore(pointer));
|
||||
const examples = mediaType.examples || {};
|
||||
const examples = mediaType.examples || mediaType.formExamples || {};
|
||||
const examplesKeys = Object.keys(examples);
|
||||
const setActivateExampleName = useActivateExample(mediaContent);
|
||||
|
||||
const { exampleKey } = useExampleKey(mediaType.operation, examples);
|
||||
|
||||
const { exampleKey: activeExampleKey } = useExampleKey(mediaType.operation, examples);
|
||||
const exampleKey =
|
||||
examplesKeys[
|
||||
operation.activeOneOf[mediaType?.examplesPointer || mediaType.operation.pointer]
|
||||
] || activeExampleKey;
|
||||
if (!examplesKeys.length || examplesKeys.length === 1) {
|
||||
return renderSample();
|
||||
}
|
||||
|
||||
const handleExampleChange = (key: string) => {
|
||||
const pointer =
|
||||
mediaType.examplesPointer && mediaType.examplesPointer.startsWith(mediaType.operation.pointer)
|
||||
? mediaType.examplesPointer
|
||||
: mediaType.operation.pointer;
|
||||
telemetry.sendExamplesSwitcherClickedMessage({
|
||||
exampleNumber: examplesKeys.indexOf(key),
|
||||
totalExamples: examplesKeys.length,
|
||||
|
|
@ -36,7 +43,7 @@ function ExampleComponent({ mediaType, mediaContent, renderSample }: ExampleProp
|
|||
setOperation({
|
||||
...operation,
|
||||
activeExampleName: key,
|
||||
activeOneOf: { [pointer]: examplesKeys.indexOf(key) },
|
||||
activeOneOf: { ...operation.activeOneOf, [pointer]: examplesKeys.indexOf(key) },
|
||||
requestValues: { body: null },
|
||||
});
|
||||
setActivateExampleName(key);
|
||||
|
|
|
|||
567
src/components/RequestSamples/__tests__/Example.test.tsx
Normal file
567
src/components/RequestSamples/__tests__/Example.test.tsx
Normal file
|
|
@ -0,0 +1,567 @@
|
|||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import type { MediaContentModel, MediaTypeModel } from '../../../models/index.js';
|
||||
import type { ExampleModelsMap } from '../../Samples/index.js';
|
||||
|
||||
import { Example } from '../Example.js';
|
||||
import { withTestProviders } from '../../../testProviders.js';
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock('../../Samples', () => ({
|
||||
ExampleSwitch: ({
|
||||
examples,
|
||||
exampleKey,
|
||||
onChange,
|
||||
}: {
|
||||
examples: ExampleModelsMap;
|
||||
exampleKey?: string;
|
||||
onChange: (key: string) => void;
|
||||
}) => (
|
||||
<div data-testid="example-switch">
|
||||
<select
|
||||
data-testid="example-selector"
|
||||
value={exampleKey}
|
||||
onChange={(e): void => onChange(e.target.value)}
|
||||
>
|
||||
{Object.keys(examples).map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{key}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
),
|
||||
useExampleKey: vi.fn(() => ({ exampleKey: 'bee' })),
|
||||
}));
|
||||
|
||||
import { useTelemetry, useActivateExample } from '../../../hooks/index.js';
|
||||
import { useExampleKey } from '../../Samples/index.js';
|
||||
|
||||
vi.mock('../../../hooks', async () => ({
|
||||
...(await vi.importActual('../../../hooks')),
|
||||
useActivateExample: vi.fn(() => vi.fn()),
|
||||
useTelemetry: vi.fn(() => ({
|
||||
sendExamplesSwitcherClickedMessage: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../../../jotai/operation', () => ({
|
||||
operationStore: vi.fn(() => ({
|
||||
toString: (): string => 'operationStore',
|
||||
})),
|
||||
}));
|
||||
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
const mockUseTelemetry = vi.mocked(useTelemetry);
|
||||
const mockUseActivateExample = vi.mocked(useActivateExample);
|
||||
const mockUseExampleKey = vi.mocked(useExampleKey);
|
||||
|
||||
// Mock jotai at module level
|
||||
vi.mock('jotai', async () => {
|
||||
const actual = await vi.importActual('jotai');
|
||||
return {
|
||||
...actual,
|
||||
useAtom: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockUseAtom = vi.mocked(useAtom);
|
||||
|
||||
describe('Example component', () => {
|
||||
const mockMediaType: MediaTypeModel = {
|
||||
name: 'application/json',
|
||||
examples: {
|
||||
bee: {
|
||||
mime: 'application/json',
|
||||
value: {
|
||||
petType: 'bee',
|
||||
honeyPerDay: 3.14,
|
||||
},
|
||||
rawValue: {
|
||||
petType: 'bee',
|
||||
honeyPerDay: 3.14,
|
||||
},
|
||||
},
|
||||
},
|
||||
schema: undefined,
|
||||
isRequestType: true,
|
||||
onlyRequiredInSamples: false,
|
||||
operation: {
|
||||
pointer: '/paths/~1pet/post',
|
||||
} as MediaTypeModel['operation'],
|
||||
};
|
||||
|
||||
const mockMediaContent = {
|
||||
mediaTypes: [mockMediaType],
|
||||
} as unknown as MediaContentModel;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
mockUseAtom.mockReturnValue([
|
||||
{
|
||||
activeExampleName: 'bee',
|
||||
activeOneOf: {},
|
||||
requestValues: { body: null },
|
||||
},
|
||||
vi.fn(),
|
||||
] as unknown as ReturnType<typeof useAtom>);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should render component with single example without ExampleSwitch', () => {
|
||||
const renderSample = vi.fn(() => <div data-testid="sample">Sample</div>);
|
||||
|
||||
render(
|
||||
withTestProviders(
|
||||
<Example
|
||||
mediaType={mockMediaType}
|
||||
mediaContent={mockMediaContent}
|
||||
renderSample={renderSample}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(renderSample).toHaveBeenCalledWith();
|
||||
expect(screen.queryByTestId('example-switch')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('sample')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render component without examples', () => {
|
||||
const noExamplesMediaType: MediaTypeModel = {
|
||||
...mockMediaType,
|
||||
examples: undefined,
|
||||
formExamples: undefined,
|
||||
};
|
||||
|
||||
const renderSample = vi.fn(() => <div data-testid="sample">Sample</div>);
|
||||
|
||||
render(
|
||||
withTestProviders(
|
||||
<Example
|
||||
mediaType={noExamplesMediaType}
|
||||
mediaContent={mockMediaContent}
|
||||
renderSample={renderSample}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(renderSample).toHaveBeenCalledWith();
|
||||
expect(screen.queryByTestId('example-switch')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render component with multiple examples and ExampleSwitch', () => {
|
||||
const multipleExamplesMediaType: MediaTypeModel = {
|
||||
...mockMediaType,
|
||||
examples: {
|
||||
bee: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'bee' },
|
||||
rawValue: { petType: 'bee' },
|
||||
},
|
||||
cat: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'cat' },
|
||||
rawValue: { petType: 'cat' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderSample = vi.fn(() => <div data-testid="sample">Sample</div>);
|
||||
|
||||
render(
|
||||
withTestProviders(
|
||||
<Example
|
||||
mediaType={multipleExamplesMediaType}
|
||||
mediaContent={mockMediaContent}
|
||||
renderSample={renderSample}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('example-switch')).toBeInTheDocument();
|
||||
expect(renderSample).toHaveBeenCalledWith('bee');
|
||||
});
|
||||
|
||||
it('should handle example change correctly', async () => {
|
||||
const user = userEvent.setup();
|
||||
const setOperation = vi.fn();
|
||||
const sendTelemetry = vi.fn();
|
||||
const setActivateExampleName = vi.fn();
|
||||
|
||||
mockUseAtom.mockReturnValue([
|
||||
{
|
||||
activeExampleName: 'bee',
|
||||
activeOneOf: {},
|
||||
requestValues: { body: null },
|
||||
},
|
||||
setOperation,
|
||||
] as unknown as ReturnType<typeof useAtom>);
|
||||
|
||||
mockUseTelemetry.mockReturnValue({
|
||||
sendExamplesSwitcherClickedMessage: sendTelemetry,
|
||||
} as unknown as ReturnType<typeof useTelemetry>);
|
||||
mockUseActivateExample.mockReturnValue(setActivateExampleName);
|
||||
|
||||
const multipleExamplesMediaType: MediaTypeModel = {
|
||||
...mockMediaType,
|
||||
examples: {
|
||||
bee: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'bee' },
|
||||
rawValue: { petType: 'bee' },
|
||||
},
|
||||
cat: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'cat' },
|
||||
rawValue: { petType: 'cat' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderSample = vi.fn(() => <div data-testid="sample">Sample</div>);
|
||||
|
||||
render(
|
||||
withTestProviders(
|
||||
<Example
|
||||
mediaType={multipleExamplesMediaType}
|
||||
mediaContent={mockMediaContent}
|
||||
renderSample={renderSample}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
const selector = screen.getByTestId('example-selector');
|
||||
await user.selectOptions(selector, 'cat');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendTelemetry).toHaveBeenCalledWith({
|
||||
exampleNumber: 1,
|
||||
totalExamples: 2,
|
||||
});
|
||||
expect(setOperation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
activeExampleName: 'cat',
|
||||
activeOneOf: expect.any(Object),
|
||||
requestValues: { body: null },
|
||||
}),
|
||||
);
|
||||
expect(setActivateExampleName).toHaveBeenCalledWith('cat');
|
||||
});
|
||||
});
|
||||
|
||||
it('should use formExamples when examples is undefined', () => {
|
||||
const formExamplesMediaType: MediaTypeModel = {
|
||||
...mockMediaType,
|
||||
examples: undefined,
|
||||
formExamples: {
|
||||
example1: {
|
||||
mime: 'application/x-www-form-urlencoded',
|
||||
value: { field: 'value1' },
|
||||
rawValue: { field: 'value1' },
|
||||
},
|
||||
example2: {
|
||||
mime: 'application/x-www-form-urlencoded',
|
||||
value: { field: 'value2' },
|
||||
rawValue: { field: 'value2' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderSample = vi.fn(() => <div data-testid="sample">Sample</div>);
|
||||
|
||||
render(
|
||||
withTestProviders(
|
||||
<Example
|
||||
mediaType={formExamplesMediaType}
|
||||
mediaContent={mockMediaContent}
|
||||
renderSample={renderSample}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('example-switch')).toBeInTheDocument();
|
||||
expect(renderSample).toHaveBeenCalledWith('bee');
|
||||
});
|
||||
|
||||
it('should memoize the component correctly', () => {
|
||||
const multipleExamplesMediaType: MediaTypeModel = {
|
||||
...mockMediaType,
|
||||
examples: {
|
||||
bee: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'bee' },
|
||||
rawValue: { petType: 'bee' },
|
||||
},
|
||||
cat: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'cat' },
|
||||
rawValue: { petType: 'cat' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderSample = vi.fn(() => <div data-testid="sample">Sample</div>);
|
||||
|
||||
const { rerender } = render(
|
||||
withTestProviders(
|
||||
<Example
|
||||
mediaType={multipleExamplesMediaType}
|
||||
mediaContent={mockMediaContent}
|
||||
renderSample={renderSample}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
const initialCallCount = renderSample.mock.calls.length;
|
||||
|
||||
// Rerender with the same props
|
||||
rerender(
|
||||
withTestProviders(
|
||||
<Example
|
||||
mediaType={multipleExamplesMediaType}
|
||||
mediaContent={mockMediaContent}
|
||||
renderSample={renderSample}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
// The renderSample should not be called again due to memoization
|
||||
expect(renderSample.mock.calls.length).toBe(initialCallCount);
|
||||
});
|
||||
|
||||
it('should use examplesPointer when provided for activeOneOf', () => {
|
||||
const customPointer = '/paths/~1pet/post/requestBody/content/application~1json';
|
||||
const multipleExamplesMediaType: MediaTypeModel = {
|
||||
...mockMediaType,
|
||||
examplesPointer: customPointer,
|
||||
examples: {
|
||||
bee: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'bee' },
|
||||
rawValue: { petType: 'bee' },
|
||||
},
|
||||
cat: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'cat' },
|
||||
rawValue: { petType: 'cat' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockUseAtom.mockReturnValue([
|
||||
{
|
||||
activeExampleName: 'cat',
|
||||
activeOneOf: { [customPointer]: 1 },
|
||||
requestValues: { body: null },
|
||||
},
|
||||
vi.fn(),
|
||||
] as unknown as ReturnType<typeof useAtom>);
|
||||
|
||||
const renderSample = vi.fn(() => <div data-testid="sample">Sample</div>);
|
||||
|
||||
render(
|
||||
withTestProviders(
|
||||
<Example
|
||||
mediaType={multipleExamplesMediaType}
|
||||
mediaContent={mockMediaContent}
|
||||
renderSample={renderSample}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('example-switch')).toBeInTheDocument();
|
||||
// Should render with 'cat' example since activeOneOf[customPointer] = 1
|
||||
expect(renderSample).toHaveBeenCalledWith('cat');
|
||||
});
|
||||
|
||||
it('should use examplesPointer in handleExampleChange when it starts with operation pointer', async () => {
|
||||
const user = userEvent.setup();
|
||||
const setOperation = vi.fn();
|
||||
const setActivateExampleName = vi.fn();
|
||||
const sendTelemetry = vi.fn();
|
||||
|
||||
const operationPointer = '/paths/~1pet/post';
|
||||
const examplesPointer = '/paths/~1pet/post/requestBody/content/application~1json';
|
||||
|
||||
const multipleExamplesMediaType: MediaTypeModel = {
|
||||
...mockMediaType,
|
||||
operation: {
|
||||
...mockMediaType.operation,
|
||||
pointer: operationPointer,
|
||||
} as MediaTypeModel['operation'],
|
||||
examplesPointer,
|
||||
examples: {
|
||||
bee: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'bee' },
|
||||
rawValue: { petType: 'bee' },
|
||||
},
|
||||
cat: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'cat' },
|
||||
rawValue: { petType: 'cat' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockUseAtom.mockReturnValue([
|
||||
{
|
||||
activeExampleName: 'bee',
|
||||
activeOneOf: {},
|
||||
requestValues: { body: null },
|
||||
},
|
||||
setOperation,
|
||||
] as unknown as ReturnType<typeof useAtom>);
|
||||
|
||||
mockUseTelemetry.mockReturnValue({
|
||||
sendExamplesSwitcherClickedMessage: sendTelemetry,
|
||||
} as unknown as ReturnType<typeof useTelemetry>);
|
||||
mockUseActivateExample.mockReturnValue(setActivateExampleName);
|
||||
|
||||
const renderSample = vi.fn(() => <div data-testid="sample">Sample</div>);
|
||||
|
||||
render(
|
||||
withTestProviders(
|
||||
<Example
|
||||
mediaType={multipleExamplesMediaType}
|
||||
mediaContent={mockMediaContent}
|
||||
renderSample={renderSample}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
const selector = screen.getByTestId('example-selector');
|
||||
await user.selectOptions(selector, 'cat');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setOperation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
activeExampleName: 'cat',
|
||||
activeOneOf: { [examplesPointer]: 1 },
|
||||
requestValues: { body: null },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use operation pointer in handleExampleChange when examplesPointer does not start with operation pointer', async () => {
|
||||
const user = userEvent.setup();
|
||||
const setOperation = vi.fn();
|
||||
const setActivateExampleName = vi.fn();
|
||||
const sendTelemetry = vi.fn();
|
||||
|
||||
const operationPointer = '/paths/~1pet/post';
|
||||
const examplesPointer = '/different/pointer';
|
||||
|
||||
const multipleExamplesMediaType: MediaTypeModel = {
|
||||
...mockMediaType,
|
||||
operation: {
|
||||
...mockMediaType.operation,
|
||||
pointer: operationPointer,
|
||||
} as MediaTypeModel['operation'],
|
||||
examplesPointer,
|
||||
examples: {
|
||||
bee: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'bee' },
|
||||
rawValue: { petType: 'bee' },
|
||||
},
|
||||
cat: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'cat' },
|
||||
rawValue: { petType: 'cat' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockUseAtom.mockReturnValue([
|
||||
{
|
||||
activeExampleName: 'bee',
|
||||
activeOneOf: {},
|
||||
requestValues: { body: null },
|
||||
},
|
||||
setOperation,
|
||||
] as unknown as ReturnType<typeof useAtom>);
|
||||
|
||||
mockUseTelemetry.mockReturnValue({
|
||||
sendExamplesSwitcherClickedMessage: sendTelemetry,
|
||||
} as unknown as ReturnType<typeof useTelemetry>);
|
||||
mockUseActivateExample.mockReturnValue(setActivateExampleName);
|
||||
|
||||
const renderSample = vi.fn(() => <div data-testid="sample">Sample</div>);
|
||||
|
||||
render(
|
||||
withTestProviders(
|
||||
<Example
|
||||
mediaType={multipleExamplesMediaType}
|
||||
mediaContent={mockMediaContent}
|
||||
renderSample={renderSample}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
const selector = screen.getByTestId('example-selector');
|
||||
await user.selectOptions(selector, 'cat');
|
||||
|
||||
await waitFor(() => {
|
||||
// Should use operation pointer instead of examplesPointer
|
||||
expect(setOperation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
activeExampleName: 'cat',
|
||||
activeOneOf: { [operationPointer]: 1 },
|
||||
requestValues: { body: null },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fallback to activeExampleKey from useExampleKey when activeOneOf is empty', () => {
|
||||
mockUseExampleKey.mockReturnValue({ exampleKey: 'cat' });
|
||||
|
||||
const multipleExamplesMediaType: MediaTypeModel = {
|
||||
...mockMediaType,
|
||||
examples: {
|
||||
bee: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'bee' },
|
||||
rawValue: { petType: 'bee' },
|
||||
},
|
||||
cat: {
|
||||
mime: 'application/json',
|
||||
value: { petType: 'cat' },
|
||||
rawValue: { petType: 'cat' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockUseAtom.mockReturnValue([
|
||||
{
|
||||
activeExampleName: undefined,
|
||||
activeOneOf: {},
|
||||
requestValues: { body: null },
|
||||
},
|
||||
vi.fn(),
|
||||
] as unknown as ReturnType<typeof useAtom>);
|
||||
|
||||
const renderSample = vi.fn(() => <div data-testid="sample">Sample</div>);
|
||||
|
||||
render(
|
||||
withTestProviders(
|
||||
<Example
|
||||
mediaType={multipleExamplesMediaType}
|
||||
mediaContent={mockMediaContent}
|
||||
renderSample={renderSample}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
// Should use 'cat' from useExampleKey
|
||||
expect(renderSample).toHaveBeenCalledWith('cat');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,43 +1,55 @@
|
|||
import { render, waitFor } from '@testing-library/react';
|
||||
import * as Jotai from 'jotai/index';
|
||||
|
||||
import type { OpenAPIDefinition } from '../../../types';
|
||||
import type { OperationModel } from '../../../models';
|
||||
import type { OpenAPIDefinition } from '../../../types/index.js';
|
||||
import type { OperationModel } from '../../../models/index.js';
|
||||
|
||||
import { RequestSamples } from '../RequestSamples';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { getMediaContent, getOperation } from '../../../models';
|
||||
import { withTestProviders } from '../../../testProviders';
|
||||
import * as useCodeSamplesModule from '../useCodeSamples.js';
|
||||
|
||||
import { RequestSamples } from '../RequestSamples.js';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import { getMediaContent, getOperation } from '../../../models/index.js';
|
||||
import { withTestProviders } from '../../../testProviders.js';
|
||||
import petStore from './fixtures/petstore.json';
|
||||
import type { ExtendedOpenAPIOperation } from '../../../services/index.js';
|
||||
|
||||
import definition from './fixtures/operationDefinition.json';
|
||||
import {
|
||||
languageAtom,
|
||||
} from '../../../jotai/app';
|
||||
import { globalStoreAtom } from '../../../jotai/store';
|
||||
} from '../../../jotai/app.js';
|
||||
import { globalStoreAtom } from '../../../jotai/store.js';
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: jest.fn((atom) => {
|
||||
if (atom === languageAtom) {
|
||||
return {
|
||||
activeLanguage: 'python',
|
||||
languages: ['python', 'javascript'],
|
||||
};
|
||||
}
|
||||
return jest.requireActual('jotai').useAtomValue(atom);
|
||||
}),
|
||||
useAtom: jest.fn((atom) => {
|
||||
if (atom === languageAtom) {
|
||||
return [
|
||||
{
|
||||
vi.mock('jotai', async () => {
|
||||
const actual = await vi.importActual<typeof Jotai>('jotai');
|
||||
return {
|
||||
...actual,
|
||||
useAtomValue: vi.fn((atom) => {
|
||||
if (atom === languageAtom) {
|
||||
return {
|
||||
activeLanguage: 'python',
|
||||
languages: ['python', 'javascript'],
|
||||
},
|
||||
jest.fn(),
|
||||
];
|
||||
}
|
||||
return jest.requireActual('jotai').useAtom(atom);
|
||||
}),
|
||||
};
|
||||
}
|
||||
return actual.useAtomValue(atom);
|
||||
}),
|
||||
useAtom: vi.fn((atom) => {
|
||||
if (atom === languageAtom) {
|
||||
return [
|
||||
{
|
||||
activeLanguage: 'python',
|
||||
languages: ['python', 'javascript'],
|
||||
},
|
||||
vi.fn(),
|
||||
];
|
||||
}
|
||||
return actual.useAtom(atom);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@redocly/theme/core/openapi', async () => ({
|
||||
...(await vi.importActual('@redocly/theme/core/openapi')),
|
||||
getOperationColor: vi.fn(() => 'blue'),
|
||||
}));
|
||||
|
||||
describe('Components', () => {
|
||||
|
|
@ -45,7 +57,7 @@ describe('Components', () => {
|
|||
let operation: OperationModel;
|
||||
const options = normalizeOptions({});
|
||||
|
||||
const parser = new OpenAPIParser(petStore as OpenAPIDefinition, undefined, options);
|
||||
const parser = new OpenAPIParser(petStore as unknown as OpenAPIDefinition, undefined, options);
|
||||
|
||||
const info = {
|
||||
'application/json': {
|
||||
|
|
@ -92,7 +104,13 @@ describe('Components', () => {
|
|||
},
|
||||
};
|
||||
test('should renders correctly without mimeType selector', () => {
|
||||
operation = getOperation(parser, definition, undefined, options, '');
|
||||
operation = getOperation(
|
||||
parser,
|
||||
definition as unknown as ExtendedOpenAPIOperation,
|
||||
undefined,
|
||||
options,
|
||||
'',
|
||||
);
|
||||
// @ts-ignore
|
||||
operation.requestBody.content.mediaTypes = operation.requestBody?.content?.mediaTypes.filter(
|
||||
(m) => m.name === 'application/json',
|
||||
|
|
@ -118,20 +136,20 @@ describe('Components', () => {
|
|||
});
|
||||
|
||||
test('should renders correctly with empty languages', async () => {
|
||||
jest.spyOn(Jotai, 'useAtom').mockImplementation((atom) => {
|
||||
vi.spyOn(Jotai, 'useAtom').mockImplementation((atom) => {
|
||||
if (atom === languageAtom) {
|
||||
return [
|
||||
{
|
||||
activeLanguage: undefined,
|
||||
languages: [],
|
||||
},
|
||||
jest.fn(),
|
||||
];
|
||||
vi.fn(),
|
||||
] as unknown as ReturnType<typeof Jotai.useAtom>;
|
||||
}
|
||||
return jest.requireActual('jotai').useAtom(atom);
|
||||
return [{}, vi.fn()] as unknown as ReturnType<typeof Jotai.useAtom>;
|
||||
});
|
||||
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockImplementation((atom) => {
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockImplementation((atom) => {
|
||||
if (atom === languageAtom) {
|
||||
return {
|
||||
activeLanguage: undefined,
|
||||
|
|
@ -143,7 +161,7 @@ describe('Components', () => {
|
|||
return {};
|
||||
});
|
||||
|
||||
jest.spyOn(require('../useCodeSamples'), 'useCodeSamples').mockReturnValue({
|
||||
vi.spyOn(useCodeSamplesModule, 'useCodeSamples').mockReturnValue({
|
||||
samples: [], // Empty samples array
|
||||
});
|
||||
|
||||
|
|
@ -153,7 +171,13 @@ describe('Components', () => {
|
|||
},
|
||||
});
|
||||
|
||||
operation = getOperation(parser, definition, undefined, optionsWithEmptyLanguages, '');
|
||||
operation = getOperation(
|
||||
parser,
|
||||
definition as unknown as ExtendedOpenAPIOperation,
|
||||
undefined,
|
||||
optionsWithEmptyLanguages,
|
||||
'',
|
||||
);
|
||||
const PROPS = {
|
||||
operation,
|
||||
content: getMediaContent({
|
||||
|
|
|
|||
|
|
@ -54,5 +54,6 @@
|
|||
}
|
||||
],
|
||||
"pathServers": null,
|
||||
"isWebhook": false
|
||||
"isWebhook": false,
|
||||
"isAdditionalOperation": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,45 @@
|
|||
import { jest, describe, test, expect } from '@jest/globals';
|
||||
import { vi } from 'vitest';
|
||||
import * as Jotai from 'jotai/index';
|
||||
|
||||
import type { OpenAPIDefinition } from '../../../types';
|
||||
import type { OpenAPIDefinition } from '../../../types/index.js';
|
||||
|
||||
import { getOperation } from '../../../models/index.js';
|
||||
import type { ExtendedOpenAPIOperation } from '../../../services/index.js';
|
||||
|
||||
import { getOperation } from '../../../models';
|
||||
import definition from './fixtures/operationDefinition.json';
|
||||
import { useCodeSamples } from '../useCodeSamples';
|
||||
import { replaceCircularJson } from '../../../models/__tests__/helpers';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { useCodeSamples } from '../useCodeSamples.js';
|
||||
import { replaceCircularJson } from '../../../models/__tests__/helpers.js';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import petStore from './fixtures/petstore.json';
|
||||
import { globalStoreAtom } from '../../../jotai/store';
|
||||
import { globalStoreAtom } from '../../../jotai/store.js';
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: jest.fn(),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtomValue: vi.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../hooks/useTranslate', () => ({
|
||||
useTranslate: jest.fn(),
|
||||
vi.mock('../../../hooks/useTranslate', () => ({
|
||||
useTranslate: vi.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@redocly/theme/ext/configure', () => ({
|
||||
vi.mock('@redocly/theme/ext/configure', () => ({
|
||||
__esModule: true,
|
||||
configure: jest.fn(),
|
||||
configure: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('useCodeSamples', () => {
|
||||
const options = normalizeOptions({});
|
||||
const parser = new OpenAPIParser(petStore as OpenAPIDefinition, undefined, options);
|
||||
const parser = new OpenAPIParser(petStore as unknown as OpenAPIDefinition, undefined, options);
|
||||
|
||||
test('should return all codeSamples', () => {
|
||||
const operation = getOperation(parser, definition, undefined, options, '');
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockImplementation((atom) => {
|
||||
const operation = getOperation(
|
||||
parser,
|
||||
definition as unknown as ExtendedOpenAPIOperation,
|
||||
undefined,
|
||||
options,
|
||||
'',
|
||||
);
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockImplementation((atom) => {
|
||||
if (atom === globalStoreAtom) return { options, parser };
|
||||
return {};
|
||||
});
|
||||
|
|
@ -50,8 +58,14 @@ describe('useCodeSamples', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const operation = getOperation(parser, definition, undefined, options, '');
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockImplementation((atom) => {
|
||||
const operation = getOperation(
|
||||
parser,
|
||||
definition as unknown as ExtendedOpenAPIOperation,
|
||||
undefined,
|
||||
options,
|
||||
'',
|
||||
);
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockImplementation((atom) => {
|
||||
if (atom === globalStoreAtom) return { options, parser };
|
||||
return {};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { render } from '@testing-library/react';
|
||||
|
||||
import type { OpenAPIDefinition } from '../../../types';
|
||||
import type { OperationModel } from '../../../models';
|
||||
import type { OpenAPIDefinition } from '../../../types/index.js';
|
||||
import type { OperationModel } from '../../../models/index.js';
|
||||
|
||||
import { ResponseSamples } from '../ResponseSamples';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { getOperation } from '../../../models';
|
||||
import { withTestProviders } from '../../../testProviders';
|
||||
import { ResponseSamples } from '../ResponseSamples.js';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import { getOperation } from '../../../models/index.js';
|
||||
import { withTestProviders } from '../../../testProviders.js';
|
||||
import museum from './fixtures/museum.json';
|
||||
import type { ExtendedOpenAPIOperation } from '../../../services/index.js';
|
||||
|
||||
import definitionWithContent from './fixtures/operationDefinitionWithContent.json';
|
||||
import definitionWithoutContent from './fixtures/operationDefinitionWithoutContent.json';
|
||||
|
||||
|
|
@ -15,14 +17,20 @@ describe('ResponseSamples', () => {
|
|||
let operation: OperationModel;
|
||||
const options = normalizeOptions({});
|
||||
|
||||
const parser = new OpenAPIParser(museum as OpenAPIDefinition, undefined, options);
|
||||
const parser = new OpenAPIParser(museum as unknown as OpenAPIDefinition, undefined, options);
|
||||
|
||||
it('renders PayloadSamples when response has a sample', () => {
|
||||
operation = getOperation(parser, definitionWithContent, undefined, options, '');
|
||||
operation = getOperation(
|
||||
parser,
|
||||
definitionWithContent as unknown as ExtendedOpenAPIOperation,
|
||||
undefined,
|
||||
options,
|
||||
'',
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
withTestProviders(
|
||||
<ResponseSamples operation={operation} activeResponseTab="200" onTabChange={jest.fn()} />,
|
||||
<ResponseSamples operation={operation} activeResponseTab="200" onTabChange={vi.fn()} />,
|
||||
{
|
||||
definition: parser.definition,
|
||||
},
|
||||
|
|
@ -33,11 +41,17 @@ describe('ResponseSamples', () => {
|
|||
});
|
||||
|
||||
it('renders StyledCodeBlock when response does not have a sample', () => {
|
||||
operation = getOperation(parser, definitionWithoutContent, undefined, options, '');
|
||||
operation = getOperation(
|
||||
parser,
|
||||
definitionWithoutContent as unknown as ExtendedOpenAPIOperation,
|
||||
undefined,
|
||||
options,
|
||||
'',
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
withTestProviders(
|
||||
<ResponseSamples operation={operation} activeResponseTab="200" onTabChange={jest.fn()} />,
|
||||
<ResponseSamples operation={operation} activeResponseTab="200" onTabChange={vi.fn()} />,
|
||||
{
|
||||
definition: parser.definition,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,5 +25,12 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pointer": "#/paths/~1museum-hours/get",
|
||||
"pathName": "/museum-hours",
|
||||
"httpVerb": "get",
|
||||
"pathParameters": [],
|
||||
"pathServers": [],
|
||||
"isWebhook": false,
|
||||
"isAdditionalOperation": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,12 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pointer": "#/paths/~1museum-hours/get",
|
||||
"pathName": "/museum-hours",
|
||||
"httpVerb": "get",
|
||||
"pathParameters": [],
|
||||
"pathServers": [],
|
||||
"isWebhook": false,
|
||||
"isAdditionalOperation": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import * as Jotai from 'jotai';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import type { OperationModel } from '../../../models';
|
||||
import type { OperationModel } from '../../../models/index.js';
|
||||
|
||||
import { ResponseDetails } from '../ResponseDetails';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { getField, getMediaContent } from '../../../models';
|
||||
import { globalOptionsAtom, globalStoreAtom } from '../../../jotai/store';
|
||||
import { activeMimeNameAtom } from '../../../jotai/app';
|
||||
import { ResponseDetails } from '../ResponseDetails.js';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import { getField, getMediaContent } from '../../../models/index.js';
|
||||
import { globalOptionsAtom, globalStoreAtom } from '../../../jotai/store.js';
|
||||
import { activeMimeNameAtom } from '../../../jotai/app.js';
|
||||
import { TestMemoryRouter } from '../../../testProviders.js';
|
||||
|
||||
const info = {
|
||||
'application/json': {
|
||||
|
|
@ -86,8 +86,8 @@ const response = {
|
|||
{ operation: {} as OperationModel },
|
||||
),
|
||||
],
|
||||
toggle: jest.fn(),
|
||||
expand: jest.fn(),
|
||||
toggle: vi.fn(),
|
||||
expand: vi.fn(),
|
||||
content: getMediaContent({
|
||||
parser,
|
||||
info,
|
||||
|
|
@ -97,27 +97,27 @@ const response = {
|
|||
}),
|
||||
};
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: jest.fn(),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtomValue: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('ResponseDetails component', () => {
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockImplementation((atom) => {
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockImplementation((atom) => {
|
||||
if (atom === globalOptionsAtom) {
|
||||
return options;
|
||||
}
|
||||
|
||||
if (atom === globalStoreAtom) return { options, parser };
|
||||
if (atom === activeMimeNameAtom) return ['application/json', jest.fn()];
|
||||
if (atom === activeMimeNameAtom) return ['application/json', vi.fn()];
|
||||
return {};
|
||||
});
|
||||
|
||||
it('should renders correctly', () => {
|
||||
const { container } = render(
|
||||
<MemoryRouter>
|
||||
<ResponseDetails response={response} operationPointer="" operationId=" " />
|
||||
</MemoryRouter>,
|
||||
<TestMemoryRouter>
|
||||
<ResponseDetails response={response} operationId=" " />
|
||||
</TestMemoryRouter>,
|
||||
);
|
||||
expect(container.childNodes.length).toEqual(5);
|
||||
});
|
||||
|
|
@ -125,13 +125,9 @@ describe('ResponseDetails component', () => {
|
|||
it('should render description as markdown', () => {
|
||||
const expectedTags = ['p'];
|
||||
const { container } = render(
|
||||
<MemoryRouter>
|
||||
<ResponseDetails
|
||||
response={{ ...response, headers: [] }}
|
||||
operationPointer=""
|
||||
operationId=""
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
<TestMemoryRouter>
|
||||
<ResponseDetails response={{ ...response, headers: [] }} operationId="" />
|
||||
</TestMemoryRouter>,
|
||||
);
|
||||
const markdownContainer = container.querySelector('.redoc-markdown');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import { render } from '@testing-library/react';
|
||||
|
||||
import { ResponseHeaders } from '../ResponseHeaders';
|
||||
import { PropertiesTable } from '../../common';
|
||||
import { ResponseHeaders } from '../ResponseHeaders.js';
|
||||
import { PropertiesTable } from '../../common/index.js';
|
||||
|
||||
jest.mock('../../common', () => ({
|
||||
PropertiesTable: jest.fn(() => <div data-testid="properties-table" />),
|
||||
vi.mock('../../common', () => ({
|
||||
PropertiesTable: vi.fn(() => <div data-testid="properties-table" />),
|
||||
}));
|
||||
|
||||
jest.mock('../../PropertyDetails', () => ({
|
||||
Field: jest.fn(() => <div data-testid="property-details" />),
|
||||
vi.mock('../../PropertyDetails', () => ({
|
||||
Field: vi.fn(() => <div data-testid="property-details" />),
|
||||
}));
|
||||
|
||||
describe('ResponseHeaders', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders nothing if headers are not defined', () => {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,21 @@ function OneOfSchemaComponent({
|
|||
const [store, setOperationStore] = useAtom(operationStore(schema.operationPointer));
|
||||
const oneOfIdxLocation = useOneOfLocationIdx(oneOf, oneOfLevel);
|
||||
|
||||
const activeOneOfIdx = oneOfIdxLocation === -1 ? 0 : oneOfIdxLocation;
|
||||
const options = oneOf.map((subSchema, idx) => ({
|
||||
label: subSchema.title || subSchema.typePrefix + subSchema.displayType,
|
||||
value: idx,
|
||||
}));
|
||||
const activeExampleNameIndex = options.findIndex(
|
||||
(option) => option.label === store.activeExampleName,
|
||||
);
|
||||
|
||||
const activeOneOfIdx =
|
||||
oneOfIdxLocation === -1
|
||||
? activeExampleNameIndex !== -1
|
||||
? activeExampleNameIndex
|
||||
: 0
|
||||
: oneOfIdxLocation;
|
||||
|
||||
const activeSchemaIndex =
|
||||
store.activeOneOf?.[schema.pointer] !== undefined
|
||||
? store.activeOneOf[schema.pointer]
|
||||
|
|
@ -43,11 +57,6 @@ function OneOfSchemaComponent({
|
|||
return null;
|
||||
}
|
||||
|
||||
const options = oneOf.map((subSchema, idx) => ({
|
||||
label: subSchema.title || subSchema.typePrefix + subSchema.displayType,
|
||||
value: idx,
|
||||
}));
|
||||
|
||||
const handleChange = (value: number) => {
|
||||
onChange?.({
|
||||
pointer: schema.pointer,
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import * as Jotai from 'jotai';
|
||||
|
||||
import type { OperationModel } from '../../../models';
|
||||
import type { OpenAPIDefinition } from '../../../types';
|
||||
import type { OperationModel } from '../../../models/index.js';
|
||||
import type { OpenAPIDefinition } from '../../../types/index.js';
|
||||
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { getSchema } from '../../../models';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import { getSchema } from '../../../models/index.js';
|
||||
import schemasExpansionLevel from './fixtures/schemaExpansionLevel.json';
|
||||
import { Schema } from '../Schema';
|
||||
import { Schema } from '../Schema.js';
|
||||
import { TestBrowserRouter } from '../../../testProviders.js';
|
||||
|
||||
const options = normalizeOptions({});
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: jest.fn(),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtomValue: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('ArraySchema', () => {
|
||||
it("should render Schema with Array when schemasExpansionLevel is 'all'", async () => {
|
||||
jest
|
||||
.spyOn(Jotai, 'useAtomValue')
|
||||
.mockReturnValue(normalizeOptions({ schemasExpansionLevel: 'all' }));
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(
|
||||
normalizeOptions({ schemasExpansionLevel: 'all' }),
|
||||
);
|
||||
const parser = new OpenAPIParser(
|
||||
schemasExpansionLevel as OpenAPIDefinition,
|
||||
undefined,
|
||||
|
|
@ -41,16 +41,16 @@ describe('ArraySchema', () => {
|
|||
});
|
||||
|
||||
const { getByText } = render(<Schema schema={schema} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(getByText('Dumb Property 3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render Schema with Array when schemasExpansionLevel is '3'", async () => {
|
||||
jest
|
||||
.spyOn(Jotai, 'useAtomValue')
|
||||
.mockReturnValue(normalizeOptions({ schemasExpansionLevel: '3' }));
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(
|
||||
normalizeOptions({ schemasExpansionLevel: '3' }),
|
||||
);
|
||||
const parser = new OpenAPIParser(
|
||||
schemasExpansionLevel as OpenAPIDefinition,
|
||||
undefined,
|
||||
|
|
@ -70,7 +70,7 @@ describe('ArraySchema', () => {
|
|||
});
|
||||
|
||||
const { getByText, queryByText } = render(<Schema schema={schema} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(getByText('Dumb Property 1')).toBeInTheDocument();
|
||||
|
|
@ -78,9 +78,9 @@ describe('ArraySchema', () => {
|
|||
});
|
||||
|
||||
it('should automatically expand nested array when parent element is expanded', async () => {
|
||||
jest
|
||||
.spyOn(Jotai, 'useAtomValue')
|
||||
.mockReturnValue(normalizeOptions({ schemasExpansionLevel: '4' }));
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(
|
||||
normalizeOptions({ schemasExpansionLevel: '4' }),
|
||||
);
|
||||
const parser = new OpenAPIParser(
|
||||
schemasExpansionLevel as OpenAPIDefinition,
|
||||
undefined,
|
||||
|
|
@ -100,16 +100,16 @@ describe('ArraySchema', () => {
|
|||
});
|
||||
|
||||
const { getByText } = render(<Schema schema={schema} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(getByText('Dumb Property 1 in nested array')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render inner Schema with Array when schemasExpansionLevel is '0'", async () => {
|
||||
jest
|
||||
.spyOn(Jotai, 'useAtomValue')
|
||||
.mockReturnValue(normalizeOptions({ schemasExpansionLevel: '0' }));
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(
|
||||
normalizeOptions({ schemasExpansionLevel: '0' }),
|
||||
);
|
||||
const parser = new OpenAPIParser(
|
||||
schemasExpansionLevel as OpenAPIDefinition,
|
||||
undefined,
|
||||
|
|
@ -129,7 +129,7 @@ describe('ArraySchema', () => {
|
|||
});
|
||||
|
||||
const { queryByText } = render(<Schema schema={schema} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(queryByText('Dumb Property')).toBeNull();
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import * as Jotai from 'jotai';
|
||||
|
||||
import type { OperationModel } from '../../../models';
|
||||
import type { OpenAPIDefinition } from '../../../types';
|
||||
import type { OperationModel } from '../../../models/index.js';
|
||||
import type { OpenAPIDefinition } from '../../../types/index.js';
|
||||
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { getSchema } from '../../../models';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import { getSchema } from '../../../models/index.js';
|
||||
import schemasExpansionLevel from './fixtures/schemaExpansionLevel.json';
|
||||
import requiredField from './fixtures/requiredField.json';
|
||||
import { Schema } from '../Schema';
|
||||
import { Schema } from '../Schema.js';
|
||||
import { TestBrowserRouter } from '../../../testProviders.js';
|
||||
|
||||
const options = normalizeOptions({});
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: jest.fn(),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtomValue: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('ObjectSchema', () => {
|
||||
it("should render Schema when schemasExpansionLevel is 'all'", async () => {
|
||||
jest
|
||||
.spyOn(Jotai, 'useAtomValue')
|
||||
.mockReturnValue(normalizeOptions({ schemasExpansionLevel: 'all' }));
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(
|
||||
normalizeOptions({ schemasExpansionLevel: 'all' }),
|
||||
);
|
||||
const parser = new OpenAPIParser(
|
||||
schemasExpansionLevel as OpenAPIDefinition,
|
||||
undefined,
|
||||
|
|
@ -42,16 +42,16 @@ describe('ObjectSchema', () => {
|
|||
});
|
||||
|
||||
const { getByText } = render(<Schema schema={schema} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(getByText('Dumb Property 6')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render Schema when schemasExpansionLevel is '3'", async () => {
|
||||
jest
|
||||
.spyOn(Jotai, 'useAtomValue')
|
||||
.mockReturnValue(normalizeOptions({ schemasExpansionLevel: '3' }));
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(
|
||||
normalizeOptions({ schemasExpansionLevel: '3' }),
|
||||
);
|
||||
const parser = new OpenAPIParser(
|
||||
schemasExpansionLevel as OpenAPIDefinition,
|
||||
undefined,
|
||||
|
|
@ -71,7 +71,7 @@ describe('ObjectSchema', () => {
|
|||
});
|
||||
|
||||
const { getByText, queryByText } = render(<Schema schema={schema} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(getByText('Dumb Property 3')).toBeInTheDocument();
|
||||
|
|
@ -79,9 +79,9 @@ describe('ObjectSchema', () => {
|
|||
});
|
||||
|
||||
it("should not render inner Schema when schemasExpansionLevel is '0'", async () => {
|
||||
jest
|
||||
.spyOn(Jotai, 'useAtomValue')
|
||||
.mockReturnValue(normalizeOptions({ schemasExpansionLevel: '0' }));
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(
|
||||
normalizeOptions({ schemasExpansionLevel: '0' }),
|
||||
);
|
||||
const parser = new OpenAPIParser(
|
||||
schemasExpansionLevel as OpenAPIDefinition,
|
||||
undefined,
|
||||
|
|
@ -101,14 +101,14 @@ describe('ObjectSchema', () => {
|
|||
});
|
||||
|
||||
const { queryByText } = render(<Schema schema={schema} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(queryByText('Test Sub Category')).toBeNull();
|
||||
});
|
||||
|
||||
it("should render Schema when required is 'category' -> 'sub'", async () => {
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockReturnValue(normalizeOptions({}));
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(normalizeOptions({}));
|
||||
const parser = new OpenAPIParser(requiredField as OpenAPIDefinition, undefined, options);
|
||||
|
||||
const schema = getSchema({
|
||||
|
|
@ -124,7 +124,7 @@ describe('ObjectSchema', () => {
|
|||
});
|
||||
|
||||
const { getByText, queryByText } = render(<Schema schema={schema} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(getByText('Dumb Property 2')).toBeInTheDocument();
|
||||
|
|
@ -132,7 +132,7 @@ describe('ObjectSchema', () => {
|
|||
});
|
||||
|
||||
it("should render Schema when required is 'tags' -> 'sub' at Array schema", async () => {
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockReturnValue(normalizeOptions({}));
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(normalizeOptions({}));
|
||||
const parser = new OpenAPIParser(requiredField as OpenAPIDefinition, undefined, options);
|
||||
|
||||
const schema = getSchema({
|
||||
|
|
@ -148,7 +148,7 @@ describe('ObjectSchema', () => {
|
|||
});
|
||||
|
||||
const { getByText, queryByText } = render(<Schema schema={schema} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(getByText('Dumb Property 1')).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import type { OpenAPIDefinition } from '../../../types';
|
||||
import type { OperationModel } from '../../../models';
|
||||
import type { OpenAPIDefinition } from '../../../types/index.js';
|
||||
import type { OperationModel } from '../../../models/index.js';
|
||||
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { getSchema } from '../../../models';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import { getSchema } from '../../../models/index.js';
|
||||
import oneOfWithDiscriminator from './fixtures/oneOfWithDiscriminator.json';
|
||||
import { Schema } from '../Schema';
|
||||
import { Schema } from '../Schema.js';
|
||||
import { TestBrowserRouter } from '../../../testProviders.js';
|
||||
|
||||
const options = normalizeOptions({});
|
||||
describe('OneOfSchema', () => {
|
||||
|
|
@ -38,7 +38,7 @@ describe('OneOfSchema', () => {
|
|||
);
|
||||
|
||||
const { getByText } = render(<Schema schema={schema} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
|
||||
expect(getByText('Type of the shape - Shape')).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { act, fireEvent, render } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import type { OpenAPIDefinition } from '../../../types';
|
||||
import type { OperationModel } from '../../../models';
|
||||
import type { OpenAPIDefinition } from '../../../types/index.js';
|
||||
import type { OperationModel } from '../../../models/index.js';
|
||||
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { getSchema } from '../../../models';
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services/index.js';
|
||||
import { getSchema } from '../../../models/index.js';
|
||||
import oneOfWithDiscriminator from './fixtures/oneOfWithDiscriminator.json';
|
||||
import { Schema } from '../Schema';
|
||||
import { Schema } from '../Schema.js';
|
||||
import { TestBrowserRouter } from '../../../testProviders.js';
|
||||
|
||||
const options = normalizeOptions({});
|
||||
describe('SchemaView', () => {
|
||||
|
|
@ -33,7 +33,7 @@ describe('SchemaView', () => {
|
|||
});
|
||||
|
||||
const { getByText } = render(<Schema schema={schema} />, {
|
||||
wrapper: BrowserRouter,
|
||||
wrapper: TestBrowserRouter,
|
||||
});
|
||||
expect(getByText('Shape')).toBeInTheDocument();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {},
|
||||
"info": { "version": "1.0", "title": "Test" },
|
||||
"paths": {},
|
||||
"tags": [
|
||||
{
|
||||
"name": "pet_model",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {},
|
||||
"info": { "version": "1.0", "title": "Test" },
|
||||
"paths": {},
|
||||
"tags": [
|
||||
{
|
||||
"name": "pet_model",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useOneOfLocationIdx } from '../useOneOfLocationIdx';
|
||||
import { useOneOfLocationIdx } from '../useOneOfLocationIdx.js';
|
||||
|
||||
jest.mock('../../../hooks/useLocation', () => ({
|
||||
useLocation: jest.fn().mockReturnValue({
|
||||
vi.mock('../../../hooks/useLocation', () => ({
|
||||
useLocation: vi.fn().mockReturnValue({
|
||||
pathname: '/',
|
||||
hash: '#plans/postplan/t=request&path=&oneof=1/trial&oneof=0/period',
|
||||
}),
|
||||
|
|
@ -11,7 +11,7 @@ jest.mock('../../../hooks/useLocation', () => ({
|
|||
|
||||
describe('useOneOfLocationIdx', () => {
|
||||
it('should return the index of the oneOf item in the hash', () => {
|
||||
const oneOf = [{}, {}, {}];
|
||||
const oneOf = [{}, {}, {}] as unknown as Parameters<typeof useOneOfLocationIdx>[0];
|
||||
const level = 1;
|
||||
const { result } = renderHook(() => useOneOfLocationIdx(oneOf, level));
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ describe('useOneOfLocationIdx', () => {
|
|||
});
|
||||
|
||||
it('should return -1 if the oneOf item is not found in the hash', () => {
|
||||
const oneOf = [{}, {}, {}];
|
||||
const oneOf = [{}, {}, {}] as unknown as Parameters<typeof useOneOfLocationIdx>[0];
|
||||
const level = 3;
|
||||
const { result } = renderHook(() => useOneOfLocationIdx(oneOf, level));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { OpenAPIParser } from '../../services/OpenAPIParser.js';
|
||||
import type { ContentItemModel } from '../../models/types.js';
|
||||
|
||||
import { useSearchDialog } from '@redocly/theme/core/openapi';
|
||||
import { useSearchDialog, SearchSessionProvider } from '@redocly/theme/core/openapi';
|
||||
|
||||
import { SearchDialog, SearchTrigger } from './index.js';
|
||||
import { useSearch } from '../../hooks/useSearch.js';
|
||||
|
|
@ -13,7 +13,7 @@ export type SearchProps = {
|
|||
parser: OpenAPIParser;
|
||||
};
|
||||
|
||||
export function Search({ flatItems, parser }: SearchProps) {
|
||||
function SearchContent({ flatItems, parser }: SearchProps) {
|
||||
const telemetry = useTelemetry();
|
||||
const { isOpen, onOpen, onClose } = useSearchDialog();
|
||||
const { search, isReady } = useSearch(flatItems, parser);
|
||||
|
|
@ -32,6 +32,14 @@ export function Search({ flatItems, parser }: SearchProps) {
|
|||
);
|
||||
}
|
||||
|
||||
export function Search(props: SearchProps) {
|
||||
return (
|
||||
<SearchSessionProvider>
|
||||
<SearchContent {...props} />
|
||||
</SearchSessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const SearchWrapper = styled.div`
|
||||
display: flex;
|
||||
margin: var(--sidebar-margin-horizontal);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import type { SecurityRequirement } from '../../models/index.js';
|
||||
|
|
@ -8,13 +8,23 @@ import { useModalScrollLock } from '@redocly/theme/core/openapi';
|
|||
|
||||
import { SecurityButton } from './SecurityButton.js';
|
||||
import { SecurityModal } from './SecurityModal.js';
|
||||
import { useTelemetry } from '../../hooks/index.js';
|
||||
|
||||
interface SecurityProps {
|
||||
securities: SecurityRequirement[];
|
||||
}
|
||||
|
||||
interface SecurityContext {
|
||||
securityTypes: string[];
|
||||
schemesCount: number;
|
||||
isCombined: boolean;
|
||||
modalOpenTime: number;
|
||||
}
|
||||
|
||||
export function Security({ securities }: SecurityProps): ReactElement | null {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const telemetry = useTelemetry();
|
||||
const securityContextRef = useRef<SecurityContext | null>(null);
|
||||
|
||||
useModalScrollLock(isModalVisible);
|
||||
|
||||
|
|
@ -22,12 +32,58 @@ export function Security({ securities }: SecurityProps): ReactElement | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
const handleViewDetailsClick = (): void => {
|
||||
const allSchemes = securities.flatMap((security) => security.schemes);
|
||||
const securityTypes = [...new Set(allSchemes.map((scheme) => scheme.type))];
|
||||
const schemesCount = allSchemes.length;
|
||||
const isCombined = securityTypes.length > 1;
|
||||
const modalOpenTime = Date.now();
|
||||
|
||||
securityContextRef.current = {
|
||||
securityTypes,
|
||||
schemesCount,
|
||||
isCombined,
|
||||
modalOpenTime,
|
||||
};
|
||||
|
||||
telemetry.sendViewSecurityDetailsClickedMessage({
|
||||
id: 'openapi-docs-security-button',
|
||||
object: 'button',
|
||||
uri: window.location.href,
|
||||
securityTypes,
|
||||
schemesCount,
|
||||
isCombined,
|
||||
});
|
||||
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleModalClose = (): void => {
|
||||
if (securityContextRef.current) {
|
||||
const timeInModalMs = Date.now() - securityContextRef.current.modalOpenTime;
|
||||
|
||||
telemetry.sendViewSecurityDetailsClosedMessage({
|
||||
id: 'openapi-docs-security-button',
|
||||
object: 'button',
|
||||
uri: window.location.href,
|
||||
securityTypes: securityContextRef.current.securityTypes,
|
||||
schemesCount: securityContextRef.current.schemesCount,
|
||||
isCombined: securityContextRef.current.isCombined,
|
||||
timeInModalMs,
|
||||
});
|
||||
|
||||
securityContextRef.current = null;
|
||||
}
|
||||
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecurityButton securities={securities} onClick={() => setIsModalVisible(true)} />
|
||||
<SecurityButton securities={securities} onClick={handleViewDetailsClick} />
|
||||
{isModalVisible && (
|
||||
<Portal mountId="api-content">
|
||||
<SecurityModal securities={securities} onClose={() => setIsModalVisible(false)} />
|
||||
<SecurityModal securities={securities} onClose={handleModalClose} />
|
||||
</Portal>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { act, fireEvent, render } from '@testing-library/react';
|
||||
// import { act } from 'react-dom/test-utils'; // For async act
|
||||
|
||||
import type { SecurityRequirement } from '../../../models';
|
||||
import type { SecurityRequirement } from '../../../models/index.js';
|
||||
|
||||
import { Security } from '../Security';
|
||||
import { Security } from '../Security.js';
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: jest.fn(() => ({ options: { hideSecuritySection: false } })),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtomValue: vi.fn(() => ({ options: { hideSecuritySection: false } })),
|
||||
}));
|
||||
jest.mock('../SecurityModal', () => ({
|
||||
SecurityModal: jest.fn(() => <div>Mocked SecurityModal</div>),
|
||||
vi.mock('../SecurityModal', async () => ({
|
||||
SecurityModal: vi.fn(() => <div>Mocked SecurityModal</div>),
|
||||
}));
|
||||
|
||||
describe('Security Component', () => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import type { SecurityRequirement } from '../../../models';
|
||||
import type { SecurityRequirement } from '../../../models/index.js';
|
||||
|
||||
import { SecurityButton } from '../SecurityButton';
|
||||
import { SecurityButton } from '../SecurityButton.js';
|
||||
|
||||
describe('SecurityButton Component', () => {
|
||||
const securities = [
|
||||
|
|
@ -46,7 +46,7 @@ describe('SecurityButton Component', () => {
|
|||
});
|
||||
|
||||
it('calls the onClick callback when the button is clicked', () => {
|
||||
const onClickMock = jest.fn();
|
||||
const onClickMock = vi.fn();
|
||||
const { getByRole } = render(<SecurityButton securities={securities} onClick={onClickMock} />);
|
||||
|
||||
fireEvent.click(getByRole('button', { name: 'View details' }));
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { render, fireEvent, act } from '@testing-library/react';
|
||||
|
||||
import type { ExtendedOpenAPISecurityScheme, SecurityRequirement } from '../../../models';
|
||||
import type { ExtendedOpenAPISecurityScheme, SecurityRequirement } from '../../../models/index.js';
|
||||
|
||||
import { SecurityModal } from '../SecurityModal';
|
||||
import { SecurityModal } from '../SecurityModal.js';
|
||||
|
||||
describe('SecurityModal Component', () => {
|
||||
const securities = [
|
||||
|
|
@ -122,7 +122,7 @@ describe('SecurityModal Component', () => {
|
|||
});
|
||||
|
||||
it('calls onClose callback when the Close button is clicked', () => {
|
||||
const onCloseMock = jest.fn();
|
||||
const onCloseMock = vi.fn();
|
||||
const { getByTestId } = render(<SecurityModal securities={securities} onClose={onCloseMock} />);
|
||||
|
||||
fireEvent.click(getByTestId('close'));
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import * as Jotai from 'jotai';
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { ServerList } from '../ServerList';
|
||||
import { normalizeOptions } from '../../../services';
|
||||
import { ServerList } from '../ServerList.js';
|
||||
import { normalizeOptions } from '../../../services/index.js';
|
||||
|
||||
import type { OpenAPIServer } from '../../../types';
|
||||
import type { OpenAPIServer } from '../../../types/index.js';
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...(jest.requireActual('jotai') as object),
|
||||
useAtomValue: jest.fn(),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtomValue: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('ServerList component', () => {
|
||||
jest.spyOn(Jotai, 'useAtomValue').mockReturnValue(normalizeOptions({}));
|
||||
vi.spyOn(Jotai, 'useAtomValue').mockReturnValue(normalizeOptions({}));
|
||||
const servers = [
|
||||
{
|
||||
url: 'http://petstore.swagger.io/v2',
|
||||
|
|
@ -29,7 +29,7 @@ describe('ServerList component', () => {
|
|||
name: 'Stage server',
|
||||
},
|
||||
];
|
||||
const translateMock = jest.fn().mockImplementation((_, defaultValue) => defaultValue);
|
||||
const translateMock = vi.fn().mockImplementation((_, defaultValue) => defaultValue);
|
||||
it('should render copy button', () => {
|
||||
render(<ServerList servers={servers} path="/pet" translate={translateMock} />);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { ReactElement } from 'react';
|
|||
import type { OperationModel } from '../../models/index.js';
|
||||
|
||||
import { Dropdown } from '@redocly/theme/components/Dropdown/Dropdown';
|
||||
import { getOperationColor } from '@redocly/theme/core/openapi';
|
||||
|
||||
import { HttpVerb } from '../common/index.js';
|
||||
import { PathWrapper, Path } from './styled.js';
|
||||
|
|
@ -15,13 +16,18 @@ export const ServerListDropdown = ({
|
|||
operation,
|
||||
className,
|
||||
}: ServerListDropdownProps): ReactElement => {
|
||||
const httpColor = getOperationColor({
|
||||
isAdditionalOperation: operation.isAdditionalOperation,
|
||||
httpVerb: operation.httpVerb,
|
||||
});
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className={className}
|
||||
active={false}
|
||||
trigger={
|
||||
<PathWrapper variant="ghost">
|
||||
<HttpVerb color={operation.httpVerb}>{operation.httpVerb}</HttpVerb>
|
||||
<HttpVerb color={httpColor}>{operation.httpVerb}</HttpVerb>
|
||||
<Path>{operation.path}</Path>
|
||||
</PathWrapper>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ function convertOpenAPIDocs2Sidebar(data: {
|
|||
hasActiveSubItem,
|
||||
modified: true,
|
||||
deprecated: item.deprecated,
|
||||
isAdditionalOperation: item.isAdditionalOperation,
|
||||
});
|
||||
break;
|
||||
case 'section':
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { AnimatedChevronButton } from '../AnimatedChevronButton';
|
||||
import { AnimatedChevronButton } from '../AnimatedChevronButton.js';
|
||||
|
||||
describe('AnimatedChevronButton', () => {
|
||||
it('should render without crashing', () => {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,28 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { useAtom } from 'jotai';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
import * as useIsMobile from '../../../hooks/useIsMobile';
|
||||
import { FloatingButton } from '../FloatingButton.js';
|
||||
import { TestMemoryRouter } from '../../../testProviders.js';
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtom: jest.fn(),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtom: vi.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../hooks/useIsMobile');
|
||||
vi.mock('../../../hooks/useIsMobile');
|
||||
|
||||
const useAtomMock = useAtom as jest.Mock;
|
||||
const useIsMobileMock = jest.spyOn(useIsMobile, 'useIsMobile');
|
||||
const useAtomMock = useAtom as Mock;
|
||||
const useIsMobileMock = vi.spyOn(useIsMobile, 'useIsMobile');
|
||||
|
||||
describe('FloatingButton', () => {
|
||||
const setIsSidebarOpened = jest.fn();
|
||||
const setIsSidebarOpened = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render button', () => {
|
||||
|
|
@ -27,9 +30,9 @@ describe('FloatingButton', () => {
|
|||
useIsMobileMock.mockReturnValue(false);
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<TestMemoryRouter>
|
||||
<FloatingButton />
|
||||
</MemoryRouter>,
|
||||
</TestMemoryRouter>,
|
||||
);
|
||||
expect(screen.getByTestId('floating-button')).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -39,9 +42,9 @@ describe('FloatingButton', () => {
|
|||
useIsMobileMock.mockReturnValue(false);
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<TestMemoryRouter>
|
||||
<FloatingButton />
|
||||
</MemoryRouter>,
|
||||
</TestMemoryRouter>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('floating-button'));
|
||||
|
|
@ -53,17 +56,17 @@ describe('FloatingButton', () => {
|
|||
useIsMobileMock.mockReturnValue(true);
|
||||
|
||||
const { rerender } = render(
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<TestMemoryRouter initialEntries={['/']}>
|
||||
<FloatingButton />
|
||||
</MemoryRouter>,
|
||||
</TestMemoryRouter>,
|
||||
);
|
||||
|
||||
expect(setIsSidebarOpened).toHaveBeenCalledWith(false);
|
||||
|
||||
rerender(
|
||||
<MemoryRouter initialEntries={['/new-path']}>
|
||||
<TestMemoryRouter initialEntries={['/new-path']}>
|
||||
<FloatingButton />
|
||||
</MemoryRouter>,
|
||||
</TestMemoryRouter>,
|
||||
);
|
||||
|
||||
expect(setIsSidebarOpened).toHaveBeenCalledWith(false);
|
||||
|
|
@ -74,15 +77,15 @@ describe('FloatingButton', () => {
|
|||
useIsMobileMock.mockReturnValue(false);
|
||||
|
||||
const { rerender } = render(
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<TestMemoryRouter initialEntries={['/']}>
|
||||
<FloatingButton />
|
||||
</MemoryRouter>,
|
||||
</TestMemoryRouter>,
|
||||
);
|
||||
|
||||
rerender(
|
||||
<MemoryRouter initialEntries={['/new-path']}>
|
||||
<TestMemoryRouter initialEntries={['/new-path']}>
|
||||
<FloatingButton />
|
||||
</MemoryRouter>,
|
||||
</TestMemoryRouter>,
|
||||
);
|
||||
|
||||
expect(setIsSidebarOpened).not.toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -1,42 +1,45 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { useAtom } from 'jotai';
|
||||
import { LayoutVariant } from '@redocly/config';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
import { SidebarActions } from '../SidebarActions.js';
|
||||
import * as useIsMobileModule from '../../../hooks/useIsMobile.js';
|
||||
import * as useTelemetryModule from '../../../hooks/useTelemetry.js';
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtom: jest.fn(),
|
||||
vi.mock('jotai', async () => ({
|
||||
...(await vi.importActual('jotai')),
|
||||
useAtom: vi.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../hooks/useIsMobile.js', () => ({
|
||||
useIsMobile: jest.fn(),
|
||||
vi.mock('../../../hooks/useIsMobile.js', () => ({
|
||||
useIsMobile: vi.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../hooks/useTelemetry.js', () => ({
|
||||
useTelemetry: jest.fn(),
|
||||
vi.mock('../../../hooks/useTelemetry.js', () => ({
|
||||
useTelemetry: vi.fn(),
|
||||
}));
|
||||
|
||||
const useAtomMock = useAtom as jest.Mock;
|
||||
const useIsMobileMock = useIsMobileModule.useIsMobile as jest.Mock;
|
||||
const useTelemetryMock = useTelemetryModule.useTelemetry as jest.Mock;
|
||||
const useAtomMock = useAtom as Mock;
|
||||
const useIsMobileMock = useIsMobileModule.useIsMobile as Mock;
|
||||
const useTelemetryMock = useTelemetryModule.useTelemetry as Mock;
|
||||
|
||||
describe('SidebarActions', () => {
|
||||
const setLayout = jest.fn();
|
||||
const setSidebarCollapsed = jest.fn();
|
||||
const setIsSidebarOpened = jest.fn();
|
||||
const setLayout = vi.fn();
|
||||
const setSidebarCollapsed = vi.fn();
|
||||
const setIsSidebarOpened = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
// Clean up any dark mode class that might be left from previous tests
|
||||
document.documentElement.classList.remove('dark');
|
||||
|
||||
// Set up default mocks
|
||||
useIsMobileMock.mockReturnValue(false);
|
||||
useTelemetryMock.mockReturnValue({
|
||||
sendChangeLayoutButtonClickedMessage: jest.fn(),
|
||||
sendChangeLayoutButtonClickedMessage: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -86,7 +89,7 @@ describe('SidebarActions', () => {
|
|||
btn.querySelector('[data-component-name="icons/SidePanelOpenIcon/SidePanelOpenIcon"]'),
|
||||
);
|
||||
|
||||
fireEvent.click(collapseButton);
|
||||
fireEvent.click(collapseButton as Element);
|
||||
|
||||
expect(setIsSidebarOpened).toHaveBeenCalledWith(false);
|
||||
expect(setSidebarCollapsed).toHaveBeenCalledWith(true);
|
||||
|
|
@ -104,7 +107,7 @@ describe('SidebarActions', () => {
|
|||
const viewModeElement =
|
||||
document.querySelector('[data-component-name*="HorizontalViewIcon"]') ||
|
||||
document.querySelector('[data-component-name*="VerticalViewIcon"]');
|
||||
fireEvent.click(viewModeElement);
|
||||
fireEvent.click(viewModeElement as Element);
|
||||
|
||||
expect(setLayout).toHaveBeenCalledWith(LayoutVariant.THREE_PANEL);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { render, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { Tabs } from '../Tabs';
|
||||
import { useTabsState } from '../useTabsState';
|
||||
import { Tabs } from '../Tabs.js';
|
||||
import { useTabsState } from '../useTabsState.js';
|
||||
|
||||
describe('Tabs', () => {
|
||||
const TestComponent = () => {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { memo } from 'react';
|
|||
import type { TFunction } from '@redocly/theme/core/openapi';
|
||||
import type { OperationsNavigationProps } from './types.js';
|
||||
|
||||
import { getOperationColor } from '@redocly/theme/core/openapi';
|
||||
|
||||
import { joinWithSeparator } from '../../services/index.js';
|
||||
import { encodeBackSlashes } from '../../utils/index.js';
|
||||
import { HttpVerb, NavigationBadge } from '../common/index.js';
|
||||
|
|
@ -26,9 +28,14 @@ function OperationNavigationItemsComponent({
|
|||
<>
|
||||
<Heading>{title}</Heading>
|
||||
{items.map((item) => {
|
||||
const { id, href, deprecated, badges } = item;
|
||||
const { id, href, deprecated, badges, isAdditionalOperation } = item;
|
||||
const title = item.type === 'operation' ? item.path : item.name;
|
||||
const httpVerb = item.type === 'operation' ? item.httpVerb : item.type;
|
||||
const httpColor = getOperationColor({
|
||||
isAdditionalOperation,
|
||||
deprecated,
|
||||
httpVerb,
|
||||
});
|
||||
|
||||
return (
|
||||
<Item
|
||||
|
|
@ -38,7 +45,7 @@ function OperationNavigationItemsComponent({
|
|||
onClick={() => onClick(joinWithSeparator(routingBasePath, encodeBackSlashes(href)))}
|
||||
>
|
||||
<span>
|
||||
<HttpVerb color={deprecated ? 'http-deprecated' : httpVerb}>{httpVerb}</HttpVerb>
|
||||
<HttpVerb color={httpColor}>{httpVerb}</HttpVerb>
|
||||
<Path>{title}</Path>
|
||||
{deprecated && (
|
||||
<NavigationBadge deprecated>
|
||||
|
|
|
|||
|
|
@ -26,35 +26,65 @@ export function OperationsNavigationComponent({
|
|||
const layout = useAtomValue(layoutAtom);
|
||||
const isStacked = layout === LayoutVariant.STACKED;
|
||||
|
||||
const { operations, webhooks, schemas, mcpTools, showMoreCount, totalCount } = useMemo(() => {
|
||||
const {
|
||||
operations,
|
||||
webhooks,
|
||||
schemas,
|
||||
mcpTools,
|
||||
showMoreCount,
|
||||
totalCount,
|
||||
mcpResources,
|
||||
mcpPrompts,
|
||||
} = useMemo(() => {
|
||||
const all = items.filter((item) => isRenderableMenuItem(item));
|
||||
const list = isExpanded ? all : all.slice(0, MAX_OPERATIONS);
|
||||
|
||||
const { operations, webhooks, schemas, mcpTools } = list.reduce(
|
||||
const {
|
||||
operations,
|
||||
additionalOperations,
|
||||
webhooks,
|
||||
schemas,
|
||||
mcpTools,
|
||||
mcpResources,
|
||||
mcpPrompts,
|
||||
} = list.reduce(
|
||||
(acc, item) => {
|
||||
if (item.type === 'operation' && (item as OperationMenuItem).isWebhook) {
|
||||
acc.webhooks.push(item as OperationMenuItem);
|
||||
} else if (item.type === 'operation') {
|
||||
acc.operations.push(item as OperationMenuItem);
|
||||
const operationItem = item as OperationMenuItem;
|
||||
if (operationItem.isAdditionalOperation) {
|
||||
acc.additionalOperations.push(operationItem);
|
||||
} else {
|
||||
acc.operations.push(operationItem);
|
||||
}
|
||||
} else if (item.type === 'schema') {
|
||||
acc.schemas.push(item);
|
||||
} else if (item.type === 'mcp') {
|
||||
} else if (item.type === 'tool') {
|
||||
acc.mcpTools.push(item);
|
||||
} else if (item.type === 'rsrc') {
|
||||
acc.mcpResources.push(item);
|
||||
} else if (item.type === 'prompt') {
|
||||
acc.mcpPrompts.push(item);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
operations: [] as OperationMenuItem[],
|
||||
additionalOperations: [] as OperationMenuItem[],
|
||||
webhooks: [] as OperationMenuItem[],
|
||||
schemas: [] as GroupModel[],
|
||||
mcpTools: [] as GroupModel[],
|
||||
mcpResources: [] as GroupModel[],
|
||||
mcpPrompts: [] as GroupModel[],
|
||||
},
|
||||
);
|
||||
return {
|
||||
operations,
|
||||
operations: [...operations, ...additionalOperations],
|
||||
schemas,
|
||||
mcpTools,
|
||||
webhooks,
|
||||
mcpResources,
|
||||
mcpPrompts,
|
||||
showMoreCount: !isExpanded && all.length > MAX_OPERATIONS ? all.length - MAX_OPERATIONS : 0,
|
||||
totalCount: all.length,
|
||||
};
|
||||
|
|
@ -86,6 +116,24 @@ export function OperationsNavigationComponent({
|
|||
title={translate('openapi.mcp.tools', 'MCP Tools')}
|
||||
/>
|
||||
)}
|
||||
{mcpResources.length > 0 && (
|
||||
<OperationNavigationItems
|
||||
items={mcpResources}
|
||||
onClick={handleOnClick}
|
||||
routingBasePath={routingBasePath}
|
||||
translate={translate}
|
||||
title={translate('openapi.mcp.resources', 'MCP Resources')}
|
||||
/>
|
||||
)}
|
||||
{mcpPrompts.length > 0 && (
|
||||
<OperationNavigationItems
|
||||
items={mcpPrompts}
|
||||
onClick={handleOnClick}
|
||||
routingBasePath={routingBasePath}
|
||||
translate={translate}
|
||||
title={translate('openapi.mcp.prompts', 'MCP Prompts')}
|
||||
/>
|
||||
)}
|
||||
{operations.length > 0 && (
|
||||
<OperationNavigationItems
|
||||
items={operations}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { ViewNested } from '../ViewNested';
|
||||
import { ViewNested } from '../ViewNested.js';
|
||||
|
||||
describe('ViewNested', () => {
|
||||
it('should render nested array content directly without show properties button', () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { render } from '@testing-library/react';
|
||||
|
||||
import { ExampleSwitch } from '../../Samples';
|
||||
import { exampleXML, examples } from '../mocks/examples';
|
||||
import { ExampleSwitch } from '../../Samples/index.js';
|
||||
import { exampleXML, examples } from '../mocks/examples.js';
|
||||
|
||||
describe('ExampleSwitch', () => {
|
||||
const exampleKey = 'cat';
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { normalizeOptions, OpenAPIParser } from '../../../services';
|
||||
import { getOperation } from '../../../models';
|
||||
import {
|
||||
normalizeOptions,
|
||||
OpenAPIParser,
|
||||
type ExtendedOpenAPIOperation,
|
||||
} from '../../../services/index.js';
|
||||
import { getOperation } from '../../../models/index.js';
|
||||
import testDefinition from '../../MediaTypeSwitch/__tests__/fixtures/test-definition.json';
|
||||
import { useExampleKey } from '../../Samples/use-example-key';
|
||||
import { useExampleKey } from '../../Samples/use-example-key.js';
|
||||
import type { OpenAPIDefinition } from '../../../types/index.js';
|
||||
|
||||
describe('useExampleKey method', () => {
|
||||
const options = normalizeOptions({});
|
||||
const parser = new OpenAPIParser(testDefinition, undefined, options);
|
||||
const parser = new OpenAPIParser(
|
||||
testDefinition as unknown as OpenAPIDefinition,
|
||||
undefined,
|
||||
options,
|
||||
);
|
||||
|
||||
test('should take firstKey as exampleKey if activeExampleName and defaultExampleName are missing', () => {
|
||||
const operation = getOperation(
|
||||
|
|
@ -21,7 +30,7 @@ describe('useExampleKey method', () => {
|
|||
isWebhook: false,
|
||||
operationId: 'getPet',
|
||||
...testDefinition.paths['/test'].get,
|
||||
},
|
||||
} as unknown as ExtendedOpenAPIOperation,
|
||||
undefined,
|
||||
options,
|
||||
'',
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { vi } from 'vitest';
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { ClearButton } from '../../common/ClearButton';
|
||||
import { ClearButton } from '../../common/ClearButton/index.js';
|
||||
|
||||
describe('ClearButton component', () => {
|
||||
const handleClear = jest.fn();
|
||||
const handleClear = vi.fn();
|
||||
|
||||
it('render component correctly', () => {
|
||||
const { container } = render(<ClearButton handleClear={handleClear} />);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { render, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { Select, SimpleSelect } from '../../common/Select';
|
||||
import { Select, SimpleSelect } from '../../common/Select/index.js';
|
||||
|
||||
describe('Select component', () => {
|
||||
const onChange = jest.fn();
|
||||
const onChange = vi.fn();
|
||||
const options = [
|
||||
{ idx: 0, title: 'First option', value: 'option1' },
|
||||
{ idx: 1, title: 'Second option', value: 'option2' },
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('render Select component correctly', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ClearButton component render component correctly 1`] = `
|
||||
exports[`ClearButton component > render component correctly 1`] = `
|
||||
.c0 {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Select component render Select component correctly 1`] = `
|
||||
.c1 {
|
||||
-webkit-flex-shrink: 0;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c1 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Select component > render Select component correctly 1`] = `
|
||||
.c0 {
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
|
|
@ -87,6 +75,18 @@ exports[`Select component render Select component correctly 1`] = `
|
|||
box-shadow: none;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
-webkit-flex-shrink: 0;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c1 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
|
|
@ -130,19 +130,7 @@ exports[`Select component render Select component correctly 1`] = `
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`Select component render SimpleSelect dark variant correctly 1`] = `
|
||||
.c2 {
|
||||
-webkit-flex-shrink: 0;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c2 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
exports[`Select component > render SimpleSelect dark variant correctly 1`] = `
|
||||
.c0 {
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
|
|
@ -240,6 +228,18 @@ exports[`Select component render SimpleSelect dark variant correctly 1`] = `
|
|||
box-shadow: none;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
-webkit-flex-shrink: 0;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c2 path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import type { SelectOption } from '@redocly/theme/core/openapi';
|
||||
import type { SegmentedOption } from '@redocly/theme/core/openapi';
|
||||
|
||||
import { Dropdown as DropdownTheme } from '@redocly/theme/components/Dropdown/Dropdown';
|
||||
import { CheckmarkIcon } from '@redocly/theme/icons/CheckmarkIcon/CheckmarkIcon';
|
||||
|
|
@ -15,8 +15,8 @@ import { useTranslate } from '../../../hooks/index.js';
|
|||
|
||||
export interface DropdownProps<T> {
|
||||
value: T;
|
||||
onChange: ({ label, value }: SelectOption<T>) => void;
|
||||
options: SelectOption<T>[];
|
||||
onChange: ({ label, value }: SegmentedOption<T>) => void;
|
||||
options: SegmentedOption<T>[];
|
||||
className?: string;
|
||||
triggerVariant?: 'ghost' | 'outlined';
|
||||
triggerSize?: string;
|
||||
|
|
@ -35,7 +35,7 @@ function DropdownComponent<T>({
|
|||
const [searchValue, setSearchValue] = useState('');
|
||||
const translate = useTranslate();
|
||||
|
||||
const activeLabel = options.find((opt) => opt.value === value)?.label;
|
||||
const activeOption = options.find((opt) => opt.value === value);
|
||||
|
||||
const filteredOptions = useMemo(
|
||||
() =>
|
||||
|
|
@ -45,7 +45,7 @@ function DropdownComponent<T>({
|
|||
[options, searchValue],
|
||||
);
|
||||
|
||||
if (options.length === 1) return <Title>{activeLabel}</Title>;
|
||||
if (options.length === 1) return <Title>{activeOption?.element || activeOption?.label}</Title>;
|
||||
|
||||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchValue(event.target.value);
|
||||
|
|
@ -55,15 +55,18 @@ function DropdownComponent<T>({
|
|||
if (!withSearch) {
|
||||
return (
|
||||
<>
|
||||
{options.map((opt) => (
|
||||
<StyledDropdownMenuItem
|
||||
key={opt.label}
|
||||
active={opt.value === value}
|
||||
onAction={() => onChange(opt)}
|
||||
suffix={opt.value === value && <StyledCheckmarkIcon />}
|
||||
>
|
||||
{opt.label}
|
||||
</StyledDropdownMenuItem>
|
||||
{options.map((opt, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{opt.divider}
|
||||
<StyledDropdownMenuItem
|
||||
key={opt.label}
|
||||
active={opt.value === value}
|
||||
onAction={() => onChange(opt)}
|
||||
suffix={opt.value === value && <StyledCheckmarkIcon />}
|
||||
>
|
||||
{opt.element || opt.label}
|
||||
</StyledDropdownMenuItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
|
@ -91,15 +94,18 @@ function DropdownComponent<T>({
|
|||
/>
|
||||
<ScrollableContainer>
|
||||
{filteredOptions.length ? (
|
||||
filteredOptions.map((opt) => (
|
||||
<StyledDropdownMenuItem
|
||||
key={opt.label}
|
||||
active={opt.value === value}
|
||||
onAction={() => onChange(opt)}
|
||||
suffix={opt.value === value && <StyledCheckmarkIcon />}
|
||||
>
|
||||
{opt.label}
|
||||
</StyledDropdownMenuItem>
|
||||
filteredOptions.map((opt, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{opt.divider}
|
||||
<StyledDropdownMenuItem
|
||||
key={opt.label}
|
||||
active={opt.value === value}
|
||||
onAction={() => onChange(opt)}
|
||||
suffix={opt.value === value && <StyledCheckmarkIcon />}
|
||||
>
|
||||
{opt.element || opt.label}
|
||||
</StyledDropdownMenuItem>
|
||||
</React.Fragment>
|
||||
))
|
||||
) : (
|
||||
<StyledNoResultsDropdownMenuItem
|
||||
|
|
@ -116,7 +122,7 @@ function DropdownComponent<T>({
|
|||
className={className}
|
||||
trigger={
|
||||
<Button variant={triggerVariant} size={triggerSize} type="button">
|
||||
{activeLabel}
|
||||
{activeOption?.element || activeOption?.label}
|
||||
</Button>
|
||||
}
|
||||
withArrow
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import type { SelectOption } from '@redocly/theme/core/openapi';
|
||||
import type { SegmentedOption } from '@redocly/theme/core/openapi';
|
||||
import type { SchemaModel } from '../../../models/index.js';
|
||||
|
||||
import { Segmented } from '@redocly/theme/components/Segmented/Segmented';
|
||||
import { isUndefined } from '@redocly/theme/core/openapi';
|
||||
import { Segmented } from '@redocly/theme/components/Segmented/Segmented';
|
||||
|
||||
import { Dropdown } from '../Dropdown/index.js';
|
||||
import { operationStore } from '../../../jotai/operation.js';
|
||||
|
|
@ -14,7 +14,7 @@ const LIMIT_FOR_SEGMENTED = 5;
|
|||
const ENABLE_SEARCH_THRESHOLD = 7;
|
||||
|
||||
interface SchemaSelectionProps {
|
||||
options: SelectOption<number>[];
|
||||
options: SegmentedOption<number>[];
|
||||
onChange?: (idx: number) => void;
|
||||
pointer: string;
|
||||
schema: SchemaModel;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user