Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { render } from '@/test-utils';
import { screen, fireEvent } from '@testing-library/react';
import { describe, expect, test, vi } from 'vitest';
import { AssociatedArticlesForm } from './AssociatedArticlesForm';

const onOpenAlert = vi.fn();

describe('AssociatedArticlesForm — new bibcode auto-commit on blur', () => {
test('commits a typed bibcode to the list when focus leaves the add-bibcode group', () => {
render(<AssociatedArticlesForm onOpenAlert={onOpenAlert} />);

const stagingInput = screen.getByRole('textbox', { name: 'add new bibcode' });
fireEvent.change(stagingInput, { target: { value: '2021arXiv210100001A' } });
fireEvent.blur(stagingInput);

// Committed state: value appears as a registered input row
expect(screen.getByRole('textbox', { name: 'bibcode' })).toBeInTheDocument();
// Staging input is cleared
expect(stagingInput).toHaveValue('');
});

test('does not commit when staging input is empty on blur', () => {
render(<AssociatedArticlesForm onOpenAlert={onOpenAlert} />);

const stagingInput = screen.getByRole('textbox', { name: 'add new bibcode' });
fireEvent.blur(stagingInput);

// No committed row
expect(screen.queryByRole('textbox', { name: 'bibcode' })).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {

import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { ChangeEvent, KeyboardEvent, MouseEventHandler, useEffect, useRef, useState } from 'react';
import { ChangeEvent, FocusEvent, KeyboardEvent, MouseEventHandler, useEffect, useRef, useState } from 'react';
import { FormProvider, useFieldArray, useForm, useFormContext, useWatch } from 'react-hook-form';
import { omit } from 'ramda';

Expand Down Expand Up @@ -332,6 +332,7 @@ export const AssociatedTable = () => {
const handleAddAssociatedBibcode = () => {
append({ value: newAssociatedBibcode });
setNewAssociatedBibcode('');
newAssociatedBibcodeRef.current.focus();
};

const handleKeydownNewBibcode = (e: KeyboardEvent<HTMLInputElement>) => {
Expand All @@ -340,11 +341,15 @@ export const AssociatedTable = () => {
}
};

useEffect(() => {
if (newAssociatedBibcode === '') {
newAssociatedBibcodeRef.current.focus();
const handleBlurNewBibcodeGroup = (e: FocusEvent<HTMLElement>) => {
if (e.currentTarget.contains(e.relatedTarget as Node)) {
return;
}
if (newAssociatedBibcode.length > 0) {
append({ value: newAssociatedBibcode });
setNewAssociatedBibcode('');
}
}, [newAssociatedBibcode]);
};

return (
<>
Expand Down Expand Up @@ -423,7 +428,7 @@ export const AssociatedTable = () => {
</FormControl>

<FormControl>
<HStack>
<HStack onBlur={handleBlurNewBibcodeGroup}>
<Input
onChange={handleNewAssociatedBibcodeChange}
value={newAssociatedBibcode}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { render } from '@/test-utils';
import { screen, fireEvent, waitFor } from '@testing-library/react';
import { FormProvider, useForm } from 'react-hook-form';
import { ReactNode } from 'react';
import { describe, expect, test } from 'vitest';
import { ReferencesField } from './ReferencesField';
import { FormValues } from './types';

const Wrapper = ({ children }: { children: ReactNode }) => {
const methods = useForm<FormValues>({ defaultValues: { references: [] } });
return <FormProvider {...methods}>{children}</FormProvider>;
};

describe('ReferencesField — new reference auto-commit on blur', () => {
test('commits a typed reference to the table when focus leaves the new-reference row', async () => {
render(
<Wrapper>
<ReferencesField />
</Wrapper>,
);

const refInput = await screen.findByRole('textbox');
fireEvent.change(refInput, { target: { value: '2021arXiv210100001A' } });
fireEvent.blur(refInput);

await waitFor(() => {
expect(screen.getByText('2021arXiv210100001A')).toBeInTheDocument();
});
expect(refInput).toHaveValue('');
});

test('does not commit when the staged reference is empty on blur', async () => {
render(
<Wrapper>
<ReferencesField />
</Wrapper>,
);

const refInput = await screen.findByRole('textbox');
fireEvent.blur(refInput);

// No committed rows — header row + new-entry row = 2 rows
expect(screen.getAllByRole('row')).toHaveLength(2);
});

test('does not auto-commit when focus moves between siblings within the new-reference row', async () => {
render(
<Wrapper>
<ReferencesField />
</Wrapper>,
);

const refInput = await screen.findByRole('textbox');
fireEvent.change(refInput, { target: { value: '2021arXiv210100001A' } });

const addButton = screen.getByRole('button', { name: 'add Reference' });

fireEvent.blur(refInput, { relatedTarget: addButton });

expect(screen.getAllByRole('row')).toHaveLength(2);
});
});
18 changes: 16 additions & 2 deletions src/components/FeedbackForms/MissingRecord/ReferencesField.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CheckIcon, CloseIcon, DeleteIcon, EditIcon } from '@chakra-ui/icons';
import { FormControl, FormLabel, HStack, IconButton, Input, Table, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import { Select, SelectOption } from '@/components/Select';
import { ChangeEvent, KeyboardEvent, MouseEvent, useRef, useState } from 'react';
import { ChangeEvent, FocusEvent, KeyboardEvent, MouseEvent, useRef, useState } from 'react';
import { FormValues, IReference, ReferenceType, referenceTypes } from './types';
import { SelectInstance } from 'react-select';
import { useFieldArray } from 'react-hook-form';
Expand Down Expand Up @@ -33,6 +33,7 @@ export const ReferencesTable = ({ editable }: { editable: boolean }) => {

// New row being added
const [newReference, setNewReference] = useState<IReference>({ type: 'Bibcode', reference: '' });
const [newTypeMenuIsOpen, setNewTypeMenuIsOpen] = useState(false);

// Existing row being edited
const [editReference, setEditReference] = useState<{ index: number; reference: IReference }>({
Expand Down Expand Up @@ -107,9 +108,20 @@ export const ReferencesTable = ({ editable }: { editable: boolean }) => {
handleAddReference();
}
};

const handleBlurNewRefGroup = (e: FocusEvent<HTMLTableRowElement>) => {
if (e.currentTarget.contains(e.relatedTarget as Node) || newTypeMenuIsOpen) {
return;
}
if (newReferenceIsValid) {
append(newReference);
setNewReference({ type: 'Bibcode', reference: '' });
}
};

// Row for adding new Reference
const newReferenceTableRow = (
<Tr>
<Tr onBlur={handleBlurNewRefGroup}>
<Td color="gray.200">{references.length + 1}</Td>
<Td>
{isClient && (
Expand All @@ -122,6 +134,8 @@ export const ReferencesTable = ({ editable }: { editable: boolean }) => {
stylesTheme="default.sm"
onChange={handleNewTypeChange}
menuPortalTarget={document.body}
onMenuOpen={() => setNewTypeMenuIsOpen(true)}
onMenuClose={() => setNewTypeMenuIsOpen(false)}
ref={newReferenceInputRef}
/>
)}
Expand Down
70 changes: 70 additions & 0 deletions src/components/FeedbackForms/MissingRecord/UrlsField.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { render } from '@/test-utils';
import { screen, fireEvent, waitFor } from '@testing-library/react';
import { FormProvider, useForm } from 'react-hook-form';
import { ReactNode } from 'react';
import { describe, expect, test } from 'vitest';
import { UrlsField } from './UrlsField';
import { FormValues } from './types';

const Wrapper = ({ children }: { children: ReactNode }) => {
const methods = useForm<FormValues>({ defaultValues: { urls: [] } });
return <FormProvider {...methods}>{children}</FormProvider>;
};

describe('UrlsField — new URL auto-commit on blur', () => {
test('commits a typed URL to the table when focus leaves the new-URL row', async () => {
render(
<Wrapper>
<UrlsField />
</Wrapper>,
);

// useIsClient delays rendering the Select until after mount
const urlInput = await screen.findByRole('textbox');
fireEvent.change(urlInput, { target: { value: 'https://arxiv.org/abs/2101.00001' } });
fireEvent.blur(urlInput);

// Committed state: URL appears as text in the table (not in an input)
await waitFor(() => {
expect(screen.getByText('https://arxiv.org/abs/2101.00001')).toBeInTheDocument();
});
// Staging input is reset to the arXiv default prefix
expect(urlInput).not.toHaveValue('https://arxiv.org/abs/2101.00001');
});

test('does not commit when the staged URL is invalid on blur', async () => {
render(
<Wrapper>
<UrlsField />
</Wrapper>,
);

const urlInput = await screen.findByRole('textbox');
// Default value "https://arxiv.org/" is invalid (pathname length === 1)
fireEvent.blur(urlInput);

// No committed rows (header row + 1 new-entry row = row count stays at 2)
const rows = screen.getAllByRole('row');
expect(rows).toHaveLength(2);
});

test('does not auto-commit when focus moves between siblings within the new-URL row', async () => {
render(
<Wrapper>
<UrlsField />
</Wrapper>,
);

const urlInput = await screen.findByRole('textbox');
fireEvent.change(urlInput, { target: { value: 'https://arxiv.org/abs/2101.00001' } });

const addButton = screen.getByRole('button', { name: 'add url' });

// Blur with relatedTarget set to the Add button (sibling within the same <Tr>)
// The contains() guard should detect this and skip auto-commit
fireEvent.blur(urlInput, { relatedTarget: addButton });

// URL should NOT be committed — header row + new-entry row = 2 rows
expect(screen.getAllByRole('row')).toHaveLength(2);
});
});
17 changes: 15 additions & 2 deletions src/components/FeedbackForms/MissingRecord/UrlsField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { CheckIcon, CloseIcon, DeleteIcon, EditIcon } from '@chakra-ui/icons';
import { FormControl, FormLabel, HStack, IconButton, Input, Table, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import { Select, SelectOption } from '@/components/Select';

import { ChangeEvent, KeyboardEvent, MouseEvent, useRef, useState } from 'react';
import { ChangeEvent, FocusEvent, KeyboardEvent, MouseEvent, useRef, useState } from 'react';
import { useFieldArray } from 'react-hook-form';
import { SelectInstance } from 'react-select';
import { FormValues, IResourceUrl, ResourceUrlType, resourceUrlTypes } from './types';
Expand Down Expand Up @@ -40,6 +40,7 @@ export const UrlsTable = ({ editable }: { editable: boolean }) => {

// New row being added
const [newUrl, setNewUrl] = useState<IResourceUrl>({ type: 'arXiv', url: `${ARXIV_ORIGIN}/` });
const [newTypeMenuIsOpen, setNewTypeMenuIsOpen] = useState(false);

// Existing row being edited
const [editUrl, setEditUrl] = useState<{ index: number; url: IResourceUrl }>({
Expand Down Expand Up @@ -140,9 +141,19 @@ export const UrlsTable = ({ editable }: { editable: boolean }) => {
}
};

const handleBlurNewUrlGroup = (e: FocusEvent<HTMLTableRowElement>) => {
if (e.currentTarget.contains(e.relatedTarget as Node) || newTypeMenuIsOpen) {
return;
}
if (newUrlIsValid) {
append(newUrl);
setNewUrl({ type: 'arXiv', url: `${ARXIV_ORIGIN}/` });
}
};

// Row for adding new url
const newUrlTableRow = (
<Tr>
<Tr onBlur={handleBlurNewUrlGroup}>
<Td color="gray.200">{urls.length + 1}</Td>
<Td>
{isClient && (
Expand All @@ -155,6 +166,8 @@ export const UrlsTable = ({ editable }: { editable: boolean }) => {
stylesTheme="default.sm"
onChange={handleNewTypeChange}
menuPortalTarget={document.body}
onMenuOpen={() => setNewTypeMenuIsOpen(true)}
onMenuClose={() => setNewTypeMenuIsOpen(false)}
ref={newURLTypeInputRef}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { render } from '@/test-utils';
import { screen, fireEvent } from '@testing-library/react';
import { describe, expect, test, vi } from 'vitest';
import { MissingReferenceForm } from './MissingReferenceForm';

const onOpenAlert = vi.fn();

describe('MissingReferenceTable — new reference auto-commit on blur', () => {
test('commits a typed reference pair to the table when focus leaves the new-row group', () => {
render(<MissingReferenceForm onOpenAlert={onOpenAlert} />);

const [citingInput, citedInput] = screen.getAllByPlaceholderText('1998ApJ...501L..41Y');
fireEvent.change(citingInput, { target: { value: '2021arXiv210100001A' } });
fireEvent.change(citedInput, { target: { value: '2020ApJ...900..100B' } });
fireEvent.blur(citedInput);

// Committed state: both values appear as text in the table
expect(screen.getByText('2021arXiv210100001A')).toBeInTheDocument();
expect(screen.getByText('2020ApJ...900..100B')).toBeInTheDocument();
// citing persists for next entry; cited is cleared
expect(citingInput).toHaveValue('2021arXiv210100001A');
expect(citedInput).toHaveValue('');
});

test('does not commit when only one field is filled on blur', () => {
render(<MissingReferenceForm onOpenAlert={onOpenAlert} />);

const [citingInput] = screen.getAllByPlaceholderText('1998ApJ...501L..41Y');
fireEvent.change(citingInput, { target: { value: '2021arXiv210100001A' } });
fireEvent.blur(citingInput);

// No committed row — text would appear as non-placeholder cell text
expect(screen.queryByText('2021arXiv210100001A')).not.toBeInTheDocument();
});

test('does not auto-commit when focus moves between citing and cited inputs', () => {
render(<MissingReferenceForm onOpenAlert={onOpenAlert} />);

const [citingInput, citedInput] = screen.getAllByPlaceholderText('1998ApJ...501L..41Y');
fireEvent.change(citingInput, { target: { value: '2021arXiv210100001A' } });
fireEvent.change(citedInput, { target: { value: '2020ApJ...900..100B' } });

// Blur from citing to cited — both are inside the same new-row <Tr>
fireEvent.blur(citingInput, { relatedTarget: citedInput });

// Guard should prevent premature commit
expect(screen.queryByText('2021arXiv210100001A')).not.toBeInTheDocument();
expect(screen.queryByText('2020ApJ...900..100B')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
IconButton,
FormErrorMessage,
} from '@chakra-ui/react';
import { useState, ChangeEvent, MouseEvent, useRef, KeyboardEvent } from 'react';
import { useState, ChangeEvent, FocusEvent, MouseEvent, useRef, KeyboardEvent } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { FormValues, Reference } from './types';

Expand Down Expand Up @@ -104,6 +104,16 @@ export const MissingReferenceTable = () => {
}
};

const handleBlurNewRefGroup = (e: FocusEvent<HTMLTableRowElement>) => {
if (e.currentTarget.contains(e.relatedTarget as Node)) {
return;
}
if (newReference.citing.length > 0 && newReference.cited.length > 0) {
append(newReference);
setNewReference({ citing: newReference.citing, cited: '' });
}
};

return (
<>
<FormLabel>Missing References</FormLabel>
Expand Down Expand Up @@ -206,7 +216,7 @@ export const MissingReferenceTable = () => {
</Tr>
),
)}
<Tr>
<Tr onBlur={handleBlurNewRefGroup}>
<Td color="gray.200">{references.length + 1}</Td>
<Td>
<FormControl isInvalid={!!errors.references?.message}>
Expand Down
Loading