-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: improve captcha component and add tests (#36)
* refactor: improve recaptcha logic - allows us to reuse same google recaptcha script (load only once) - use raw loader for vite to source the javascript as string * feat: init test cases for recaptcha functions and component * feat: complete test case for captcha component * fix: improve test cases * fix: extend custom property for global window object Co-authored-by: Micah Thomas <micah.thomas@bigcommerce.com>
- Loading branch information
Showing
7 changed files
with
277 additions
and
118 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { | ||
beforeAll, | ||
afterEach, | ||
describe, | ||
it, | ||
expect, | ||
vi, | ||
} from 'vitest' | ||
import { | ||
ThemeFrame, | ||
} from '../ThemeFrame' | ||
import { | ||
render, screen, | ||
} from '../../utils/test-utils' | ||
import { | ||
Captcha, loadCaptchaScript, loadCaptchaWidgetHandlers, | ||
} from './Captcha' | ||
|
||
declare global { | ||
interface Window { | ||
INITIALIZE_CAPTCHA_testid?: Function | ||
} | ||
} | ||
|
||
const TEST_SITE_KEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI' | ||
const CAPTCHA_URL = 'https://www.google.com/recaptcha/api.js?render=explicit' | ||
const INIT_BROWSER:{head:string, body:string} = { | ||
head: '', body: '', | ||
} | ||
|
||
beforeAll(() => { | ||
INIT_BROWSER.head = document.head.innerHTML | ||
INIT_BROWSER.body = document.body.innerHTML | ||
}) | ||
|
||
afterEach(() => { | ||
window.INITIALIZE_CAPTCHA_testid = undefined | ||
document.head.innerHTML = INIT_BROWSER.head | ||
document.body.innerHTML = INIT_BROWSER.body | ||
}) | ||
|
||
describe('loadCaptchaScript', () => { | ||
it('should inject recaptcha script to Document', () => { | ||
loadCaptchaScript(document) | ||
const [recaptchaScript] = document.getElementsByTagName('script') | ||
expect(recaptchaScript.src).toBe(CAPTCHA_URL) | ||
}) | ||
}) | ||
|
||
describe('loadCaptchaWidgetHandlers', () => { | ||
it('should inject script', () => { | ||
loadCaptchaWidgetHandlers(document, 'testid') | ||
const [recaptchaHandlersScript] = document.getElementsByTagName('script') | ||
expect(recaptchaHandlersScript.innerHTML.length).toBeGreaterThan(0) | ||
}) | ||
it('should replace text by id', () => { | ||
loadCaptchaWidgetHandlers(document, 'testid') | ||
const [recaptchaHandlersScript] = document.getElementsByTagName('script') | ||
expect(recaptchaHandlersScript.innerHTML).toMatch('testid') | ||
}) | ||
}) | ||
|
||
describe('Captcha', () => { | ||
it('should render the captcha wrapper', () => { | ||
vi.useFakeTimers() | ||
render( | ||
<ThemeFrame title="test-frame"> | ||
<Captcha | ||
siteKey={TEST_SITE_KEY} | ||
theme="dark" | ||
size="normal" | ||
/> | ||
</ThemeFrame>, | ||
) | ||
vi.advanceTimersToNextTimer() | ||
const iframe: HTMLIFrameElement = screen.getByTitle('test-frame') | ||
const iframeDocument = iframe.contentDocument as Document | ||
const [captchaWrapper] = iframeDocument.body.getElementsByTagName('div') | ||
|
||
expect(captchaWrapper.id).toMatch('widget') | ||
expect(captchaWrapper.dataset.sitekey).toBe(TEST_SITE_KEY) | ||
expect(captchaWrapper.dataset.theme).toBe('dark') | ||
expect(captchaWrapper.dataset.size).toBe('normal') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { | ||
Component, | ||
ContextType, | ||
} from 'react' | ||
import { | ||
ThemeFrameContext, | ||
} from '@/components/ThemeFrame' | ||
import FRAME_HANDLER_CODE from './frameCaptchaCode.js?raw' | ||
|
||
const CAPTCHA_URL = 'https://www.google.com/recaptcha/api.js?render=explicit' | ||
const CAPTCHA_VARIABLES: Record<string, string> = { | ||
PREFIX: '', | ||
PARENT_ORIGIN: window.location.origin, | ||
CAPTCHA_SUCCESS: 'captcha-success', | ||
CAPTCHA_ERROR: 'captcha-error', | ||
CAPTCHA_EXPIRED: 'captcha-expired', | ||
} | ||
|
||
export interface CaptchaProps { | ||
siteKey: string; | ||
size?: 'compact' | 'normal'; | ||
theme?: 'dark' | 'light'; | ||
onSuccess?: () => void; | ||
onError?: () => void; | ||
onExpired?: () => void; | ||
} | ||
|
||
export function loadCaptchaScript(iframeDocument: Document) { | ||
if (iframeDocument.head.querySelector(`script[src="${CAPTCHA_URL}"]`) === null) { | ||
const captchaScript = iframeDocument.createElement('script') | ||
captchaScript.src = CAPTCHA_URL | ||
iframeDocument.head.appendChild(captchaScript) | ||
} | ||
} | ||
|
||
export function loadCaptchaWidgetHandlers(iframeDocument: Document, widgetId: string) { | ||
let code = FRAME_HANDLER_CODE | ||
|
||
CAPTCHA_VARIABLES.PREFIX = widgetId | ||
const variableNames = Object.keys(CAPTCHA_VARIABLES) | ||
for (let i = 0; i < variableNames.length; i += 1) { | ||
const variableName = variableNames[i] | ||
code = code.replace( | ||
RegExp(variableName, 'g'), | ||
CAPTCHA_VARIABLES[variableName], | ||
) | ||
} | ||
|
||
const handlerScript = iframeDocument.createElement('script') | ||
handlerScript.innerHTML = code | ||
iframeDocument.head.appendChild(handlerScript) | ||
} | ||
|
||
export function generateWidgetId() { | ||
return `widget_${Date.now()}` | ||
} | ||
|
||
export class Captcha extends Component<CaptchaProps> { | ||
static contextType = ThemeFrameContext | ||
|
||
declare context: ContextType<typeof ThemeFrameContext> | ||
|
||
_initialized: boolean | ||
|
||
_widgetId: string | ||
|
||
constructor(props: CaptchaProps) { | ||
super(props) | ||
|
||
this._widgetId = generateWidgetId() | ||
this._initialized = false | ||
} | ||
|
||
componentDidMount() { | ||
this.initializeCaptchaInFrame() | ||
} | ||
|
||
componentWillUnmount() { | ||
if (this._initialized) { | ||
window.removeEventListener('message', this.onMessage) | ||
} | ||
} | ||
|
||
onMessage = (event: MessageEvent) => { | ||
if (event?.data?.startsWith(this._widgetId)) { | ||
const message = event.data.slice(this._widgetId.length) | ||
const data = JSON.parse(message) | ||
switch (data.type) { | ||
case CAPTCHA_VARIABLES.CAPTCHA_SUCCESS: | ||
this.props.onSuccess?.() | ||
break | ||
|
||
case CAPTCHA_VARIABLES.CAPTCHA_ERROR: | ||
this.props.onError?.() | ||
break | ||
|
||
case CAPTCHA_VARIABLES.CAPTCHA_EXPIRED: | ||
this.props.onExpired?.() | ||
break | ||
|
||
default: | ||
break | ||
} | ||
} | ||
} | ||
|
||
initializeCaptchaInFrame() { | ||
const iframeDocument = this.context | ||
if (iframeDocument === null || this._initialized) { | ||
return | ||
} | ||
|
||
loadCaptchaScript(iframeDocument) | ||
loadCaptchaWidgetHandlers(iframeDocument, this._widgetId) | ||
window.addEventListener('message', this.onMessage, false) | ||
|
||
this._initialized = true | ||
} | ||
|
||
render() { | ||
return ( | ||
<div | ||
id={this._widgetId} | ||
data-sitekey={this.props.siteKey} | ||
data-theme={this.props.theme} | ||
data-size={this.props.size} | ||
/> | ||
) | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
apps/storefront/src/components/captcha/frameCaptchaCode.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* eslint-disable */ | ||
|
||
window.INITIALIZE_CAPTCHA_PREFIX = function () { | ||
if (window.grecaptcha === undefined) { | ||
return; | ||
} | ||
|
||
if (window.WIDGET_TIMER_PREFIX !== null) { | ||
window.clearInterval(window.WIDGET_TIMER_PREFIX); | ||
window.WIDGET_TIMER_PREFIX = null; | ||
} | ||
|
||
var sendMessage = function (eventType, payload) { | ||
window.parent.postMessage( | ||
"PREFIX" + | ||
JSON.stringify({ | ||
type: eventType, | ||
payload: payload, | ||
}), | ||
"PARENT_ORIGIN" | ||
); | ||
}; | ||
|
||
window.grecaptcha.render( | ||
"PREFIX", | ||
{ | ||
callback: function (token) { | ||
sendMessage("CAPTCHA_SUCCESS", token); | ||
}, | ||
"error-callback": function () { | ||
sendMessage("CAPTCHA_ERROR", null); | ||
}, | ||
"expired-callback": function () { | ||
sendMessage("CAPTCHA_EXPIRED", null); | ||
}, | ||
}, | ||
true | ||
); | ||
}; | ||
|
||
window.WIDGET_TIMER_PREFIX = window.setInterval( | ||
window.INITIALIZE_CAPTCHA_PREFIX, | ||
250 | ||
); |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.