DanSnow / vue-recaptcha

Google ReCAPTCHA component for Vue.js
http://dansnow.github.io/vue-recaptcha/
MIT License
869 stars 134 forks source link

Mocking grecaptcha in tests #92

Closed aphofstede closed 7 years ago

aphofstede commented 7 years ago

Using Vue-test-utils and Sinon, I'm mocking the execute() function in my contact form tests, so I can bypass the actual validation, but I suspect some things remain in global scope; grecaptchaMock.execute.called only succeeds the first time. Any examples of usage in tests would be greatly appreciated.

import { mount } from 'vue-test-utils';
import ContactForm from '../assets/js/components/ContactForm.vue';
import expect from 'expect';
import Vue from 'vue'
import moxios from 'moxios'
import sinon from 'sinon'
import { equal } from 'assert'

// ... snip

describe ('Contact Form', () => {
    let wrapper, grecaptchaMock, widgetId;

    beforeEach(() => {
        // Ajax stub
        moxios.install()

        // reCaptcha stub
        widgetId = "WidgetId"
        grecaptchaMock = sinon.stub({
            render: function (options) { },
            reset: function (id) { },
            getResponse: function (id) { },
            execute: function() { }
        })
        grecaptchaMock.render.returns(widgetId)
        grecaptchaMock.execute.callsFake(function fakeFn() {
            wrapper.vm.onVerify('testtoken')
        })
        window.grecaptcha = grecaptchaMock

        // Mount the Vue component
        wrapper = mount(ContactForm)
    });
    afterEach(() => {
        // reCaptcha stub
        grecaptchaMock = null
        widgetId = null
        delete window.grecaptcha;

        // Ajax stub
        moxios.uninstall()
    });

// ... snip

    it ('should have errors if input was omitted', (done) => {

// ... snip (moxios setup)

        sinon.spy(wrapper.vm, "onVerify")

        // When the user submits the for without input
        wrapper.find('form').trigger('submit')

        // Then the execute flow should follow
        expect(grecaptchaMock.execute.called).toBe(true); // XXX only works if no tests were run before this
        expect(wrapper.vm.onVerify.called).toBe(true); // XXX seems to work every time

// ... snip
DanSnow commented 7 years ago

It is because vue-recaptcha will keep a reference to window.grecaptcha. How about reset the mock by calling execute.resetHistory ?

aphofstede commented 7 years ago

Aren't all Vue instances (and their references) cleared automatically after each test? I tried using the reset methods from sinon, but the problem is that the stub gets bypassed by the existing reference that vue-recaptcha still seems to hold.. Looking at vue-recaptcha.js, I can't quite figure out which var to nuke to make it reconfigure itself with the new window object.

DanSnow commented 7 years ago

Briefly, it's keep the reference in a global variable but not in the component instance. So I think that using the reset methods should be the solution. Are you loading the real recaptcha in test environment? vue-recaptcha will load the first grecaptcha object which it found. Maybe remove the real recaptcha, configure the mock, and call window.vueRecaptchaApiLoaded to tell vue-recaptcha to load the grecaptcha then it will work.

Another way is using the test keys. https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha-v2-what-should-i-do But in my experience, it will only pass the captcha at the first time with the same recaptcha instance. Not like the document said that it will always pass. Also you can't configure it that make it will pass or not.

aphofstede commented 7 years ago

Thanks for your response @DanSnow !

I don't pass any key in the frontend component test since I stub all the functions it doesn't communicate with Google at all. I guess I don't strictly need to verify that the stub was called; since onVerify() on my own component does get called successfully each run, but it might indicate something is not "clean" with this (sub)component.

Note: I'm cleaning up in afterEach() and reconstructing everything from scratch in beforeEach(). Using window.vueRecaptchaApiLoaded() didn't help. I also tried constructing the component before grecaptcha is added to the window object, but it didn't make a difference.

DanSnow commented 7 years ago

OK. I'll remove the reference of window.grecaptcha and always use window.grecaptcha directly.

DanSnow commented 7 years ago

@aphofstede Can you help me to test it? Just download and copy dist/vue-recaptcha.js to your project.

aphofstede commented 7 years ago

Works!

DanSnow commented 7 years ago

Thanks. This will include in next release. I'll close this issue.