How can I test Stripe.js using poltergeist and Capybara?
Asked Answered
D

3

11

I've been going nuts trying to write an automated test for my user sign up page. Users will be charged a recurring subscription via Stripe. They input their basic details (email, password, etc) and their credit card details on the same form, then the following flow happens:

  1. (On the client-side) stripe.js makes an AJAX request to Stripe's servers, which (assuming everything is valid) returns a credit card token.
  2. My javascript fills in a hidden input in the HTML form with the credit card token, and submits the form to my Rails server.
  3. (Now on the server-side): I validate the user's basic details. If they're invalid, return (because there's no point charging them via Stripe if e.g. their email address is invalid so they can't create an account anyway.)
  4. If they're valid, attempt to create a Stripe::Customer object, add the right subscription and charge them using Stripe's ruby gem etc.

All of this works perfectly fine... except I can't figure out how to test it. Testing step #4 is easy enough as it takes place on the server-side so I can mock out the Stripe calls with a gem like VCR.

Step #1 is what's giving me trouble. I've tried to test this using both puffing-billy and the stripe-ruby-mock gem, but nothing works. Here's my own javascript (simplified):

    var stripeResponseHandler = function (status, response) {
      console.log("response handler called");
      if (response.error) {
        // show the errors on the form
      } else {
        // insert the token into the form so it gets submitted to the server
        $("#credit_card_token").val(response.id);

        // Now submit the form.
        $form.get(0).submit();
      }
    }


    $form.submit(function (event) {
      // Disable the submit button to prevent repeated clicks
      $submitBtn.prop("disabled", true);
      event.preventDefault();

      console.log("creating token...");
      Stripe.createToken(
        // Get the credit card details from the form
        // and input them here.
      }, stripeResponseHandler);

      // Prevent the form from submitting the normal way.
      return false;
    });

Just to reiterate, this all works fine when I test it manually. But my automated tests fail:

 Failure/Error: expect{submit_form}.to change{User.count}.by(1)
   expected result to have changed by 1, but was changed by 0

When I try to use the gem puffing-billy, it seems to be caching stripe.js itself (which is loaded from Stripe's own servers at js.stripe.com, not served from my own app, as Stripe don't support this.), but the call initiated by Stripe.createToken isn't being cached. In fact, when I log into my Stripe server logs, it doesn't seem that the call is even been made (or at least Stripe isn't receiving it.)

Note those console.log statements in my JS above. When I run my test suite, the line "creating token..." gets printed, but "response handler called." doesn't. Looks like the response handler is never being called.

I've left out some details because this question is already very long, but can add more on request. What am I doing wrong here? How can I test my sign up page?

UPDATE See [my comment on this Github issue] on stripe-ruby-mock for more info on what I've tried and failed.

Damle answered 25/4, 2015 at 5:27 Comment(4)
What does your feature spec look like?Wandering
Do you see any errors in the JS console?Switzer
Also, I didn't find any info on Stripe.createToken, shouldn't it be Stripe.card.createToken?Switzer
Stripe.createToken definitely exists, like I said this all works when I test it manually, I'm just struggling to write an automated test for it.Damle
P
3

If I understand correctly...

Capybara won't know about your ajax requests. You should be able to stub out AJAX requests with Sinatra. Have it return a fixtures much the same as VCR.

Here's an article on it.

https://robots.thoughtbot.com/using-capybara-to-test-javascript-that-makes-http

You need to boot the Sinatra app in Capybara and then match the URLs in your ajax calls.

Something like:

class FakeContinousIntegration < Sinatra::Base
  def self.boot
    instance = new
    Capybara::Server.new(instance).tap { |server| server.boot }
  end

  get '/some/ajax'
    # send ajax back to capybara
  end
end

When you boot the server, it will return the address and port which you can write to a config that your js can use.

@server = App.boot

Then I use the address and port to config the JS app

def write_js_config
  config['api'] = "http://#{@server.host}:#{@server.port}"
  config.to_json
end

In spec_helper.rb send in the config to the js so your script points to your sinatra app. Mine compiles with gulp. So I just build the config into to is before the tests run:

system('gulp build --env capybara')
Persnickety answered 27/4, 2015 at 11:15 Comment(9)
I have already tried exactly this, see my Github comment in the link I just added to my question. My problem is that, no matter how I set things up, stripe.js just doesn't seem to be calling my "Fake" app. I've changed Stripe.endpoint in the JS to point to my Sinatra app and changed Stripe.api_base in the Ruby and my app just never gets called. (And my tests aren't calling the real Stripe server either - nothing in my logs.) How can I find out where the call is going wrong?Damle
(To clarify: I've added some logging functionally to my Sinatra app, and I can see that its actions (get/post /v1/tokens etc ) don't get called when I run my tests.Damle
Let me add a bit more. I can do this with an angular js app so it should be working... wierdPersnickety
If you console.log the api URL in your app does it match the port and address returned from booting your sinatra app?Persnickety
Thanks for updating your answer, but shouldn't @server = App.boot be @server = FakeContinousIntegration.boot?Damle
And where does write_js_config get called?Damle
I eventually got it working with a version of this answer. Thanks for the help.Damle
Hello @GeorgeMillo , I'm having the exact same problem as you did in this question. However the answer is not clear for me yet. Where do you write the FakeContinuousIntegration class? How you boot the sinatra app when running the test? How to tell strip.js to connect to the sinatra app instead of the real stripe? Will really appreciate your helpZoba
Setting Stripe.endpoint in Stripe.js is not officially supported. This approach was viable until recently, but Stripe.js changed some things, and setting Stripe.endpoint is no longer a reliable way to point Stripe.js to your fake server. Therefore, if you want to point to your own fake API server, you will need to use your own stub of Stripe.js itself rather than the real Stripe.js.Liris
W
1

I've had tests which worked on manual fail in Capybara/poltergeist due to timeout. In my case, the solution was to wait for all AJAX requests to finish. Reference

Not sure whether Stripe.js uses JQuery internally, try checking for a condition set by stripeResponseHandler.

Wandering answered 27/4, 2015 at 11:10 Comment(2)
I've already tried the wait_for_ajax trick, but I didn't think to use a condition in stripeResponseHandler instead of of jQuery.active... which come to think of it is definitely the right idea because I'm pretty sure Stripe doesn't use jQuery! Let me try that and see if it works.....Damle
Doesn't work. Added window.stripeFlag = true right before my call to createToken. Added window.stripeFlag = false as the first line of the stripeResponseHandler callback. Changed the method in wait_for_ajax to page.evaluate_script("window.stripeFlag").strip == "false" (and added wait_for_ajax to my specs... well, actually, it was already there). And it still doesn't work. stripeResponseHandler never gets called; it seems that stripe.js just isn't hitting the external API, either Stripe's real one or my own stubbed one.Damle
M
1

In addition to the wait_for_ajax trick mentioned, it looks like you are calling expect before your database was updated. One way to check that would be to add a breakpoint in your code(binding.pry), and check if it is a race condition issue or not.

Also, as per Capybara's documentation, introducing an expectation of a UI change makes it 'smartly' wait for ajax calls to finish:

expect(page).not_to have_content('Enter credit card details')
Maisey answered 2/5, 2015 at 18:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.