@@ -2,7 +2,14 @@ import { Assertion, AssertionError } from "@assertive-ts/core";
22import { get } from "dot-prop-immutable" ;
33import { ReactTestInstance } from "react-test-renderer" ;
44
5- import { instanceToString , isEmpty } from "./helpers/helpers" ;
5+ import {
6+ instanceToString ,
7+ isEmpty ,
8+ getFlattenedStyle ,
9+ styleToString ,
10+ textMatches ,
11+ } from "./helpers/helpers" ;
12+ import { AssertiveStyle , TestableTextMatcher , TextContent } from "./helpers/types" ;
613
714export class ElementAssertion extends Assertion < ReactTestInstance > {
815 public constructor ( actual : ReactTestInstance ) {
@@ -200,6 +207,129 @@ export class ElementAssertion extends Assertion<ReactTestInstance> {
200207 } ) ;
201208 }
202209
210+ /**
211+ * Asserts that a component has the specified style(s) applied.
212+ *
213+ * This method supports both single style objects and arrays of style objects.
214+ * It checks if all specified style properties match on the target element.
215+ *
216+ * @example
217+ * ```
218+ * expect(element).toHaveStyle({ backgroundColor: "red" });
219+ * expect(element).toHaveStyle([{ backgroundColor: "red" }]);
220+ * ```
221+ *
222+ * @param style - A style object to check for.
223+ * @returns the assertion instance
224+ */
225+ public toHaveStyle ( style : AssertiveStyle ) : this {
226+ const stylesOnElement : AssertiveStyle = get ( this . actual , "props.style" , { } ) ;
227+
228+ const flattenedElementStyle = getFlattenedStyle ( stylesOnElement ) ;
229+ const flattenedStyle = getFlattenedStyle ( style ) ;
230+
231+ const hasStyle = Object . keys ( flattenedStyle )
232+ . every ( key => flattenedElementStyle [ key ] === flattenedStyle [ key ] ) ;
233+
234+ const error = new AssertionError ( {
235+ actual : this . actual ,
236+ message : `Expected element ${ this . toString ( ) } to have style: \n${ styleToString ( flattenedStyle ) } ` ,
237+ } ) ;
238+
239+ const invertedError = new AssertionError ( {
240+ actual : this . actual ,
241+ message : `Expected element ${ this . toString ( ) } NOT to have style: \n${ styleToString ( flattenedStyle ) } ` ,
242+ } ) ;
243+
244+ return this . execute ( {
245+ assertWhen : hasStyle ,
246+ error,
247+ invertedError,
248+ } ) ;
249+ }
250+
251+ /**
252+ * Check if the element has text content matching the provided string,
253+ * RegExp, or function.
254+ *
255+ * @example
256+ * ```
257+ * expect(element).toHaveTextContent("Hello World");
258+ * expect(element).toHaveTextContent(/Hello/);
259+ * expect(element).toHaveTextContent(text => text.startsWith("Hello"));
260+ * ```
261+ *
262+ * @param text - The text to check for.
263+ * @returns the assertion instance
264+ */
265+ public toHaveTextContent ( text : TestableTextMatcher ) : this {
266+ const actualTextContent = this . getTextContent ( this . actual ) ;
267+ const matchesText = textMatches ( actualTextContent , text ) ;
268+
269+ const error = new AssertionError ( {
270+ actual : this . actual ,
271+ message : `Expected element ${ this . toString ( ) } to have text content matching '` +
272+ `${ text . toString ( ) } '.` ,
273+ } ) ;
274+
275+ const invertedError = new AssertionError ( {
276+ actual : this . actual ,
277+ message :
278+ `Expected element ${ this . toString ( ) } NOT to have text content matching '` +
279+ `${ text . toString ( ) } '.` ,
280+ } ) ;
281+
282+ return this . execute ( {
283+ assertWhen : matchesText ,
284+ error,
285+ invertedError,
286+ } ) ;
287+ }
288+
289+ private getTextContent ( element : ReactTestInstance ) : string {
290+ if ( ! element ) {
291+ return "" ;
292+ }
293+
294+ if ( typeof element === "string" ) {
295+ return element ;
296+ }
297+
298+ if ( typeof element . props ?. value === "string" ) {
299+ return element . props . value ;
300+ }
301+
302+ return this . collectText ( element ) . join ( " " ) ;
303+ }
304+
305+ private collectText = ( element : TextContent ) : string [ ] => {
306+ if ( typeof element === "string" ) {
307+ return [ element ] ;
308+ }
309+
310+ if ( Array . isArray ( element ) ) {
311+ return element . flatMap ( child => this . collectText ( child ) ) ;
312+ }
313+
314+ if ( element && ( typeof element === "object" && "props" in element ) ) {
315+ const value = element . props ?. value as TextContent ;
316+ if ( typeof value === "string" ) {
317+ return [ value ] ;
318+ }
319+
320+ const children = ( element . props ?. children as ReactTestInstance [ ] ) ?? element . children ;
321+ if ( ! children ) {
322+ return [ ] ;
323+ }
324+
325+ return Array . isArray ( children )
326+ ? children . flatMap ( this . collectText )
327+ : this . collectText ( children ) ;
328+ }
329+
330+ return [ ] ;
331+ } ;
332+
203333 private isElementDisabled ( element : ReactTestInstance ) : boolean {
204334 const { type } = element ;
205335 const elementType = type . toString ( ) ;
@@ -208,10 +338,10 @@ export class ElementAssertion extends Assertion<ReactTestInstance> {
208338 }
209339
210340 return (
211- get ( element , "props.aria-disabled" )
212- || get ( element , "props.disabled" , false )
213- || get ( element , "props.accessibilityState.disabled" , false )
214- || get < ReactTestInstance , string [ ] > ( element , "props.accessibilityStates" , [ ] ) . includes ( "disabled" )
341+ get ( element , "props.aria-disabled" )
342+ || get ( element , "props.disabled" , false )
343+ || get ( element , "props.accessibilityState.disabled" , false )
344+ || get < ReactTestInstance , string [ ] > ( element , "props.accessibilityStates" , [ ] ) . includes ( "disabled" )
215345 ) ;
216346 }
217347
0 commit comments