diff --git a/.travis.yml b/.travis.yml index 3d72df1..fe4a4eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,12 @@ node_js: before_script: - npm install -g grunt-cli bower - bower install + - "export CHROME_BIN=chromium-browser" - "export DISPLAY=:99.0" - "sh -e /etc/init.d/xvfb start" script: - grunt karma:ci + +after_script: + - grunt coverage diff --git a/Gruntfile.js b/Gruntfile.js index 82606fd..54762f3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -52,7 +52,7 @@ module.exports = function (grunt) { }, ci: { configFile: 'karma.conf.js', - browsers: ['Chrome', 'Firefox', 'FirefoxNightly'], + browsers: ['Chrome_travis_ci', 'Firefox', 'FirefoxNightly'], singleRun: true } }, diff --git a/README.md b/README.md index 65e820b..c411114 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ AngularJS reCaptcha [![Build Status](https://travis-ci.org/VividCortex/angular-recaptcha.svg?branch=master)](https://travis-ci.org/VividCortex/angular-recaptcha) [![Coverage Status](https://coveralls.io/repos/VividCortex/angular-recaptcha/badge.svg?branch=master)](https://coveralls.io/r/VividCortex/angular-recaptcha?branch=master) +![image](https://img.shields.io/npm/dm/angular-recaptcha.svg) Add a [reCaptcha](https://www.google.com/recaptcha/intro/index.html) to your [AngularJS](angularjs.org) project. @@ -41,20 +42,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 @@ -131,9 +119,12 @@ You can optionally pass a __theme__ the captcha should use, as an html attribute size="---- compact or normal ----" type="'---- audio or image ----'" key="'---- YOUR PUBLIC KEY GOES HERE ----'" + lang="---- language code ----" > ``` +**Language Codes**: https://developers.google.com/recaptcha/docs/language + In this case we are specifying that the captcha should use the theme named _light_. Listeners @@ -153,6 +144,7 @@ There are three listeners you can use with the directive, `on-create`, `on-succe on-create="setWidgetId(widgetId)" on-success="setResponse(response)" on-expire="cbExpiration()" + lang="" > ``` @@ -205,19 +197,24 @@ myApp.config(function(vcRecaptchaServiceProvider){ vcRecaptchaServiceProvider.setStoken('--- YOUR GENERATED SECURE TOKEN ---') vcRecaptchaServiceProvider.setSize('---- compact or normal ----') vcRecaptchaServiceProvider.setType('---- audio or image ----') + vcRecaptchaServiceProvider.setLang('---- language code ----') }); ``` +**Language Codes**: https://developers.google.com/recaptcha/docs/language + You can also set all of the values at once. ```javascript myApp.config(function(vcRecaptchaServiceProvider){ vcRecaptchaServiceProvider.setDefaults({ - key: '---- YOUR PUBLIC KEY GOES HERE ----', - theme: '---- light or dark ----', - stoken: '--- YOUR GENERATED SECURE TOKEN ---', - size: '---- compact or normal ----', - type: '---- audio or image ----' + key: '---- YOUR PUBLIC KEY GOES HERE ----', + theme: '---- light or dark ----', + stoken: '--- YOUR GENERATED SECURE TOKEN ---', + size: '---- compact or normal ----', + type: '---- audio or image ----', + lang: '---- language code ----' + }); }); ``` Note: any value omitted will be undefined, even if previously set. @@ -236,6 +233,7 @@ Differences with the old reCaptcha Recent Changelog ---------------- +- 3.0.0 - Removed the need to include the Google recaptcha api. - 2.2.3 - Removed _cleanup_ after creating the captcha element. - 2.0.1 - Fixed onload when using ng-route and recaptcha is placed in a secondary view. - 2.0.0 - Rewritten service to support new reCaptcha diff --git a/bower.json b/bower.json index ff08c10..ff091b7 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "angular-recaptcha", - "version": "3.0.4", + "version": "4.0.3", "keywords": ["angular", "captcha", "recaptcha", "vividcortex", "human", "form", "validation", "signup", "security", "login"], "main": "release/angular-recaptcha.js", "ignore": [ @@ -14,7 +14,6 @@ "angular": "1.*" }, "devDependencies": { - "angular-mocks": "~1.*", - "jquery": "~2.1.3" + "angular-mocks": "~1.*" } } diff --git a/demo/usage.html b/demo/usage.html index 0180e32..fe7abce 100644 --- a/demo/usage.html +++ b/demo/usage.html @@ -8,10 +8,6 @@ - - - - diff --git a/karma.conf.js b/karma.conf.js index 33425f1..5f5eecb 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -15,7 +15,6 @@ module.exports = function (config) { // list of files / patterns to load in the browser files: [ - 'bower_components/jquery/dist/jquery.min.js', 'bower_components/angular/angular.js', 'bower_components/angular-mocks/angular-mocks.js', @@ -23,6 +22,7 @@ module.exports = function (config) { 'src/module.js', 'src/*.js', + 'tests/*.driver.js', 'tests/*_test.js' ], @@ -70,6 +70,12 @@ module.exports = function (config) { // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher browsers: ['PhantomJS', 'Chrome', 'IE', 'Safari', 'Firefox', 'FirefoxNightly', 'ChromeCanary'], + customLaunchers: { + Chrome_travis_ci: { + base: 'Chrome', + flags: ['--no-sandbox'] + } + }, // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits diff --git a/package.json b/package.json index a0ec7dd..ef260a3 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,17 @@ { "name": "angular-recaptcha", - "version": "3.0.4", + "version": "4.0.3", "description": "An AngularJS module to ease usage of reCaptcha inside a form", "author": "VividCortex", "license": "MIT", "homepage": "https://github.com/vividcortex/angular-recaptcha", + "contributors": [ + { + "name" : "Eduardo Daniel Cuomo", + "email" : "reduardo7@gmail.com", + "url" : "https://github.com/reduardo7/angular-recaptcha" + } + ], "main": "index.js", "repository": { "type": "git", @@ -14,23 +21,23 @@ "test": "grunt test" }, "devDependencies": { - "bower": "^1.3.3", - "grunt": "~0.4.2", - "grunt-bump": "0.0.13", - "grunt-cli": "~0.1.11", - "grunt-contrib-concat": "~0.3.0", - "grunt-contrib-jshint": "~0.8.0", - "grunt-contrib-uglify": "~0.4.0", - "grunt-karma": "^0.10.1", - "grunt-karma-coveralls": "^2.5.3", - "jasmine-core": "^2.2.0", - "karma": "^0.12.31", - "karma-chrome-launcher": "^0.1.7", - "karma-coverage": "^0.2.7", - "karma-firefox-launcher": "^0.1.4", - "karma-ie-launcher": "^0.1.5", - "karma-jasmine": "^0.3.5", - "karma-phantomjs-launcher": "^0.1.4", - "karma-safari-launcher": "^0.1.1" + "bower": "^1.8.0", + "grunt": "^1.0.1", + "grunt-bump": "^0.8.0", + "grunt-cli": "^1.2.0", + "grunt-contrib-concat": "^1.0.1", + "grunt-contrib-jshint": "^1.1.0", + "grunt-contrib-uglify": "^2.0.0", + "grunt-karma": "^2.0.0", + "grunt-karma-coveralls": "^2.5.4", + "jasmine-core": "^2.5.2", + "karma": "^1.3.0", + "karma-chrome-launcher": "^2.0.0", + "karma-coverage": "^1.1.1", + "karma-firefox-launcher": "^1.0.0", + "karma-ie-launcher": "^1.0.0", + "karma-jasmine": "^1.0.2", + "karma-phantomjs-launcher": "^1.0.2", + "karma-safari-launcher": "^1.0.0" } } diff --git a/release/angular-recaptcha.js b/release/angular-recaptcha.js index d90dd39..4255e05 100644 --- a/release/angular-recaptcha.js +++ b/release/angular-recaptcha.js @@ -1,7 +1,7 @@ /** - * angular-recaptcha build:2016-07-19 - * https://github.com/vividcortex/angular-recaptcha - * Copyright (c) 2016 VividCortex + * @license angular-recaptcha build:2017-02-02 + * https://github.com/vividcortex/angular-recaptcha + * Copyright (c) 2017 VividCortex **/ /*global angular, Recaptcha */ @@ -90,6 +90,15 @@ config.type = type; }; + /** + * Sets the reCaptcha language which will be used by default is not specified in a specific directive instance. + * + * @param lang The reCaptcha language. + */ + provider.setLang = function(lang){ + config.lang = lang; + }; + /** * Sets the reCaptcha configuration values which will be used by default is not specified in a specific directive instance. * @@ -100,8 +109,8 @@ provider.onLoadFunctionName = onLoadFunctionName; }; - provider.$get = ['$rootScope','$window', '$q', function ($rootScope, $window, $q) { - var deferred = $q.defer(), promise = deferred.promise, recaptcha; + provider.$get = ['$rootScope','$window', '$q', '$document', function ($rootScope, $window, $q, $document) { + var deferred = $q.defer(), promise = deferred.promise, instances = {}, recaptcha; $window.vcRecaptchaApiLoadedCallback = $window.vcRecaptchaApiLoadedCallback || []; @@ -138,6 +147,13 @@ // Check if grecaptcha is not defined already. if (ng.isDefined($window.grecaptcha)) { callback(); + } else { + // Generate link on demand + var script = $window.document.createElement('script'); + script.async = true; + script.defer = true; + script.src = 'https://www.google.com/recaptcha/api.js?onload='+provider.onLoadFunctionName+'&render=explicit'; + $document.find('body').append(script); } return { @@ -156,12 +172,15 @@ conf.stoken = conf.stoken || config.stoken; conf.size = conf.size || config.size; conf.type = conf.type || config.type; + conf.hl = conf.lang || config.lang; if (!conf.sitekey || conf.sitekey.length !== 40) { throwNoKeyException(); } return getRecaptcha().then(function (recaptcha) { - return recaptcha.render(elm, conf); + var widgetId = recaptcha.render(elm, conf); + instances[widgetId] = elm; + return widgetId; }); }, @@ -171,13 +190,45 @@ reload: function (widgetId) { validateRecaptchaInstance(); - // $log.info('Reloading captcha'); recaptcha.reset(widgetId); // Let everyone know this widget has been reset. $rootScope.$broadcast('reCaptchaReset', widgetId); }, + /** + * Get/Set reCaptcha language + */ + useLang: function (widgetId, lang) { + var instance = instances[widgetId]; + + if (instance) { + var iframe = instance.querySelector('iframe'); + if (lang) { + // Setter + if (iframe && iframe.src) { + var s = iframe.src; + if (/[?&]hl=/.test(s)) { + s = s.replace(/([?&]hl=)\w+/, '$1' + lang); + } else { + s += ((s.indexOf('?') === -1) ? '?' : '&') + 'hl=' + lang; + } + + iframe.src = s; + } + } else { + // Getter + if (iframe && iframe.src && /[?&]hl=\w+/.test(iframe.src)) { + return iframe.src.replace(/.+[?&]hl=(\w+)([^\w].+)?/, '$1'); + } else { + return null; + } + } + } else { + throw new Error('reCaptcha Widget ID not exists', widgetId); + } + }, + /** * Gets the response from the reCaptcha widget. * @@ -189,6 +240,20 @@ validateRecaptchaInstance(); return recaptcha.getResponse(widgetId); + }, + + /** + * Gets reCaptcha instance and configuration + */ + getInstance: function (widgetId) { + return instances[widgetId]; + }, + + /** + * Destroy reCaptcha instance. + */ + destroy: function (widgetId) { + delete instances[widgetId]; } }; @@ -215,6 +280,7 @@ theme: '=?', size: '=?', type: '=?', + lang: '=?', tabindex: '=?', required: '=?', onCreate: '&', @@ -247,6 +313,7 @@ stoken: scope.stoken || attrs.stoken || null, theme: scope.theme || attrs.theme || null, type: scope.type || attrs.type || null, + lang: scope.lang || attrs.lang || null, tabindex: scope.tabindex || attrs.tabindex || null, size: scope.size || attrs.size || null, 'expired-callback': expired @@ -299,6 +366,8 @@ } function cleanup(){ + vcRecaptcha.destroy(scope.widgetId); + // removes elements reCaptcha added. ng.element($document[0].querySelectorAll('.pls-container')).parent().remove(); } diff --git a/release/angular-recaptcha.min.js b/release/angular-recaptcha.min.js index b7761aa..0e43083 100644 --- a/release/angular-recaptcha.min.js +++ b/release/angular-recaptcha.min.js @@ -1,7 +1,7 @@ /** - * angular-recaptcha build:2016-07-19 - * https://github.com/vividcortex/angular-recaptcha - * Copyright (c) 2016 VividCortex + * @license angular-recaptcha build:2017-02-02 + * https://github.com/vividcortex/angular-recaptcha + * Copyright (c) 2017 VividCortex **/ -!function(a){"use strict";a.module("vcRecaptcha",[])}(angular),function(a){"use strict";function b(){throw new Error('You need to set the "key" attribute to your public reCaptcha key. If you don\'t have a key, please get one from https://www.google.com/recaptcha/admin/create')}var c=a.module("vcRecaptcha");c.provider("vcRecaptchaService",function(){var c=this,d={};c.onLoadFunctionName="vcRecaptchaApiLoaded",c.setDefaults=function(b){a.copy(b,d)},c.setSiteKey=function(a){d.key=a},c.setTheme=function(a){d.theme=a},c.setStoken=function(a){d.stoken=a},c.setSize=function(a){d.size=a},c.setType=function(a){d.type=a},c.setOnLoadFunctionName=function(a){c.onLoadFunctionName=a},c.$get=["$rootScope","$window","$q",function(e,f,g){function h(){return j?g.when(j):l}function i(){if(!j)throw new Error("reCaptcha has not been loaded yet.")}var j,k=g.defer(),l=k.promise;f.vcRecaptchaApiLoadedCallback=f.vcRecaptchaApiLoadedCallback||[];var m=function(){j=f.grecaptcha,k.resolve(j)};return f.vcRecaptchaApiLoadedCallback.push(m),f[c.onLoadFunctionName]=function(){f.vcRecaptchaApiLoadedCallback.forEach(function(a){a()})},a.isDefined(f.grecaptcha)&&m(),{create:function(a,c){return c.sitekey=c.key||d.key,c.theme=c.theme||d.theme,c.stoken=c.stoken||d.stoken,c.size=c.size||d.size,c.type=c.type||d.type,c.sitekey&&40===c.sitekey.length||b(),h().then(function(b){return b.render(a,c)})},reload:function(a){i(),j.reset(a),e.$broadcast("reCaptchaReset",a)},getResponse:function(a){return i(),j.getResponse(a)}}}]})}(angular),function(a){"use strict";var b=a.module("vcRecaptcha");b.directive("vcRecaptcha",["$document","$timeout","vcRecaptchaService",function(b,c,d){return{restrict:"A",require:"?^^form",scope:{response:"=?ngModel",key:"=?",stoken:"=?",theme:"=?",size:"=?",type:"=?",tabindex:"=?",required:"=?",onCreate:"&",onSuccess:"&",onExpire:"&"},link:function(e,f,g,h){function i(){h&&h.$setValidity("recaptcha",null),l()}function j(){c(function(){e.response="",k(),e.onExpire({widgetId:e.widgetId})})}function k(){h&&h.$setValidity("recaptcha",e.required===!1?null:Boolean(e.response))}function l(){a.element(b[0].querySelectorAll(".pls-container")).parent().remove()}e.widgetId=null,h&&a.isDefined(g.required)&&e.$watch("required",k);var m=e.$watch("key",function(b){var h=function(a){c(function(){e.response=a,k(),e.onSuccess({response:a,widgetId:e.widgetId})})};d.create(f[0],{callback:h,key:b,stoken:e.stoken||g.stoken||null,theme:e.theme||g.theme||null,type:e.type||g.type||null,tabindex:e.tabindex||g.tabindex||null,size:e.size||g.size||null,"expired-callback":j}).then(function(b){k(),e.widgetId=b,e.onCreate({widgetId:b}),e.$on("$destroy",i),e.$on("reCaptchaReset",function(c,d){(a.isUndefined(d)||b===d)&&(e.response="",k())})}),m()})}}}])}(angular); \ No newline at end of file +!function(a){"use strict";a.module("vcRecaptcha",[])}(angular),function(a){"use strict";function b(){throw new Error('You need to set the "key" attribute to your public reCaptcha key. If you don\'t have a key, please get one from https://www.google.com/recaptcha/admin/create')}var c=a.module("vcRecaptcha");c.provider("vcRecaptchaService",function(){var c=this,d={};c.onLoadFunctionName="vcRecaptchaApiLoaded",c.setDefaults=function(b){a.copy(b,d)},c.setSiteKey=function(a){d.key=a},c.setTheme=function(a){d.theme=a},c.setStoken=function(a){d.stoken=a},c.setSize=function(a){d.size=a},c.setType=function(a){d.type=a},c.setLang=function(a){d.lang=a},c.setOnLoadFunctionName=function(a){c.onLoadFunctionName=a},c.$get=["$rootScope","$window","$q","$document",function(e,f,g,h){function i(){return k?g.when(k):m}function j(){if(!k)throw new Error("reCaptcha has not been loaded yet.")}var k,l=g.defer(),m=l.promise,n={};f.vcRecaptchaApiLoadedCallback=f.vcRecaptchaApiLoadedCallback||[];var o=function(){k=f.grecaptcha,l.resolve(k)};if(f.vcRecaptchaApiLoadedCallback.push(o),f[c.onLoadFunctionName]=function(){f.vcRecaptchaApiLoadedCallback.forEach(function(a){a()})},a.isDefined(f.grecaptcha))o();else{var p=f.document.createElement("script");p.async=!0,p.defer=!0,p.src="https://www.google.com/recaptcha/api.js?onload="+c.onLoadFunctionName+"&render=explicit",h.find("body").append(p)}return{create:function(a,c){return c.sitekey=c.key||d.key,c.theme=c.theme||d.theme,c.stoken=c.stoken||d.stoken,c.size=c.size||d.size,c.type=c.type||d.type,c.hl=c.lang||d.lang,c.sitekey&&40===c.sitekey.length||b(),i().then(function(b){var d=b.render(a,c);return n[d]=a,d})},reload:function(a){j(),k.reset(a),e.$broadcast("reCaptchaReset",a)},useLang:function(a,b){var c=n[a];if(!c)throw new Error("reCaptcha Widget ID not exists",a);var d=c.querySelector("iframe");if(!b)return d&&d.src&&/[?&]hl=\w+/.test(d.src)?d.src.replace(/.+[?&]hl=(\w+)([^\w].+)?/,"$1"):null;if(d&&d.src){var e=d.src;/[?&]hl=/.test(e)?e=e.replace(/([?&]hl=)\w+/,"$1"+b):e+=(e.indexOf("?")===-1?"?":"&")+"hl="+b,d.src=e}},getResponse:function(a){return j(),k.getResponse(a)},getInstance:function(a){return n[a]},destroy:function(a){delete n[a]}}}]})}(angular),function(a){"use strict";var b=a.module("vcRecaptcha");b.directive("vcRecaptcha",["$document","$timeout","vcRecaptchaService",function(b,c,d){return{restrict:"A",require:"?^^form",scope:{response:"=?ngModel",key:"=?",stoken:"=?",theme:"=?",size:"=?",type:"=?",lang:"=?",tabindex:"=?",required:"=?",onCreate:"&",onSuccess:"&",onExpire:"&"},link:function(e,f,g,h){function i(){h&&h.$setValidity("recaptcha",null),l()}function j(){c(function(){e.response="",k(),e.onExpire({widgetId:e.widgetId})})}function k(){h&&h.$setValidity("recaptcha",e.required===!1?null:Boolean(e.response))}function l(){d.destroy(e.widgetId),a.element(b[0].querySelectorAll(".pls-container")).parent().remove()}e.widgetId=null,h&&a.isDefined(g.required)&&e.$watch("required",k);var m=e.$watch("key",function(b){var h=function(a){c(function(){e.response=a,k(),e.onSuccess({response:a,widgetId:e.widgetId})})};d.create(f[0],{callback:h,key:b,stoken:e.stoken||g.stoken||null,theme:e.theme||g.theme||null,type:e.type||g.type||null,lang:e.lang||g.lang||null,tabindex:e.tabindex||g.tabindex||null,size:e.size||g.size||null,"expired-callback":j}).then(function(b){k(),e.widgetId=b,e.onCreate({widgetId:b}),e.$on("$destroy",i),e.$on("reCaptchaReset",function(c,d){(a.isUndefined(d)||b===d)&&(e.response="",k())})}),m()})}}}])}(angular); \ No newline at end of file diff --git a/src/directive.js b/src/directive.js index 66b81ea..1bc9d50 100644 --- a/src/directive.js +++ b/src/directive.js @@ -16,6 +16,7 @@ theme: '=?', size: '=?', type: '=?', + lang: '=?', tabindex: '=?', required: '=?', onCreate: '&', @@ -48,6 +49,7 @@ stoken: scope.stoken || attrs.stoken || null, theme: scope.theme || attrs.theme || null, type: scope.type || attrs.type || null, + lang: scope.lang || attrs.lang || null, tabindex: scope.tabindex || attrs.tabindex || null, size: scope.size || attrs.size || null, 'expired-callback': expired @@ -100,6 +102,8 @@ } function cleanup(){ + vcRecaptcha.destroy(scope.widgetId); + // removes elements reCaptcha added. ng.element($document[0].querySelectorAll('.pls-container')).parent().remove(); } diff --git a/src/service.js b/src/service.js index 6f3aa0e..787a6a2 100644 --- a/src/service.js +++ b/src/service.js @@ -76,6 +76,15 @@ config.type = type; }; + /** + * Sets the reCaptcha language which will be used by default is not specified in a specific directive instance. + * + * @param lang The reCaptcha language. + */ + provider.setLang = function(lang){ + config.lang = lang; + }; + /** * Sets the reCaptcha configuration values which will be used by default is not specified in a specific directive instance. * @@ -86,8 +95,8 @@ provider.onLoadFunctionName = onLoadFunctionName; }; - provider.$get = ['$rootScope','$window', '$q', function ($rootScope, $window, $q) { - var deferred = $q.defer(), promise = deferred.promise, recaptcha; + provider.$get = ['$rootScope','$window', '$q', '$document', function ($rootScope, $window, $q, $document) { + var deferred = $q.defer(), promise = deferred.promise, instances = {}, recaptcha; $window.vcRecaptchaApiLoadedCallback = $window.vcRecaptchaApiLoadedCallback || []; @@ -124,6 +133,13 @@ // Check if grecaptcha is not defined already. if (ng.isDefined($window.grecaptcha)) { callback(); + } else { + // Generate link on demand + var script = $window.document.createElement('script'); + script.async = true; + script.defer = true; + script.src = 'https://www.google.com/recaptcha/api.js?onload='+provider.onLoadFunctionName+'&render=explicit'; + $document.find('body').append(script); } return { @@ -142,12 +158,15 @@ conf.stoken = conf.stoken || config.stoken; conf.size = conf.size || config.size; conf.type = conf.type || config.type; + conf.hl = conf.lang || config.lang; if (!conf.sitekey || conf.sitekey.length !== 40) { throwNoKeyException(); } return getRecaptcha().then(function (recaptcha) { - return recaptcha.render(elm, conf); + var widgetId = recaptcha.render(elm, conf); + instances[widgetId] = elm; + return widgetId; }); }, @@ -157,13 +176,45 @@ reload: function (widgetId) { validateRecaptchaInstance(); - // $log.info('Reloading captcha'); recaptcha.reset(widgetId); // Let everyone know this widget has been reset. $rootScope.$broadcast('reCaptchaReset', widgetId); }, + /** + * Get/Set reCaptcha language + */ + useLang: function (widgetId, lang) { + var instance = instances[widgetId]; + + if (instance) { + var iframe = instance.querySelector('iframe'); + if (lang) { + // Setter + if (iframe && iframe.src) { + var s = iframe.src; + if (/[?&]hl=/.test(s)) { + s = s.replace(/([?&]hl=)\w+/, '$1' + lang); + } else { + s += ((s.indexOf('?') === -1) ? '?' : '&') + 'hl=' + lang; + } + + iframe.src = s; + } + } else { + // Getter + if (iframe && iframe.src && /[?&]hl=\w+/.test(iframe.src)) { + return iframe.src.replace(/.+[?&]hl=(\w+)([^\w].+)?/, '$1'); + } else { + return null; + } + } + } else { + throw new Error('reCaptcha Widget ID not exists', widgetId); + } + }, + /** * Gets the response from the reCaptcha widget. * @@ -175,6 +226,20 @@ validateRecaptchaInstance(); return recaptcha.getResponse(widgetId); + }, + + /** + * Gets reCaptcha instance and configuration + */ + getInstance: function (widgetId) { + return instances[widgetId]; + }, + + /** + * Destroy reCaptcha instance. + */ + destroy: function (widgetId) { + delete instances[widgetId]; } }; diff --git a/tests/directive_test.js b/tests/directive_test.js index 6191ad9..accc812 100644 --- a/tests/directive_test.js +++ b/tests/directive_test.js @@ -186,5 +186,32 @@ describe('directive: vcRecaptcha', function () { widgetId: undefined }); }); + + it('the widget should be using the setted language', function () { + var element = angular.element('
'), + + _fakeCreate = function (element, config) { + config.callback(config.lang); + return { + then: function (cb) { + cb(); + } + }; + }; + + spyOn(vcRecaptchaService, 'create').and.callFake(_fakeCreate); + + $compile(element)($scope); + $scope.$digest(); + $timeout.flush(); + + expect($scope.onSuccess).toHaveBeenCalledWith({ + response: 'es', + widgetId: undefined + }); + }); }); }); 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.driver.js b/tests/service.driver.js new file mode 100644 index 0000000..84a269c --- /dev/null +++ b/tests/service.driver.js @@ -0,0 +1,51 @@ +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.find = mockDocument.find; + + return _this; + }, + mockWindow: function (mockWindow) { + mockModules.$window.document = mockWindow.document; + + 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 62ca9d1..e5a36fb 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(function () { + driver = new ServiceDriver(); + }); - beforeEach(inject(function (_vcRecaptchaService_, _$window_) { - vcRecaptchaService = _vcRecaptchaService_; - $window = _$window_; + describe('with loaded api', function () { + var grecaptchaMock; - $window.vcRecaptchaApiLoaded = jasmine.createSpy('vcRecaptchaApiLoaded'); - })); + beforeEach(function () { + driver + .given.apiLoaded(grecaptchaMock = jasmine.createSpyObj('grecaptcha', ['render', 'getResponse', 'reset'])) + .when.created(); + }); - describe('create', function () { - it('should call recaptcha.render', inject(function ($rootScope) { - var _element = '', - _key = '1234567890123456789012345678901234567890', - _fn = angular.noop, + it('should call recaptcha.render', function () { + var _element = '', + _key = '1234567890123456789012345678901234567890', + _fn = angular.noop, _confRender = { sitekey: _key, key: _key, @@ -28,39 +29,99 @@ describe('service', function () { theme: undefined, stoken: undefined, size: undefined, - type: undefined + type: undefined, + 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(grecaptchaMock.getResponse).toHaveBeenCalledWith(_widgetId); + }); + + it('should call useLang', function () { + var _element = angular.element('')[0], + _key = '1234567890123456789012345678901234567890'; + + driver.when.notifyThatApiLoaded(); + + driver.service.create(_element, { + key: _key + }).then(function (widgetId) { + var instance = driver.service.getInstance(widgetId); + expect(instance).toEqual(_element); + + driver.service.useLang(widgetId, 'es'); + expect(driver.service.useLang(widgetId)).toEqual('es'); + }); + + driver.applyChanges(); + }); + }); + + describe('without loaded api', function () { + var scriptTagSpy, + appendSpy, + funcName; + + beforeEach(function () { + scriptTagSpy = jasmine.createSpy('scriptTagSpy'); + appendSpy = jasmine.createSpy('appendSpy'); + + driver + .given.onLoadFunctionName(funcName = 'my-func') + .given.mockDocument({ + find: function () { + return { + append: appendSpy + }; + } + }) + .given.mockWindow({ + document: { + createElement: function () { + return scriptTagSpy; + } + } + }) + .when.created(); + + }); + + it('should add script tag to body', function () { + expect(scriptTagSpy.async).toBe(true); + expect(scriptTagSpy.defer).toBe(true); + expect(appendSpy).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'); + }); - expect($window.grecaptcha.getResponse).toHaveBeenCalledWith(_widgetId); + it('should validate that recaptcha is loaded', function () { + expect(driver.service.reload).toThrowError('reCaptcha has not been loaded yet.'); }); }); });