Switch to Axios, Panel component test
This commit is contained in:
@@ -4,7 +4,6 @@ import ArticleSelector from "./components/ArticleSelector/ArticleSelector";
|
||||
import Panel from "./components/Panel/Panel";
|
||||
import TOC from "./components/TOC/TOC";
|
||||
|
||||
import useNavState from "./store/navStore";
|
||||
import useUIStore from "./store/uiStore";
|
||||
|
||||
import styles from "./App.module.css";
|
||||
@@ -13,7 +12,6 @@ import { useTOC } from "./hooks/toc";
|
||||
|
||||
function App() {
|
||||
const { numPanels, addPanel } = useUIStore();
|
||||
const { celexId, articleId } = useNavState();
|
||||
const { data: toc, isPending, error } = useTOC();
|
||||
|
||||
if (isPending) {
|
||||
@@ -35,11 +33,9 @@ function App() {
|
||||
{Array.from({ length: numPanels }, (_, index) => (
|
||||
<Panel
|
||||
key={index}
|
||||
celexId={celexId!}
|
||||
language={
|
||||
Object.values(Language)[index % Object.values(Language).length]
|
||||
}
|
||||
articleId={articleId!}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
128
frontend/src/components/Panel/Panel.test.tsx
Normal file
128
frontend/src/components/Panel/Panel.test.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { getArticle } from "../../lib/api";
|
||||
import { Language } from "../../lib/types";
|
||||
import useNavState from "../../store/navStore";
|
||||
import useUIStore from "../../store/uiStore";
|
||||
import Panel from "./Panel";
|
||||
|
||||
jest.mock("../../store/uiStore");
|
||||
jest.mock("../../store/navStore");
|
||||
jest.mock("../../lib/api");
|
||||
jest.mock("../../constants", () =>
|
||||
Promise.resolve({
|
||||
API_URL: "http://localhost:8000/api", // Mock the API_URL to a local server for testing
|
||||
})
|
||||
);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
const renderWithClient = (ui: React.ReactElement) => {
|
||||
const { rerender, ...result } = render(
|
||||
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
|
||||
);
|
||||
return {
|
||||
...result,
|
||||
rerender: (rerenderUi: React.ReactElement) =>
|
||||
rerender(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{rerenderUi}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const wrapper = ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
describe("Panel Component", () => {
|
||||
const mockSetSelectedParagraphId = jest.fn();
|
||||
const mockUseUIStore = {
|
||||
selectedParagraphId: null,
|
||||
setSelectedParagraphId: mockSetSelectedParagraphId,
|
||||
};
|
||||
const mockNavState = {
|
||||
celexId: "123",
|
||||
articleId: 1,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(useNavState).mockReturnValue(mockNavState);
|
||||
jest.mocked(useUIStore).mockReturnValue(mockUseUIStore);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
test("renders loading state", () => {
|
||||
(getArticle as jest.Mock).mockReturnValue(new Promise(() => {}));
|
||||
const { getByText } = render(<Panel />, { wrapper });
|
||||
expect(getByText("Loading...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders error state", async () => {
|
||||
(getArticle as jest.Mock).mockRejectedValue(new Error("Failed to fetch"));
|
||||
const { findByText } = renderWithClient(<Panel />);
|
||||
expect(
|
||||
await findByText("An error has occurred: Failed to fetch")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders article content", async () => {
|
||||
const mockData = `<div class='paragraph' data-paragraph-id='1'>Test Content</div>`;
|
||||
jest.mocked(getArticle).mockResolvedValue(mockData);
|
||||
|
||||
const result = renderWithClient(<Panel />);
|
||||
expect(await result.findByText("Test Content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("highlights a paragraph on click", async () => {
|
||||
const mockData = `
|
||||
<div class='paragraph' data-paragraph-id='1'>Paragraph 1</div>
|
||||
<div class='paragraph' data-paragraph-id='2'>Paragraph 2</div>
|
||||
`;
|
||||
(getArticle as jest.Mock).mockResolvedValue(mockData);
|
||||
|
||||
const result = renderWithClient(<Panel />);
|
||||
|
||||
const paragraph1 = await result.findByText("Paragraph 1");
|
||||
const paragraph2 = await result.findByText("Paragraph 2");
|
||||
|
||||
fireEvent.click(paragraph1);
|
||||
expect(paragraph1.classList.contains("highlight")).toBe(true);
|
||||
expect(paragraph2.classList.contains("highlight")).toBe(false);
|
||||
expect(mockSetSelectedParagraphId).toHaveBeenCalledWith("1");
|
||||
|
||||
fireEvent.click(paragraph2);
|
||||
expect(paragraph1.classList.contains("highlight")).toBe(false);
|
||||
expect(paragraph2.classList.contains("highlight")).toBe(true);
|
||||
expect(mockSetSelectedParagraphId).toHaveBeenCalledWith("2");
|
||||
});
|
||||
|
||||
test("renders LanguageSwitcher and updates language", async () => {
|
||||
jest
|
||||
.mocked(getArticle)
|
||||
.mockResolvedValue(
|
||||
"<div class='paragraph' data-paragraph-id='1'>Test Content</div>"
|
||||
);
|
||||
const result = renderWithClient(<Panel language={Language.FRA} />);
|
||||
|
||||
const languageSwitcher = await result.findByRole("combobox");
|
||||
expect(languageSwitcher).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(languageSwitcher, { target: { value: Language.ENG } });
|
||||
expect(jest.mocked(getArticle)).toHaveBeenCalledWith(
|
||||
"123",
|
||||
1,
|
||||
Language.ENG
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,31 +1,25 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import DOMPurify from "dompurify";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { getArticle } from "../../lib/api";
|
||||
import { Language } from "../../lib/types";
|
||||
import useUIStore from "../../store/uiStore";
|
||||
import LanguageSwitcher from "../LanguageSwitcher/LanguageSwitcher";
|
||||
|
||||
import { useArticle } from "../../hooks/useArticle";
|
||||
import useNavState from "../../store/navStore";
|
||||
import "../../styles/PanelContent.css";
|
||||
import styles from "./Panel.module.css";
|
||||
|
||||
type PanelProps = {
|
||||
celexId: string;
|
||||
language?: Language;
|
||||
articleId: number;
|
||||
};
|
||||
|
||||
function Panel({ celexId, language, articleId }: PanelProps) {
|
||||
function Panel({ language }: PanelProps) {
|
||||
const { selectedParagraphId, setSelectedParagraphId } = useUIStore();
|
||||
|
||||
const [lang, setLang] = useState(language || Language.ENG);
|
||||
const articleRef = useRef<HTMLDivElement>(null);
|
||||
const { data, isPending, error } = useQuery({
|
||||
queryKey: ["article", celexId, articleId, lang],
|
||||
queryFn: () => getArticle(celexId, articleId, lang),
|
||||
enabled: !!celexId && !!articleId,
|
||||
});
|
||||
const { articleId, celexId } = useNavState();
|
||||
const { data, isPending, error } = useArticle(celexId, articleId, lang);
|
||||
|
||||
useEffect(() => {
|
||||
const articleElement = articleRef.current;
|
||||
@@ -85,7 +79,7 @@ function Panel({ celexId, language, articleId }: PanelProps) {
|
||||
<div
|
||||
ref={articleRef}
|
||||
lang={lang.substring(0, 2)}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(data) || "" }}
|
||||
dangerouslySetInnerHTML={{ __html: data }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
3
frontend/src/constants.ts
Normal file
3
frontend/src/constants.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
const { VITE_API_URL: API_URL } = import.meta.env;
|
||||
|
||||
export { API_URL };
|
||||
15
frontend/src/hooks/useArticle.ts
Normal file
15
frontend/src/hooks/useArticle.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getArticle } from "../lib/api";
|
||||
import { Language } from "../lib/types";
|
||||
|
||||
export const useArticle = (
|
||||
celexId: string | null,
|
||||
articleId: number | null,
|
||||
lang: Language
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ["article", celexId, articleId, lang],
|
||||
queryFn: () => getArticle(celexId!, articleId!, lang),
|
||||
enabled: !!celexId && !!articleId,
|
||||
});
|
||||
};
|
||||
@@ -1,6 +1,14 @@
|
||||
import Axios from "axios";
|
||||
import { API_URL } from "../constants";
|
||||
import { Division, Language } from "./types";
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
const axios = Axios.create({
|
||||
baseURL: API_URL,
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
async function getArticle(
|
||||
celexId: string,
|
||||
@@ -10,16 +18,16 @@ async function getArticle(
|
||||
console.debug(
|
||||
`Fetching article ${article} for CELEX ID ${celexId} in ${language} language`
|
||||
);
|
||||
const response = await fetch(
|
||||
`${API_URL}/${celexId}/articles/${article}/${language}`
|
||||
const response = await axios.get<string>(
|
||||
`${celexId}/articles/${article}/${language}`
|
||||
);
|
||||
return await response.text();
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getArticleIds(celexId: string): Promise<number[]> {
|
||||
console.debug(`Fetching article list for CELEX ID ${celexId}`);
|
||||
const response = await fetch(`${API_URL}/${celexId}/articles`);
|
||||
return await response.json();
|
||||
const response = await axios.get<number[]>(`${celexId}/articles`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getToc(
|
||||
@@ -27,8 +35,8 @@ async function getToc(
|
||||
language: Language
|
||||
): Promise<Division[]> {
|
||||
console.debug(`Fetching TOC for CELEX ID ${celexId}`);
|
||||
const response = await fetch(`${API_URL}/${celexId}/toc/${language}`);
|
||||
return (await response.json()) as Division[];
|
||||
const response = await axios.get<Division[]>(`${celexId}/toc/${language}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export { getArticle, getArticleIds, getToc };
|
||||
|
||||
Reference in New Issue
Block a user