From 2f42e9acdbe9b7b50ead8da470f2899a37094f72 Mon Sep 17 00:00:00 2001
From: HexWarrior6 <3083512469@qq.com>
Date: Fri, 14 Nov 2025 10:13:20 +0800
Subject: [PATCH 1/4] feat(search): add export search results to CSV
functionality
---
frontend/src/components/SearchArticles.tsx | 29 ++++++-
frontend/src/styles/SearchPage.module.scss | 36 +++++++++
frontend/src/utils/csv.utils.ts | 91 ++++++++++++++++++++++
3 files changed, 153 insertions(+), 3 deletions(-)
create mode 100644 frontend/src/utils/csv.utils.ts
diff --git a/frontend/src/components/SearchArticles.tsx b/frontend/src/components/SearchArticles.tsx
index 480fee6..9fbbad9 100644
--- a/frontend/src/components/SearchArticles.tsx
+++ b/frontend/src/components/SearchArticles.tsx
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from "react";
import { Article, EvidenceType, ArticleStatus } from "../types/article";
+import { exportToCSV } from "../utils/csv.utils";
import styles from "../styles/SearchPage.module.scss";
// Enhanced search functionality with real-time suggestions and search history
@@ -542,9 +543,31 @@ const SearchArticles: React.FC = () => {
Found {results.length} results
-
- Sorting:
{sortField}{" "}
- {sortDirection === "asc" ? "↑" : "↓"}
+
+
+ Sorting: {sortField}{" "}
+ {sortDirection === "asc" ? "↑" : "↓"}
+
+
diff --git a/frontend/src/styles/SearchPage.module.scss b/frontend/src/styles/SearchPage.module.scss
index d514dbb..b7cfd70 100644
--- a/frontend/src/styles/SearchPage.module.scss
+++ b/frontend/src/styles/SearchPage.module.scss
@@ -445,6 +445,12 @@
color: #374151;
}
+.resultsActions {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
.sortInfo {
font-size: 0.875rem;
color: #6b7280;
@@ -455,6 +461,36 @@
color: #374151;
}
+.exportButton {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ background-color: #10b981; // Green background
+ color: white;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.15s;
+
+ &:hover:not(:disabled) {
+ background-color: #059669; // Darker green on hover
+ }
+
+ &:disabled {
+ background-color: #9ca3af; // Gray when disabled
+ cursor: not-allowed;
+ }
+}
+
+.exportIcon {
+ width: 16px;
+ height: 16px;
+}
+
.tableContainer {
overflow-x: auto;
border-radius: 8px;
diff --git a/frontend/src/utils/csv.utils.ts b/frontend/src/utils/csv.utils.ts
new file mode 100644
index 0000000..c34f6b9
--- /dev/null
+++ b/frontend/src/utils/csv.utils.ts
@@ -0,0 +1,91 @@
+/**
+ * Utility function to export article data as CSV
+ */
+import { Article } from '../types/article';
+
+export const exportToCSV = (articles: Article[], filename: string = 'search-results.csv'): void => {
+ if (!articles || articles.length === 0) {
+ console.warn('No articles to export');
+ return;
+ }
+
+ // Define CSV headers
+ const headers = [
+ 'ID',
+ 'Title',
+ 'Authors',
+ 'Source',
+ 'Year',
+ 'DOI',
+ 'Claim',
+ 'Evidence Type',
+ 'Status',
+ 'Submitter ID',
+ 'Submitter Email',
+ 'Reviewer ID',
+ 'Review Comment',
+ 'Created At',
+ 'Updated At',
+ 'Is Duplicate',
+ 'Duplicate Of'
+ ];
+
+ // Prepare CSV content
+ const csvContent = [
+ headers.join(','),
+ ...articles.map(article => {
+ return [
+ `"${escapeCsvField(article.customId)}"`,
+ `"${escapeCsvField(article.title)}"`,
+ `"${escapeCsvField(article.authors)}"`,
+ `"${escapeCsvField(article.source)}"`,
+ `"${escapeCsvField(article.pubyear)}"`,
+ `"${escapeCsvField(article.doi)}"`,
+ `"${escapeCsvField(article.claim)}"`,
+ `"${escapeCsvField(article.evidence)}"`,
+ `"${escapeCsvField(article.status)}"`,
+ `"${escapeCsvField(article.submitterId || '')}"`,
+ `"${escapeCsvField(article.submitterEmail || '')}"`,
+ `"${escapeCsvField(article.reviewerId || '')}"`,
+ `"${escapeCsvField(article.reviewComment || '')}"`,
+ `"${escapeCsvField(article.createdAt || '')}"`,
+ `"${escapeCsvField(article.updatedAt || '')}"`,
+ `"${escapeCsvField(article.isDuplicate ? 'Yes' : 'No')}"`,
+ `"${escapeCsvField(article.duplicateOf || '')}"`
+ ].join(',');
+ })
+ ].join('\n');
+
+ // Create and download the CSV file
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+
+ if (link.download !== undefined) {
+ const url = URL.createObjectURL(blob);
+ link.setAttribute('href', url);
+ link.setAttribute('download', filename);
+ link.style.visibility = 'hidden';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ } else {
+ console.error('CSV download is not supported in this browser');
+ }
+};
+
+// Helper function to escape CSV fields that might contain commas, quotes, or newlines
+const escapeCsvField = (field: string | number | boolean | undefined | null): string => {
+ if (field === null || field === undefined) {
+ return '';
+ }
+
+ const str = String(field);
+
+ // If the string contains commas, double quotes, or newlines, wrap it in double quotes
+ // Also escape any double quotes by replacing them with two double quotes
+ if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
+ return '"' + str.replace(/"/g, '""') + '"';
+ }
+
+ return str;
+};
\ No newline at end of file
From f50a1af43519b02b8348151a62e9b8e0e5a365bd Mon Sep 17 00:00:00 2001
From: HexWarrior6 <3083512469@qq.com>
Date: Fri, 14 Nov 2025 10:48:52 +0800
Subject: [PATCH 2/4] feat(form): add notification email selection feature and
optimize styling
---
frontend/src/components/SubmitterForm.tsx | 101 ++++++++++++----------
frontend/src/styles/design-system.scss | 52 +++++++++++
2 files changed, 107 insertions(+), 46 deletions(-)
diff --git a/frontend/src/components/SubmitterForm.tsx b/frontend/src/components/SubmitterForm.tsx
index c08fb37..f71de9c 100644
--- a/frontend/src/components/SubmitterForm.tsx
+++ b/frontend/src/components/SubmitterForm.tsx
@@ -2,6 +2,7 @@ import React, { useState } from "react";
import { Formik, Form, Field, ErrorMessage, FormikProps } from "formik";
import * as Yup from "yup";
import { EvidenceType } from "../types/article";
+import { useAuth } from "../contexts/AuthContext";
interface ArticleFormData {
customId: string;
@@ -12,7 +13,7 @@ interface ArticleFormData {
doi: string;
claim: string;
evidence: string;
- submitterEmail: string;
+ useEmailForNotification: boolean;
}
interface SubmitArticleProps {
@@ -45,15 +46,14 @@ const validationSchema = Yup.object().shape({
.required("Claim is required")
.min(10, "Claim must be at least 10 characters long"),
evidence: Yup.string().required("Evidence type is required"),
- submitterEmail: Yup.string()
- .required("Email is required for notification of review results")
- .email("Please enter a valid email address"),
+ useEmailForNotification: Yup.boolean(),
});
const SubmitterForm: React.FC = ({ onSubmitSuccess }) => {
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);
+ const { user } = useAuth();
const handleSubmit = async (values: ArticleFormData) => {
setSubmitting(true);
@@ -61,10 +61,32 @@ const SubmitterForm: React.FC = ({ onSubmitSuccess }) => {
setSuccess(null);
try {
+ // Prepare the submission payload
+ // If customId is empty, don't include it in the request so the backend can auto-generate it
+ let submissionValues;
+ if (!values.customId?.trim()) {
+ // Omit customId if it's empty
+ const { customId: _, ...valuesWithoutCustomId } = values;
+ submissionValues = valuesWithoutCustomId;
+ } else {
+ submissionValues = values;
+ }
+
+ // Conditionally include submitterEmail based on user selection
+ let finalSubmissionValues;
+ if (values.useEmailForNotification && user?.email) {
+ finalSubmissionValues = { ...submissionValues, submitterEmail: user.email };
+ } else {
+ finalSubmissionValues = { ...submissionValues, submitterEmail: '' };
+ }
+
+ // Remove the useEmailForNotification field from the final submission
+ const { useEmailForNotification, ...cleanSubmissionValues } = finalSubmissionValues;
+
// Check if ID already exists - only if customId is provided
- if (values.customId && values.customId.trim()) {
+ if (cleanSubmissionValues.customId && cleanSubmissionValues.customId.trim()) {
const idCheckResponse = await fetch(
- `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/articles/${values.customId}`,
+ `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/articles/${cleanSubmissionValues.customId}`,
{
method: "GET",
headers: {
@@ -75,22 +97,11 @@ const SubmitterForm: React.FC = ({ onSubmitSuccess }) => {
if (idCheckResponse.ok) {
throw new Error(
- `Article with ID '${values.customId}' already exists. Please choose a different ID.`
+ `Article with ID '${cleanSubmissionValues.customId}' already exists. Please choose a different ID.`
);
}
}
- // Prepare the submission payload
- // If customId is empty, don't include it in the request so the backend can auto-generate it
- let submissionValues;
- if (!values.customId?.trim()) {
- // Omit customId if it's empty
- const { customId: _, ...valuesWithoutCustomId } = values;
- submissionValues = valuesWithoutCustomId;
- } else {
- submissionValues = values;
- }
-
// Proceed with submission
const response = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/articles/submit`,
@@ -100,7 +111,7 @@ const SubmitterForm: React.FC = ({ onSubmitSuccess }) => {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
- body: JSON.stringify(submissionValues),
+ body: JSON.stringify(cleanSubmissionValues),
}
);
@@ -152,7 +163,7 @@ const SubmitterForm: React.FC = ({ onSubmitSuccess }) => {
return (
{success && (
-
+
- {success}
+
{success}
)}
{error && (
-
+
- {error}
+
{error}
)}
@@ -195,7 +206,7 @@ const SubmitterForm: React.FC
= ({ onSubmitSuccess }) => {
doi: "",
claim: "",
evidence: "",
- submitterEmail: "",
+ useEmailForNotification: true, // Default to checked
}}
validationSchema={validationSchema}
onSubmit={handleSubmit}
@@ -335,26 +346,24 @@ const SubmitterForm: React.FC = ({ onSubmitSuccess }) => {
-
-
-
-
- You will receive review results at this email address
-
+
+
+
+
+
diff --git a/frontend/src/styles/design-system.scss b/frontend/src/styles/design-system.scss
index 850114c..4cd9539 100644
--- a/frontend/src/styles/design-system.scss
+++ b/frontend/src/styles/design-system.scss
@@ -309,6 +309,14 @@ body {
border: 1px solid var(--warning-200);
}
+// Alert icon styling
+.alert-icon {
+ width: 1.25rem;
+ height: 1.25rem;
+ margin-right: var(--spacing-sm);
+ flex-shrink: 0;
+}
+
// Utility classes
.text-center {
text-align: center;
@@ -350,6 +358,50 @@ body {
margin-top: var(--spacing-lg);
}
+.ml-sm {
+ margin-left: var(--spacing-sm);
+}
+
+.mr-sm {
+ margin-right: var(--spacing-sm);
+}
+
+// Checkbox styling
+.form-checkbox {
+ width: 1.25rem;
+ height: 1.25rem;
+ min-width: 1.25rem;
+ min-height: 1.25rem;
+ border: 2px solid var(--neutral-400);
+ border-radius: var(--border-radius-sm);
+ appearance: none;
+ cursor: pointer;
+ position: relative;
+ transition: all var(--transition-fast);
+
+ &:checked {
+ background-color: var(--primary-600);
+ border-color: var(--primary-600);
+
+ &::after {
+ content: '';
+ position: absolute;
+ left: 4px;
+ top: 1px;
+ width: 5px;
+ height: 10px;
+ border: solid white;
+ border-width: 0 2px 2px 0;
+ transform: rotate(45deg);
+ }
+ }
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
+ }
+}
+
.d-flex {
display: flex;
}
From 278fe6b383cd38062fd4fdb046c490275e8b5d4c Mon Sep 17 00:00:00 2001
From: HexWarrior6 <3083512469@qq.com>
Date: Fri, 14 Nov 2025 11:11:09 +0800
Subject: [PATCH 3/4] refactor(SubmitterForm): refactor form submission logic
to clarify submission data structure and resolve Vercel deployment issues
---
frontend/src/components/SubmitterForm.tsx | 23 +++++++++++++----------
1 file changed, 13 insertions(+), 10 deletions(-)
diff --git a/frontend/src/components/SubmitterForm.tsx b/frontend/src/components/SubmitterForm.tsx
index f71de9c..0a78433 100644
--- a/frontend/src/components/SubmitterForm.tsx
+++ b/frontend/src/components/SubmitterForm.tsx
@@ -63,7 +63,7 @@ const SubmitterForm: React.FC = ({ onSubmitSuccess }) => {
try {
// Prepare the submission payload
// If customId is empty, don't include it in the request so the backend can auto-generate it
- let submissionValues;
+ let submissionValues: Partial | ArticleFormData;
if (!values.customId?.trim()) {
// Omit customId if it's empty
const { customId: _, ...valuesWithoutCustomId } = values;
@@ -73,20 +73,17 @@ const SubmitterForm: React.FC = ({ onSubmitSuccess }) => {
}
// Conditionally include submitterEmail based on user selection
- let finalSubmissionValues;
+ let finalSubmissionValuesWithNotification;
if (values.useEmailForNotification && user?.email) {
- finalSubmissionValues = { ...submissionValues, submitterEmail: user.email };
+ finalSubmissionValuesWithNotification = { ...submissionValues, submitterEmail: user.email };
} else {
- finalSubmissionValues = { ...submissionValues, submitterEmail: '' };
+ finalSubmissionValuesWithNotification = { ...submissionValues, submitterEmail: '' };
}
- // Remove the useEmailForNotification field from the final submission
- const { useEmailForNotification, ...cleanSubmissionValues } = finalSubmissionValues;
-
// Check if ID already exists - only if customId is provided
- if (cleanSubmissionValues.customId && cleanSubmissionValues.customId.trim()) {
+ if (finalSubmissionValuesWithNotification.customId && finalSubmissionValuesWithNotification.customId.trim()) {
const idCheckResponse = await fetch(
- `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/articles/${cleanSubmissionValues.customId}`,
+ `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/articles/${finalSubmissionValuesWithNotification.customId}`,
{
method: "GET",
headers: {
@@ -97,11 +94,17 @@ const SubmitterForm: React.FC = ({ onSubmitSuccess }) => {
if (idCheckResponse.ok) {
throw new Error(
- `Article with ID '${cleanSubmissionValues.customId}' already exists. Please choose a different ID.`
+ `Article with ID '${finalSubmissionValuesWithNotification.customId}' already exists. Please choose a different ID.`
);
}
}
+ // Extract only the properties we need for the submission
+ const {
+ useEmailForNotification, // eslint-disable-line @typescript-eslint/no-unused-vars
+ ...cleanSubmissionValues
+ } = finalSubmissionValuesWithNotification;
+
// Proceed with submission
const response = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/articles/submit`,
From 9f58828ed2eefc6e7f97c5acb724e70a691fd466 Mon Sep 17 00:00:00 2001
From: HexWarrior6 <3083512469@qq.com>
Date: Fri, 14 Nov 2025 12:21:55 +0800
Subject: [PATCH 4/4] test(SearchArticles): update the test to match the new
search form implementation
---
.../components/SearchArticles.simple.test.tsx | 11 +-
.../src/components/SearchArticles.test.tsx | 118 +++++++++++-------
2 files changed, 82 insertions(+), 47 deletions(-)
diff --git a/frontend/src/components/SearchArticles.simple.test.tsx b/frontend/src/components/SearchArticles.simple.test.tsx
index c9e13ce..8dfbb26 100644
--- a/frontend/src/components/SearchArticles.simple.test.tsx
+++ b/frontend/src/components/SearchArticles.simple.test.tsx
@@ -15,9 +15,14 @@ describe('SearchArticles Component', () => {
test('renders search form correctly', () => {
render();
-
- expect(screen.getByText('Search SE Evidence Articles')).toBeInTheDocument();
- expect(screen.getByLabelText('Keywords (Title, Authors, Claim)')).toBeInTheDocument();
+
+ // Check for the main search input with the placeholder text
+ expect(screen.getByPlaceholderText('Enter keywords to search across titles, authors, and claims...')).toBeInTheDocument();
+
+ // Check for the search button
expect(screen.getByRole('button', { name: 'Search' })).toBeInTheDocument();
+
+ // Check for the Advanced Filters header
+ expect(screen.getByText('Advanced Filters')).toBeInTheDocument();
});
});
\ No newline at end of file
diff --git a/frontend/src/components/SearchArticles.test.tsx b/frontend/src/components/SearchArticles.test.tsx
index 9ba474a..8e5608b 100644
--- a/frontend/src/components/SearchArticles.test.tsx
+++ b/frontend/src/components/SearchArticles.test.tsx
@@ -45,15 +45,33 @@ describe('SearchArticles Component', () => {
test('renders search form correctly', () => {
render();
-
- expect(screen.getByText('Search SE Evidence Articles')).toBeInTheDocument();
- expect(screen.getByLabelText('Keywords (Title, Authors, Claim)')).toBeInTheDocument();
- expect(screen.getByLabelText('SE Practice (Evidence Type)')).toBeInTheDocument();
- expect(screen.getByLabelText('Publication Year From')).toBeInTheDocument();
- expect(screen.getByLabelText('Publication Year To')).toBeInTheDocument();
- expect(screen.getByLabelText('Authors')).toBeInTheDocument();
- expect(screen.getByLabelText('Status')).toBeInTheDocument();
- expect(screen.getByLabelText('Source')).toBeInTheDocument();
+
+ // Check for the main search input with the placeholder text
+ expect(screen.getByPlaceholderText('Enter keywords to search across titles, authors, and claims...')).toBeInTheDocument();
+
+ // Check for the Advanced Filters header
+ expect(screen.getByText('Advanced Filters')).toBeInTheDocument();
+
+ // Click the advanced filters header to expand it
+ fireEvent.click(screen.getByText('Advanced Filters'));
+
+ // Check for evidence type select (now visible after expansion)
+ expect(screen.getByLabelText('Select SE Practice type')).toBeInTheDocument();
+
+ // Check for status select
+ expect(screen.getByLabelText('Select article status')).toBeInTheDocument();
+
+ // Check for year from and to inputs
+ expect(screen.getByLabelText('Publication year from')).toBeInTheDocument();
+ expect(screen.getByLabelText('Publication year to')).toBeInTheDocument();
+
+ // Check for authors input
+ expect(screen.getByLabelText('Filter by authors')).toBeInTheDocument();
+
+ // Check for source input
+ expect(screen.getByLabelText('Filter by source')).toBeInTheDocument();
+
+ // Check for the search button
expect(screen.getByRole('button', { name: 'Search' })).toBeInTheDocument();
});
@@ -64,13 +82,13 @@ describe('SearchArticles Component', () => {
});
render();
-
- const keywordInput = screen.getByLabelText('Keywords (Title, Authors, Claim)');
+
+ const keywordInput = screen.getByPlaceholderText('Enter keywords to search across titles, authors, and claims...');
fireEvent.change(keywordInput, { target: { value: 'test keyword' } });
-
+
const searchButton = screen.getByRole('button', { name: 'Search' });
fireEvent.click(searchButton);
-
+
await waitFor(() => {
expect(fetch).toHaveBeenCalledWith(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/articles/search/advanced?keywords=test+keyword&sortBy=createdAt&sortDirection=desc`
@@ -85,13 +103,16 @@ describe('SearchArticles Component', () => {
});
render();
-
- const evidenceTypeSelect = screen.getByLabelText('SE Practice (Evidence Type)');
+
+ // Click the advanced filters header to expand it
+ fireEvent.click(screen.getByText('Advanced Filters'));
+
+ const evidenceTypeSelect = screen.getByLabelText('Select SE Practice type');
fireEvent.change(evidenceTypeSelect, { target: { value: EvidenceType.MODERATELY_SUPPORTS } });
-
+
const searchButton = screen.getByRole('button', { name: 'Search' });
fireEvent.click(searchButton);
-
+
await waitFor(() => {
expect(fetch).toHaveBeenCalledWith(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/articles/search/advanced?evidenceType=Moderately+Supports&sortBy=createdAt&sortDirection=desc`
@@ -106,16 +127,19 @@ describe('SearchArticles Component', () => {
});
render();
-
- const yearFromInput = screen.getByLabelText('Publication Year From');
+
+ // Click the advanced filters header to expand it
+ fireEvent.click(screen.getByText('Advanced Filters'));
+
+ const yearFromInput = screen.getByLabelText('Publication year from');
fireEvent.change(yearFromInput, { target: { value: '2020' } });
-
- const yearToInput = screen.getByLabelText('Publication Year To');
+
+ const yearToInput = screen.getByLabelText('Publication year to');
fireEvent.change(yearToInput, { target: { value: '2022' } });
-
+
const searchButton = screen.getByRole('button', { name: 'Search' });
fireEvent.click(searchButton);
-
+
await waitFor(() => {
expect(fetch).toHaveBeenCalledWith(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/articles/search/advanced?pubYearFrom=2020&pubYearTo=2022&sortBy=createdAt&sortDirection=desc`
@@ -130,20 +154,23 @@ describe('SearchArticles Component', () => {
});
render();
-
+
+ // Click the advanced filters header to expand it
+ fireEvent.click(screen.getByText('Advanced Filters'));
+
// Apply multiple filters
- const keywordInput = screen.getByLabelText('Keywords (Title, Authors, Claim)');
+ const keywordInput = screen.getByPlaceholderText('Enter keywords to search across titles, authors, and claims...');
fireEvent.change(keywordInput, { target: { value: 'test' } });
-
- const evidenceTypeSelect = screen.getByLabelText('SE Practice (Evidence Type)');
+
+ const evidenceTypeSelect = screen.getByLabelText('Select SE Practice type');
fireEvent.change(evidenceTypeSelect, { target: { value: EvidenceType.WEAK_AGAINST } });
-
- const authorsInput = screen.getByLabelText('Authors');
+
+ const authorsInput = screen.getByLabelText('Filter by authors');
fireEvent.change(authorsInput, { target: { value: 'Doe' } });
-
+
const searchButton = screen.getByRole('button', { name: 'Search' });
fireEvent.click(searchButton);
-
+
await waitFor(() => {
const fetchCallUrl = (global.fetch as jest.MockedFunction).mock.calls[0][0];
expect(fetchCallUrl).toContain('keywords=test');
@@ -161,14 +188,14 @@ describe('SearchArticles Component', () => {
});
render();
-
+
const searchButton = screen.getByRole('button', { name: 'Search' });
fireEvent.click(searchButton);
-
+
await waitFor(() => {
expect(screen.getByText('Found 2 results')).toBeInTheDocument();
});
-
+
expect(screen.getByText('Test Article 1')).toBeInTheDocument();
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Journal of Testing')).toBeInTheDocument();
@@ -179,10 +206,10 @@ describe('SearchArticles Component', () => {
(global.fetch as jest.MockedFunction).mockRejectedValueOnce(new Error('Network error'));
render();
-
+
const searchButton = screen.getByRole('button', { name: 'Search' });
fireEvent.click(searchButton);
-
+
await waitFor(() => {
expect(screen.getByText('Network error')).toBeInTheDocument();
});
@@ -190,22 +217,25 @@ describe('SearchArticles Component', () => {
test('clears all filters', async () => {
render();
-
+
+ // Click the advanced filters header to expand it
+ fireEvent.click(screen.getByText('Advanced Filters'));
+
// Apply some filters
- const keywordInput = screen.getByLabelText('Keywords (Title, Authors, Claim)');
+ const keywordInput = screen.getByPlaceholderText('Enter keywords to search across titles, authors, and claims...');
fireEvent.change(keywordInput, { target: { value: 'test' } });
-
- const evidenceTypeSelect = screen.getByLabelText('SE Practice (Evidence Type)');
+
+ const evidenceTypeSelect = screen.getByLabelText('Select SE Practice type');
fireEvent.change(evidenceTypeSelect, { target: { value: EvidenceType.WEAK_AGAINST } });
-
+
// Verify filters are applied
expect(keywordInput).toHaveValue('test');
expect(evidenceTypeSelect).toHaveValue(EvidenceType.WEAK_AGAINST);
-
+
// Click clear filters
- const clearButton = screen.getByRole('button', { name: 'Clear Filters' });
+ const clearButton = screen.getByRole('button', { name: 'Clear All' });
fireEvent.click(clearButton);
-
+
// Verify filters are cleared
expect(keywordInput).toHaveValue('');
expect(evidenceTypeSelect).toHaveValue('');