|
1 | 1 | /* |
2 | | - * Copyright 2016 Palantir Technologies, Inc. All rights reserved. |
| 2 | + * Copyright 2024 Palantir Technologies, Inc. All rights reserved. |
3 | 3 | * |
4 | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | 5 | * you may not use this file except in compliance with the License. |
|
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 |
|
17 | | -import { assert } from "chai"; |
18 | | -import { mount } from "enzyme"; |
| 17 | +import { render, screen } from "@testing-library/react"; |
| 18 | +import userEvent from "@testing-library/user-event"; |
| 19 | +import { expect } from "chai"; |
19 | 20 | import * as React from "react"; |
20 | 21 | import { spy } from "sinon"; |
21 | 22 |
|
22 | | -import { AnchorButton, Button, type ButtonProps, Classes, Icon, Spinner } from "../../src"; |
| 23 | +import { IconNames } from "@blueprintjs/icons"; |
23 | 24 |
|
24 | | -describe("Buttons:", () => { |
25 | | - buttonTestSuite(Button, "button"); |
26 | | - buttonTestSuite(AnchorButton, "a"); |
| 25 | +import { AnchorButton, Button, Classes } from "../../src"; |
| 26 | + |
| 27 | +describe("<Button>", () => { |
| 28 | + commonTests(Button); |
| 29 | + |
| 30 | + it("should attach ref", () => { |
| 31 | + const ref = React.createRef<HTMLButtonElement>(); |
| 32 | + render(<Button ref={ref} />); |
| 33 | + |
| 34 | + expect(ref.current).to.exist; |
| 35 | + expect(ref.current).to.be.instanceOf(HTMLButtonElement); |
| 36 | + }); |
27 | 37 | }); |
28 | 38 |
|
29 | | -function buttonTestSuite(component: React.FC<any>, tagName: string) { |
30 | | - describe(`<${component.displayName!.split(".")[1]}>`, () => { |
31 | | - let containerElement: HTMLElement | undefined; |
32 | | - |
33 | | - beforeEach(() => { |
34 | | - containerElement = document.createElement("div"); |
35 | | - document.body.appendChild(containerElement); |
36 | | - }); |
37 | | - afterEach(() => { |
38 | | - containerElement?.remove(); |
39 | | - }); |
40 | | - |
41 | | - it("renders its contents", () => { |
42 | | - const wrapper = button({ className: "foo" }); |
43 | | - const el = wrapper.find(tagName); |
44 | | - assert.isTrue(el.exists()); |
45 | | - assert.isTrue(el.hasClass(Classes.BUTTON)); |
46 | | - assert.isTrue(el.hasClass("foo")); |
47 | | - }); |
48 | | - |
49 | | - it('icon="style" renders Icon as first child', () => { |
50 | | - const wrapper = button({ icon: "style" }); |
51 | | - const firstChild = wrapper.find(Icon).at(0); |
52 | | - assert.strictEqual(firstChild.prop("icon"), "style"); |
53 | | - }); |
54 | | - |
55 | | - it("renders the button text prop", () => { |
56 | | - const wrapper = button({ text: "some text" }, true); |
57 | | - assert.equal(wrapper.text(), "some text"); |
58 | | - }); |
59 | | - |
60 | | - it("renders the button text prop", () => { |
61 | | - const wrapper = mount(<Button data-test-foo="bar" />); |
62 | | - assert.isTrue(wrapper.find('[data-test-foo="bar"]').exists()); |
63 | | - }); |
64 | | - |
65 | | - it("wraps string children in spans", () => { |
66 | | - // so text can be hidden when loading |
67 | | - const wrapper = button({}, "raw string", <em>not a string</em>); |
68 | | - assert.lengthOf(wrapper.find("span"), 1, "span not found"); |
69 | | - assert.lengthOf(wrapper.find("em"), 1, "em not found"); |
70 | | - }); |
71 | | - |
72 | | - it("renders span if text={0}", () => { |
73 | | - const wrapper = button({ text: 0 }, true); |
74 | | - assert.equal(wrapper.text(), "0"); |
75 | | - }); |
76 | | - |
77 | | - it('doesn\'t render a text span if children=""', () => { |
78 | | - const wrapper = button({}, ""); |
79 | | - assert.lengthOf(wrapper.find("span"), 0); |
80 | | - }); |
81 | | - |
82 | | - it('doesn\'t render a text span if text=""', () => { |
83 | | - const wrapper = button({ text: "" }); |
84 | | - assert.lengthOf(wrapper.find("span"), 0); |
85 | | - }); |
86 | | - |
87 | | - it("accepts textClassName prop", () => { |
88 | | - const wrapper = button({ text: "text", textClassName: "text-class" }); |
89 | | - assert.isTrue(wrapper.find(".text-class").exists()); |
90 | | - }); |
91 | | - |
92 | | - it("renders a loading spinner when the loading prop is true", () => { |
93 | | - const wrapper = button({ loading: true }); |
94 | | - assert.lengthOf(wrapper.find(Spinner), 1); |
95 | | - }); |
96 | | - |
97 | | - it("button is disabled when the loading prop is true", () => { |
98 | | - const wrapper = button({ loading: true }); |
99 | | - assert.isTrue(wrapper.find(tagName).hasClass(Classes.DISABLED)); |
100 | | - }); |
101 | | - |
102 | | - // This tests some subtle (potentialy unexpected) behavior, but it was an API decision we |
103 | | - // made a long time ago which we rely on and should not break. |
104 | | - // See https://github.com/palantir/blueprint/issues/3819#issuecomment-1189478596 |
105 | | - it("button is disabled when the loading prop is true, even if disabled={false}", () => { |
106 | | - const wrapper = button({ disabled: false, loading: true }); |
107 | | - assert.isTrue(wrapper.find(tagName).hasClass(Classes.DISABLED)); |
108 | | - }); |
109 | | - |
110 | | - it("clicking button triggers onClick prop", () => { |
111 | | - const onClick = spy(); |
112 | | - button({ onClick }).simulate("click"); |
113 | | - assert.equal(onClick.callCount, 1); |
114 | | - }); |
115 | | - |
116 | | - it("clicking disabled button does not trigger onClick prop", () => { |
117 | | - const onClick = spy(); |
118 | | - // full DOM mount so `button` element will ignore click |
119 | | - button({ disabled: true, onClick }, true).simulate("click"); |
120 | | - assert.equal(onClick.callCount, 0); |
121 | | - }); |
122 | | - |
123 | | - it("pressing enter triggers onKeyDown props with any modifier flags", () => { |
124 | | - checkKeyEventCallbackInvoked("onKeyDown", "keydown", "Enter"); |
125 | | - }); |
126 | | - |
127 | | - it("pressing space triggers onKeyDown props with any modifier flags", () => { |
128 | | - checkKeyEventCallbackInvoked("onKeyDown", "keydown", " "); |
129 | | - }); |
130 | | - |
131 | | - it("calls onClick when enter key released", () => { |
132 | | - checkClickTriggeredOnKeyUp({ key: "Enter" }); |
133 | | - }); |
134 | | - |
135 | | - it("calls onClick when space key released", () => { |
136 | | - checkClickTriggeredOnKeyUp({ key: " " }); |
137 | | - }); |
138 | | - |
139 | | - it("attaches ref with createRef", () => { |
140 | | - const ref = React.createRef<HTMLButtonElement>(); |
141 | | - const wrapper = button({ ref }); |
142 | | - wrapper.update(); |
143 | | - assert.isTrue( |
144 | | - ref.current instanceof (tagName === "button" ? HTMLButtonElement : HTMLAnchorElement), |
145 | | - `ref.current should be a(n) ${tagName} element`, |
146 | | - ); |
147 | | - }); |
148 | | - |
149 | | - it("attaches ref with useRef", () => { |
150 | | - let buttonRef: React.RefObject<any> | undefined; |
151 | | - const Component = component; |
152 | | - |
153 | | - const Test = () => { |
154 | | - buttonRef = React.useRef<any>(null); |
155 | | - |
156 | | - return <Component ref={buttonRef} />; |
157 | | - }; |
158 | | - |
159 | | - const wrapper = mount(<Test />); |
160 | | - wrapper.update(); |
161 | | - |
162 | | - assert.isTrue( |
163 | | - buttonRef?.current instanceof (tagName === "button" ? HTMLButtonElement : HTMLAnchorElement), |
164 | | - `ref.current should be a(n) ${tagName} element`, |
165 | | - ); |
166 | | - }); |
167 | | - |
168 | | - function button(props: ButtonProps, ...children: React.ReactNode[]) { |
169 | | - const element = React.createElement(component, props, ...children); |
170 | | - return mount(element, { attachTo: containerElement }); |
171 | | - } |
172 | | - |
173 | | - function checkClickTriggeredOnKeyUp(keyEventProps: Partial<React.KeyboardEvent<any>>) { |
174 | | - // we need to listen for real DOM events here, since the implementation of this feature uses |
175 | | - // HTMLElement#click() - see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click |
176 | | - const onContainerClick = spy(); |
177 | | - containerElement?.addEventListener("click", onContainerClick); |
178 | | - const wrapper = button({ text: "Test" }); |
179 | | - |
180 | | - wrapper.find(`.${Classes.BUTTON}`).hostNodes().simulate("keyup", keyEventProps); |
181 | | - assert.isTrue(onContainerClick.calledOnce, "Expected a click event to bubble up to container element"); |
182 | | - } |
183 | | - |
184 | | - function checkKeyEventCallbackInvoked(callbackPropName: string, eventName: string, key: string) { |
185 | | - const callback = spy(); |
186 | | - |
187 | | - // ButtonProps doesn't include onKeyDown or onKeyUp in its |
188 | | - // definition, even though Buttons support those props. Casting as |
189 | | - // `any` gets around that for the purpose of these tests. |
190 | | - const wrapper = button({ [callbackPropName]: callback } as any); |
191 | | - const eventProps = { key, shiftKey: true, metaKey: true }; |
192 | | - wrapper.simulate(eventName, eventProps); |
193 | | - |
194 | | - // check that the callback was invoked with modifier key flags included |
195 | | - assert.equal(callback.callCount, 1); |
196 | | - assert.isTrue(callback.firstCall.args[0].shiftKey); |
197 | | - assert.isTrue(callback.firstCall.args[0].metaKey); |
198 | | - } |
| 39 | +describe("<AnchorButton>", () => { |
| 40 | + commonTests(AnchorButton); |
| 41 | + |
| 42 | + it("should attach ref", () => { |
| 43 | + const ref = React.createRef<HTMLAnchorElement>(); |
| 44 | + render(<AnchorButton ref={ref} />); |
| 45 | + |
| 46 | + expect(ref.current).to.exist; |
| 47 | + expect(ref.current).to.be.instanceOf(HTMLAnchorElement); |
| 48 | + }); |
| 49 | +}); |
| 50 | + |
| 51 | +function commonTests(Component: typeof Button | typeof AnchorButton) { |
| 52 | + it("should render its contents", () => { |
| 53 | + render(<Component className="foo" text="test" />); |
| 54 | + const button = screen.getByRole("button", { name: "test" }); |
| 55 | + |
| 56 | + expect(button).to.exist; |
| 57 | + expect(button.classList.contains(Classes.BUTTON)).to.be.true; |
| 58 | + expect(button.classList.contains("foo")).to.be.true; |
| 59 | + }); |
| 60 | + |
| 61 | + it("should render an icon", () => { |
| 62 | + render(<Component icon={IconNames.STYLE} />); |
| 63 | + const button = screen.getByRole("button"); |
| 64 | + |
| 65 | + expect(button.querySelector(`[data-icon="${IconNames.STYLE}"]`)).to.exist; |
| 66 | + }); |
| 67 | + |
| 68 | + it("should render additional props", () => { |
| 69 | + render(<Component data-test-foo="bar" />); |
| 70 | + const button = screen.getByRole("button"); |
| 71 | + |
| 72 | + expect(button.getAttribute("data-test-foo")).to.equal("bar"); |
| 73 | + }); |
| 74 | + |
| 75 | + it("should render when text prop is provided with a numeric value", () => { |
| 76 | + render(<Component text={0} />); |
| 77 | + const button = screen.getByRole("button", { name: "0" }); |
| 78 | + |
| 79 | + expect(button).to.exist; |
| 80 | + }); |
| 81 | + |
| 82 | + it("should not render a text span when children are empty", () => { |
| 83 | + render(<Component />); |
| 84 | + const button = screen.getByRole("button"); |
| 85 | + |
| 86 | + expect(button.querySelector("span")).to.not.exist; |
| 87 | + }); |
| 88 | + |
| 89 | + it("should not render a text span when text prop is empty", () => { |
| 90 | + render(<Component text="" />); |
| 91 | + const button = screen.getByRole("button"); |
| 92 | + |
| 93 | + expect(button.querySelector("span")).to.not.exist; |
| 94 | + }); |
| 95 | + |
| 96 | + it("should accept textClassName prop", () => { |
| 97 | + render(<Component text="text" textClassName="foo" />); |
| 98 | + const button = screen.getByRole("button"); |
| 99 | + |
| 100 | + expect(button.querySelector(".foo")).to.exist; |
| 101 | + }); |
| 102 | + |
| 103 | + it("should render a spinner while loading", () => { |
| 104 | + render(<Component loading={true} />); |
| 105 | + const spinner = screen.getByRole("progressbar", { name: /loading/i }); |
| 106 | + |
| 107 | + expect(spinner).to.exist; |
| 108 | + }); |
| 109 | + |
| 110 | + it("should disable button while loading", async () => { |
| 111 | + const onClick = spy(); |
| 112 | + render(<Component loading={true} onClick={onClick} />); |
| 113 | + const button = screen.getByRole("button"); |
| 114 | + |
| 115 | + await userEvent.click(button); |
| 116 | + |
| 117 | + expect(onClick.called).to.be.false; |
| 118 | + }); |
| 119 | + |
| 120 | + // This tests some subtle (potentialy unexpected) behavior, but it was an API decision we |
| 121 | + // made a long time ago which we rely on and should not break. |
| 122 | + // See https://github.com/palantir/blueprint/issues/3819#issuecomment-1189478596 |
| 123 | + it("should disable button while loading, even when disabled prop is explicity set to false", async () => { |
| 124 | + const onClick = spy(); |
| 125 | + render(<Component loading={true} disabled={false} onClick={onClick} />); |
| 126 | + const button = screen.getByRole("button"); |
| 127 | + |
| 128 | + await userEvent.click(button); |
| 129 | + |
| 130 | + expect(onClick.called).to.be.false; |
| 131 | + }); |
| 132 | + |
| 133 | + it("should trigger onClick when clicked", async () => { |
| 134 | + const onClick = spy(); |
| 135 | + render(<Component onClick={onClick} />); |
| 136 | + const button = screen.getByRole("button"); |
| 137 | + |
| 138 | + await userEvent.click(button); |
| 139 | + |
| 140 | + expect(onClick.called).to.be.true; |
| 141 | + }); |
| 142 | + |
| 143 | + it("should call onClick when enter key is pressed", async () => { |
| 144 | + const onClick = spy(); |
| 145 | + render(<Component onClick={onClick} />); |
| 146 | + const button = screen.getByRole("button"); |
| 147 | + |
| 148 | + await userEvent.type(button, "{enter}"); |
| 149 | + |
| 150 | + expect(onClick.called).to.be.true; |
| 151 | + }); |
| 152 | + |
| 153 | + it("should call onClick when space key is pressed", async () => { |
| 154 | + const onClick = spy(); |
| 155 | + render(<Component onClick={onClick} />); |
| 156 | + const button = screen.getByRole("button"); |
| 157 | + |
| 158 | + await userEvent.type(button, "{space}"); |
| 159 | + |
| 160 | + expect(onClick.called).to.be.true; |
199 | 161 | }); |
200 | 162 | } |
0 commit comments