diff --git a/frontend/src/components/FileDropZone.jsx b/frontend/src/components/FileDropZone.jsx
index 6acdaa2..e7c9ab9 100644
--- a/frontend/src/components/FileDropZone.jsx
+++ b/frontend/src/components/FileDropZone.jsx
@@ -9,15 +9,29 @@ function escapeRegex(str) {
/**
* Build a regex that matches an animal's identifier in a filename.
- * Matches only when the ID is not surrounded by other alphanumeric chars,
- * so "2852" matches "rat_2852_session" but not "28520" or "X2852Y".
+ * Tries both animal_id_string and animal_name (OR logic).
+ *
+ * Rules:
+ * - Match must not be preceded or followed by an alphanumeric char,
+ * so "2852" matches "rat_2852_session" but not "28520" or "X2852Y".
+ * - Multi-word names ("Rat A") allow any separator (space, dash, underscore)
+ * between words, so "Rat A" matches "rat_a_session" and "Rat-A-data".
*/
function buildAnimalRegex(animal) {
- const ids = [];
- if (animal.animal_id_string) ids.push(escapeRegex(animal.animal_id_string));
- if (animal.animal_name) ids.push(escapeRegex(animal.animal_name));
- if (ids.length === 0) return null;
- return new RegExp(`(? {
expect(screen.getByText(/unmatched/i)).toBeInTheDocument();
});
+ // ── animal_name matching ───────────────────────────────────────────────────
+
+ it('matches by animal_name when the name appears literally in the filename', async () => {
+ const animal = { id: 'b1', animal_name: 'Homer', animal_id_string: null };
+ const row = { animal, status: { id: 's-b1', animal_id: 'b1', date: `${DATE}T00:00:00.000Z` } };
+ render();
+ dropFiles([makeFile(`${DATE}_Homer_session.csv`)]);
+ await waitFor(() => screen.getByTestId('drop-preview'));
+ expect(screen.queryByText(/unmatched/i)).not.toBeInTheDocument();
+ expect(screen.getByText(`${DATE}_Homer_session.csv`)).toBeInTheDocument();
+ });
+
+ it('matches multi-word animal_name with underscores as separator in filename', async () => {
+ const animal = { id: 'b1', animal_name: 'Rat A', animal_id_string: null };
+ const row = { animal, status: { id: 's-b1', animal_id: 'b1', date: `${DATE}T00:00:00.000Z` } };
+ render();
+ dropFiles([makeFile(`${DATE}_Rat_A_trials.csv`)]);
+ await waitFor(() => screen.getByTestId('drop-preview'));
+ expect(screen.queryByText(/unmatched/i)).not.toBeInTheDocument();
+ });
+
+ it('matches multi-word animal_name with dashes as separator in filename', async () => {
+ const animal = { id: 'b1', animal_name: 'Rat A', animal_id_string: null };
+ const row = { animal, status: { id: 's-b1', animal_id: 'b1', date: `${DATE}T00:00:00.000Z` } };
+ render();
+ dropFiles([makeFile(`${DATE}-Rat-A-session.bin`)]);
+ await waitFor(() => screen.getByTestId('drop-preview'));
+ expect(screen.queryByText(/unmatched/i)).not.toBeInTheDocument();
+ });
+
+ it('matches by animal_name even when animal_id_string is also set (OR logic)', async () => {
+ const animal = { id: 'b1', animal_name: 'Rat A', animal_id_string: '2852' };
+ const row = { animal, status: { id: 's-b1', animal_id: 'b1', date: `${DATE}T00:00:00.000Z` } };
+ render();
+ // File uses name, not ID
+ dropFiles([makeFile(`${DATE}_Rat_A_ephys.bin`)]);
+ await waitFor(() => screen.getByTestId('drop-preview'));
+ expect(screen.queryByText(/unmatched/i)).not.toBeInTheDocument();
+ });
+
+ it('does not match a partial name overlap (Rat should not match Rationale)', async () => {
+ const animal = { id: 'b1', animal_name: 'Rat', animal_id_string: null };
+ const row = { animal, status: { id: 's-b1', animal_id: 'b1', date: `${DATE}T00:00:00.000Z` } };
+ render();
+ dropFiles([makeFile('Rationale_study_data.csv')]);
+ await waitFor(() => screen.getByTestId('drop-preview'));
+ expect(screen.getByText(/unmatched/i)).toBeInTheDocument();
+ });
+
it('shows warning for subjects with no matched files', async () => {
render();
// Only drop file for 2852 — 3076 and 3077 get no match