Skip to content

Commit 27d0001

Browse files
suany0805lopenchikdquistanchala
authored
feat(native): Add toBeDisabled (#140)
Co-authored-by: Carolina Lopez <calopez@twilio.com> Co-authored-by: Karla Quistanchala <karly.btr.97@hotmail.com>
1 parent 6a68827 commit 27d0001

File tree

6 files changed

+265
-6
lines changed

6 files changed

+265
-6
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,8 @@ node_modules/
1717
# VSCode
1818
.vscode/
1919

20+
# idea
21+
.idea/
22+
2023
# Packages
2124
*.tgz

packages/native/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@
3434
"test": "NODE_ENV=test mocha"
3535
},
3636
"dependencies": {
37+
"dot-prop-immutable": "^2.1.1",
3738
"fast-deep-equal": "^3.1.3",
3839
"tslib": "^2.6.2"
3940
},
4041
"devDependencies": {
4142
"@assertive-ts/core": "workspace:^",
42-
"@testing-library/react-native": "^12.4.4",
43+
"@testing-library/react-native": "^12.9.0",
4344
"@types/mocha": "^10.0.6",
4445
"@types/node": "^20.11.19",
4546
"@types/react": "^18.2.70",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { Assertion, AssertionError } from "@assertive-ts/core";
2+
import { get } from "dot-prop-immutable";
3+
import { ReactTestInstance } from "react-test-renderer";
4+
5+
export class ElementAssertion extends Assertion<ReactTestInstance> {
6+
public constructor(actual: ReactTestInstance) {
7+
super(actual);
8+
}
9+
10+
public override toString = (): string => {
11+
if (this.actual === null) {
12+
return "null";
13+
}
14+
15+
return `<${this.actual.type.toString()} ... />`;
16+
};
17+
18+
/**
19+
* Check if the component is disabled or has been disabled by an ancestor.
20+
*
21+
* @example
22+
* ```
23+
* expect(component).toBeDisabled();
24+
* ```
25+
*
26+
* @returns the assertion instance
27+
*/
28+
public toBeDisabled(): this {
29+
const error = new AssertionError({
30+
actual: this.actual,
31+
message: `Expected element ${this.toString()} to be disabled.`,
32+
});
33+
const invertedError = new AssertionError({
34+
actual: this.actual,
35+
message: `Expected element ${this.toString()} to NOT be disabled.`,
36+
});
37+
38+
return this.execute({
39+
assertWhen: this.isElementDisabled(this.actual) || this.isAncestorDisabled(this.actual),
40+
error,
41+
invertedError,
42+
});
43+
}
44+
45+
/**
46+
* Check if the component is enabled.
47+
*
48+
* @example
49+
* ```
50+
* expect(component).toBeEnabled();
51+
* ```
52+
* @returns the assertion instance
53+
*/
54+
public toBeEnabled(): this {
55+
const error = new AssertionError({
56+
actual: this.actual,
57+
message: `Expected element ${this.toString()} to be enabled.`,
58+
});
59+
const invertedError = new AssertionError({
60+
actual: this.actual,
61+
message: `Expected element ${this.toString()} to NOT be enabled.`,
62+
});
63+
64+
return this.execute({
65+
assertWhen: !this.isElementDisabled(this.actual) && !this.isAncestorDisabled(this.actual),
66+
error,
67+
invertedError,
68+
});
69+
}
70+
71+
private isElementDisabled(element: ReactTestInstance): boolean {
72+
const { type } = element;
73+
const elementType = type.toString();
74+
if (elementType === "TextInput" && element?.props?.editable === false) {
75+
return true;
76+
}
77+
78+
return (
79+
get(element, "props.aria-disabled")
80+
|| get(element, "props.disabled", false)
81+
|| get(element, "props.accessibilityState.disabled", false)
82+
|| get<ReactTestInstance, string[]>(element, "props.accessibilityStates", []).includes("disabled")
83+
);
84+
}
85+
86+
private isAncestorDisabled(element: ReactTestInstance): boolean {
87+
const { parent } = element;
88+
return parent !== null && (this.isElementDisabled(element) || this.isAncestorDisabled(parent));
89+
}
90+
}

packages/native/src/main.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Plugin } from "@assertive-ts/core";
2+
import { ReactTestInstance } from "react-test-renderer";
3+
4+
import { ElementAssertion } from "./lib/ElementAssertion";
5+
6+
declare module "@assertive-ts/core" {
7+
8+
export interface Expect {
9+
// eslint-disable-next-line @typescript-eslint/prefer-function-type
10+
(actual: ReactTestInstance): ElementAssertion;
11+
}
12+
}
13+
14+
const ElementPlugin: Plugin<ReactTestInstance, ElementAssertion> = {
15+
Assertion: ElementAssertion,
16+
insertAt: "top",
17+
predicate: (actual): actual is ReactTestInstance =>
18+
typeof actual === "object"
19+
&& actual !== null
20+
&& "instance" in actual
21+
&& typeof actual.instance === "object"
22+
&& "type" in actual
23+
&& typeof actual.type === "object"
24+
&& "props" in actual
25+
&& typeof actual.props === "object"
26+
&& "parent" in actual
27+
&& typeof actual.parent === "object"
28+
&& "children" in actual
29+
&& typeof actual.children === "object",
30+
};
31+
32+
export const NativePlugin = [ElementPlugin];
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { AssertionError, expect } from "@assertive-ts/core";
2+
import { render } from "@testing-library/react-native";
3+
import {
4+
View,
5+
TextInput,
6+
} from "react-native";
7+
8+
import { ElementAssertion } from "../../src/lib/ElementAssertion";
9+
10+
describe("[Unit] ElementAssertion.test.ts", () => {
11+
describe(".toBeDisabled", () => {
12+
context("when the element is TextInput", () => {
13+
context("and the element is not editable", () => {
14+
it("returns the assertion instance", () => {
15+
const element = render(
16+
<TextInput testID="id" editable={false} />,
17+
);
18+
const test = new ElementAssertion(element.getByTestId("id"));
19+
expect(test.toBeDisabled()).toBe(test);
20+
expect(test.not.toBeEnabled()).toBeEqual(test);
21+
expect(() => test.toBeEnabled())
22+
.toThrowError(AssertionError)
23+
.toHaveMessage("Expected element <TextInput ... /> to be enabled.");
24+
});
25+
});
26+
27+
context("and the element is editable", () => {
28+
it("throws an error", () => {
29+
const reactElement = render(<TextInput editable={true} testID="id" />);
30+
const test = new ElementAssertion(reactElement.getByTestId("id"));
31+
32+
expect(() => test.toBeDisabled())
33+
.toThrowError(AssertionError)
34+
.toHaveMessage("Expected element <TextInput ... /> to be disabled.");
35+
expect(() => test.not.toBeEnabled())
36+
.toThrowError(AssertionError)
37+
.toHaveMessage("Expected element <TextInput ... /> to NOT be enabled.");
38+
});
39+
});
40+
});
41+
42+
context("when the parent has property aria-disabled", () => {
43+
context("if parent aria-disabled = true", () => {
44+
it("returns assertion instance for parent and child element", () => {
45+
const element = render(
46+
<View aria-disabled={true} testID="parentId">
47+
<View testID="childId">
48+
<TextInput />
49+
</View>
50+
</View>,
51+
);
52+
53+
const parent = new ElementAssertion(element.getByTestId("parentId"));
54+
const child = new ElementAssertion(element.getByTestId("childId"));
55+
expect(parent.toBeDisabled()).toBe(parent);
56+
expect(child.toBeDisabled()).toBe(child);
57+
expect(() => parent.toBeEnabled())
58+
.toThrowError(AssertionError)
59+
.toHaveMessage("Expected element <View ... /> to be enabled.");
60+
expect(() => parent.not.toBeDisabled())
61+
.toThrowError(AssertionError)
62+
.toHaveMessage("Expected element <View ... /> to NOT be disabled.");
63+
});
64+
});
65+
66+
context("if parent aria-disabled = false", () => {
67+
it("throws an error for parent and child element", () => {
68+
const element = render(
69+
<View aria-disabled={false} testID="parentId">
70+
<View testID="childId">
71+
<TextInput />
72+
</View>
73+
</View>,
74+
);
75+
76+
const parent = new ElementAssertion(element.getByTestId("parentId"));
77+
const child = new ElementAssertion(element.getByTestId("childId"));
78+
79+
expect(parent.toBeEnabled()).toBeEqual(parent);
80+
expect(parent.not.toBeDisabled()).toBeEqual(parent);
81+
expect(() => parent.toBeDisabled())
82+
.toThrowError(AssertionError)
83+
.toHaveMessage("Expected element <View ... /> to be disabled.");
84+
expect(() => parent.not.toBeEnabled())
85+
.toThrowError(AssertionError)
86+
.toHaveMessage("Expected element <View ... /> to NOT be enabled.");
87+
expect(() => child.toBeDisabled())
88+
.toThrowError(AssertionError)
89+
.toHaveMessage("Expected element <View ... /> to be disabled.");
90+
expect(() => child.not.toBeEnabled())
91+
.toThrowError(AssertionError)
92+
.toHaveMessage("Expected element <View ... /> to NOT be enabled.");
93+
});
94+
});
95+
});
96+
97+
context("when the element contains property aria-disabled", () => {
98+
const element = render(
99+
<View testID="parentId">
100+
<View aria-disabled={true} testID="childId">
101+
<TextInput />
102+
</View>
103+
</View>,
104+
);
105+
106+
const parent = new ElementAssertion(element.getByTestId("parentId"));
107+
const child = new ElementAssertion(element.getByTestId("childId"));
108+
109+
context("if child contains aria-disabled = true", () => {
110+
it("returns assertion instance for child element", () => {
111+
expect(child.toBeDisabled()).toBe(child);
112+
expect(() => child.toBeEnabled())
113+
.toThrowError(AssertionError)
114+
.toHaveMessage("Expected element <View ... /> to be enabled.");
115+
expect(() => child.not.toBeDisabled())
116+
.toThrowError(AssertionError)
117+
.toHaveMessage("Expected element <View ... /> to NOT be disabled.");
118+
});
119+
120+
it("returns error for parent element", () => {
121+
expect(parent.toBeEnabled()).toBeEqual(parent);
122+
expect(() => parent.toBeDisabled())
123+
.toThrowError(AssertionError)
124+
.toHaveMessage("Expected element <View ... /> to be disabled.");
125+
expect(() => parent.not.toBeEnabled())
126+
.toThrowError(AssertionError)
127+
.toHaveMessage("Expected element <View ... /> to NOT be enabled.");
128+
});
129+
});
130+
});
131+
});
132+
});

yarn.lock

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,13 @@ __metadata:
8686
resolution: "@assertive-ts/native@workspace:packages/native"
8787
dependencies:
8888
"@assertive-ts/core": "workspace:^"
89-
"@testing-library/react-native": "npm:^12.4.4"
89+
"@testing-library/react-native": "npm:^12.9.0"
9090
"@types/mocha": "npm:^10.0.6"
9191
"@types/node": "npm:^20.11.19"
9292
"@types/react": "npm:^18.2.70"
9393
"@types/react-test-renderer": "npm:^18.0.7"
9494
"@types/sinon": "npm:^17.0.3"
95+
dot-prop-immutable: "npm:^2.1.1"
9596
fast-deep-equal: "npm:^3.1.3"
9697
mocha: "npm:^10.3.0"
9798
react: "npm:^18.2.0"
@@ -2958,9 +2959,9 @@ __metadata:
29582959
languageName: node
29592960
linkType: hard
29602961

2961-
"@testing-library/react-native@npm:^12.4.4":
2962-
version: 12.8.1
2963-
resolution: "@testing-library/react-native@npm:12.8.1"
2962+
"@testing-library/react-native@npm:^12.9.0":
2963+
version: 12.9.0
2964+
resolution: "@testing-library/react-native@npm:12.9.0"
29642965
dependencies:
29652966
jest-matcher-utils: "npm:^29.7.0"
29662967
pretty-format: "npm:^29.7.0"
@@ -2973,7 +2974,7 @@ __metadata:
29732974
peerDependenciesMeta:
29742975
jest:
29752976
optional: true
2976-
checksum: 10/eaa09cb560a469c686b8eb0ee8085bb54654a481e6bcf9eb5bc7b756c5303ca6b5c17ab2ef1479b8c245ac153ac69907d47c30ec9b496a29a6e459baa3d3f5d9
2977+
checksum: 10/dcee1d836e76198a2c397fbcb7db24a40e2c45b2dcbca266a4a5d8a802a859a1e8c50755336a2d70f9eec478de964951673b78acb2e03c007b2bee5b8d8766d1
29772978
languageName: node
29782979
linkType: hard
29792980

0 commit comments

Comments
 (0)