When transforming a function into a custom command, the expected outcome may vary

There is a function that retrieves all CSS properties and values of an element:

function fetchAllCssPropertiesAndValues(element: Element) {
    const style = window.getComputedStyle(element)
    const propertiesAndValues = {}

    for (let i = 0; i < style.length; i++) {
        const propertyName = style[i]
        const value = style.getPropertyValue(propertyName)
        propertiesAndValues[propertyName] = value
    }

    return propertiesAndValues
}

To use it, simply do:

cy.get('#my-element').then($el => {
    console.log(fetchAllCssPropertiesAndValues($el[0]))
})

When making it into a custom command, the expected CSS properties are not retrieved:

This is the custom command in progress:

export {}

declare global {
    namespace Cypress {
        interface Chainable {
            fetchAllCssPropertiesAndValues: typeof fetchAllCssPropertiesAndValues
        }
    }
}

const fetchAllCssPropertiesAndValues = (element: Element) => {
    const style = window.getComputedStyle(element)
    const propertiesAndValues = {}

    for (let i = 0; i < style.length; i++) {
        const propertyName = style[i]
        const value = style.getPropertyValue(propertyName)
        propertiesAndValues[propertyName] = value
    }

    return propertiesAndValues
}

Cypress.Commands.add('fetchAllCssPropertiesAndValues', fetchAllCssPropertiesAndValues)

Utilize the custom command like this:

cy.get('my-element').then($el => { 
  console.log(cy.fetchAllCssPropertiesAndValues($el[0])) 
})

Double check if there may be any mistakes or misunderstandings in implementation.

Answer №1

While utilizing a personalized command is acceptable, it lacks the capability to retry any modified CSS.

For instance, consider a scenario where a page initiates an API call and modifies the CSS to display: none if there is no content to display.

This miniature application replicates that situation by using a setTimeout() function to postpone the change in CSS properties.

<body>
  <div style="display: block"></div>
  <script>
    setTimeout(() => {
      const div = document.querySelector('div')
      div.style.setProperty('display', 'none')
    }, 1000)
  </script>
</body>

Upon conducting a test with the Custom Command, it encounters failure

Cypress.Commands.add('getAllCssPropertiesAndValues', {prevSubject: true},          
  ($element) => {
    const element = $element[0]   
    return getAllCssPropertiesAndValues(element)
  }
)

cy.get('div')
  .getAllCssPropertiesAndValues()
  .its('display')
  .should('eq', 'none')      // anticipating change after 1 second

https://i.sstatic.net/YjhcV9Cx.png

However, employing a Custom Query results in success

Cypress.Commands.addQuery('getAllCssPropertiesAndValues', 
  function() {
    return ($element) => {
      const element = $element[0]
      return getAllCssPropertiesAndValues(element)
    }
})

cy.get('div')
  .getAllCssPropertiesAndValues()
  .its('display')
  .should('eq', 'none')

https://i.sstatic.net/p0hdJSfg.png

Save the Custom Query in /cypress/support/commands.ts for universal accessibility.


Refer to the documentation here Custom Queries.

To summarize the disparity, the Custom Query presents an inner function that recurs each time the assertion fails. Conversely, the Custom Command executes once and prohibits the retry.

Hence, any custom code delivering a subset value of the element discovered by cy.get('#my-element'), whether it pertains to a child element, inner-text, or CSS properties, proves to be more versatile when classified as a query rather than a command.

Answer №2

To achieve the desired functionality, a custom command needs to be created that acts as a child command, similar to how 'find' works in cy.get().find().

export {}

declare global {
    namespace Cypress {
        interface Chainable {
            getAllCssPropertiesAndValues: typeof getAllCssPropertiesAndValues
        }
    }
}

const getAllCssPropertiesAndValues = ($element: jQuery<Element>) => {

    const element = $element[0]   // get the element from jquery

    const style = window.getComputedStyle(element)
    const propertiesAndValues = {}

    for (let i = 0; i < style.length; i++) {
        const propertyName = style[i]
        const value = style.getPropertyValue(propertyName)
        propertiesAndValues[propertyName] = value
    }

    return propertiesAndValues
}

Cypress.Commands.add(
  'getAllCssPropertiesAndValues', 
  {prevSubject: true},            <-- indicates is chained from another cmd
  getAllCssPropertiesAndValues
)

Usage:

cy.get('my-selector').getAllCssPropertiesAndValues().then(css => {
  console.log(css)
})

The element is passed via chain from get() to getAllCssPropertiesAndValues(), eliminating the need to specify it as a parameter.

Similar to cy.get(this).find(that), with custom commands the Cypress pipeline passes data down the chain as the "subject."

Answer №3

Unlike standard JavaScript functions, Cypress commands do not return their values in the same way. When a Cypress command returns a value, it is of type Chainable<T>, where T represents the type of the returned value. Let's look at an example:

/// Creating a custom command
Cypress.Commands.add('returnOne', () => {
  return 1;
});

// The variable foo is of type Chainable<number>
const foo = cy.returnOne();

To access the actual values wrapped by the Chainable, you need to "unwrap" it by using cy.then() on the returned value.

// Example using the custom command
cy.returnOne().then((one) => {
  console.log(one);
});

// Another example with your code
cy.get('my-element').then($el => { 
  cy.getAllCssPropertiesAndValues($el[0]).then((props) => {
    console.log(props)
  })
})

While there may be other issues related to the sync/async nature of Cypress command chains, the main problem often arises when trying to console.log() a Chainable.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best way to retrieve session data in both a server component and a client component using NextAuth and Next.js?

As I delve into the realm of Next.js and utilize NextAuth for authentication in my application, I've discovered that Next-auth handles the session and token management. My objective is to extract the email of the authenticated user from the data store ...

Utilizing a configuration file with Vue's `use` method

Within my index.ts file, I am setting up the configuration for the Google reCAPTCHA component using a specific sitekey: Vue.use(VueReCaptcha, { siteKey: 'my_redacted_sitekey' }); This setup occurs prior to the initialization of the new Vue({ ...

Creating a dynamic multi-line list in Ionic application is a breeze!

I'm a beginner in Ionic and I am interested in creating a Multi-line List similar to how we generate list-views in Android with custom views and using an Array of custom objects programmatically. Can Multi-line Lists be generated with data from an Ar ...

The error message "Can't resolve all parameters for CustomerService" is preventing Angular from injecting HttpClient

I have a customerService where I am attempting to inject httpClient. The error occurs on the line where I commented //error happens on this line. Everything works fine until I try to inject httpClient into my service. The error message is: `compiler.js: ...

Obtain a reference to a class using a method decorator

My goal is to implement the following syntax: @Controller('/user') class UserController { @Action('/') get() { } } Now in the definition of decorators: function Controller (options) { return function(target: any) { let id ...

The parameter type 'string | null' cannot be assigned to the argument type 'string'. The type 'null' is not compatible with the type 'string'.ts(2345)

Issue: The parameter type 'string | null' is not compatible with the expected type 'string'. The value 'null' is not a valid string.ts(2345) Error on Line: this.setSession(res.body._id, res.headers.get('x-access-token&ap ...

Unable to retrieve a property from a variable with various data types

In my implementation, I have created a versatile type that can be either of another type or an interface, allowing me to utilize it in functions. type Value = string | number interface IUser { name: string, id: string } export type IGlobalUser = Value | IU ...

Angular's Spanning Powers

How can I make a button call different methods when "Select" or "Change" is clicked? <button type="button" class="btn btn-default" *ngIf="!edit" class="btn btn-default"> <span *ngIf="isNullOrUndefined(class?.classForeignId)">Select</spa ...

Is there a way to create an interpolated string using a negative lookahead condition?

When analyzing my code for imports, I will specifically be searching for imports that do not end with -v3. Here are some examples: @ui/components <- this will match @ui/components/forms/field <- this will match @ui/components-v3 ...

Centralize all form statuses in a single component, conveniently organized into separate tabs

I am working on a component that consists of 2 tabs, each containing a form: tab1.component.ts : ngOnInit() { this.params = getParameters(); } getParameters() { return { text : 'sample' form: { status: this.f ...

I have successfully set up my Next.js app using Tailwind CSS and DaisyUI, and now I am working on integrating Cypress for

I am currently working on a Nextjs app with tailwind and daisyUI, but I am facing challenges with configuring Cypress. When I try to implement Cypress, only the HTML appears and I can't seem to get it to work properly. Here is a snippet of my code: / ...

Using immer JS to update a nested value has been successfully completed

What is the most efficient way to recursively change a value using a recursive function call in a Produce of Immer? The WhatsappState represents the general reducer type, with Message being the message structure for the application/db. type WhatsappState = ...

Creating a class extension without a duplicate constructor (using private parameters)

As I was attempting to improve Angular's ComponetFixture, I discovered a limitation due to the absence of a copying constructor for this class. (Or am I mistaken?) Imagine we have a class: class A { constructor(public pub, private priv) { } } Now ...

Storing multiple items in an array using LocalForage

I have a challenge where I need to add multiple items to an array without overriding them. My initial approach was like this: localForage.getItem("data", (err, results) => { console.log('results', results) // var dataArray ...

The ActivatedRoute snapshot does not function properly when used in the TypeScript class constructor

Currently, I am encountering a challenge with TypeScript and Angular 2. The structure of my TS class is as follows: 'import { Component, OnInit } from '@angular/core'; import {ActivatedRoute} from '@angular/router'; @Component({ ...

Incorporating interactive buttons within Leaflet popups

I'm facing an issue with adding buttons to a Leaflet popup that appears when clicking on the map. My goal is to have the popup display 2 buttons: Start from Here Go to this Location The desired outcome looks like this sketch: ___________________ ...

Can a Vue application be made type-safe without the need for transpilation?

Is it possible for Vue to be type-safe when used without transpilation (without a build step) as well? Can TypeScript or Flow be used to ensure type safety? ...

"Linking a Next.js application with Azure's Application Insights for advanced insights

Trying to include my Next.js (TypeScript) app logs in Azure Application Insights has been a bit challenging. The documentation provided poor guidance, so I decided to follow this solution: https://medium.com/@nirbhayluthra/integrating-azure-application-ins ...

Explore Angular's ability to transform a nested observable object into a different object

My task involves mapping a field from a sub object in the response JSON to the parent object The response I receive looks like this: { "id": 1, "name": "file-1", "survey": { "identifier": 1, "displayedValue": survey-1 } } I am attempting ...

What is the best way to fetch data before a component is rendered on the screen?

I am facing an issue with fetching data from a local server in Node. When I try to render the component, the array 'users' from the state appears to be empty, resulting in no users being displayed on the screen as intended. What's strange is ...