Skip to content

Commit d09e8c9

Browse files
authored
Update button tests to use RTL (#7106)
1 parent be3f54f commit d09e8c9

File tree

3 files changed

+186
-177
lines changed

3 files changed

+186
-177
lines changed

packages/core/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@
7474
"@blueprintjs/karma-build-scripts": "workspace:^",
7575
"@blueprintjs/node-build-scripts": "workspace:^",
7676
"@blueprintjs/test-commons": "workspace:^",
77+
"@testing-library/dom": "^10.4.0",
7778
"@testing-library/react": "^12.1.5",
79+
"@testing-library/user-event": "^13.5.0",
7880
"@types/use-sync-external-store": "0.0.6",
7981
"enzyme": "^3.11.0",
8082
"karma": "^6.4.2",
Lines changed: 139 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 Palantir Technologies, Inc. All rights reserved.
2+
* Copyright 2024 Palantir Technologies, Inc. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,187 +14,149 @@
1414
* limitations under the License.
1515
*/
1616

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";
1920
import * as React from "react";
2021
import { spy } from "sinon";
2122

22-
import { AnchorButton, Button, type ButtonProps, Classes, Icon, Spinner } from "../../src";
23+
import { IconNames } from "@blueprintjs/icons";
2324

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+
});
2737
});
2838

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;
199161
});
200162
}

0 commit comments

Comments
 (0)