diff --git a/packages/native/src/lib/ElementAssertion.ts b/packages/native/src/lib/ElementAssertion.ts index d1350b9..4abdb73 100644 --- a/packages/native/src/lib/ElementAssertion.ts +++ b/packages/native/src/lib/ElementAssertion.ts @@ -2,17 +2,15 @@ import { Assertion, AssertionError } from "@assertive-ts/core"; import { get } from "dot-prop-immutable"; import { ReactTestInstance } from "react-test-renderer"; +import { instanceToString, isEmpty } from "./helpers/helpers"; + export class ElementAssertion extends Assertion { public constructor(actual: ReactTestInstance) { super(actual); } public override toString = (): string => { - if (this.actual === null) { - return "null"; - } - - return `<${this.actual.type.toString()} ... />`; + return instanceToString(this.actual); }; /** @@ -32,7 +30,7 @@ export class ElementAssertion extends Assertion { }); const invertedError = new AssertionError({ actual: this.actual, - message: `Expected element ${this.toString()} to NOT be disabled.`, + message: `Expected element ${this.toString()} NOT to be disabled.`, }); return this.execute({ @@ -43,7 +41,7 @@ export class ElementAssertion extends Assertion { } /** - * Check if the component is enabled. + * Check if the component is enabled and has not been disabled by an ancestor. * * @example * ``` @@ -58,7 +56,7 @@ export class ElementAssertion extends Assertion { }); const invertedError = new AssertionError({ actual: this.actual, - message: `Expected element ${this.toString()} to NOT be enabled.`, + message: `Expected element ${this.toString()} NOT to be enabled.`, }); return this.execute({ @@ -68,6 +66,104 @@ export class ElementAssertion extends Assertion { }); } + /** + * Check if the element is empty. + * + * @example + * ``` + * expect(element).toBeEmpty(); + * ``` + * + * @returns the assertion instance + */ + public toBeEmpty(): this { + const error = new AssertionError({ + actual: this.actual, + message: `Expected element ${this.toString()} to be empty.`, + }); + const invertedError = new AssertionError({ + actual: this.actual, + message: `Expected element ${this.toString()} NOT to be empty.`, + }); + + return this.execute({ + assertWhen: isEmpty(this.actual.children), + error, + invertedError, + }); + } + + /** + * Check if the element is visible and has not been hidden by an ancestor. + * + * @example + * ``` + * expect(element).toBeVisible(); + * ``` + * + * @returns the assertion instance + */ + public toBeVisible(): this { + const error = new AssertionError({ + actual: this.actual, + message: `Expected element ${this.toString()} to be visible.`, + }); + const invertedError = new AssertionError({ + actual: this.actual, + message: `Expected element ${this.toString()} NOT to be visible.`, + }); + + return this.execute({ + assertWhen: this.isElementVisible(this.actual) && !this.isAncestorNotVisible(this.actual), + error, + invertedError, + }); + } + + /** + * Check if an element is contained within another element. + * + * @example + * ``` + * expect(parent).toContainElement(child); + * ``` + * + * @param element - The element to check for. + * @returns the assertion instance + */ + public toContainElement(element: ReactTestInstance): this { + const error = new AssertionError({ + actual: this.actual, + message: `Expected element ${this.toString()} to contain element ${instanceToString(element)}.`, + }); + const invertedError = new AssertionError({ + actual: this.actual, + message: `Expected element ${this.toString()} NOT to contain element ${instanceToString(element)}.`, + }); + + const isElementContained = ( + parentElement: ReactTestInstance, + childElement: ReactTestInstance, + ): boolean => { + if (parentElement === null || childElement === null) { + return false; + } + + return ( + parentElement.findAll( + node => + node.type === childElement.type && node.props === childElement.props, + ).length > 0 + ); + }; + + return this.execute({ + assertWhen: isElementContained(this.actual, element), + error, + invertedError, + }); + } + private isElementDisabled(element: ReactTestInstance): boolean { const { type } = element; const elementType = type.toString(); @@ -87,4 +183,24 @@ export class ElementAssertion extends Assertion { const { parent } = element; return parent !== null && (this.isElementDisabled(element) || this.isAncestorDisabled(parent)); } + + private isElementVisible(element: ReactTestInstance): boolean { + const { type } = element; + const elementType = type.toString(); + if (elementType === "Modal" && !element?.props?.visible === true) { + return false; + } + + return ( + get(element, "props.style.display") !== "none" + && get(element, "props.style.opacity") !== 0 + && get(element, "props.accessibilityElementsHidden") !== true + && get(element, "props.importantForAccessibility") !== "no-hide-descendants" + ); + } + + private isAncestorNotVisible(element: ReactTestInstance): boolean { + const { parent } = element; + return parent !== null && (!this.isElementVisible(element) || this.isAncestorNotVisible(parent)); + } } diff --git a/packages/native/src/lib/helpers/helpers.ts b/packages/native/src/lib/helpers/helpers.ts new file mode 100644 index 0000000..6ab4db1 --- /dev/null +++ b/packages/native/src/lib/helpers/helpers.ts @@ -0,0 +1,33 @@ +import { ReactTestInstance } from "react-test-renderer"; + +/** + * Checks if a value is empty. + * + * @param value - The value to check. + * @returns `true` if the value is empty, `false` otherwise. + */ +export function isEmpty(value: unknown): boolean { + if (!value) { + return true; + } + + if (Array.isArray(value)) { + return value.length === 0; + } + + return false; +} + +/** + * Converts a ReactTestInstance to a string representation. + * + * @param instance - The ReactTestInstance to convert. + * @returns A string representation of the instance. + */ +export function instanceToString(instance: ReactTestInstance | null): string { + if (instance === null) { + return "null"; + } + + return `<${instance.type.toString()} ... />`; +} diff --git a/packages/native/test/lib/ElementAssertion.test.tsx b/packages/native/test/lib/ElementAssertion.test.tsx index 6db8aba..cfb4b97 100644 --- a/packages/native/test/lib/ElementAssertion.test.tsx +++ b/packages/native/test/lib/ElementAssertion.test.tsx @@ -3,6 +3,8 @@ import { render } from "@testing-library/react-native"; import { View, TextInput, + Text, + Modal, } from "react-native"; import { ElementAssertion } from "../../src/lib/ElementAssertion"; @@ -34,7 +36,7 @@ describe("[Unit] ElementAssertion.test.ts", () => { .toHaveMessage("Expected element to be disabled."); expect(() => test.not.toBeEnabled()) .toThrowError(AssertionError) - .toHaveMessage("Expected element to NOT be enabled."); + .toHaveMessage("Expected element NOT to be enabled."); }); }); }); @@ -59,7 +61,7 @@ describe("[Unit] ElementAssertion.test.ts", () => { .toHaveMessage("Expected element to be enabled."); expect(() => parent.not.toBeDisabled()) .toThrowError(AssertionError) - .toHaveMessage("Expected element to NOT be disabled."); + .toHaveMessage("Expected element NOT to be disabled."); }); }); @@ -83,13 +85,13 @@ describe("[Unit] ElementAssertion.test.ts", () => { .toHaveMessage("Expected element to be disabled."); expect(() => parent.not.toBeEnabled()) .toThrowError(AssertionError) - .toHaveMessage("Expected element to NOT be enabled."); + .toHaveMessage("Expected element NOT to be enabled."); expect(() => child.toBeDisabled()) .toThrowError(AssertionError) .toHaveMessage("Expected element to be disabled."); expect(() => child.not.toBeEnabled()) .toThrowError(AssertionError) - .toHaveMessage("Expected element to NOT be enabled."); + .toHaveMessage("Expected element NOT to be enabled."); }); }); }); @@ -114,7 +116,7 @@ describe("[Unit] ElementAssertion.test.ts", () => { .toHaveMessage("Expected element to be enabled."); expect(() => child.not.toBeDisabled()) .toThrowError(AssertionError) - .toHaveMessage("Expected element to NOT be disabled."); + .toHaveMessage("Expected element NOT to be disabled."); }); it("returns error for parent element", () => { @@ -124,9 +126,209 @@ describe("[Unit] ElementAssertion.test.ts", () => { .toHaveMessage("Expected element to be disabled."); expect(() => parent.not.toBeEnabled()) .toThrowError(AssertionError) - .toHaveMessage("Expected element to NOT be enabled."); + .toHaveMessage("Expected element NOT to be enabled."); }); }); }); }); + + describe(".toBeEmpty", () => { + context("when the element is empty", () => { + it("returns the assertion instance", () => { + const element = render(); + const test = new ElementAssertion(element.getByTestId("id")); + + expect(test.toBeEmpty()).toBe(test); + expect(() => test.not.toBeEmpty()) + .toThrowError(AssertionError) + .toHaveMessage("Expected element NOT to be empty."); + }); + }); + + context("when the element is NOT empty", () => { + it("throws an error", () => { + const element = render( + + {"Not empty"} + , + ); + const test = new ElementAssertion(element.getByTestId("id")); + + expect(test.not.toBeEmpty()).toBeEqual(test); + expect(() => test.toBeEmpty()) + .toThrowError(AssertionError) + .toHaveMessage("Expected element to be empty."); + }); + }); + }); + + describe (".toBeVisible", () => { + context("when the modal is visible", () => { + it("returns the assertion instance", () => { + const element = render( + , + ); + const test = new ElementAssertion(element.getByTestId("id")); + + expect(test.toBeVisible()).toBe(test); + expect(() => test.not.toBeVisible()) + .toThrowError(AssertionError) + .toHaveMessage("Expected element NOT to be visible."); + }); + }); + + context("when the element contains 'display' property", () => { + it("returns the assertion instance", () => { + const element = render( + , + ); + const test = new ElementAssertion(element.getByTestId("id")); + + expect(test.toBeVisible()).toBe(test); + expect(() => test.not.toBeVisible()) + .toThrowError(AssertionError) + .toHaveMessage("Expected element NOT to be visible."); + }); + }); + + context("when the element contains 'accessibilityElementsHidden' property", () => { + it("returns the assertion instance", () => { + const element = render( + , + ); + const test = new ElementAssertion(element.getByTestId("id")); + + expect(test.toBeVisible()).toBe(test); + expect(() => test.not.toBeVisible()) + .toThrowError(AssertionError) + .toHaveMessage("Expected element NOT to be visible."); + }); + }); + + context("when the element contains 'importantForAccessibility' property", () => { + it("returns the assertion instance", () => { + const element = render( + , + ); + const test = new ElementAssertion(element.getByTestId("id")); + + expect(test.toBeVisible()).toBe(test); + expect(() => test.not.toBeVisible()) + .toThrowError(AssertionError) + .toHaveMessage("Expected element NOT to be visible."); + }); + }); + + context("when the parent element contains 'opacity' property", () => { + context("if parent opacity = 0", () => { + const element = render( + + + , + ); + + const parent = new ElementAssertion(element.getByTestId("parentId")); + const child = new ElementAssertion(element.getByTestId("childId")); + + it("returns assertion instance for NOT visible elements", () => { + expect(parent.not.toBeVisible()).toBeEqual(parent); + expect(child.not.toBeVisible()).toBeEqual(child); + }); + + it("throws an error for visible elements", () => { + expect(() => parent.toBeVisible()) + .toThrowError(AssertionError) + .toHaveMessage("Expected element to be visible."); + expect(() => child.toBeVisible()) + .toThrowError(AssertionError) + .toHaveMessage("Expected element to be visible."); + }); + }); + + context("if child opacity = 0", () => { + const element = render( + + + , + ); + + const parent = new ElementAssertion(element.getByTestId("parentId")); + const child = new ElementAssertion(element.getByTestId("childId")); + + it("returns assertion instance for NOT visible elements", () => { + expect(parent.toBeVisible()).toBeEqual(parent); + expect(child.not.toBeVisible()).toBeEqual(child); + }); + + it("throws an error for visible elements", () => { + expect(() => parent.not.toBeVisible()) + .toThrowError(AssertionError) + .toHaveMessage("Expected element NOT to be visible."); + expect(() => child.toBeVisible()) + .toThrowError(AssertionError) + .toHaveMessage("Expected element to be visible."); + }); + }); + }); + }); + + describe (".toContainElement", () => { + const element = render( + + + + + + , + ); + + const container = element.getByTestId("grandParentId"); + const containerElementAssertion = new ElementAssertion(container); + const parent = element.getByTestId("parentId"); + const parentElementAssertion = new ElementAssertion(parent); + const child = element.getByTestId("childId"); + const text = element.getByTestId("textId"); + const textElementAssertion = new ElementAssertion(text); + + context("when the element has children", () => { + context("and the target element is found in the children's element", () => { + it("returns the assertion instance", () => { + expect(containerElementAssertion.toContainElement(parent)).toBe(containerElementAssertion); + expect(containerElementAssertion.toContainElement(child)).toBe(containerElementAssertion); + expect(containerElementAssertion.toContainElement(text)).toBe(containerElementAssertion); + expect(parentElementAssertion.toContainElement(child)).toBe(parentElementAssertion); + }); + + it("throws an error for negative assertion", () => { + expect(() => containerElementAssertion.not.toContainElement(parent)) + .toThrowError(AssertionError) + .toHaveMessage("Expected element NOT to contain element ."); + expect(() => containerElementAssertion.not.toContainElement(text)) + .toThrowError(AssertionError) + .toHaveMessage("Expected element NOT to contain element ."); + }); + }); + + context("and the target element is NOT found in the children's element", () => { + it("throws an error", () => { + expect(() => parentElementAssertion.toContainElement(text)) + .toThrowError(AssertionError) + .toHaveMessage("Expected element to contain element ."); + }); + + it("returns the assertion instance for negative assertion", () => { + expect(parentElementAssertion.not.toContainElement(text)).toBeEqual(parentElementAssertion); + expect(parentElementAssertion.not.toContainElement(container)).toBeEqual(parentElementAssertion); + }); + }); + }); + + context("when the element does NOT have children", () => { + it("throws an error", () => { + expect(() => textElementAssertion.toContainElement(parent)) + .toThrowError(AssertionError) + .toHaveMessage("Expected element to contain element ."); + }); + }); + }); });