@sproutsocial/seeds-react-tooltip 1.1.19 → 1.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,32 +1,13 @@
1
- /* eslint-disable testing-library/no-unnecessary-act */
2
1
  import React from "react";
3
2
  import {
4
3
  render,
5
4
  screen,
6
- fireEvent,
7
- act,
5
+ waitFor,
8
6
  } from "@sproutsocial/seeds-react-testing-library";
9
7
  import Icon from "@sproutsocial/seeds-react-icon";
10
8
  import Tooltip from "../Tooltip";
11
9
  import Button from "@sproutsocial/seeds-react-button";
12
10
 
13
- jest.mock("popper.js", () => {
14
- const PopperJS = jest.requireActual("popper.js");
15
- const mockDestroy = jest.fn();
16
- const mockScheduleUpdate = jest.fn();
17
-
18
- return class Popper {
19
- static placements = PopperJS.placements;
20
-
21
- constructor() {
22
- return {
23
- destroy: mockDestroy,
24
- scheduleUpdate: mockScheduleUpdate,
25
- };
26
- }
27
- };
28
- });
29
-
30
11
  jest.mock("@sproutsocial/seeds-react-portal", () => {
31
12
  const Portal = ({ children }: { children: React.ReactNode }) => (
32
13
  <div>{children}</div>
@@ -38,28 +19,21 @@ jest.mock("@sproutsocial/seeds-react-portal", () => {
38
19
  DisablePortalToBodyContext: Portal.DisablePortalToBodyContext,
39
20
  };
40
21
  });
22
+
41
23
  describe("Tooltip", () => {
42
24
  describe("rendering all valid `React.Node`s", () => {
43
25
  it("should render string content", async () => {
44
26
  const contentText = "hey";
45
- render(<Tooltip content={contentText}>hi</Tooltip>);
46
- // This will cause a console error (but still pass) until react 16.9
47
- act(() => {
48
- fireEvent.mouseOver(screen.getByText("hi"));
49
- });
27
+ const { user } = render(<Tooltip content={contentText}>hi</Tooltip>);
28
+ await user.hover(screen.getByText("hi"));
50
29
  await screen.findByText(contentText);
51
30
  expect(screen.getByText(contentText)).toBeInTheDocument();
52
-
53
- // TODO
54
- // await runA11yCheck();
55
31
  });
56
32
 
57
33
  it("should render number content", async () => {
58
34
  const contentText = 123;
59
- render(<Tooltip content={contentText}>hi</Tooltip>);
60
- act(() => {
61
- fireEvent.mouseOver(screen.getByText("hi"));
62
- });
35
+ const { user } = render(<Tooltip content={contentText}>hi</Tooltip>);
36
+ await user.hover(screen.getByText("hi"));
63
37
  await screen.findByDataQaLabel({
64
38
  "popout-isopen": "true",
65
39
  });
@@ -67,10 +41,8 @@ describe("Tooltip", () => {
67
41
  });
68
42
 
69
43
  it("should render React Element content", async () => {
70
- render(<Tooltip content={<p>hey</p>}>hi</Tooltip>);
71
- act(() => {
72
- fireEvent.mouseOver(screen.getByText("hi"));
73
- });
44
+ const { user } = render(<Tooltip content={<p>hey</p>}>hi</Tooltip>);
45
+ await user.hover(screen.getByText("hi"));
74
46
  await screen.findByDataQaLabel({
75
47
  "popout-isopen": "true",
76
48
  });
@@ -78,10 +50,10 @@ describe("Tooltip", () => {
78
50
  });
79
51
 
80
52
  it("should render $Iterable<string> content", async () => {
81
- render(<Tooltip content={["hello", "world"]}>hi</Tooltip>);
82
- act(() => {
83
- fireEvent.mouseOver(screen.getByText("hi"));
84
- });
53
+ const { user } = render(
54
+ <Tooltip content={["hello", "world"]}>hi</Tooltip>
55
+ );
56
+ await user.hover(screen.getByText("hi"));
85
57
  await screen.findByText(/hello/i);
86
58
  expect(screen.getByText(/hello/i)).toBeInTheDocument();
87
59
  await screen.findByText(/world/i);
@@ -89,14 +61,12 @@ describe("Tooltip", () => {
89
61
  });
90
62
 
91
63
  it("should render $Iterable<React$Element<any>> content", async () => {
92
- render(
64
+ const { user } = render(
93
65
  <Tooltip content={[<p key="1">hello</p>, <p key="2">world</p>]}>
94
66
  hi
95
67
  </Tooltip>
96
68
  );
97
- act(() => {
98
- fireEvent.mouseOver(screen.getByText("hi"));
99
- });
69
+ await user.hover(screen.getByText("hi"));
100
70
  await screen.findByText(/hello/i);
101
71
  expect(screen.getByText(/hello/i)).toBeInTheDocument();
102
72
  await screen.findByText(/world/i);
@@ -105,11 +75,8 @@ describe("Tooltip", () => {
105
75
 
106
76
  it("should not render null content", async () => {
107
77
  expect.assertions(1);
108
- render(<Tooltip content={null}>hi</Tooltip>);
109
- act(() => {
110
- fireEvent.mouseOver(screen.getByText("hi"));
111
- });
112
- // @see https://jestjs.io/docs/tutorial-async#rejects
78
+ const { user } = render(<Tooltip content={null}>hi</Tooltip>);
79
+ await user.hover(screen.getByText("hi"));
113
80
  await expect(
114
81
  screen.findByDataQaLabel({
115
82
  "popout-isopen": "true",
@@ -119,11 +86,8 @@ describe("Tooltip", () => {
119
86
 
120
87
  it("should not render undefined content", async () => {
121
88
  expect.assertions(1);
122
- render(<Tooltip content={undefined}>hi</Tooltip>);
123
- act(() => {
124
- fireEvent.mouseOver(screen.getByText("hi"));
125
- });
126
- // @see https://jestjs.io/docs/tutorial-async#rejects
89
+ const { user } = render(<Tooltip content={undefined}>hi</Tooltip>);
90
+ await user.hover(screen.getByText("hi"));
127
91
  await expect(
128
92
  screen.findByDataQaLabel({
129
93
  "popout-isopen": "true",
@@ -132,7 +96,7 @@ describe("Tooltip", () => {
132
96
  });
133
97
 
134
98
  it("should not add aria-expanded to elements without it", async () => {
135
- render(
99
+ const { user } = render(
136
100
  <Tooltip content="hello">
137
101
  <Button>hi</Button>
138
102
  </Tooltip>
@@ -140,23 +104,17 @@ describe("Tooltip", () => {
140
104
 
141
105
  const button = screen.getByRole("button", { name: "hi" });
142
106
 
143
- // Should not have aria-expanded initially
144
107
  expect(button).not.toHaveAttribute("aria-expanded");
145
108
 
146
- // Hover to open tooltip
147
- act(() => {
148
- fireEvent.mouseOver(button);
149
- });
109
+ await user.hover(button.closest("[data-qa-tooltip]")!);
150
110
 
151
- // Wait for tooltip
152
111
  await screen.findByText(/hello/i);
153
112
 
154
- // Should still not have aria-expanded (tooltips don't use this attribute)
155
113
  expect(button).not.toHaveAttribute("aria-expanded");
156
114
  });
157
115
 
158
116
  it("should preserve existing aria-expanded attribute", async () => {
159
- render(
117
+ const { user } = render(
160
118
  <Tooltip content="hello">
161
119
  <Button aria-expanded="false">hi</Button>
162
120
  </Tooltip>
@@ -164,18 +122,12 @@ describe("Tooltip", () => {
164
122
 
165
123
  const button = screen.getByRole("button", { name: "hi" });
166
124
 
167
- // Should preserve the existing aria-expanded value
168
125
  expect(button).toHaveAttribute("aria-expanded", "false");
169
126
 
170
- // Hover to open tooltip
171
- act(() => {
172
- fireEvent.mouseOver(button);
173
- });
127
+ await user.hover(button.closest("[data-qa-tooltip]")!);
174
128
 
175
- // Wait for tooltip
176
129
  await screen.findByText(/hello/i);
177
130
 
178
- // Should still preserve aria-expanded="false" (drawer/disclosure pattern)
179
131
  expect(button).toHaveAttribute("aria-expanded", "false");
180
132
  });
181
133
 
@@ -188,13 +140,12 @@ describe("Tooltip", () => {
188
140
 
189
141
  const textElement = screen.getByTestId("text-element");
190
142
 
191
- // Non-interactive elements should not get aria-expanded
192
143
  expect(textElement).not.toHaveAttribute("aria-expanded");
193
144
  });
194
145
  });
195
146
 
196
147
  it("should render when wrapped around an icon", async () => {
197
- render(
148
+ const { user } = render(
198
149
  <Tooltip content="hey">
199
150
  <Icon
200
151
  data-testid="icon"
@@ -203,10 +154,7 @@ describe("Tooltip", () => {
203
154
  />
204
155
  </Tooltip>
205
156
  );
206
- // This will cause a console error (but still pass) until react 16.9
207
- act(() => {
208
- fireEvent.mouseOver(screen.getByTestId("icon"));
209
- });
157
+ await user.hover(screen.getByTestId("icon").closest("[data-qa-tooltip]")!);
210
158
  await expect(
211
159
  screen.findByDataQaLabel({
212
160
  "popout-isopen": "true",
@@ -227,7 +175,7 @@ describe("Tooltip", () => {
227
175
  });
228
176
 
229
177
  it("should have aria-describedby when tooltip is open", async () => {
230
- render(
178
+ const { user } = render(
231
179
  <Tooltip content="Tooltip content">
232
180
  <Button data-testid="trigger">Trigger</Button>
233
181
  </Tooltip>
@@ -235,15 +183,10 @@ describe("Tooltip", () => {
235
183
 
236
184
  const trigger = screen.getByTestId("trigger");
237
185
 
238
- // Hover to open tooltip
239
- act(() => {
240
- fireEvent.mouseOver(trigger);
241
- });
186
+ await user.hover(trigger.closest("[data-qa-tooltip]")!);
242
187
 
243
- // Wait for tooltip to appear
244
188
  await screen.findByText("Tooltip content");
245
189
 
246
- // Check that aria-describedby is now present
247
190
  expect(trigger).toHaveAttribute("aria-describedby");
248
191
  const tooltipId = trigger.getAttribute("aria-describedby");
249
192
  expect(tooltipId).toBeTruthy();
@@ -251,7 +194,7 @@ describe("Tooltip", () => {
251
194
  });
252
195
 
253
196
  it("should remove aria-describedby when tooltip closes", async () => {
254
- render(
197
+ const { user } = render(
255
198
  <Tooltip content="Tooltip content">
256
199
  <Button data-testid="trigger">Trigger</Button>
257
200
  </Tooltip>
@@ -259,33 +202,21 @@ describe("Tooltip", () => {
259
202
 
260
203
  const trigger = screen.getByTestId("trigger");
261
204
 
262
- // Open tooltip
263
- act(() => {
264
- fireEvent.mouseOver(trigger);
265
- });
205
+ await user.hover(trigger.closest("[data-qa-tooltip]")!);
266
206
 
267
- // Wait for tooltip to appear
268
207
  await screen.findByText("Tooltip content");
269
208
 
270
- // Verify aria-describedby is present
271
209
  expect(trigger).toHaveAttribute("aria-describedby");
272
210
 
273
- // Close tooltip
274
- act(() => {
275
- fireEvent.mouseOut(trigger);
276
- });
211
+ await user.unhover(trigger.closest("[data-qa-tooltip]")!);
277
212
 
278
- // Wait for tooltip to close (using a small delay)
279
- await act(async () => {
280
- await new Promise((resolve) => setTimeout(resolve, 200));
213
+ await waitFor(() => {
214
+ expect(trigger).not.toHaveAttribute("aria-describedby");
281
215
  });
282
-
283
- // Verify aria-describedby is removed
284
- expect(trigger).not.toHaveAttribute("aria-describedby");
285
216
  });
286
217
 
287
218
  it("should have aria-describedby pointing to element with role='tooltip'", async () => {
288
- render(
219
+ const { user } = render(
289
220
  <Tooltip content="Tooltip content">
290
221
  <Button data-testid="trigger">Trigger</Button>
291
222
  </Tooltip>
@@ -293,33 +224,25 @@ describe("Tooltip", () => {
293
224
 
294
225
  const trigger = screen.getByTestId("trigger");
295
226
 
296
- // Open tooltip
297
- act(() => {
298
- fireEvent.mouseOver(trigger);
299
- });
227
+ await user.hover(trigger.closest("[data-qa-tooltip]")!);
300
228
 
301
- // Wait for tooltip to appear
302
229
  const tooltipElement = await screen.findByText("Tooltip content");
303
230
 
304
- // Get the aria-describedby value
305
231
  const tooltipId = trigger.getAttribute("aria-describedby");
306
232
  expect(tooltipId).toBeTruthy();
307
233
 
308
- // Find the tooltip by role
309
234
  const tooltipByRole = screen.getByRole("tooltip");
310
235
  expect(tooltipByRole).toBeInTheDocument();
311
236
 
312
- // Verify the ID matches
313
237
  expect(tooltipByRole).toHaveAttribute("id", tooltipId);
314
238
 
315
- // Verify the tooltip contains the content
316
239
  expect(tooltipByRole).toContainElement(tooltipElement);
317
240
  });
318
241
  });
319
242
 
320
243
  describe("legacyMouseInteraction prop", () => {
321
244
  it("should allow hovering over tooltip content when legacyMouseInteraction is true", async () => {
322
- render(
245
+ const { user } = render(
323
246
  <Tooltip content="Tooltip content" legacyMouseInteraction>
324
247
  <Button data-testid="trigger">Trigger</Button>
325
248
  </Tooltip>
@@ -327,44 +250,23 @@ describe("Tooltip", () => {
327
250
 
328
251
  const trigger = screen.getByTestId("trigger");
329
252
 
330
- // Hover over trigger to open tooltip
331
- act(() => {
332
- fireEvent.mouseOver(trigger);
333
- });
253
+ await user.hover(trigger.closest("[data-qa-tooltip]")!);
334
254
 
335
- // Wait for tooltip to appear
336
255
  const tooltipContent = await screen.findByText("Tooltip content");
337
256
  expect(tooltipContent).toBeInTheDocument();
338
257
 
339
- // Move mouse away from trigger
340
- act(() => {
341
- fireEvent.mouseOut(trigger);
342
- });
343
-
344
- // Hover over the tooltip content itself
345
- act(() => {
346
- fireEvent.mouseEnter(tooltipContent);
347
- });
348
-
349
- // Tooltip should remain open
350
- await act(async () => {
351
- await new Promise((resolve) => setTimeout(resolve, 200));
352
- });
353
- expect(tooltipContent).toBeInTheDocument();
258
+ await user.unhover(trigger.closest("[data-qa-tooltip]")!);
259
+ await user.hover(tooltipContent);
354
260
 
355
- // Move mouse away from tooltip content
356
- act(() => {
357
- fireEvent.mouseLeave(tooltipContent);
261
+ await waitFor(() => {
262
+ expect(tooltipContent).toBeInTheDocument();
358
263
  });
359
264
 
360
- // Tooltip should eventually close
361
- await act(async () => {
362
- await new Promise((resolve) => setTimeout(resolve, 200));
363
- });
265
+ await user.unhover(tooltipContent);
364
266
  });
365
267
 
366
268
  it("should not have mouse handlers on tooltip content when legacyMouseInteraction is false", async () => {
367
- render(
269
+ const { user } = render(
368
270
  <Tooltip content="Tooltip content">
369
271
  <Button data-testid="trigger">Trigger</Button>
370
272
  </Tooltip>
@@ -372,24 +274,12 @@ describe("Tooltip", () => {
372
274
 
373
275
  const trigger = screen.getByTestId("trigger");
374
276
 
375
- // Hover over trigger to open tooltip
376
- act(() => {
377
- fireEvent.mouseOver(trigger);
378
- });
277
+ await user.hover(trigger.closest("[data-qa-tooltip]")!);
379
278
 
380
- // Wait for tooltip to appear
381
279
  const tooltipContent = await screen.findByText("Tooltip content");
382
280
  expect(tooltipContent).toBeInTheDocument();
383
281
 
384
- // Verify mouse handlers are not attached (tooltip should close immediately when leaving trigger)
385
- act(() => {
386
- fireEvent.mouseOut(trigger);
387
- });
388
-
389
- // Without legacyMouseInteraction, tooltip closes immediately (exitDelay: 0)
390
- await act(async () => {
391
- await new Promise((resolve) => setTimeout(resolve, 100));
392
- });
282
+ await user.unhover(trigger.closest("[data-qa-tooltip]")!);
393
283
  });
394
284
  });
395
285
  });
@@ -0,0 +1,56 @@
1
+ import { mapPlacement } from "../Tooltip";
2
+
3
+ describe("mapPlacement", () => {
4
+ it("maps top placements", () => {
5
+ expect(mapPlacement("top")).toEqual({ side: "top", align: "center" });
6
+ expect(mapPlacement("top-start")).toEqual({ side: "top", align: "start" });
7
+ expect(mapPlacement("top-end")).toEqual({ side: "top", align: "end" });
8
+ });
9
+
10
+ it("maps bottom placements", () => {
11
+ expect(mapPlacement("bottom")).toEqual({ side: "bottom", align: "center" });
12
+ expect(mapPlacement("bottom-start")).toEqual({
13
+ side: "bottom",
14
+ align: "start",
15
+ });
16
+ expect(mapPlacement("bottom-end")).toEqual({
17
+ side: "bottom",
18
+ align: "end",
19
+ });
20
+ });
21
+
22
+ it("maps left placements", () => {
23
+ expect(mapPlacement("left")).toEqual({ side: "left", align: "center" });
24
+ expect(mapPlacement("left-start")).toEqual({
25
+ side: "left",
26
+ align: "start",
27
+ });
28
+ expect(mapPlacement("left-end")).toEqual({ side: "left", align: "end" });
29
+ });
30
+
31
+ it("maps right placements", () => {
32
+ expect(mapPlacement("right")).toEqual({ side: "right", align: "center" });
33
+ expect(mapPlacement("right-start")).toEqual({
34
+ side: "right",
35
+ align: "start",
36
+ });
37
+ expect(mapPlacement("right-end")).toEqual({ side: "right", align: "end" });
38
+ });
39
+
40
+ // `auto*` deliberately anchors to `side: "top"` and relies on base-ui's
41
+ // runtime collision flipping rather than Popper-style best-initial-side
42
+ // selection. This is the documented, intentional behavior — see the
43
+ // changeset and the `placement` prop JSDoc in TooltipTypes.ts.
44
+ it("anchors auto placements to top with align from the suffix", () => {
45
+ expect(mapPlacement("auto")).toEqual({ side: "top", align: "center" });
46
+ expect(mapPlacement("auto-start")).toEqual({
47
+ side: "top",
48
+ align: "start",
49
+ });
50
+ expect(mapPlacement("auto-end")).toEqual({ side: "top", align: "end" });
51
+ });
52
+
53
+ it("falls back to top/center for unknown placements", () => {
54
+ expect(mapPlacement("nonsense")).toEqual({ side: "top", align: "center" });
55
+ });
56
+ });