diff --git a/packages/native/src/lib/ElementAssertion.ts b/packages/native/src/lib/ElementAssertion.ts index ed7a11d..328e73a 100644 --- a/packages/native/src/lib/ElementAssertion.ts +++ b/packages/native/src/lib/ElementAssertion.ts @@ -2,9 +2,9 @@ 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"; +import { instanceToString, isEmpty, testableTextMatcherToString, textMatches } from "./helpers/helpers"; import { getFlattenedStyle } from "./helpers/styles"; -import { AssertiveStyle } from "./helpers/types"; +import { AssertiveStyle, TestableTextMatcher, WithTextContent } from "./helpers/types"; export class ElementAssertion extends Assertion { public constructor(actual: ReactTestInstance) { @@ -243,6 +243,88 @@ export class ElementAssertion extends Assertion { }); } + /** + * Check if the element has text content matching the provided string, + * RegExp, or function. + * + * @example + * ``` + * expect(element).toHaveTextContent("Hello World"); + * expect(element).toHaveTextContent(/Hello/); + * expect(element).toHaveTextContent(text => text.startsWith("Hello")); + * ``` + * + * @param text - The text to check for. + * @returns the assertion instance + */ + public toHaveTextContent(text: TestableTextMatcher): this { + const actualTextContent = this.getTextContent(this.actual); + const matchesText = textMatches(actualTextContent, text); + + const error = new AssertionError({ + actual: this.actual, + message: `Expected element ${this.toString()} to have text content matching '` + + `${testableTextMatcherToString(text)}'.`, + }); + + const invertedError = new AssertionError({ + actual: this.actual, + message: + `Expected element ${this.toString()} NOT to have text content matching '` + + `${testableTextMatcherToString(text)}'.`, + }); + + return this.execute({ + assertWhen: matchesText, + error, + invertedError, + }); + } + + private getTextContent(element: ReactTestInstance): string { + if (!element) { + return ""; + } + + if (typeof element === "string") { + return element; + } + + if (typeof element.props?.value === "string") { + return element.props.value; + } + + return this.collectText(element).join(" "); + } + + private collectText = (element: WithTextContent): string[] => { + if (typeof element === "string") { + return [element]; + } + + if (Array.isArray(element)) { + return element.flatMap(child => this.collectText(child)); + } + + if (element && typeof element === "object" && "props" in element) { + const value = element.props?.value as WithTextContent; + if (typeof value === "string") { + return [value]; + } + + const children = (element.props?.children as ReactTestInstance[]) ?? element.children; + if (!children) { + return []; + } + + return Array.isArray(children) + ? children.flatMap(this.collectText) + : this.collectText(children); + } + + return []; + }; + private isElementDisabled(element: ReactTestInstance): boolean { const { type } = element; const elementType = type.toString(); diff --git a/packages/native/src/lib/helpers/helpers.ts b/packages/native/src/lib/helpers/helpers.ts index 6ab4db1..3d6e745 100644 --- a/packages/native/src/lib/helpers/helpers.ts +++ b/packages/native/src/lib/helpers/helpers.ts @@ -1,5 +1,7 @@ import { ReactTestInstance } from "react-test-renderer"; +import { TestableTextMatcher } from "./types"; + /** * Checks if a value is empty. * @@ -31,3 +33,62 @@ export function instanceToString(instance: ReactTestInstance | null): string { return `<${instance.type.toString()} ... />`; } + +/** + * Converts a TestableTextMatcher to a string representation. + * + * @param matcher - The matcher to convert. + * @returns A string representation of the matcher. + * @throws Error if the matcher is not a string, RegExp, or function. + */ +export function testableTextMatcherToString(matcher: TestableTextMatcher): string { + if (typeof matcher === "string") { + return `String: "${matcher}"`; + } + + if (matcher instanceof RegExp) { + return `RegExp: ${matcher.toString()}`; + } + + if (typeof matcher === "function") { + return `Function: ${matcher.toString()}`; + } + + throw new Error("Matcher must be a string, RegExp, or function."); +} + +/** + * Checks if a text matches a given matcher. + * + * @param text - The text to check. + * @param matcher - The matcher to use for comparison. + * @returns `true` if the text matches the matcher, `false` otherwise. + * @throws Error if the matcher is not a string, RegExp, or function. + * @example + * ```ts + * textMatches("Hello World", "Hello World"); // true + * textMatches("Hello World", /Hello/); // true + * textMatches("Hello World", (text) => text.startsWith("Hello")); // true + * textMatches("Hello World", "Goodbye"); // false + * textMatches("Hello World", /Goodbye/); // false + * textMatches("Hello World", (text) => text.startsWith("Goodbye")); // false + * ``` + */ +export function textMatches( + text: string, + matcher: TestableTextMatcher, +): boolean { + if (typeof matcher === "string") { + return text.includes(matcher); + } + + if (matcher instanceof RegExp) { + return matcher.test(text); + } + + if (typeof matcher === "function") { + return matcher(text); + } + + throw new Error("Matcher must be a string, RegExp, or function."); +} diff --git a/packages/native/src/lib/helpers/types.ts b/packages/native/src/lib/helpers/types.ts index f2263f9..a69986f 100644 --- a/packages/native/src/lib/helpers/types.ts +++ b/packages/native/src/lib/helpers/types.ts @@ -1,4 +1,5 @@ import { ImageStyle, StyleProp, TextStyle, ViewStyle } from "react-native"; +import { ReactTestInstance } from "react-test-renderer"; /** * Type representing a style that can be applied to a React Native component. @@ -17,3 +18,17 @@ export type AssertiveStyle = StyleProp