hotwired / turbo

The speed of a single-page web application without having to write any JavaScript
https://turbo.hotwired.dev
MIT License
6.65k stars 421 forks source link

Can't get Turbo to work with Cypress or RSpec / Capybara #638

Closed nilesmc closed 2 years ago

nilesmc commented 2 years ago

Im attempting to do a POC so that we can more widely adopt Turbo across 5 rails app, but Im running into a blocker. I can't seem to get a feature to work in CI.

Im using Turbo an a page w a simple select.

= turbo_frame_tag "widget-index", 'data-controller': 'turbo-frame-select' do
  .widget-card-header
    select 
     data-test-id="Widget-year-select" 
     data-action="turbo-frame-select#onChange"
     type="select"
     name="filter"
       - @widget_years.each do |year|
        option[selected=("selected" if (year == @search_year)) value="#{widget_index_path(company_id: @company.abbreviation, year: year)}"] #{year}

  #widgets_results data-test-id="widgets-results"
      = render WidgetCardComponent.with_collection(@widgets)

The AC is:

When the user selects a year, the results for the year should appear.

I've got a super simple stimulus controller I added to try to make sure caching is not an issue:

// turbo_frame_select_controller.js

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  onChange (event) {
    const frame = this.element;
    frame.src = event.target.value;
  )}
}

Ive written an overwrite for select to work with Turbo events in Cypresses' command.js.

Cypress.Commands.overwrite("select", (originalFn, subject, ...args) => {
  // Check if turbo is active for this select event
  const turboFrame = subject[0].closest('[data-controller="turbo-frame-select"]');
  const isSelect = subject[0].tagName === 'SELECT';

  if (isSelect && turboFrame) {
    // Wait for turbo to finish loading the page before proceeding with the next Cypress instructions.
    // First, get the document
    cy.document({ log: false }).then($document => {
      console.log('mutating observer last child');
      Cypress.log({
        $el: subject,
        name: 'select',
        displayName: 'select',
        message: 'select and wait for page to load',
        consoleProps: () => ({ subject: subject })
      });

      // Make Cypress wait for this promise which waits for the turbFrame to cause a mutation
      return new Cypress.Promise(resolve => {
        // Once we receive the mutuation,
        const onTurboFrameMutate = function(mutationList, observer) {
          for(const mutation of mutationList) {
            const isChild = mutation.type === 'childList';
            const lastChild = mutationList.length -1 !== mutationList.indexOf(mutation)

            if (isChild  && lastChild) {
              console.log('A child node has been added or removed.');
              turboFrameObserver.disconnect();

              // signal to Cypress that we're done
              resolve()
            }
          }
        }

        // createa new observer
        const turboFrameObserver = new MutationObserver(onTurboFrameMutate);

        // Options for the observer (which mutations to observe)
        const config = { attributes: true, childList: true, subtree: true };

        // Add our logic as observer
        turboFrameObserver.observe(turboFrame, config);

        // Finally, we are ready to perform the actual select operation
        originalFn(subject, ...args);
      })
    });
  } else {
    // Not a normal select on an <select> tag, turbo will not interfere here
    return originalFn(subject, ...args);
  }
});

I am calling the select method in this context:

cy.visit(widgetIndexPath);

// Make sure header is loaded
cy.contains("h1", "Cash management");

// Current year selected
cy.get("@aYearSelector").find("option:selected").contains(currentYear);

// Select last year
   cy.get("@aYearSelector")
     .find("option")
     .its('length')
     .then((length) => {
       cy.get("@aYearSelector")
         .select(length - 1, {force: true})
         .then(()=> {
           // Triggers after year selector changes
           cy.wait('@previousYearRequest').then(({response}) => {
             expect(response.statusCode).to.eq(200);

             cy.get("[data-test-id='widget-year-select']").as('newYearSelector');
             cy.get("[data-test-id='widgets-results']").as('newResults');

             cy.get("@newYearSelector").find("option:selected").contains(previousYear);

             // Note: We have 13 months of data in the data layer from the factories.
             // The months count backwards from the current month which will be spread out
             // over 2 years.
             cy.get('@newResults')

The test passes 100/100 times locally, but is only sucesful 1/4 - 1/3 runs on Cypress Dashboard...

Giving the following error:


cy.then() timed out after waiting 6000ms.

Your callback function returned a promise that never resolved.

The callback function was:

$document => {

  console.log('mutating observer last child');

Screen Shot 2022-07-18 at 6 31 15 PM

Ive tried just about everyhting I can think of...

Including implementing the test in Rspec / Capybara, but the feature does not work in the same way there as well, the page times out and never refreshes the data it should.

This seems like it should work.

Does anyone have any ideas why this is timing out? Is there something missing my configuration? Is there anyone out there that has had success w e2e / integration tests like this w Turbo?

nilesmc commented 2 years ago

I also tried a version of the select override above w an event listener:

  // Make Cypress wait for this promise which waits for the turbolinks:load event
  return new Cypress.Promise(resolve => {
    // Once we receive the event,
    const onTurboLoad = () => {
      // clean up
      $document.removeEventListener('turbo:frame-load', onTurboLoad);

      // signal to Cypress that we're done
      resolve();
    }
    // Add our logic as event listener
    $document.addEventListener('turbo:frame-load', onTurboLoad);

    // Finally, we are ready to perform the actual select operation
    originalFn(subject, ...args);
  })
dhh commented 2 years ago

Please use discuss.hotwired.dev for usage help ✌️

nilesmc commented 2 years ago

@dhh - Cool. Done, I think it needs your approval over there: https://discuss.hotwired.dev/t/akismet-has-temporarily-hidden-your-post/4325