From 66e79c1780aefbdf3351d2a82581929f6d5918da Mon Sep 17 00:00:00 2001 From: Experiments DB Dev Date: Thu, 23 Apr 2026 13:33:10 -0400 Subject: [PATCH] fix: match filenames against both animal_id_string and animal_name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-word names ("Rat A") now allow any word separator in the filename — space, underscore, or dash — so "Rat_A_session.csv" and "Rat-A-data.bin" both match. Single-word names and numeric IDs are unchanged. Partial overlaps still rejected ("Rat" won't hit "Rationale"). Tests: 5 new name-based match cases (24 total). Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/FileDropZone.jsx | 28 ++++++++++---- frontend/tests/FileDropZone.test.jsx | 49 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) 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