Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(native): Add toBeDisabled #140

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ node_modules/
# VSCode
.vscode/

# idea
.idea/

# Packages
*.tgz
3 changes: 2 additions & 1 deletion packages/native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@
"test": "NODE_ENV=test mocha"
},
"dependencies": {
"dot-prop-immutable": "^2.1.1",
"fast-deep-equal": "^3.1.3",
"tslib": "^2.6.2"
},
"devDependencies": {
"@assertive-ts/core": "workspace:^",
"@testing-library/react-native": "^12.4.4",
"@testing-library/react-native": "^12.9.0",
"@types/mocha": "^10.0.6",
"@types/node": "^20.11.19",
"@types/react": "^18.2.70",
Expand Down
77 changes: 77 additions & 0 deletions packages/native/src/lib/ElementAssertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Assertion, AssertionError } from "@assertive-ts/core";
import { get } from "dot-prop-immutable";
import { ReactTestInstance } from "react-test-renderer";

export class ElementAssertion extends Assertion<ReactTestInstance> {
public constructor(actual: ReactTestInstance) {
super(actual);
}

public override toString = (): string => {
if (this.actual === null) {
return "null";
}

return `<${this.actual.type.toString()} testID="${this.actual.props.testID}"... />`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure about this? Not all elements will have a testID. Also, using the testID should be our last resource when using tools like testing-library.

};

/**
* Check if the component is disabled.
*
* @example
* ```
* expect(component).toBeDisabled();
* ```
*
* @returns the assertion instance
*/
public toBeDisabled(): this {
const error = new AssertionError({
actual: this.actual,
message: `Expected element ${this.toString()} to be disabled.`,
});
const invertedError = new AssertionError({
actual: this.actual,
message: `Received element ${this.toString()} not to be disabled.`,
});

return this.execute({
assertWhen: this.isElementDisabled(this.actual) || this.isAncestorDisabled(this.actual),
error,
invertedError,
});
}

/**
* Check if the component is enabled.
*
* @example
* ```
* expect(component).toBeEnabled();
* ```
* @returns the assertion instance
*/
public toBeEnabled(): this {
return this.not.toBeDisabled();
}

private isElementDisabled(element: ReactTestInstance): boolean {
const { type } = element;
const elementType = type.toString();
if (elementType === "TextInput" && element?.props?.editable === false) {
return true;
}

return (
get(element, "props.aria-disabled")
|| get(element, "props.disabled", false)
|| get(element, "props.accessibilityState.disabled", false)
|| get<ReactTestInstance, string[]>(element, "props.accessibilityStates", []).includes("disabled")
);
}

private isAncestorDisabled(element: ReactTestInstance): boolean {
const { parent } = element;
return parent !== null && (this.isElementDisabled(element) || this.isAncestorDisabled(parent));
}
}
32 changes: 32 additions & 0 deletions packages/native/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Plugin } from "@assertive-ts/core";
import { ReactTestInstance } from "react-test-renderer";

import { ElementAssertion } from "./lib/ElementAssertion";

declare module "@assertive-ts/core" {

export interface Expect {
// eslint-disable-next-line @typescript-eslint/prefer-function-type
(actual: ReactTestInstance): ElementAssertion;
}
}

const ElementPlugin: Plugin<ReactTestInstance, ElementAssertion> = {
Assertion: ElementAssertion,
insertAt: "top",
predicate: (actual): actual is ReactTestInstance =>
typeof actual === "object"
&& actual !== null
&& "instance" in actual
&& typeof actual.instance === "object"
&& "type" in actual
&& typeof actual.type === "object"
&& "props" in actual
&& typeof actual.props === "object"
&& "parent" in actual
&& typeof actual.parent === "object"
&& "children" in actual
&& typeof actual.children === "object",
Comment on lines +26 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these props still present if the element does not have a parent or children? 🤔

};

export const NativePlugin = [ElementPlugin];
106 changes: 106 additions & 0 deletions packages/native/test/lib/ElementAssertion.test.tsx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're missing tests for .not.toBeDisabled() and not.ToBeEnabled(). The messaging is different, so having some unit tests is good. I'd test them together with the not inverted test cases to make things simpler, check the core package for examples 🙂

Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { AssertionError, expect } from "@assertive-ts/core";
import { render } from "@testing-library/react-native";
import {
View,
TextInput,
} from "react-native";

import { ElementAssertion } from "../../src/lib/ElementAssertion";

describe("[Unit] ElementAssertion.test.ts", () => {
describe(".toBeDisabled", () => {
context("when the element is TextInput", () => {
context("and the element is not editable", () => {
it("returns the assertion instance", () => {
const element = render(
<TextInput testID="id" editable={false} />,
);
const test = new ElementAssertion(element.getByTestId("id"));
expect(test.toBeDisabled()).toBe(test);
});
});

context("and the element is editable", () => {
it("throws an error", () => {
const reactElement = render(<TextInput editable={true} testID="id" />);
const test = new ElementAssertion(reactElement.getByTestId("id"));

expect(() => test.toBeDisabled())
.toThrowError(AssertionError)
.toHaveMessage('Expected element <TextInput testID="id"... /> to be disabled.');
});
});
});

context("when the parent has property aria-disabled", () => {
context("if parent aria-disabled = true", () => {
it("returns assertion instance for parent and child element", () => {
const element = render(
<View aria-disabled={true} testID="parentId">
<View testID="childId">
<TextInput />
</View>
</View>,
);

const parent = new ElementAssertion(element.getByTestId("parentId"));
const child = new ElementAssertion(element.getByTestId("childId"));
expect(parent.toBeDisabled()).toBe(parent);
expect(child.toBeDisabled()).toBe(child);
});
});

context("if parent aria-disabled = false", () => {
it("throws an error for parent and child element", () => {
const element = render(
<View aria-disabled={false} testID="parentId">
<View testID="childId">
<TextInput />
</View>
</View>,
);

const parent = new ElementAssertion(element.getByTestId("parentId"));
const child = new ElementAssertion(element.getByTestId("childId"));

expect(parent.toBeEnabled()).toBeEqual(parent);
expect(() => parent.toBeDisabled())
.toThrowError(AssertionError)
.toHaveMessage('Expected element <View testID="parentId"... /> to be disabled.');
expect(() => child.toBeDisabled())
.toThrowError(AssertionError)
.toHaveMessage('Expected element <View testID="childId"... /> to be disabled.');
});
});
});

context("when the element contains property aria-disabled", () => {
const element = render(
<View testID="parentId">
<View aria-disabled={true} testID="childId">
<TextInput />
</View>
</View>,
);

const parent = new ElementAssertion(element.getByTestId("parentId"));
const child = new ElementAssertion(element.getByTestId("childId"));

context("if child contains aria-disabled = true", () => {
it("returns assertion instance for child element", () => {
expect(child.toBeDisabled()).toBe(child);
expect(() => child.toBeEnabled())
.toThrowError(AssertionError)
.toHaveMessage("Received element <View testID=\"childId\"... /> not to be disabled.");
});

it("returns error for parent element", () => {
expect(parent.toBeEnabled()).toBe(parent);
expect(() => parent.toBeDisabled())
.toThrowError(AssertionError)
.toHaveMessage("Expected element <View testID=\"parentId\"... /> to be disabled.");
});
});
});
});
});
11 changes: 6 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,13 @@ __metadata:
resolution: "@assertive-ts/native@workspace:packages/native"
dependencies:
"@assertive-ts/core": "workspace:^"
"@testing-library/react-native": "npm:^12.4.4"
"@testing-library/react-native": "npm:^12.9.0"
"@types/mocha": "npm:^10.0.6"
"@types/node": "npm:^20.11.19"
"@types/react": "npm:^18.2.70"
"@types/react-test-renderer": "npm:^18.0.7"
"@types/sinon": "npm:^17.0.3"
dot-prop-immutable: "npm:^2.1.1"
fast-deep-equal: "npm:^3.1.3"
mocha: "npm:^10.3.0"
react: "npm:^18.2.0"
Expand Down Expand Up @@ -2958,9 +2959,9 @@ __metadata:
languageName: node
linkType: hard

"@testing-library/react-native@npm:^12.4.4":
version: 12.8.1
resolution: "@testing-library/react-native@npm:12.8.1"
"@testing-library/react-native@npm:^12.9.0":
version: 12.9.0
resolution: "@testing-library/react-native@npm:12.9.0"
dependencies:
jest-matcher-utils: "npm:^29.7.0"
pretty-format: "npm:^29.7.0"
Expand All @@ -2973,7 +2974,7 @@ __metadata:
peerDependenciesMeta:
jest:
optional: true
checksum: 10/eaa09cb560a469c686b8eb0ee8085bb54654a481e6bcf9eb5bc7b756c5303ca6b5c17ab2ef1479b8c245ac153ac69907d47c30ec9b496a29a6e459baa3d3f5d9
checksum: 10/dcee1d836e76198a2c397fbcb7db24a40e2c45b2dcbca266a4a5d8a802a859a1e8c50755336a2d70f9eec478de964951673b78acb2e03c007b2bee5b8d8766d1
languageName: node
linkType: hard

Expand Down
Loading