11import { Assertion , AssertionError } from "@assertive-ts/core" ;
22import { get } from "dot-prop-immutable" ;
3- import { Children } from "react" ;
43import { ReactTestInstance } from "react-test-renderer" ;
54
6- import { instanceToString } from "./helpers/helpers" ;
5+ import { isAncestorDisabled , isElementDisabled , isAncestorNotVisible , isElementVisible } from "./helpers/accesibility" ;
6+ import { getFlattenedStyle , styleToString } from "./helpers/styles" ;
7+ import { getTextContent , textMatches } from "./helpers/text" ;
8+ import { AssertiveStyle , TestableTextMatcher } from "./helpers/types" ;
9+ import { isEmpty , instanceToString , isElementContained } from "./helpers/utils" ;
710
811export class ElementAssertion extends Assertion < ReactTestInstance > {
912 public constructor ( actual : ReactTestInstance ) {
@@ -35,7 +38,7 @@ export class ElementAssertion extends Assertion<ReactTestInstance> {
3538 } ) ;
3639
3740 return this . execute ( {
38- assertWhen : this . isElementDisabled ( this . actual ) || this . isAncestorDisabled ( this . actual ) ,
41+ assertWhen : isElementDisabled ( this . actual ) || isAncestorDisabled ( this . actual ) ,
3942 error,
4043 invertedError,
4144 } ) ;
@@ -61,7 +64,7 @@ export class ElementAssertion extends Assertion<ReactTestInstance> {
6164 } ) ;
6265
6366 return this . execute ( {
64- assertWhen : ! this . isElementDisabled ( this . actual ) && ! this . isAncestorDisabled ( this . actual ) ,
67+ assertWhen : ! isElementDisabled ( this . actual ) && ! isAncestorDisabled ( this . actual ) ,
6568 error,
6669 invertedError,
6770 } ) ;
@@ -88,7 +91,7 @@ export class ElementAssertion extends Assertion<ReactTestInstance> {
8891 } ) ;
8992
9093 return this . execute ( {
91- assertWhen : Children . count ( this . actual . props . children ) === 0 ,
94+ assertWhen : isEmpty ( this . actual . children ) ,
9295 error,
9396 invertedError,
9497 } ) ;
@@ -115,49 +118,152 @@ export class ElementAssertion extends Assertion<ReactTestInstance> {
115118 } ) ;
116119
117120 return this . execute ( {
118- assertWhen : this . isElementVisible ( this . actual ) && this . isAncestorVisible ( this . actual ) ,
121+ assertWhen : isElementVisible ( this . actual ) && ! isAncestorNotVisible ( this . actual ) ,
119122 error,
120123 invertedError,
121124 } ) ;
122125 }
123126
124- private isElementDisabled ( element : ReactTestInstance ) : boolean {
125- const { type } = element ;
126- const elementType = type . toString ( ) ;
127- if ( elementType === "TextInput" && element ?. props ?. editable === false ) {
128- return true ;
129- }
130-
131- return (
132- get ( element , "props.aria-disabled" )
133- || get ( element , "props.disabled" , false )
134- || get ( element , "props.accessibilityState.disabled" , false )
135- || get < ReactTestInstance , string [ ] > ( element , "props.accessibilityStates" , [ ] ) . includes ( "disabled" )
136- ) ;
127+ /**
128+ * Check if an element is contained within another element.
129+ *
130+ * @example
131+ * ```
132+ * expect(parent).toContainElement(child);
133+ * ```
134+ *
135+ * @param element - The element to check for.
136+ * @returns the assertion instance
137+ */
138+ public toContainElement ( element : ReactTestInstance ) : this {
139+ const error = new AssertionError ( {
140+ actual : this . actual ,
141+ message : `Expected element ${ this . toString ( ) } to contain element ${ instanceToString ( element ) } .` ,
142+ } ) ;
143+ const invertedError = new AssertionError ( {
144+ actual : this . actual ,
145+ message : `Expected element ${ this . toString ( ) } NOT to contain element ${ instanceToString ( element ) } .` ,
146+ } ) ;
147+
148+ return this . execute ( {
149+ assertWhen : isElementContained ( this . actual , element ) ,
150+ error,
151+ invertedError,
152+ } ) ;
137153 }
138154
139- private isAncestorDisabled ( element : ReactTestInstance ) : boolean {
140- const { parent } = element ;
141- return parent !== null && ( this . isElementDisabled ( element ) || this . isAncestorDisabled ( parent ) ) ;
155+ /**
156+ * Check if the element has a specific property or a specific property value.
157+ *
158+ * @example
159+ * ```
160+ * expect(element).toHaveProp("propName");
161+ * expect(element).toHaveProp("propName", "propValue");
162+ * ```
163+ *
164+ * @param propName - The name of the prop to check for.
165+ * @param value - The value of the prop to check for.
166+ * @returns the assertion instance
167+ */
168+ public toHaveProp ( propName : string , value ?: unknown ) : this {
169+ const propValue : unknown = get ( this . actual , `props.${ propName } ` , undefined ) ;
170+ const hasProp = propValue !== undefined ;
171+ const isPropEqual = value === undefined || propValue === value ;
172+
173+ const errorMessage = value === undefined
174+ ? `Expected element ${ this . toString ( ) } to have prop '${ propName } '.`
175+ : `Expected element ${ this . toString ( ) } to have prop '${ propName } ' with value '${ String ( value ) } '.` ;
176+
177+ const invertedErrorMessage = value === undefined
178+ ? `Expected element ${ this . toString ( ) } NOT to have prop '${ propName } '.`
179+ : `Expected element ${ this . toString ( ) } NOT to have prop '${ propName } ' with value '${ String ( value ) } '.` ;
180+
181+ const error = new AssertionError ( { actual : this . actual , message : errorMessage } ) ;
182+ const invertedError = new AssertionError ( { actual : this . actual , message : invertedErrorMessage } ) ;
183+
184+ return this . execute ( {
185+ assertWhen : hasProp && isPropEqual ,
186+ error,
187+ invertedError,
188+ } ) ;
142189 }
143190
144- private isElementVisible ( element : ReactTestInstance ) : boolean {
145- const { type } = element ;
191+ /**
192+ * Asserts that a component has the specified style(s) applied.
193+ *
194+ * This method supports both single style objects and arrays of style objects.
195+ * It checks if all specified style properties match on the target element.
196+ *
197+ * @example
198+ * ```
199+ * expect(element).toHaveStyle({ backgroundColor: "red" });
200+ * expect(element).toHaveStyle([{ backgroundColor: "red" }]);
201+ * ```
202+ *
203+ * @param style - A style object to check for.
204+ * @returns the assertion instance
205+ */
206+ public toHaveStyle ( style : AssertiveStyle ) : this {
207+ const stylesOnElement : AssertiveStyle = get ( this . actual , "props.style" , { } ) ;
208+
209+ const flattenedElementStyle = getFlattenedStyle ( stylesOnElement ) ;
210+ const flattenedStyle = getFlattenedStyle ( style ) ;
211+
212+ const hasStyle = Object . keys ( flattenedStyle )
213+ . every ( key => flattenedElementStyle [ key ] === flattenedStyle [ key ] ) ;
214+
215+ const error = new AssertionError ( {
216+ actual : this . actual ,
217+ message : `Expected element ${ this . toString ( ) } to have style: \n${ styleToString ( flattenedStyle ) } ` ,
218+ } ) ;
146219
147- if ( type . toString ( ) === "Modal" ) {
148- return Boolean ( element . props ?. visible ) ;
149- }
220+ const invertedError = new AssertionError ( {
221+ actual : this . actual ,
222+ message : `Expected element ${ this . toString ( ) } NOT to have style: \n${ styleToString ( flattenedStyle ) } ` ,
223+ } ) ;
150224
151- return (
152- get ( element , "props.style.display" ) !== "none"
153- && get ( element , "props.style.opacity" ) !== 0
154- && get ( element , "props.accessibilityElementsHidden" ) !== true
155- && get ( element , "props.importantForAccessibility" ) !== "no-hide-descendants"
156- ) ;
225+ return this . execute ( {
226+ assertWhen : hasStyle ,
227+ error,
228+ invertedError,
229+ } ) ;
157230 }
158231
159- private isAncestorVisible ( element : ReactTestInstance ) : boolean {
160- const { parent } = element ;
161- return parent === null || ( this . isElementVisible ( parent ) && this . isAncestorVisible ( parent ) ) ;
232+ /**
233+ * Check if the element has text content matching the provided string,
234+ * RegExp, or function.
235+ *
236+ * @example
237+ * ```
238+ * expect(element).toHaveTextContent("Hello World");
239+ * expect(element).toHaveTextContent(/Hello/);
240+ * expect(element).toHaveTextContent(text => text.startsWith("Hello"));
241+ * ```
242+ *
243+ * @param text - The text to check for.
244+ * @returns the assertion instance
245+ */
246+ public toHaveTextContent ( text : TestableTextMatcher ) : this {
247+ const actualTextContent = getTextContent ( this . actual ) ;
248+ const matchesText = textMatches ( actualTextContent , text ) ;
249+
250+ const error = new AssertionError ( {
251+ actual : this . actual ,
252+ message : `Expected element ${ this . toString ( ) } to have text content matching '` +
253+ `${ text . toString ( ) } '.` ,
254+ } ) ;
255+
256+ const invertedError = new AssertionError ( {
257+ actual : this . actual ,
258+ message :
259+ `Expected element ${ this . toString ( ) } NOT to have text content matching '` +
260+ `${ text . toString ( ) } '.` ,
261+ } ) ;
262+
263+ return this . execute ( {
264+ assertWhen : matchesText ,
265+ error,
266+ invertedError,
267+ } ) ;
162268 }
163269}
0 commit comments