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
Expand Up @@ -1985,53 +1985,181 @@ test('pending LIKE debounce still applies after rerender recreates updateDataMas
reduxState,
);

fireEvent.change(
screen.getByPlaceholderText('Type to search (contains)...'),
{
target: { value: 'Jen' },
const input = screen.getByPlaceholderText('Type to search (contains)...');

// Batch the keystroke and the rerender into one update so the deferred
// value hasn't settled yet when updateDataMask's identity changes --
// useDeferredValue has no fixed delay, so letting the keystroke flush on
// its own (as a real fixed-delay debounce would still be pending) would
// settle it before the rerender and not exercise the stale-ref path.
act(() => {
fireEvent.change(input, { target: { value: 'Jen' } });
rerender(
<SelectFilterPlugin
{...buildSelectFilterProps({
formData: { operatorType: SelectFilterOperatorType.Contains },
filterState: { value: undefined, label: 'external change' },
setDataMask: setDataMaskMock,
})}
/>,
);
});

await waitFor(() => {
expect(setDataMaskMock).toHaveBeenCalledWith(
expect.objectContaining({
extraFormData: {
filters: [
{
col: 'gender',
op: 'ILIKE',
val: '%Jen%',
},
],
},
}),
);
});
});

test('pending LIKE debounce is canceled when operatorType switches back to Exact', async () => {
jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const likeProps = buildSelectFilterProps({
formData: { operatorType: SelectFilterOperatorType.Contains },
filterState: { value: undefined },
setDataMask: setDataMaskMock,
});

const reduxState = {
useRedux: true,
initialState: {
nativeFilters: {
filters: { 'test-filter': { name: 'Test Filter' } },
},
dataMask: {
'test-filter': {
extraFormData: {},
filterState: { value: undefined },
},
},
},
};

const { rerender } = render(
<SelectFilterPlugin {...likeProps} />,
reduxState,
);

setDataMaskMock.mockClear();
const input = screen.getByPlaceholderText('Type to search (contains)...');
const callsBeforeSwitch = setDataMaskMock.mock.calls.length;

rerender(
<SelectFilterPlugin
{...buildSelectFilterProps({
formData: { operatorType: SelectFilterOperatorType.Contains },
filterState: { value: undefined, label: 'external change' },
setDataMask: setDataMaskMock,
})}
/>,
);
// Same batching as the sibling test above: the keystroke and the switch
// away from the LIKE operator land in the same update, before the
// deferred value has a chance to settle on its own.
act(() => {
fireEvent.change(input, { target: { value: 'Jen' } });
rerender(
<SelectFilterPlugin
{...buildSelectFilterProps({
formData: { operatorType: SelectFilterOperatorType.Exact },
filterState: { value: undefined },
setDataMask: setDataMaskMock,
})}
/>,
);
});

await act(async () => {});
act(() => {
jest.advanceTimersByTime(500);
jest.advanceTimersByTime(1000);
});

expect(setDataMaskMock).toHaveBeenCalledWith(
// Rerendering with fresh (non-memoized) props can independently re-run
// this plugin's own default-value initialisation, unrelated to the LIKE
// debounce this test is pinning -- so assert no *new* call carries the
// 'Jen' ILIKE payload, rather than that setDataMask was never called again.
const callsSinceSwitch = setDataMaskMock.mock.calls
.slice(callsBeforeSwitch)
.map(call => call[0]);
expect(callsSinceSwitch).not.toContainEqual(
expect.objectContaining({
extraFormData: {
filters: [
{
col: 'gender',
op: 'ILIKE',
val: '%Jen%',
},
],
extraFormData: expect.objectContaining({
filters: [expect.objectContaining({ op: 'ILIKE', val: '%Jen%' })],
}),
}),
);
});

test('switching Exact then back to LIKE does not replay the discarded pending edit', async () => {
jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const buildProps = (operatorType: SelectFilterOperatorType) =>
buildSelectFilterProps({
formData: { operatorType },
filterState: { value: undefined },
setDataMask: setDataMaskMock,
});
const reduxState = {
useRedux: true,
initialState: {
nativeFilters: {
filters: { 'test-filter': { name: 'Test Filter' } },
},
dataMask: {
'test-filter': {
extraFormData: {},
filterState: { value: undefined },
},
},
},
};

const { rerender } = render(
<SelectFilterPlugin {...buildProps(SelectFilterOperatorType.Contains)} />,
reduxState,
);
const input = screen.getByPlaceholderText('Type to search (contains)...');

act(() => {
fireEvent.change(input, { target: { value: 'Jen' } });
rerender(
<SelectFilterPlugin {...buildProps(SelectFilterOperatorType.Exact)} />,
);
});
await act(async () => {});
act(() => {
jest.advanceTimersByTime(1000);
});

const callsBeforeReturn = setDataMaskMock.mock.calls.length;
rerender(
<SelectFilterPlugin {...buildProps(SelectFilterOperatorType.Contains)} />,
);
await act(async () => {});
act(() => {
jest.advanceTimersByTime(1000);
});

expect(
setDataMaskMock.mock.calls.slice(callsBeforeReturn).map(call => call[0]),
).not.toContainEqual(
expect.objectContaining({
extraFormData: expect.objectContaining({
filters: [expect.objectContaining({ op: 'ILIKE', val: '%Jen%' })],
}),
}),
);
});

test('pending LIKE debounce is canceled when operatorType switches back to Exact', async () => {
test('clear-all does not restore a LIKE edit still pending in the deferred value', async () => {
jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const likeProps = buildSelectFilterProps({
formData: { operatorType: SelectFilterOperatorType.Contains },
filterState: { value: undefined },
setDataMask: setDataMaskMock,
});

const reduxState = {
useRedux: true,
initialState: {
Expand All @@ -2051,31 +2179,169 @@ test('pending LIKE debounce is canceled when operatorType switches back to Exact
<SelectFilterPlugin {...likeProps} />,
reduxState,
);
const input = screen.getByPlaceholderText('Type to search (contains)...');

fireEvent.change(
screen.getByPlaceholderText('Type to search (contains)...'),
{
target: { value: 'Jen' },
// Type and fire clear-all in one update, then release the trigger, so the
// deferred value still holds the typed text when the trigger goes away.
act(() => {
fireEvent.change(input, { target: { value: 'Jen' } });
rerender(
<SelectFilterPlugin
{...likeProps}
clearAllTrigger={{ 'test-filter': true }}
/>,
);
});
const callsBeforeRelease = setDataMaskMock.mock.calls.length;
rerender(<SelectFilterPlugin {...likeProps} clearAllTrigger={undefined} />);
Comment thread
rusackas marked this conversation as resolved.
Resolved
await act(async () => {});
Comment thread
rusackas marked this conversation as resolved.
Resolved
act(() => {
jest.advanceTimersByTime(1000);
});

expect(input).toHaveValue('');
expect(
setDataMaskMock.mock.calls.slice(callsBeforeRelease).map(call => call[0]),
).not.toContainEqual(
expect.objectContaining({
extraFormData: expect.objectContaining({
filters: [expect.objectContaining({ op: 'ILIKE', val: '%Jen%' })],
}),
}),
);
});

const likeReduxState = {
useRedux: true,
initialState: {
nativeFilters: {
filters: { 'test-filter': { name: 'Test Filter' } },
},
dataMask: {
'test-filter': {
extraFormData: {},
filterState: { value: undefined },
},
},
},
};

test('switching match type discards the pending LIKE edit and clears the displayed text', async () => {
jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const buildProps = (operatorType: SelectFilterOperatorType) =>
buildSelectFilterProps({
formData: { operatorType },
filterState: { value: undefined },
setDataMask: setDataMaskMock,
});

const { rerender } = render(
<SelectFilterPlugin {...buildProps(SelectFilterOperatorType.Contains)} />,
likeReduxState,
);
const input = screen.getByPlaceholderText('Type to search (contains)...');

act(() => {
fireEvent.change(input, { target: { value: 'Jen' } });
rerender(
<SelectFilterPlugin
{...buildProps(SelectFilterOperatorType.StartsWith)}
/>,
);
});
await act(async () => {});
act(() => {
jest.advanceTimersByTime(1000);
});

expect(
screen.getByPlaceholderText('Type to search (starts with)...'),
).toHaveValue('');
});

test('re-entering the same LIKE text after a parent reset commits a new mask', async () => {
jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const buildProps = (value: string[] | undefined, defaultValue?: string[]) =>
buildSelectFilterProps({
formData: {
operatorType: SelectFilterOperatorType.Contains,
defaultValue,
},
filterState: { value },
setDataMask: setDataMaskMock,
});
const containsJen = expect.objectContaining({
extraFormData: expect.objectContaining({
filters: [expect.objectContaining({ op: 'ILIKE', val: '%Jen%' })],
}),
});

const { rerender } = render(
<SelectFilterPlugin {...buildProps(undefined, undefined)} />,
likeReduxState,
);
const input = screen.getByPlaceholderText('Type to search (contains)...');

fireEvent.change(input, { target: { value: 'Jen' } });
act(() => {
jest.advanceTimersByTime(1000);
});
expect(setDataMaskMock).toHaveBeenCalledWith(containsJen);

// The parent echoes the committed value, then resets it to a different
// default, which moves the committed mask away from 'Jen'.
rerender(<SelectFilterPlugin {...buildProps(['Jen'])} />);
rerender(<SelectFilterPlugin {...buildProps(undefined, ['Bob'])} />);
await act(async () => {});
expect(input).toHaveValue('');

setDataMaskMock.mockClear();
fireEvent.change(input, { target: { value: 'Jen' } });
await act(async () => {});
act(() => {
jest.advanceTimersByTime(1000);
});
await act(async () => {});

rerender(
<SelectFilterPlugin
{...buildSelectFilterProps({
formData: { operatorType: SelectFilterOperatorType.Exact },
filterState: { value: undefined },
setDataMask: setDataMaskMock,
})}
/>,
expect(setDataMaskMock).toHaveBeenCalledWith(containsJen);
});

test('Clear All publishes an empty server search after a settled search', async () => {
jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const props = buildSelectFilterProps({
formData: { searchAllOptions: true, multiSelect: true },
filterState: { value: ['boy'] },
setDataMask: setDataMaskMock,
});

const { rerender } = render(
<SelectFilterPlugin {...props} />,
likeReduxState,
);
const combobox = screen.getAllByRole('combobox')[0];
await userEvent.click(combobox);
await userEvent.type(combobox, 'ab');
act(() => {
jest.advanceTimersByTime(1000);
});
await waitFor(() => {
const lastCall = setDataMaskMock.mock.calls.at(-1)?.[0];
expect(lastCall?.ownState?.search).toBe('ab');
});

rerender(
<SelectFilterPlugin {...props} clearAllTrigger={{ 'test-filter': true }} />,
);
await act(async () => {});
act(() => {
jest.advanceTimersByTime(500);
jest.advanceTimersByTime(1000);
});

expect(setDataMaskMock).not.toHaveBeenCalled();
const lastCall = setDataMaskMock.mock.calls.at(-1)?.[0];
expect(lastCall?.ownState?.search).toBe('');
});

test('renders standard Select dropdown when operatorType is Exact', () => {
Expand Down
Loading
Loading