From b296f81b91c87383fd7cb7cc94ae1f1f6e3121b5 Mon Sep 17 00:00:00 2001 From: Felix Mosheev Date: Sat, 7 Jan 2017 16:24:08 +0200 Subject: [PATCH 1/4] Improvement: Generate recaptcha script on demand. --- src/service.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/service.js b/src/service.js index 27956dd..4933593 100644 --- a/src/service.js +++ b/src/service.js @@ -95,7 +95,7 @@ provider.onLoadFunctionName = onLoadFunctionName; }; - provider.$get = ['$rootScope','$window', '$q', function ($rootScope, $window, $q) { + provider.$get = ['$rootScope','$window', '$q', '$document', function ($rootScope, $window, $q, $document) { var deferred = $q.defer(), promise = deferred.promise, instances = {}, recaptcha; $window.vcRecaptchaApiLoadedCallback = $window.vcRecaptchaApiLoadedCallback || []; @@ -133,6 +133,13 @@ // Check if grecaptcha is not defined already. if (ng.isDefined($window.grecaptcha)) { callback(); + } else { + // Generate link on demand + var script = $document.get(0).createElement('script'); + script.async = true; + script.defer = true; + script.src = 'https://www.google.com/recaptcha/api.js?onload='+provider.onLoadFunctionName+'&render=explicit'; + $document.get(0).body.appendChild(script); } return { From 6ecec83ef3479494ce219d0786e0dec2d78dc8e0 Mon Sep 17 00:00:00 2001 From: Felix Mosheev Date: Tue, 10 Jan 2017 21:44:24 +0200 Subject: [PATCH 2/4] Refactor service tests and add tests for script tag generation --- karma.conf.js | 1 + tests/service.driver.js | 46 ++++++++++++++++ tests/service_test.js | 117 +++++++++++++++++++++++++--------------- 3 files changed, 122 insertions(+), 42 deletions(-) create mode 100644 tests/service.driver.js diff --git a/karma.conf.js b/karma.conf.js index 5b8e794..19c0606 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -23,6 +23,7 @@ module.exports = function (config) { 'src/module.js', 'src/*.js', + 'tests/*.driver.js', 'tests/*_test.js' ], diff --git a/tests/service.driver.js b/tests/service.driver.js new file mode 100644 index 0000000..34e63b9 --- /dev/null +++ b/tests/service.driver.js @@ -0,0 +1,46 @@ +function ServiceDriver() { + var _this = this; + var mockModules = { + $window: {}, + $document: {} + }; + + module(mockModules); // mock all the properties + + this.given = { + apiLoaded: function (mockRecaptcha) { + mockModules.$window.grecaptcha = mockRecaptcha; + + return _this; + }, + onLoadFunctionName: function (funcName) { + module(function (vcRecaptchaServiceProvider) { + vcRecaptchaServiceProvider.setOnLoadFunctionName(funcName); + }); + return _this; + }, + mockDocument: function (mockDocument) { + mockModules.$document.get = mockDocument.get; + + return _this; + } + }; + + this.when = { + created: function () { + inject(function (vcRecaptchaService) { + _this.service = vcRecaptchaService; + }) + }, + notifyThatApiLoaded: function () { + mockModules.$window.vcRecaptchaApiLoaded(); + return _this; + } + }; +} + +ServiceDriver.prototype.applyChanges = function () { + inject(function ($rootScope) { + $rootScope.$digest(); + }); +}; \ No newline at end of file diff --git a/tests/service_test.js b/tests/service_test.js index 4f69153..b44a660 100644 --- a/tests/service_test.js +++ b/tests/service_test.js @@ -1,26 +1,27 @@ describe('service', function () { 'use strict'; - var vcRecaptchaService, $window; + var driver; - beforeEach(module('vcRecaptcha', function ($provide) { - $provide.constant('$window', { - grecaptcha: jasmine.createSpyObj('grecaptcha', ['render', 'getResponse', 'reset']) - }); - })); + beforeEach(module('vcRecaptcha')); - beforeEach(inject(function (_vcRecaptchaService_, _$window_) { - vcRecaptchaService = _vcRecaptchaService_; - $window = _$window_; + beforeEach(function () { + driver = new ServiceDriver(); + }); - $window.vcRecaptchaApiLoaded = jasmine.createSpy('vcRecaptchaApiLoaded'); - })); + describe('with loaded api', function () { + var grecaptchaMock; - describe('create', function () { - it('should call recaptcha.render', inject(function ($rootScope) { - var _element = '
', - _key = '1234567890123456789012345678901234567890', - _fn = angular.noop, + beforeEach(function () { + driver + .given.apiLoaded(grecaptchaMock = jasmine.createSpyObj('grecaptcha', ['render', 'getResponse', 'reset'])) + .when.created(); + }); + + it('should call recaptcha.render', function () { + var _element = '
', + _key = '1234567890123456789012345678901234567890', + _fn = angular.noop, _confRender = { sitekey: _key, key: _key, @@ -32,57 +33,89 @@ describe('service', function () { hl: undefined }; - $window.vcRecaptchaApiLoaded(); + driver.when.notifyThatApiLoaded(); - vcRecaptchaService.create(_element, { + driver.service.create(_element, { key: _confRender.key, callback: _fn }); - $rootScope.$digest(); + driver.applyChanges(); - expect($window.grecaptcha.render).toHaveBeenCalledWith(_element, _confRender); - })); - }); + expect(grecaptchaMock.render).toHaveBeenCalledWith(_element, _confRender); + }); - describe('reload', function () { it('should call reset', function () { var _widgetId = 123; - vcRecaptchaService.reload(_widgetId); + driver.service.reload(_widgetId); - expect($window.grecaptcha.reset).toHaveBeenCalledWith(_widgetId); + expect(grecaptchaMock.reset).toHaveBeenCalledWith(_widgetId); }); - }); - describe('getResponse', function () { it('should call getResponse', function () { var _widgetId = 123; - vcRecaptchaService.getResponse(_widgetId); + driver.service.getResponse(_widgetId); - expect($window.grecaptcha.getResponse).toHaveBeenCalledWith(_widgetId); + expect(grecaptchaMock.getResponse).toHaveBeenCalledWith(_widgetId); }); - }); - describe('useLang', function () { - it('should call useLang', inject(function ($rootScope) { - var _element = angular.element('
')[0], - _key = '1234567890123456789012345678901234567890'; + it('should call useLang', function () { + var _element = angular.element('
')[0], + _key = '1234567890123456789012345678901234567890'; - $window.vcRecaptchaApiLoaded(); + driver.when.notifyThatApiLoaded(); - vcRecaptchaService.create(_element, { + driver.service.create(_element, { key: _key }).then(function (widgetId) { - var instance = vcRecaptchaService.getInstance(widgetId); + var instance = driver.service.getInstance(widgetId); expect(instance).toEqual(_element); - vcRecaptchaService.useLang(widgetId, 'es'); - expect(vcRecaptchaService.useLang(widgetId)).toEqual('es'); - }) + driver.service.useLang(widgetId, 'es'); + expect(driver.service.useLang(widgetId)).toEqual('es'); + }); + + driver.applyChanges(); + }); + }); - $rootScope.$digest(); - })); + describe('without loaded api', function () { + var scriptTagSpy, + appendChildSpy, + funcName; + + beforeEach(function () { + scriptTagSpy = jasmine.createSpy('scriptTagSpy'); + appendChildSpy = jasmine.createSpy('appendChildSpy'); + + driver + .given.onLoadFunctionName(funcName = 'my-func') + .given.mockDocument({ + get: function () { + return { + createElement: function () { + return scriptTagSpy; + }, + body: { + appendChild: appendChildSpy + } + }; + } + }) + .when.created(); + + }); + + it('should add script tag to body', function () { + expect(scriptTagSpy.async).toBe(true); + expect(scriptTagSpy.defer).toBe(true); + expect(appendChildSpy).toHaveBeenCalledWith(scriptTagSpy); + }); + + it('should add callback function name to src', function () { + expect(scriptTagSpy.src).toBe('https://www.google.com/recaptcha/api.js?onload=' + funcName + '&render=explicit'); + }); }); }); From ba287ea0cd61e4f7a6427038ecf7910e3856bedd Mon Sep 17 00:00:00 2001 From: Felix Mosheev Date: Fri, 13 Jan 2017 11:52:50 +0200 Subject: [PATCH 3/4] Update docs regarding script include --- README.md | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/README.md b/README.md index d0ce05b..243d457 100644 --- a/README.md +++ b/README.md @@ -41,20 +41,7 @@ See [the demo file](demo/usage.html) for a quick usage example. - First, you need to get a valid recaptcha key for your domain. Go to http://www.google.com/recaptcha. -- Include the reCaptcha [API](https://developers.google.com/recaptcha/docs/display#AJAX) using this script in your HTML: - -```html - -``` - -As you can see, we are specifying a `onload` callback, which will notify the angular service once the api is ready for usage. - -The `onload` callback name defaults to `vcRecaptchaApiLoaded`, but can be overridden by the service provider via `vcRecaptchaServiceProvider.setOnLoadFunctionName('myOtherFunctionName');`. - -- Also include the vc-recaptcha script and make your angular app depend on the `vcRecaptcha` module. +- Include the vc-recaptcha script and make your angular app depend on the `vcRecaptcha` module. ```html From 7bdc4ac98da2aae91ec396439e0f28349ce543e3 Mon Sep 17 00:00:00 2001 From: Felix Mosheev Date: Fri, 13 Jan 2017 12:50:37 +0200 Subject: [PATCH 4/4] Added missing tests --- tests/provider.driver.js | 35 +++++++++++++++ tests/provider_test.js | 94 ++++++++++++++++++++++++++++++++++++++++ tests/service_test.js | 4 ++ 3 files changed, 133 insertions(+) create mode 100644 tests/provider.driver.js create mode 100644 tests/provider_test.js diff --git a/tests/provider.driver.js b/tests/provider.driver.js new file mode 100644 index 0000000..299f256 --- /dev/null +++ b/tests/provider.driver.js @@ -0,0 +1,35 @@ +function ProviderDriver() { + var _this = this; + var mockModules = { + $window: {} + }; + + module(mockModules); // mock all the properties + + this.given = { + recaptchaLoaded: function (recaptchaMock) { + mockModules.$window.grecaptcha = recaptchaMock; + return _this; + } + }; + + this.when = { + created: function () { + module(function (vcRecaptchaServiceProvider) { + _this.provider = vcRecaptchaServiceProvider; + }); + + inject(); // needed for angular-mocks to kick off + + return _this; + }, + callingCreate: function () { + inject(function (vcRecaptchaService, $rootScope) { + vcRecaptchaService.create(null, {}); + $rootScope.$digest(); + }); + + return this; + } + }; +} \ No newline at end of file diff --git a/tests/provider_test.js b/tests/provider_test.js new file mode 100644 index 0000000..89bd06c --- /dev/null +++ b/tests/provider_test.js @@ -0,0 +1,94 @@ +describe('provider', function () { + 'use strict'; + + var driver, + recaptchaMock, + key; + + beforeEach(module('vcRecaptcha')); + + beforeEach(function () { + driver = new ProviderDriver(); + driver.given.recaptchaLoaded(recaptchaMock = jasmine.createSpyObj('recaptchaMock', ['render'])) + .when.created(); + + driver.provider.setSiteKey(key = '1234567890123456789012345678901234567890'); + }); + + it('should setDefaults', function () { + var modifiedKey = key.substring(0, 39) + 'x'; + + driver.provider.setDefaults({key: modifiedKey}); + + driver.when.callingCreate(); + + var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; + + expect(callArgs).toEqual(jasmine.objectContaining({sitekey: modifiedKey})); + }); + + it('should setSiteKey', function () { + driver.provider.setSiteKey(key); + + driver.when.callingCreate(); + + var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; + + expect(callArgs).toEqual(jasmine.objectContaining({sitekey: key})); + }); + + it('should setTheme', function () { + var theme = 'theme'; + driver.provider.setTheme(theme); + + driver.when.callingCreate(); + + var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; + + expect(callArgs).toEqual(jasmine.objectContaining({theme: theme})); + }); + + it('should setStoken', function () { + var stoken = 'stoken'; + driver.provider.setStoken(stoken); + + driver.when.callingCreate(); + + var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; + + expect(callArgs).toEqual(jasmine.objectContaining({stoken: stoken})); + }); + + it('should setSize', function () { + var size = 'size'; + driver.provider.setSize(size); + + driver.when.callingCreate(); + + var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; + + expect(callArgs).toEqual(jasmine.objectContaining({size: size})); + }); + + it('should setType', function () { + var type = 'type'; + driver.provider.setType(type); + + driver.when.callingCreate(); + + var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; + + expect(callArgs).toEqual(jasmine.objectContaining({type: type})); + }); + + it('should setLang', function () { + var lang = 'en'; + driver.provider.setLang(lang); + + driver.when.callingCreate(); + + var callArgs = recaptchaMock.render.calls.mostRecent().args[1]; + + expect(callArgs).toEqual(jasmine.objectContaining({hl: lang})); + }); +}); diff --git a/tests/service_test.js b/tests/service_test.js index b44a660..5caabfc 100644 --- a/tests/service_test.js +++ b/tests/service_test.js @@ -117,5 +117,9 @@ describe('service', function () { it('should add callback function name to src', function () { expect(scriptTagSpy.src).toBe('https://www.google.com/recaptcha/api.js?onload=' + funcName + '&render=explicit'); }); + + it('should validate that recaptcha is loaded', function () { + expect(driver.service.reload).toThrowError('reCaptcha has not been loaded yet.'); + }); }); });