diff --git a/src/__tests__/field/captcha/third_party_captcha.test.jsx b/src/__tests__/field/captcha/third_party_captcha.test.jsx index 31adbdbe3..fcff12d97 100644 --- a/src/__tests__/field/captcha/third_party_captcha.test.jsx +++ b/src/__tests__/field/captcha/third_party_captcha.test.jsx @@ -20,6 +20,7 @@ const createLockMock = ({ describe('ThirdPartyCaptcha', () => { let prevWindow; + let counter = 0; beforeAll(() => { prevWindow = global.window; global.window.grecaptcha = { @@ -37,7 +38,11 @@ describe('ThirdPartyCaptcha', () => { }) }; global.window.turnstile = { - render: jest.fn() + render: jest.fn(), + reset: () => { + global.window.turnstile.render(...global.window.turnstile.render.mock.calls[counter]); + counter++; + } }; }); afterAll(() => { @@ -179,6 +184,7 @@ describe('ThirdPartyCaptcha', () => { hl={'en'} isValid={true} value={undefined} + onChange={jest.fn()} /> ).instance(); act(() => { @@ -198,9 +204,31 @@ describe('ThirdPartyCaptcha', () => { 'expired-callback': expect.any(Function), 'error-callback': expect.any(Function), language: 'en', - theme: 'light' + theme: 'light', + retry: 'never', + 'response-field': false }); }); + + it('should retry 3 times on error and then set value to BYPASS_CAPTCHA dummy token for failOpen', () => { + const renderParams = global.window.turnstile.render.mock.calls[0][1]; + for (let i = 0; i < 3; i++) { + const renderParams = global.window.turnstile.render.mock.calls[i][1]; + act(() => { + renderParams['error-callback'](); + }); + const { retryCount } = wrapper.state; + const { value } = wrapper.props; + expect(retryCount).toBe(i + 1); + expect(value).toBe(undefined); + } + + act(() => renderParams['error-callback']()); + + const { onChange } = wrapper.props; + expect(onChange.mock.calls).toHaveLength(1); + expect(onChange.mock.calls[0][0]).toBe('BYPASS_CAPTCHA'); + }); }); describe('recaptcha enterprise', () => { diff --git a/src/field/captcha/third_party_captcha.jsx b/src/field/captcha/third_party_captcha.jsx index 6a5da9676..cfe3eee66 100644 --- a/src/field/captcha/third_party_captcha.jsx +++ b/src/field/captcha/third_party_captcha.jsx @@ -148,7 +148,21 @@ export class ThirdPartyCaptcha extends React.Component { renderParams = { ...renderParams, language: this.props.hl, - theme: 'light' + theme: 'light', + retry: 'never', + 'response-field': false, + 'error-callback': () => { + if (this.state.retryCount < MAX_RETRY) { + getCaptchaProvider(this.props.provider).reset(this.widgetId); + this.setState(prevState => ({ + retryCount: prevState.retryCount + 1 + })); + } else { + // similar implementation to ARKOSE_PROVIDER failOpen + this.changeHandler('BYPASS_CAPTCHA'); + } + return true; + } }; } return renderParams; @@ -163,13 +177,13 @@ export class ThirdPartyCaptcha extends React.Component { async: true, defer: true }; - if (provider === ARKOSE_PROVIDER) { + if (provider === ARKOSE_PROVIDER || provider === AUTH0_V2_CAPTCHA_PROVIDER) { attributes['data-callback'] = callbackName; attributes['onerror'] = () => { if (this.state.retryCount < MAX_RETRY) { removeScript(scriptUrl); loadScript(scriptUrl, attributes); - this.setState((prevState) => ({ + this.setState(prevState => ({ retryCount: prevState.retryCount + 1 })); return; @@ -177,7 +191,7 @@ export class ThirdPartyCaptcha extends React.Component { removeScript(scriptUrl); this.changeHandler('BYPASS_CAPTCHA'); }; - window[callbackName] = (arkose) => { + window[callbackName] = arkose => { callback(arkose); }; } else { @@ -185,7 +199,7 @@ export class ThirdPartyCaptcha extends React.Component { delete window[callbackName]; callback(); }; - + if (provider === FRIENDLY_CAPTCHA_PROVIDER) { attributes['onload'] = window[callbackName]; }