Skip to content

Commit 7750352

Browse files
authored
Reset Select and MultiSelect value when external value changes to undefined (#606)
* fix: actually run effects in useUpdateEffect * test: decorate rerenders with ClickUIProvider * test(SingleSelect): add cases with external value change * test(MultiSelect): add cases with external value change * test(useUpdateEffect): add tests
1 parent 89c0389 commit 7750352

File tree

5 files changed

+163
-11
lines changed

5 files changed

+163
-11
lines changed

src/components/Select/MultiSelect.test.tsx

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe("MultiSelect", () => {
6464
expect(queryByText("Content0")).not.toBeNull();
6565
});
6666

67-
it("should always respect given value in select", () => {
67+
it("should prioritize external value over internal state", () => {
6868
const onSelect = vi.fn();
6969
const { queryByText, getByTestId, getByText } = renderSelect({
7070
value: ["content0", "content1"],
@@ -87,6 +87,51 @@ describe("MultiSelect", () => {
8787
expect(queryByTestingText(selectTrigger, "Content3")).toBeNull();
8888
});
8989

90+
it("should react to external value change", () => {
91+
const { getByTestId, rerender } = renderCUI(
92+
<MultiSelect
93+
value={["content0", "content1"]}
94+
options={selectOptions}
95+
/>
96+
);
97+
const selectTrigger = getByTestId("select-trigger");
98+
expect(selectTrigger).not.toBeNull();
99+
expect(queryByTestingText(selectTrigger, "Content0")).not.toBeNull();
100+
expect(
101+
queryByTestingText(selectTrigger, "Content1 long text content")
102+
).not.toBeNull();
103+
rerender(
104+
<MultiSelect
105+
value={["content0", "content3"]}
106+
options={selectOptions}
107+
/>
108+
);
109+
expect(queryByTestingText(selectTrigger, "Content0")).not.toBeNull();
110+
expect(queryByTestingText(selectTrigger, "Content1 long text content")).toBeNull();
111+
expect(queryByTestingText(selectTrigger, "Content3")).not.toBeNull();
112+
});
113+
114+
it("shuold fall back to placeholder if value changes to undefined", () => {
115+
const { getByTestId, rerender } = renderCUI(
116+
<MultiSelect
117+
value={["content0", "content1"]}
118+
placeholder="noop"
119+
options={selectOptions}
120+
/>
121+
);
122+
const selectTrigger = getByTestId("select-trigger");
123+
expect(selectTrigger).not.toBeNull();
124+
expect(queryByTestingText(selectTrigger, "noop")).toBeNull();
125+
rerender(
126+
<MultiSelect
127+
value={undefined}
128+
placeholder="noop"
129+
options={selectOptions}
130+
/>
131+
);
132+
expect(queryByTestingText(selectTrigger, "noop")).not.toBeNull();
133+
});
134+
90135
it("should respect given defaultValue in select", () => {
91136
const { getByTestId } = renderSelect({
92137
defaultValue: ["content0", "content2"],

src/components/Select/SingleSelect.test.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ describe("Select", () => {
9393
expect(queryByText("Content0")).toBeNull();
9494
});
9595

96-
it("should always respect given value in select", () => {
96+
it("should prioritize external value over internal state", () => {
9797
const onSelect = vi.fn();
9898
const { queryByText, getByTestId, getByText } = renderSelect({
9999
value: "content0",
@@ -115,6 +115,46 @@ describe("Select", () => {
115115
expect(queryByTestingText(selectTrigger, "Content0")).not.toBeNull();
116116
});
117117

118+
it("should react to external value change", () => {
119+
const { getByTestId, rerender } = renderCUI(
120+
<Select
121+
value="content0"
122+
options={selectOptions}
123+
/>
124+
);
125+
const selectTrigger = getByTestId("select-trigger");
126+
expect(selectTrigger).not.toBeNull();
127+
expect(queryByTestingText(selectTrigger, "Content0")).not.toBeNull();
128+
rerender(
129+
<Select
130+
value="content3"
131+
options={selectOptions}
132+
/>
133+
);
134+
expect(queryByTestingText(selectTrigger, "Content3")).not.toBeNull();
135+
});
136+
137+
it("shuold fall back to placeholder if value changes to undefined", () => {
138+
const { getByTestId, rerender } = renderCUI(
139+
<Select
140+
value="content0"
141+
placeholder="noop"
142+
options={selectOptions}
143+
/>
144+
);
145+
const selectTrigger = getByTestId("select-trigger");
146+
expect(selectTrigger).not.toBeNull();
147+
expect(queryByTestingText(selectTrigger, "noop")).toBeNull();
148+
rerender(
149+
<Select
150+
value={undefined}
151+
placeholder="noop"
152+
options={selectOptions}
153+
/>
154+
);
155+
expect(queryByTestingText(selectTrigger, "noop")).not.toBeNull();
156+
});
157+
118158
it("should respect given defaultValue in select", () => {
119159
const { getByTestId } = renderSelect({
120160
defaultValue: "content0",

src/hooks/useUpdateEffect.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { renderHook } from "@testing-library/react";
2+
import { useUpdateEffect } from "./useUpdateEffect";
3+
4+
describe("useUpdateEffect", () => {
5+
it("should not run the effect on the first render", () => {
6+
const effect = vi.fn();
7+
8+
renderHook(() => useUpdateEffect(effect));
9+
10+
// The effect should not have been called yet
11+
expect(effect).not.toHaveBeenCalled();
12+
});
13+
14+
it("should run the effect on subsequent renders", () => {
15+
const effect = vi.fn();
16+
17+
const { rerender } = renderHook(() => {
18+
useUpdateEffect(effect);
19+
});
20+
21+
rerender();
22+
23+
// The effect should have been called once
24+
expect(effect).toHaveBeenCalledTimes(1);
25+
});
26+
27+
it("should run the effect when dependencies change", () => {
28+
const effect = vi.fn();
29+
30+
const { rerender } = renderHook((dependency: string = "initial_dependency") =>
31+
useUpdateEffect(effect, [dependency])
32+
);
33+
34+
rerender("updated_dependency");
35+
36+
// The effect should have been called once
37+
expect(effect).toHaveBeenCalledTimes(1);
38+
});
39+
40+
it("should not run the effect if dependencies do not change", () => {
41+
const effect = vi.fn();
42+
43+
const { rerender } = renderHook((dependency: string = "same_dependency") =>
44+
useUpdateEffect(effect, [dependency])
45+
);
46+
47+
rerender("same_dependency");
48+
49+
// The effect should not have been called again
50+
expect(effect).toHaveBeenCalledTimes(0);
51+
});
52+
});

src/hooks/useUpdateEffect.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export const useUpdateEffect: typeof useEffect = (effect, deps) => {
44
const isFirstMount = useRef(true);
55

66
useEffect(() => {
7-
if (isFirstMount) {
7+
if (isFirstMount.current) {
88
isFirstMount.current = false;
99
return;
1010
}

src/utils/test-utils.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,30 @@ import { type ThemeName } from "@/theme";
22
import { render as renderTL } from "@testing-library/react";
33
import { ClickUIProvider } from "..";
44

5+
// eslint-disable-next-line react-refresh/only-export-components
6+
const Wrapper = ({
7+
theme,
8+
children,
9+
}: {
10+
theme: ThemeName;
11+
children: React.ReactNode;
12+
}) => (
13+
<ClickUIProvider
14+
theme={theme}
15+
config={{ tooltip: { delayDuration: 0 } }}
16+
>
17+
{children}
18+
</ClickUIProvider>
19+
);
20+
521
const renderCUI = (children: React.ReactNode, theme: ThemeName = "dark") => {
6-
return renderTL(
7-
<ClickUIProvider
8-
theme={theme}
9-
config={{ tooltip: { delayDuration: 0 } }}
10-
>
11-
{children}
12-
</ClickUIProvider>
13-
);
22+
const rtlRenderResult = renderTL(<Wrapper theme={theme}>{children}</Wrapper>);
23+
24+
return {
25+
...rtlRenderResult,
26+
rerender: (rerenderChildren: React.ReactNode) =>
27+
rtlRenderResult.rerender(<Wrapper theme={theme}>{rerenderChildren}</Wrapper>),
28+
};
1429
};
1530

1631
export { renderCUI };

0 commit comments

Comments
 (0)