datajunction-ui
Version:
DataJunction UI
494 lines (408 loc) • 14.7 kB
JSX
import React from 'react';
import {
render,
fireEvent,
waitFor,
screen,
act,
} from '@testing-library/react';
import { useFormikContext } from 'formik';
import ManageDimensionLinksDialog from '../ManageDimensionLinksDialog';
import DJClientContext from '../../../providers/djclient';
// Stub FormikSelect with a plain multi-select so tests can drive the field
// without wrestling react-select's pointer-event quirks.
vi.mock('../../AddEditNodePage/FormikSelect', () => ({
FormikSelect: ({ selectOptions, formikFieldName }) => {
const { values, setFieldValue } = useFormikContext();
return (
<select
data-testid={`mock-select-${formikFieldName}`}
multiple
value={values[formikFieldName] || []}
onChange={e =>
setFieldValue(
formikFieldName,
Array.from(e.target.selectedOptions, opt => opt.value),
)
}
>
{selectOptions.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
},
}));
// Mock window.location.reload
delete window.location;
window.location = { reload: vi.fn() };
// Mock window.confirm
window.confirm = vi.fn(() => true);
const mockDjClient = {
DataJunctionAPI: {
linkDimension: vi.fn(),
unlinkDimension: vi.fn(),
addReferenceDimensionLink: vi.fn(),
removeReferenceDimensionLink: vi.fn(),
},
node: vi.fn().mockResolvedValue({
name: 'default.test_dimension',
columns: [{ name: 'id', type: 'int' }],
}),
};
describe('<ManageDimensionLinksDialog />', () => {
beforeEach(() => {
vi.clearAllMocks();
window.alert = vi.fn();
window.confirm.mockReturnValue(true);
});
const defaultProps = {
column: { name: 'test_column', type: 'int' },
node: { name: 'default.node1' },
dimensions: [
{ value: 'default.dim1', label: 'dim1 (5 links)' },
{ value: 'default.dim2', label: 'dim2 (3 links)' },
],
fkLinks: [],
onSubmit: vi.fn(),
};
it('renders the toggle button', () => {
const { getByLabelText } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...defaultProps} />
</DJClientContext.Provider>,
);
expect(getByLabelText('ManageDimensionLinksToggle')).toBeInTheDocument();
});
it('opens modal when toggle button is clicked', async () => {
const { getByLabelText, getByRole } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...defaultProps} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
await waitFor(() => {
expect(
getByRole('dialog', { name: 'ManageDimensionLinksDialog' }),
).toBeInTheDocument();
});
});
it('displays column name and type in header', async () => {
const { getByLabelText, getByText } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog
column={{ name: 'test_column', type: 'varchar' }}
node={defaultProps.node}
dimensions={defaultProps.dimensions}
fkLinks={[]}
onSubmit={defaultProps.onSubmit}
/>
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
await waitFor(() => {
expect(getByText('test_column')).toBeInTheDocument();
expect(getByText('varchar')).toBeInTheDocument();
});
});
it('shows two tabs: FK Links and Reference Links', async () => {
const { getByLabelText, getByText } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...defaultProps} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
await waitFor(() => {
expect(getByText('FK Links')).toBeInTheDocument();
expect(getByText('Reference Links')).toBeInTheDocument();
});
});
it('switches to reference links tab when clicked', async () => {
const { getByLabelText, getByText } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...defaultProps} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
await waitFor(() => {
expect(getByText('Reference Links')).toBeInTheDocument();
});
fireEvent.click(getByText('Reference Links'));
await waitFor(() => {
expect(getByText('Dimension Node *')).toBeInTheDocument();
expect(getByText('Dimension Column *')).toBeInTheDocument();
});
});
it('displays FK links when provided', async () => {
const propsWithFkLinks = {
...defaultProps,
fkLinks: ['default.dim1', 'default.dim2'],
};
const { getByLabelText } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...propsWithFkLinks} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
await waitFor(() => {
// The FK Links tab should be displayed by default
expect(getByLabelText('ManageDimensionLinksToggle')).toBeInTheDocument();
});
});
it('shows FK Links form with select dimensions field', async () => {
const { getByLabelText, getByText } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...defaultProps} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
await waitFor(() => {
expect(getByText('Select Dimensions')).toBeInTheDocument();
expect(getByText('Save')).toBeInTheDocument();
});
});
it('displays reference link form fields', async () => {
const { getByLabelText, getByText, getByPlaceholderText } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...defaultProps} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
await waitFor(() => {
expect(getByText('Reference Links')).toBeInTheDocument();
});
// Switch to reference link tab
fireEvent.click(getByText('Reference Links'));
await waitFor(() => {
expect(getByText('Dimension Node *')).toBeInTheDocument();
expect(getByText('Dimension Column *')).toBeInTheDocument();
expect(
getByPlaceholderText('e.g., birth_date, registration_date'),
).toBeInTheDocument();
expect(getByText('Add Link')).toBeInTheDocument();
});
});
it('handles removing reference link', async () => {
mockDjClient.DataJunctionAPI.removeReferenceDimensionLink.mockResolvedValue(
{
status: 200,
},
);
const propsWithReferenceLink = {
...defaultProps,
referenceLink: {
dimension: 'default.dim1',
dimension_column: 'id',
},
};
const { getByLabelText, getByText } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...propsWithReferenceLink} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
await waitFor(() => {
expect(getByText('Reference Links')).toBeInTheDocument();
});
// Switch to reference link tab
fireEvent.click(getByText('Reference Links'));
await waitFor(() => {
expect(getByText('Remove Link')).toBeInTheDocument();
});
// Remove the link
const removeButton = getByText('Remove Link');
await act(async () => {
fireEvent.click(removeButton);
});
await waitFor(() => {
expect(window.confirm).toHaveBeenCalledWith(
'Are you sure you want to remove this reference link?',
);
expect(
mockDjClient.DataJunctionAPI.removeReferenceDimensionLink,
).toHaveBeenCalledWith('default.node1', 'test_column');
expect(window.location.reload).toHaveBeenCalled();
});
});
it('does not remove reference link when user cancels confirm dialog', async () => {
window.confirm.mockReturnValueOnce(false);
const propsWithReferenceLink = {
...defaultProps,
referenceLink: {
dimension: 'default.dim1',
dimension_column: 'id',
},
};
const { getByLabelText, getByText } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...propsWithReferenceLink} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
fireEvent.click(getByText('Reference Links'));
await waitFor(() => {
expect(getByText('Remove Link')).toBeInTheDocument();
});
const removeButton = getByText('Remove Link');
await act(async () => {
fireEvent.click(removeButton);
});
expect(
mockDjClient.DataJunctionAPI.removeReferenceDimensionLink,
).not.toHaveBeenCalled();
});
it('handles failed reference link removal', async () => {
mockDjClient.DataJunctionAPI.removeReferenceDimensionLink.mockResolvedValue(
{
status: 500,
json: { message: 'Server error' },
},
);
const propsWithReferenceLink = {
...defaultProps,
referenceLink: {
dimension: 'default.dim1',
dimension_column: 'id',
},
};
const { getByLabelText, getByText } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...propsWithReferenceLink} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
fireEvent.click(getByText('Reference Links'));
await waitFor(() => {
expect(getByText('Remove Link')).toBeInTheDocument();
});
const removeButton = getByText('Remove Link');
await act(async () => {
fireEvent.click(removeButton);
});
await waitFor(() => {
expect(window.alert).toHaveBeenCalled();
expect(window.location.reload).not.toHaveBeenCalled();
});
});
it('shows Update Link button when reference link exists', async () => {
const propsWithReferenceLink = {
...defaultProps,
referenceLink: {
dimension: 'default.dim1',
dimension_column: 'user_id',
},
};
const { getByLabelText, getByText } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...propsWithReferenceLink} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
fireEvent.click(getByText('Reference Links'));
await waitFor(() => {
expect(getByText('Update Link')).toBeInTheDocument();
expect(getByText('Remove Link')).toBeInTheDocument();
});
});
it('shows validation error when submitting reference link without required fields', async () => {
const { getByLabelText, getByText, getAllByText } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...defaultProps} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
fireEvent.click(getByText('Reference Links'));
await waitFor(() => {
expect(getByText('Add Link')).toBeInTheDocument();
});
// Try to submit without filling required fields
const saveButton = getByText('Add Link');
await act(async () => {
fireEvent.click(saveButton);
});
await waitFor(() => {
const requiredErrors = getAllByText('Required');
expect(requiredErrors.length).toBeGreaterThan(0);
});
});
it('shows success message when FK link save succeeds', async () => {
mockDjClient.DataJunctionAPI.linkDimension.mockResolvedValue({
status: 201,
json: { message: 'ok' },
});
const { getByLabelText, getByText, getByTestId } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...defaultProps} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
await waitFor(() => {
expect(getByTestId('mock-select-fkDimensions')).toBeInTheDocument();
});
const select = getByTestId('mock-select-fkDimensions');
await act(async () => {
fireEvent.change(select, { target: { value: 'default.dim1' } });
});
await act(async () => {
fireEvent.click(getByText('Save'));
});
await waitFor(() => {
expect(getByTestId('success')).toHaveTextContent(
'FK links updated successfully!',
);
});
expect(mockDjClient.DataJunctionAPI.linkDimension).toHaveBeenCalledWith(
'default.node1',
'test_column',
'default.dim1',
);
});
it('surfaces server error message when FK link save fails', async () => {
mockDjClient.DataJunctionAPI.linkDimension.mockResolvedValue({
status: 422,
json: {
message:
'Dimension default.dim1 has a compound primary key (a, b); a single-column FK link cannot join it correctly.',
},
});
const { getByLabelText, getByText, getByTestId, queryByTestId } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...defaultProps} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
await waitFor(() => {
expect(getByTestId('mock-select-fkDimensions')).toBeInTheDocument();
});
const select = getByTestId('mock-select-fkDimensions');
await act(async () => {
fireEvent.change(select, { target: { value: 'default.dim1' } });
});
await act(async () => {
fireEvent.click(getByText('Save'));
});
await waitFor(() => {
expect(getByTestId('failure')).toHaveTextContent('compound primary key');
});
expect(queryByTestId('success')).not.toBeInTheDocument();
});
it('closes modal when clicking backdrop', async () => {
const { getByLabelText, getByRole, queryByRole } = render(
<DJClientContext.Provider value={mockDjClient}>
<ManageDimensionLinksDialog {...defaultProps} />
</DJClientContext.Provider>,
);
fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
await waitFor(() => {
expect(getByRole('dialog')).toBeInTheDocument();
});
// Click the backdrop
const backdrop = getByRole('dialog').parentElement;
fireEvent.click(backdrop);
await waitFor(() => {
expect(queryByRole('dialog')).not.toBeInTheDocument();
});
});
});