Skip to content

Commit

Permalink
fix(recaptcha provider): handle remount
Browse files Browse the repository at this point in the history
* add button to toggle provider visibility in dev for testing of #37
* fix recaptcha provider to support remounting #37
* add and adjust tests for the recaptcha provider to validate remount

fixes #37
  • Loading branch information
antokara committed Jun 14, 2020
1 parent d49e44c commit e72f95b
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 60 deletions.
119 changes: 69 additions & 50 deletions dev/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { render } from 'react-dom';

// app state
interface IState {
providerVisible: boolean;
v2TokenA: string | undefined;
v2ExpiredA: boolean;
v2ErrorA: boolean;
Expand All @@ -34,11 +35,13 @@ interface IState {
class App extends React.PureComponent<{}, IState> {
public constructor(props: {}) {
super(props);
this.toggleProvider = this.toggleProvider.bind(this);
this.v2CallbackA = this.v2CallbackA.bind(this);
this.v2CallbackB = this.v2CallbackB.bind(this);
this.v3CallbackA = this.v3CallbackA.bind(this);
this.v3CallbackB = this.v3CallbackB.bind(this);
this.state = {
providerVisible: true,
v2TokenA: undefined,
v2ExpiredA: false,
v2ErrorA: false,
Expand Down Expand Up @@ -66,6 +69,7 @@ class App extends React.PureComponent<{}, IState> {

public render(): React.ReactNode {
const {
providerVisible,
v2TokenA,
v2ExpiredA,
v2ErrorA,
Expand Down Expand Up @@ -94,56 +98,61 @@ class App extends React.PureComponent<{}, IState> {
}

return (
<ReCaptchaProvider
siteKeyV2={process.env.RE_CAPTCHA_V2_SITE_KEY}
siteKeyV3={process.env.RE_CAPTCHA_V3_SITE_KEY}
>
<div data-test="dummy wrapper to demonstrate react context">
<h1>my demo app</h1>

<hr />
<h2>ReCaptcha V2 - A</h2>
<ReCaptchaV2
id="my-id"
className="test"
data-test-id="my-test-id"
callback={this.v2CallbackA}
theme={EReCaptchaV2Theme.Light}
size={EReCaptchaV2Size.Normal}
/>
<h6>Token: {v2TokenA}</h6>
<h6>Expired: {v2ExpiredA ? 'yes' : 'no'}</h6>
<h6>Error: {v2ErrorA ? 'yes' : 'no'}</h6>

<hr />
<h2>ReCaptcha V2 - B (delayed render)</h2>
{v2VisibleB && (
<ReCaptchaV2
callback={this.v2CallbackB}
theme={EReCaptchaV2Theme.Dark}
size={EReCaptchaV2Size.Compact}
tabindex={0}
/>
)}
<h6>Token: {v2TokenB}</h6>
<h6>Expired: {v2ExpiredB ? 'yes' : 'no'}</h6>
<h6>Error: {v2ErrorB ? 'yes' : 'no'}</h6>

<hr />
<h2>ReCaptcha V3 - ActionA</h2>
<ReCaptchaV3 action="actionA" callback={this.v3CallbackA} />
<h3>Retrieving: {v3RetrievingA ? 'yes' : 'no'}</h3>
<h6>Token: {v3TokenA}</h6>
{RefreshTokenA}

<hr />
<h2>ReCaptcha V3 - ActionB</h2>
<ReCaptchaV3 action="actionB" callback={this.v3CallbackB} />
<h3>Retrieving: {v3RetrievingB ? 'yes' : 'no'}</h3>
<h6>Token: {v3TokenB}</h6>
{RefreshTokenB}
</div>
</ReCaptchaProvider>
<>
<button onClick={this.toggleProvider}>toggle provider</button>
{providerVisible && (
<ReCaptchaProvider
siteKeyV2={process.env.RE_CAPTCHA_V2_SITE_KEY}
siteKeyV3={process.env.RE_CAPTCHA_V3_SITE_KEY}
>
<div data-test="dummy wrapper to demonstrate react context">
<h1>my demo app</h1>

<hr />
<h2>ReCaptcha V2 - A</h2>
<ReCaptchaV2
id="my-id"
className="test"
data-test-id="my-test-id"
callback={this.v2CallbackA}
theme={EReCaptchaV2Theme.Light}
size={EReCaptchaV2Size.Normal}
/>
<h6>Token: {v2TokenA}</h6>
<h6>Expired: {v2ExpiredA ? 'yes' : 'no'}</h6>
<h6>Error: {v2ErrorA ? 'yes' : 'no'}</h6>

<hr />
<h2>ReCaptcha V2 - B (delayed render)</h2>
{v2VisibleB && (
<ReCaptchaV2
callback={this.v2CallbackB}
theme={EReCaptchaV2Theme.Dark}
size={EReCaptchaV2Size.Compact}
tabindex={0}
/>
)}
<h6>Token: {v2TokenB}</h6>
<h6>Expired: {v2ExpiredB ? 'yes' : 'no'}</h6>
<h6>Error: {v2ErrorB ? 'yes' : 'no'}</h6>

<hr />
<h2>ReCaptcha V3 - ActionA</h2>
<ReCaptchaV3 action="actionA" callback={this.v3CallbackA} />
<h3>Retrieving: {v3RetrievingA ? 'yes' : 'no'}</h3>
<h6>Token: {v3TokenA}</h6>
{RefreshTokenA}

<hr />
<h2>ReCaptcha V3 - ActionB</h2>
<ReCaptchaV3 action="actionB" callback={this.v3CallbackB} />
<h3>Retrieving: {v3RetrievingB ? 'yes' : 'no'}</h3>
<h6>Token: {v3TokenB}</h6>
{RefreshTokenB}
</div>
</ReCaptchaProvider>
)}
</>
);
}

Expand Down Expand Up @@ -220,6 +229,16 @@ class App extends React.PureComponent<{}, IState> {
});
}
}

/**
* toggles the visibility of the provider to trigger unmount/mount on demand for testing
*/
private toggleProvider(): void {
const { providerVisible } = this.state;
this.setState({
providerVisible: !providerVisible
});
}
}

render(<App />, document.getElementById('root'));
165 changes: 156 additions & 9 deletions src/provider/ReCaptchaProvider.test/reMount.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, RenderResult } from '@testing-library/react';
import { getByTestId, render, RenderResult } from '@testing-library/react';
import * as React from 'react';
import { ReCaptchaProvider } from 'src/provider/ReCaptchaProvider';
import { withContext } from 'src/provider/withContext';
Expand All @@ -10,16 +10,85 @@ import { IProps } from './helpers/IProps';
describe('ReCaptchaProvider', (): void => {
let rr: RenderResult;
let DummyComponentWithContext: React.ComponentType<IProps>;
let node: ChildNode | null;

// make sure everything is clear
beforeAll(clearDOM);
describe('first mount with required props and post onload invocation', (): void => {
beforeAll((): void => {
clearDOM();
// reset state
delete window.GoogleReCaptcha_onload;
// wrap our dummy component with the context and get its props
DummyComponentWithContext = withContext(DummyComponent);
// render our dummy component in a two level nested node
// under the provider, to test the context passing down
rr = render(
<ReCaptchaProvider>
<div>
<DummyComponentWithContext dummy="dummy-prop" otherDummy={55} />
</div>
</ReCaptchaProvider>
);
});

it('onload handler is defined', (): void => {
expect(window.GoogleReCaptcha_onload).toBeInstanceOf(Function);
});

describe('unmount and invoke onload handler', (): void => {
beforeAll((): void => {
rr.unmount();
// emulate the onload call by the google api
window.GoogleReCaptcha_onload();
});

it('onload handler is undefined', (): void => {
expect(window.GoogleReCaptcha_onload).toBeUndefined();
});
});

describe('second mount with optional props', (): void => {
beforeAll((): void => {
// wrap our dummy component with the context and get its props
DummyComponentWithContext = withContext(DummyComponent);
// new render to trigger a new mount with all optional props
rr = render(
<ReCaptchaProvider
siteKeyV2={EProps.siteKeyV2}
siteKeyV3={EProps.siteKeyV3}
langCode={EProps.langCode}
hideV3Badge={true}
>
<div>
<DummyComponentWithContext dummy="dummy-prop" otherDummy={55} />
</div>
</ReCaptchaProvider>
);
node = getByTestId(rr.container, 'dummy-test-id');
});

it('script tag gets injected only once', (): void => {
expect(document.querySelectorAll('script')).toHaveLength(1);
});

it('style tag gets injected only once', (): void => {
expect(document.querySelectorAll('style')).toHaveLength(1);
});

it('changes the component`s loaded prop to true', (): void => {
expect(node).toHaveAttribute('data-loaded', 'true');
});

it('the onload handler gets deleted', (): void => {
expect(window.GoogleReCaptcha_onload).toBeUndefined();
});
});
});

// check double render/mount not causing the
// script/style getting inject twice as well
// but without the beforeEach cleanup though
// so that the script is already there and we double render/mount...
describe('first mount with required props', (): void => {
beforeEach((): void => {
beforeAll((): void => {
clearDOM();
// reset state
delete window.GoogleReCaptcha_onload;
// wrap our dummy component with the context and get its props
DummyComponentWithContext = withContext(DummyComponent);
// render our dummy component in a two level nested node
Expand All @@ -33,8 +102,77 @@ describe('ReCaptchaProvider', (): void => {
);
});

it('onload handler is defined', (): void => {
expect(window.GoogleReCaptcha_onload).toBeInstanceOf(Function);
});

describe('second mount with optional props and post onload invocation', (): void => {
beforeAll((): void => {
// wrap our dummy component with the context and get its props
DummyComponentWithContext = withContext(DummyComponent);
// new render to trigger a new mount with all optional props
rr = render(
<ReCaptchaProvider
siteKeyV2={EProps.siteKeyV2}
siteKeyV3={EProps.siteKeyV3}
langCode={EProps.langCode}
hideV3Badge={true}
>
<div>
<DummyComponentWithContext dummy="dummy-prop" otherDummy={55} />
</div>
</ReCaptchaProvider>
);
// emulate the onload call by the google api
window.GoogleReCaptcha_onload();
node = getByTestId(rr.container, 'dummy-test-id');
});

it('script tag gets injected only once', (): void => {
expect(document.querySelectorAll('script')).toHaveLength(1);
});

it('style tag gets injected only once', (): void => {
expect(document.querySelectorAll('style')).toHaveLength(1);
});

it('changes the component`s loaded prop to true', (): void => {
expect(node).toHaveAttribute('data-loaded', 'true');
});

it('the onload handler gets deleted', (): void => {
expect(window.GoogleReCaptcha_onload).toBeUndefined();
});
});
});

describe('first mount with required props and onload invocation', (): void => {
beforeAll((): void => {
// make sure everything is clear for this scope
clearDOM();
// reset state
delete window.GoogleReCaptcha_onload;
// wrap our dummy component with the context and get its props
DummyComponentWithContext = withContext(DummyComponent);
// render our dummy component in a two level nested node
// under the provider, to test the context passing down
rr = render(
<ReCaptchaProvider>
<div>
<DummyComponentWithContext dummy="dummy-prop" otherDummy={55} />
</div>
</ReCaptchaProvider>
);
// emulate the onload call by the google api
window.GoogleReCaptcha_onload();
});

it('onload handler is not defined', (): void => {
expect(window.GoogleReCaptcha_onload).toBeUndefined();
});

describe('second mount with optional props', (): void => {
beforeEach((): void => {
beforeAll((): void => {
// wrap our dummy component with the context and get its props
DummyComponentWithContext = withContext(DummyComponent);
// new render to trigger a new mount with all optional props
Expand All @@ -50,6 +188,7 @@ describe('ReCaptchaProvider', (): void => {
</div>
</ReCaptchaProvider>
);
node = getByTestId(rr.container, 'dummy-test-id');
});

it('script tag gets injected only once', (): void => {
Expand All @@ -59,6 +198,14 @@ describe('ReCaptchaProvider', (): void => {
it('style tag gets injected only once', (): void => {
expect(document.querySelectorAll('style')).toHaveLength(1);
});

it('changes the component`s loaded prop to true', (): void => {
expect(node).toHaveAttribute('data-loaded', 'true');
});

it('the onload handler gets deleted', (): void => {
expect(window.GoogleReCaptcha_onload).toBeUndefined();
});
});
});
});
14 changes: 14 additions & 0 deletions src/provider/ReCaptchaProvider.test/withOptionalProps.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,19 @@ describe('ReCaptchaProvider', (): void => {
expect(document.querySelectorAll('style')).toHaveLength(1);
});
});

describe('window.GoogleReCaptcha_onload callback', (): void => {
beforeEach((): void => {
window.GoogleReCaptcha_onload();
});

it('the loaded prop changes to true', (): void => {
expect(node).toHaveAttribute('data-loaded', 'true');
});

it('the onload handler gets deleted', (): void => {
expect(window.GoogleReCaptcha_onload).toBeUndefined();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ describe('ReCaptchaProvider', (): void => {
it('the loaded prop changes to true', (): void => {
expect(node).toHaveAttribute('data-loaded', 'true');
});

it('the onload handler gets deleted', (): void => {
expect(window.GoogleReCaptcha_onload).toBeUndefined();
});
});
});
});
Loading

0 comments on commit e72f95b

Please sign in to comment.