Skip to content
Merged
7 changes: 1 addition & 6 deletions packages/components/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const testPathIgnorePatterns = [
// entry as soon as its ComponentName.test.tsx lands — this list is a
// checklist, not a migration-status list like the one above.
//
// Dialog, ErrorBoundary, Icon, Label, Portal, Scrim, and _internal are
// ErrorBoundary, Icon, Label, Portal, Scrim, and _internal are
// intentionally not on this checklist — they're internal/utility pieces
// covered indirectly through the components that consume them, not with
// dedicated test files of their own.
Expand All @@ -42,12 +42,7 @@ const testPathIgnorePatterns = [
// checklist — both are slated for removal/replacement (Paper -> Card,
// PressableHighlight -> Pressable) once the library migration completes,
// so we're not adding new tests for code we're about to delete.
"<rootDir>/src/components/EDSProvider/",
"<rootDir>/src/components/Input/",
"<rootDir>/src/components/Search/",
"<rootDir>/src/components/SelectionControls/",
"<rootDir>/src/components/TextArea/",
"<rootDir>/src/components/TextField/",
];

// Guard against the exact footgun these lists create: a follow-up PR adds
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from "react";
import { render, screen } from "@testing-library/react-native";
import { Text } from "react-native";
import { useToken } from "../../hooks/useToken";
import {
comfortableSpacingToken,
darkColorToken,
lightColorToken,
spaciousSpacingToken,
} from "../../styling/tokens";
import { EDSProvider } from "./index";

const TokenSpy = ({ onToken }: { onToken: (token: unknown) => void }) => {
const token = useToken();
onToken(token);
return null;
};

describe("EDSProvider", () => {
it("renders its children", () => {
render(
<EDSProvider colorScheme="light" density="comfortable">
<Text>Hello world</Text>
</EDSProvider>
);
expect(screen.getByText("Hello world")).toBeTruthy();
});

it("resolves the light color token for colorScheme=light", () => {
const onToken = jest.fn();
render(
<EDSProvider colorScheme="light" density="comfortable">
<TokenSpy onToken={onToken} />
</EDSProvider>
);
expect(onToken).toHaveBeenCalledWith(
expect.objectContaining({ colors: lightColorToken })
);
});

it("resolves the dark color token for colorScheme=dark", () => {
const onToken = jest.fn();
render(
<EDSProvider colorScheme="dark" density="comfortable">
<TokenSpy onToken={onToken} />
</EDSProvider>
);
expect(onToken).toHaveBeenCalledWith(
expect.objectContaining({ colors: darkColorToken })
);
});

it("resolves the comfortable spacing token for density=comfortable", () => {
const onToken = jest.fn();
render(
<EDSProvider colorScheme="light" density="comfortable">
<TokenSpy onToken={onToken} />
</EDSProvider>
);
expect(onToken).toHaveBeenCalledWith(
expect.objectContaining({ spacing: comfortableSpacingToken })
);
});

it("resolves the spacious spacing token for density=spacious", () => {
const onToken = jest.fn();
render(
<EDSProvider colorScheme="light" density="spacious">
<TokenSpy onToken={onToken} />
</EDSProvider>
);
expect(onToken).toHaveBeenCalledWith(
expect.objectContaining({ spacing: spaciousSpacingToken })
);
});

it("throws from useToken when used outside of an EDSProvider", () => {
// Expected: React logs the thrown error to the console during render.
const consoleError = jest
.spyOn(console, "error")
.mockImplementation(() => undefined);
expect(() => render(<TokenSpy onToken={jest.fn()} />)).toThrow(
"useToken must be called within a EDSProvider"
);
consoleError.mockRestore();
});
});
98 changes: 98 additions & 0 deletions packages/components/src/components/Input/Input.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from "react";
import { Text } from "react-native";
import { fireEvent, render, screen } from "test-utils";
import { Input } from "./index";

describe("Input", () => {
it("calls onChange with the new text", () => {
const onChange = jest.fn();
render(<Input onChange={onChange} placeholder="Type here" />);
fireEvent.changeText(screen.getByPlaceholderText("Type here"), "hi");
expect(onChange).toHaveBeenCalledWith("hi");
});

it("renders start and end text", () => {
render(<Input startText="https://" endText=".com" />);
expect(screen.getByText("https://")).toBeTruthy();
expect(screen.getByText(".com")).toBeTruthy();
});

it("renders start and end adornments", () => {
render(
<Input
startAdornment={<Text>start-adornment</Text>}
endAdornment={<Text>end-adornment</Text>}
/>
);
expect(screen.getByText("start-adornment")).toBeTruthy();
expect(screen.getByText("end-adornment")).toBeTruthy();
});

it("shows an error icon when invalid", () => {
render(<Input invalid placeholder="Type here" />);
expect(
screen.UNSAFE_getByProps({ name: "alert-circle" })
).toBeTruthy();
});

it("hides the error icon when hideErrorIcon is true", () => {
render(<Input invalid hideErrorIcon placeholder="Type here" />);
expect(
screen.UNSAFE_queryByProps({ name: "alert-circle" })
).toBeFalsy();
});

it("hides the error icon when disabled", () => {
render(<Input invalid disabled placeholder="Type here" />);
expect(
screen.UNSAFE_queryByProps({ name: "alert-circle" })
).toBeFalsy();
});

it("disables editing and exposes disabled to assistive technology", () => {
render(<Input disabled placeholder="Type here" />);
const input = screen.getByPlaceholderText("Type here");
expect(input).toHaveProp("editable", false);
expect(input).toHaveProp("accessibilityState", { disabled: true });
});

it("disables editing but does not expose disabled when readOnly", () => {
// RNTL's toBeDisabled() treats any non-editable TextInput as disabled
// regardless of accessibilityState, so it can't be used to assert this
// distinction — check the actual prop the component controls instead.
render(<Input readOnly placeholder="Type here" />);
const input = screen.getByPlaceholderText("Type here");
expect(input).toHaveProp("editable", false);
expect(input).toHaveProp("accessibilityState", { disabled: false });
});

it("merges a caller-supplied accessibilityState, with the component's own disabled value winning on conflict", () => {
render(
<Input
accessibilityState={{ selected: true, disabled: true }}
placeholder="Type here"
/>
);
expect(screen.getByPlaceholderText("Type here")).toHaveProp(
"accessibilityState",
{ selected: true, disabled: false }
);
});

it("calls the user-provided onFocus and onBlur handlers", () => {
const onFocus = jest.fn();
const onBlur = jest.fn();
render(
<Input
onFocus={onFocus}
onBlur={onBlur}
placeholder="Type here"
/>
);
const input = screen.getByPlaceholderText("Type here");
fireEvent(input, "focus");
expect(onFocus).toHaveBeenCalledTimes(1);
fireEvent(input, "blur");
expect(onBlur).toHaveBeenCalledTimes(1);
});
});
117 changes: 117 additions & 0 deletions packages/components/src/components/Search/Search.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React from "react";
import { fireEvent, render, screen } from "test-utils";
import { Search } from "./index";

describe("Search", () => {
it("renders label, description, and helperMessage", () => {
render(
<Search
label="Search products"
description="Search by name or SKU"
helperMessage="Press enter to search"
/>
);
expect(screen.getByText("Search products")).toBeTruthy();
expect(screen.getByText("Search by name or SKU")).toBeTruthy();
expect(screen.getByText("Press enter to search")).toBeTruthy();
});

it("calls onChange when text changes", () => {
const onChange = jest.fn();
render(<Search onChange={onChange} placeholder="Search" />);
fireEvent.changeText(screen.getByPlaceholderText("Search"), "shoes");
expect(onChange).toHaveBeenCalledWith("shoes");
});

it("syncs the displayed text when the value prop changes (controlled usage)", () => {
const { rerender } = render(
<Search value="shoes" placeholder="Search" />
);
expect(screen.getByPlaceholderText("Search")).toHaveProp(
"value",
"shoes"
);
rerender(<Search value="boots" placeholder="Search" />);
expect(screen.getByPlaceholderText("Search")).toHaveProp(
"value",
"boots"
);
});

it("does not show a clear button when there is no text", () => {
render(<Search placeholder="Search" />);
expect(screen.queryByLabelText("Clear search")).toBeFalsy();
});

it("shows a clear button once there is text, and clears it on press", () => {
const onChange = jest.fn();
render(
<Search
defaultValue="shoes"
onChange={onChange}
placeholder="Search"
/>
);
fireEvent.press(screen.getByLabelText("Clear search"));
expect(onChange).toHaveBeenCalledWith("");
expect(screen.getByPlaceholderText("Search")).toHaveProp(
"value",
""
);
});

it("does not show a clear button when disabled", () => {
render(<Search defaultValue="shoes" disabled placeholder="Search" />);
expect(screen.queryByLabelText("Clear search")).toBeFalsy();
});

it("does not show a clear button when readOnly", () => {
render(<Search defaultValue="shoes" readOnly placeholder="Search" />);
expect(screen.queryByLabelText("Clear search")).toBeFalsy();
});

it("does not render a Cancel button by default", () => {
render(<Search placeholder="Search" />);
expect(screen.queryByText("Cancel")).toBeFalsy();
});

it("renders a Cancel button when cancellable, clearing text and calling onCancelPress", () => {
// The Cancel button's wrapper sets pointerEvents to "none" until the
// input is focused (it's an absolutely-positioned overlay that slides
// in on focus), so it isn't press-able until the input is focused first.
Comment thread
Chibuzor-Nwemambu marked this conversation as resolved.
const onChange = jest.fn();
const onCancelPress = jest.fn();
render(
<Search
cancellable
defaultValue="shoes"
onChange={onChange}
onCancelPress={onCancelPress}
placeholder="Search"
/>
);
fireEvent(screen.getByPlaceholderText("Search"), "focus");
fireEvent.press(screen.getByText("Cancel"));
expect(onChange).toHaveBeenCalledWith("");
expect(onCancelPress).toHaveBeenCalledTimes(1);
});

it("shows the error icon when invalid", () => {
render(<Search invalid placeholder="Search" />);
expect(
screen.UNSAFE_getByProps({ name: "alert-circle" })
).toBeTruthy();
});

it("hides the error icon when disabled even if invalid", () => {
render(<Search invalid disabled placeholder="Search" />);
expect(
screen.UNSAFE_queryByProps({ name: "alert-circle" })
).toBeFalsy();
});

it("always shows the search icon", () => {
render(<Search placeholder="Search" />);
expect(screen.UNSAFE_getByProps({ name: "magnify" })).toBeTruthy();
});
});
Loading
Loading