├── .babelrc ├── .gitattributes ├── .github ├── CODEOWNERS └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── app │ ├── App.jsx │ ├── components │ │ ├── CustomFrame.jsx │ │ └── index.js │ └── examples │ │ ├── AsyncExample.jsx │ │ ├── ClassExample.jsx │ │ ├── FrameExample.jsx │ │ └── index.js ├── frame.html ├── index.html ├── index.js └── webpack.config.mjs ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── index.js └── utils.js ├── tests ├── hcaptcha.mock.js ├── hcaptcha.spec.js └── utils.test.js └── types └── index.d.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | "@babel/preset-env", 5 | "@babel/preset-react" 6 | ], 7 | "plugins": [ 8 | "add-module-exports", 9 | ["@babel/plugin-transform-runtime", { 10 | "regenerator": true 11 | }] 12 | ], 13 | "env": { 14 | "esm": { 15 | "sourceType": "unambiguous", 16 | "presets": [ 17 | [ 18 | "@babel/preset-env", 19 | { 20 | "loose": true, 21 | "modules": false 22 | } 23 | ], 24 | "@babel/preset-react" 25 | ], 26 | "plugins": [["@babel/plugin-transform-runtime", { 27 | "regenerator": true 28 | }]] 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence,they will 3 | # be requested for review when someone opens a pull request. 4 | * @hCaptcha/react-reviewers 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Build & Test 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout code 9 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 10 | - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 11 | with: 12 | node-version: '18.x' 13 | - run: npm ci 14 | - run: npm run transpile 15 | - run: npm run test 16 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 20 * * 3' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 40 | with: 41 | node-version: '18.x' 42 | 43 | # If this run was triggered by a pull request event, then checkout 44 | # the head of the pull request instead of the merge commit. 45 | - run: git checkout HEAD^2 46 | if: ${{ github.event_name == 'pull_request' }} 47 | 48 | # Initializes the CodeQL tools for scanning. 49 | - name: Initialize CodeQL 50 | uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.13 51 | with: 52 | languages: ${{ matrix.language }} 53 | # If you wish to specify custom queries, you can do so here or in a config file. 54 | # By default, queries listed here will override any specified in a config file. 55 | # Prefix the list here with "+" to use these queries and those in the config file. 56 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.13 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 https://git.io/JvXDl 65 | 66 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 67 | # and modify them (or add more) to build your code if your project 68 | # uses a compiled language 69 | 70 | #- run: | 71 | # make bootstrap 72 | # make release 73 | 74 | - name: Perform CodeQL Analysis 75 | uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.13 76 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish NPM 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: [ created ] 6 | jobs: 7 | build: 8 | name: Build & Test & Publish 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 14 | with: 15 | node-version: '18.x' 16 | registry-url: 'https://registry.npmjs.org' 17 | - run: npm ci 18 | - run: npm run transpile 19 | - run: npm run test 20 | - run: npm publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | *.code-workspace 4 | *.sublime-workspace 5 | .vscode/ 6 | .idea/ 7 | 8 | .DS_Store 9 | 10 | dist 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 hCaptcha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React hCaptcha Component Library 2 | 3 | 4 | hCaptcha Component Library for ReactJS. 5 | 6 | [hCaptcha](https://www.hcaptcha.com) is a drop-replacement for reCAPTCHA that protects user privacy. 7 | 8 | Sign up at [hCaptcha](https://www.hcaptcha.com) to get your sitekey today. **You need a sitekey to use this library.** 9 | 10 | *Also compatible with Preact.* 11 | 12 | 1. [Installation](#installation) 13 | 2. [References](#references) 14 | 3. [Debugging](#debugging) 15 | 4. [Contributing](#contributing) 16 | 17 | ## Installation 18 | 19 | You can install this library via npm with: 20 | 21 | ``` 22 | npm install @hcaptcha/react-hcaptcha --save 23 | ``` 24 | 25 | ### Implementation 26 | The two requirements for usage are the `sitekey` [prop](#props) and a `parent component` such as a `
`. The component will automatically include and load the 27 | hCaptcha API library and append it to the parent component. This is designed for ease of use with the hCaptcha API! 28 | 29 | #### Standard 30 | 31 | ```js 32 | import HCaptcha from '@hcaptcha/react-hcaptcha'; 33 | 34 | 35 | handleVerificationSuccess(token, ekey)} 38 | /> 39 | 40 | ``` 41 | 42 | #### Programmatic 43 | In the event you want to call the hCaptcha client API directly, you can do so by using the hook `useRef` and waiting for `onLoad` to be called. By waiting for `onLoad` the hCaptcha API will be ready and the hCaptcha client will have been setup. See the following example: 44 | 45 | ```js 46 | import { useEffect, useRef, useState } from "react"; 47 | import HCaptcha from "@hcaptcha/react-hcaptcha"; 48 | 49 | export default function Form() { 50 | const [token, setToken] = useState(null); 51 | const captchaRef = useRef(null); 52 | 53 | const onLoad = () => { 54 | // this reaches out to the hCaptcha JS API and runs the 55 | // execute function on it. you can use other functions as 56 | // documented here: 57 | // https://docs.hcaptcha.com/configuration#jsapi 58 | captchaRef.current.execute(); 59 | }; 60 | 61 | useEffect(() => { 62 | 63 | if (token) 64 | console.log(`hCaptcha Token: ${token}`); 65 | 66 | }, [token]); 67 | 68 | return ( 69 | 70 | 76 | 77 | ); 78 | } 79 | ``` 80 | 81 | **Typescript Support** \ 82 | If you want to reassign the component name, you could consider making a util that imports the component, then re-exports it as a default. 83 | 84 | ```ts 85 | // utils/captcha.ts 86 | import HCaptcha from '@hcaptcha/react-hcaptcha'; 87 | export default HCaptcha; 88 | 89 | // MyFormComponent.tsx 90 | import { default as RenamedCaptcha } from '../utils/captcha'; 91 | 92 | 93 | 94 | ``` 95 | 96 | #### Advanced 97 | 98 | In most real-world implementations, you'll probably be using a form library such as [Formik](https://github.com/jaredpalmer/formik) or [React Hook Form](https://github.com/react-hook-form/react-hook-form). 99 | 100 | In these instances, you'll most likely want to use `ref` to handle the callbacks as well as handle field-level validation of a `captcha` field. For an example of this, you can view this [CodeSandbox](https://codesandbox.io/s/react-hcaptchaform-example-forked-ngxge?file=/src/Form.jsx). This `ref` will point to an instance of the [hCaptcha API](https://docs.hcaptcha.com/configuration#jsapi) where can you interact directly with it. 101 | 102 | #### Passing in fields like `rqdata` to `execute()` 103 | 104 | Passing an object into the `execute(yourObj)` call will send it through to the underlying JS API. This enables support for Enterprise features like `rqdata`. A simple example is below: 105 | 106 | ``` 107 | const {sitekey, rqdata} = props; 108 | const captchaRef = React.useRef(null); 109 | 110 | const onLoad = () => { 111 | const executePayload = {}; 112 | if (rqdata) { 113 | executePayload['rqdata'] = rqdata; 114 | } 115 | captchaRef.current?.execute(executePayload); 116 | }; 117 | 118 | return ; 119 | ``` 120 | 121 | ### References 122 | 123 | #### Props 124 | 125 | |Name|Values/Type|Required|Default|Description| 126 | |---|---|---|---|---| 127 | |`sitekey`|String|**Yes**|`-`|This is your sitekey, this allows you to load captcha. If you need a sitekey, please visit [hCaptcha](https://www.hcaptcha.com), and sign up to get your sitekey.| 128 | |`size`|String (normal, compact, invisible)|No|`normal`|This specifies the "size" of the component. hCaptcha allows you to decide how big the component will appear on render, this always defaults to normal.| 129 | |`theme`|String (light, dark, contrast) or Object|No|`light`|hCaptcha supports both a light and dark theme. Defaults to light. Takes Object if custom theme is used.| 130 | |`tabindex`|Integer|No|`0`|Set the tabindex of the widget and popup. When appropriate, this can make navigation of your site more intuitive.| 131 | |`languageOverride`|String (ISO 639-2 code)|No|`auto`|hCaptcha auto-detects language via the user's browser. This overrides that to set a default UI language. See [language codes](https://hcaptcha.com/docs/languages).| 132 | |`reCaptchaCompat`|Boolean|No|`true`|Disable drop-in replacement for reCAPTCHA with `false` to prevent hCaptcha from injecting into `window.grecaptcha`.| 133 | |`id`|String|No|`random id`|Manually set the ID of the hCaptcha component. Make sure each hCaptcha component generated on a single page has its own unique ID when using this prop.| 134 | |`apihost`|String|No|`-`|See enterprise docs.| 135 | |`assethost`|String|No|`-`|See enterprise docs.| 136 | |`endpoint`|String|No|`-`|See enterprise docs.| 137 | |`host`|String|No|`-`|See enterprise docs.| 138 | |`imghost`|String|No|`-`|See enterprise docs.| 139 | |`reportapi`|String|No|`-`|See enterprise docs.| 140 | |`sentry`|String|No|`-`|See enterprise docs.| 141 | |`secureApi`|Boolean|No|`-`|See enterprise docs.| 142 | |`scriptSource`|String|No|`-`|See enterprise docs.| 143 | | `cleanup` | Boolean | No | `true` | Remove script tag after setup.| 144 | |`custom`|Boolean|No|`-`|Custom theme: see enterprise docs.| 145 | |`loadAsync`|Boolean|No|`true`|Set if the script should be loaded asynchronously.| 146 | |`scriptLocation`|Element|No|`document.head`| Location of where to append the script tag. Make sure to add it to an area that will persist to prevent loading multiple times in the same document view. Note: If `null` is provided, the `document.head` will be used.| 147 | 148 | #### Events 149 | 150 | |Event|Params|Description| 151 | |---|---|---| 152 | |`onError`|`err`|When an error occurs. Component will reset immediately after an error.| 153 | |`onVerify`|`token, eKey`|When challenge is completed. The response `token` and an `eKey` (session id) are passed along.| 154 | |`onExpire`|-|When the current token expires.| 155 | |`onLoad`|-|When the hCaptcha API loads.| 156 | |`onOpen`|-|When the user display of a challenge starts.| 157 | |`onClose`|-|When the user dismisses a challenge.| 158 | |`onChalExpired`|-|When the user display of a challenge times out with no answer.| 159 | 160 | #### Methods 161 | 162 | |Method|Description| 163 | |---|---| 164 | |`execute()`|Programmatically trigger a challenge request. Additionally, this method can be run asynchronously and returns a promise with the `token` and `eKey` when the challenge is completed.| 165 | |`getRespKey()`|Get the current challenge reference ID| 166 | |`getResponse()`|Get the current challenge response token from completed challenge| 167 | |`resetCaptcha()`|Reset the current challenge| 168 | |`setData()`|See enterprise docs.| 169 | 170 | 171 | > **Note** \ 172 | > Make sure to reset the hCaptcha state when you submit your form by calling the method `.resetCaptcha` on your hCaptcha React Component! Passcodes are one-time use, so if your user submits the same passcode twice then it will be rejected by the server the second time. 173 | 174 | Please refer to the demo for examples of basic usage and an invisible hCaptcha. 175 | 176 | Alternatively, see [this sandbox code](https://codesandbox.io/s/react-hcaptchaform-example-invisible-f7ekt) for a quick form example of invisible hCaptcha on a form submit button. 177 | 178 | Please note that "invisible" simply means that no hCaptcha button will be rendered. Whether a challenge shows up will depend on the sitekey difficulty level. Note to hCaptcha Enterprise ([BotStop](https://www.botstop.com)) users: select "Passive" or "99.9% Passive" modes to get this No-CAPTCHA behavior. 179 | 180 | 181 | 182 | 183 | ### Debugging 184 | 185 | 1. #### Invalid hCaptcha Id: 186 | This issue generally occurs when the component is re-rendered causing the current `useRef` to become stale, meaning the `ref` being used is no longer available in the DOM. 187 | 188 | 189 | 2. #### Make sure you don't double-import the api.js script 190 | Importing the JS SDK twice can cause unpredictable behavior, so don't do a direct import separately if you are using react-hcaptcha. 191 | 192 | 3. #### Make sure you are using `reCaptchaCompat=false` if you have the reCAPTCHA JS loaded on the same page. 193 | The hCaptcha "compatibility mode" will interfere with reCAPTCHA, as it adds properties with the same name. If for any reason you are running both hCaptcha and reCAPTCHA in parallel (we recommend only running hCaptcha) then please disable our compatibility mode. 194 | 195 | 196 | ### Sentry 197 | 198 | If the `sentry` flag is enabled, the upstream `hcaptcha-loader` package expects the Sentry SDK, version 8.x or later. 199 | 200 | If you have an older `@sentry/browser` client version on your site, it may take precedence over the bundled version. In this case you may see a console error like "g.setPropagationContext is not a function" due to the hcaptcha-loader trying to call methods only available on newer Sentry clients. 201 | 202 | To resolve this, update the version of the Sentry client you are including on your site to 8.x or higher. 203 | 204 | You can avoid this issue by setting the `sentry` prop to `false`. 205 | 206 | 207 | --- 208 | ### Contributing 209 | 210 | #### Scripts 211 | 212 | * `npm run start` - will start the demo app with hot reload 213 | * `npm run test` - will test the library: unit tests 214 | * `npm run build` - will build the production version 215 | 216 | 217 | #### Publishing 218 | 219 | To publish a new version, follow the next steps: 220 | 1. Bump the version in `package.json` 221 | 2. Create a [Github Release](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/managing-releases-in-a-repository#creating-a-release) with version from step 1 **without** a prefix such as `v` (e.g. `1.0.3`) 222 | * `publish` workflow will be triggered which will: build, test and deploy the package to the [npm @hcaptcha/react-hcaptcha](https://www.npmjs.com/package/@hcaptcha/react-hcaptcha). 223 | 224 | 225 | #### Running locally for development 226 | 227 | Please see: [Local Development Notes](https://docs.hcaptcha.com/#localdev). 228 | 229 | Summary: 230 | 231 | ``` 232 | sudo echo "127.0.0.1 fakelocal.com" >> /private/etc/hosts 233 | npm start -- --disable-host-check 234 | ``` 235 | 236 | open [http://fakelocal.com:9000](http://fakelocal.com:9000) to start the example. 237 | -------------------------------------------------------------------------------- /demo/app/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | import { AsyncExample, ClassExample, FrameExample } from './examples'; 4 | import { CustomFrame } from './components'; 5 | 6 | 7 | export function App() { 8 | const [frame, setFrame] = useState(null); 9 | const [frameDocument, setFrameDocument] = useState(frame); 10 | 11 | useEffect(() => { 12 | const onLoad = () => { 13 | const frame = document.getElementById('example-frame'); 14 | setFrame(frame); 15 | setFrameDocument(frame.contentWindow.document); 16 | }; 17 | 18 | if (document.readyState === 'complete') { 19 | onLoad(); 20 | } else { 21 | window.addEventListener('load', onLoad, false); 22 | return () => window.removeEventListener('load', onLoad); 23 | } 24 | }, [setFrame]); 25 | 26 | return ( 27 |
28 |
29 |

HCaptcha React Demo

30 |

31 | Set your sitekey and onVerify callback as props, and drop into your form. From here, we'll take care of the rest. 32 |

33 |
34 |
35 |

Async Example

36 | 37 |
38 |
39 |

Class Example

40 | 41 |
42 |
43 |

Frame Example

44 | 45 | 46 | 47 |
48 |
49 | ) 50 | } -------------------------------------------------------------------------------- /demo/app/components/CustomFrame.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | export function CustomFrame({ frame, children }) { 5 | const [frameDocument, setFrameDocument] = useState(frame); 6 | 7 | useEffect(() => { 8 | if (frame){ 9 | setFrameDocument(frame.contentWindow.document); 10 | } 11 | }, [frame, setFrameDocument]); 12 | 13 | return frameDocument && createPortal(children, frameDocument.body); 14 | } -------------------------------------------------------------------------------- /demo/app/components/index.js: -------------------------------------------------------------------------------- 1 | export { CustomFrame } from './CustomFrame'; -------------------------------------------------------------------------------- /demo/app/examples/AsyncExample.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react'; 2 | 3 | import HCaptcha from '../../../src/index.js'; 4 | 5 | export function AsyncExample() { 6 | const captchaRef = useRef(); 7 | 8 | const executeCaptcha = async () => { 9 | try { 10 | const res = await captchaRef.current.execute({ 11 | async: true 12 | }); 13 | console.log("Verified asynchronously: ", res); 14 | 15 | } catch (error) { 16 | console.log(error); 17 | } 18 | }; 19 | 20 | useEffect(() => { 21 | executeCaptcha(); 22 | }, []); 23 | 24 | const getResponse = () => { 25 | try { 26 | const res = captchaRef.current.getResponse(); 27 | console.log("Response: ", res); 28 | 29 | } catch (error) { 30 | console.log(error); 31 | } 32 | }; 33 | 34 | const getRespKey = () => { 35 | try { 36 | const res = captchaRef.current.getRespKey(); 37 | console.log("Response Key: ", res); 38 | 39 | } catch (error) { 40 | console.log(error); 41 | } 42 | }; 43 | 44 | const handleOpen = () => { 45 | console.log("HCaptcha [onOpen]: The user display of a challenge starts."); 46 | }; 47 | 48 | const handleClose = () => { 49 | console.log("HCaptcha [onClose]: The user dismisses a challenge."); 50 | }; 51 | 52 | const handleError = error => { 53 | console.log("HCaptcha [onError]:", error); 54 | }; 55 | 56 | const handleChallengeExpired = () => { 57 | console.log("HCaptcha [onChalExpired]: The user display of a challenge times out with no answer."); 58 | }; 59 | 60 | return ( 61 |
62 | undefined} 67 | onOpen={handleOpen} 68 | onClose={handleClose} 69 | onError={handleError} 70 | onChalExpired={handleChallengeExpired} 71 | sentry={false} 72 | /> 73 |
74 | 75 | 76 | 77 |
78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /demo/app/examples/ClassExample.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import HCaptcha from '../../../src/index.js'; 4 | 5 | 6 | export class ClassExample extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | isVerified: false, 13 | async: false, 14 | theme: 'light', 15 | }; 16 | this.captcha = React.createRef(); 17 | 18 | this.handleChange = this.handleChange.bind(this); 19 | this.handleReset = this.handleReset.bind(this); 20 | this.onVerifyCaptcha = this.onVerifyCaptcha.bind(this); 21 | this.handleOpen = this.handleOpen.bind(this); 22 | this.handleClose = this.handleClose.bind(this); 23 | this.handleError = this.handleError.bind(this); 24 | this.handleChallengeExpired = this.handleChallengeExpired.bind(this); 25 | this.handleThemeChange = this.handleThemeChange.bind(this); 26 | // Leave languageOverride unset or null for browser autodetection. 27 | // To force a language, use the code: https://hcaptcha.com/docs/languages 28 | this.languageOverride = null; // "fr"; 29 | } 30 | 31 | handleChange(event) { 32 | this.setState({ isVerified: true }); 33 | } 34 | 35 | onVerifyCaptcha(token) { 36 | console.log("Verified: " + token); 37 | this.setState({ isVerified: true }) 38 | } 39 | 40 | handleSubmit(event) { 41 | event.preventDefault() 42 | this.child.execute() 43 | } 44 | 45 | handleReset(event) { 46 | event.preventDefault() 47 | this.captcha.current.resetCaptcha() 48 | this.setState({ isVerified: false }) 49 | } 50 | 51 | handleOpen() { 52 | console.log("HCaptcha [onOpen]: The user display of a challenge starts."); 53 | } 54 | 55 | handleClose() { 56 | console.log("HCaptcha [onClose]: The user dismisses a challenge."); 57 | } 58 | 59 | handleError(error) { 60 | console.log("HCaptcha [onError]:", error); 61 | }; 62 | 63 | handleChallengeExpired() { 64 | console.log("HCaptcha [onChalExpired]: The user display of a challenge times out with no answer."); 65 | } 66 | 67 | handleThemeChange() { 68 | this.setState(state => ({ 69 | theme: state.theme === 'light' ? 'dark' : 'light', 70 | })); 71 | } 72 | 73 | render() { 74 | const { isVerified, theme } = this.state; 75 | 76 | return ( 77 |
78 | 90 |
91 | 92 | {isVerified && ( 93 | 94 | )} 95 |
96 | {isVerified && ( 97 |

Open your console to see the Verified response.

98 | )} 99 |
100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /demo/app/examples/FrameExample.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | 3 | import HCaptcha from '../../../src/index.js'; 4 | 5 | export function FrameExample({ document }) { 6 | const captchaRef = useRef(); 7 | 8 | const handleOpen = () => { 9 | console.log("HCaptcha [onOpen]: The user display of a challenge starts."); 10 | }; 11 | 12 | const handleClose = () => { 13 | console.log("HCaptcha [onClose]: The user dismisses a challenge."); 14 | }; 15 | 16 | const handleError = error => { 17 | console.log("HCaptcha [onError]:", error); 18 | }; 19 | 20 | const handleChallengeExpired = () => { 21 | console.log("HCaptcha [onChalExpired]: The user display of a challenge times out with no answer."); 22 | }; 23 | 24 | const handleVerified = (token) => { 25 | console.log("Verified: " + token); 26 | }; 27 | 28 | return ( 29 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /demo/app/examples/index.js: -------------------------------------------------------------------------------- 1 | export { AsyncExample } from './AsyncExample'; 2 | export { ClassExample } from './ClassExample'; 3 | export { FrameExample } from './FrameExample'; -------------------------------------------------------------------------------- /demo/frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hCaptcha Example iFrame 5 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hCaptcha React Demo 5 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import { App } from './app/App'; 5 | 6 | 7 | render( 8 | , 9 | document.getElementById('app') 10 | ); 11 | -------------------------------------------------------------------------------- /demo/webpack.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | import webpack from 'webpack'; 5 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | 10 | const DIST_DIR = resolve(__dirname, 'dist'); 11 | 12 | export default { 13 | mode: 'development', 14 | 15 | entry: { 16 | demo: resolve(__dirname, 'index.js'), 17 | }, 18 | 19 | output: { 20 | path: DIST_DIR, 21 | filename: '[name].js' 22 | }, 23 | 24 | resolve: { 25 | extensions: ['.json', '.js', '.jsx', '.ts', '.tsx'] 26 | }, 27 | 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.(js|jsx)$/, 32 | exclude: /node_modules/, 33 | use: ['babel-loader'] 34 | } 35 | ] 36 | }, 37 | 38 | plugins: [ 39 | new HtmlWebpackPlugin({ 40 | template: resolve(__dirname, 'index.html'), 41 | inject: true 42 | }), 43 | new HtmlWebpackPlugin({ 44 | filename: 'frame.html', 45 | template: resolve(__dirname, 'frame.html'), 46 | inject: false 47 | }) 48 | ], 49 | 50 | devServer: { 51 | port: 9000, 52 | headers: { 53 | 'Access-Control-Allow-Origin': '*', 54 | 'Access-Control-Allow-Headers': '*', 55 | 'Access-Control-Allow-Methods': '*', 56 | } 57 | } 58 | }; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // For a detailed explanation regarding each configuration property, visit: 3 | // https://jestjs.io/docs/en/configuration.html 4 | 5 | module.exports = { 6 | // Automatically clear mock calls and instances between every test 7 | clearMocks: true, 8 | 9 | // The glob patterns Jest uses to detect test files 10 | testMatch: ["**/?(*.)+(spec|test).js?(x)"], 11 | 12 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 13 | testPathIgnorePatterns: ["/node_modules/", "webpack.config.test.js"], 14 | 15 | testEnvironment: 'jsdom' 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hcaptcha/react-hcaptcha", 3 | "version": "1.12.0", 4 | "types": "types/index.d.ts", 5 | "main": "dist/index.js", 6 | "module": "dist/esm/index.js", 7 | "files": [ 8 | "src", 9 | "dist", 10 | "types" 11 | ], 12 | "description": "A React library for hCaptcha", 13 | "scripts": { 14 | "start": "webpack serve -c ./demo/webpack.config.mjs", 15 | "test": "jest", 16 | "watch": "babel src -d dist --copy-files --watch", 17 | "transpile": "babel src -d dist --copy-files", 18 | "prebuild": "rimraf dist", 19 | "build": "npm run transpile && npm run build:esm", 20 | "build:esm": "cross-env BABEL_ENV=esm babel src -d dist/esm --copy-files", 21 | "prepublishOnly": "npm run build" 22 | }, 23 | "peerDependencies": { 24 | "react": ">= 16.3.0", 25 | "react-dom": ">= 16.3.0" 26 | }, 27 | "keywords": [ 28 | "hcaptcha", 29 | "hcaptcha-react", 30 | "react", 31 | "captcha" 32 | ], 33 | "author": "hCaptcha team and contributors", 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/hCaptcha/react-hcaptcha.git" 37 | }, 38 | "license": "MIT", 39 | "devDependencies": { 40 | "@babel/cli": "^7.12.1", 41 | "@babel/core": "^7.12.10", 42 | "@babel/plugin-transform-runtime": "^7.14.5", 43 | "@babel/preset-env": "^7.12.11", 44 | "@babel/preset-react": "^7.12.10", 45 | "@jest/globals": "^29.5.0", 46 | "@types/react": "^16.0.0", 47 | "babel-loader": "^8.2.2", 48 | "babel-plugin-add-module-exports": "^1.0.4", 49 | "cross-env": "^7.0.3", 50 | "html-webpack-plugin": "^5.5.0", 51 | "jest": "^29.5.0", 52 | "jest-environment-jsdom": "^29.5.0", 53 | "react": "^16.14.0", 54 | "react-dom": "^16.14.0", 55 | "rimraf": "^3.0.2", 56 | "wait-for-expect": "^3.0.2", 57 | "webpack": "^5.76.3", 58 | "webpack-cli": "^5.0.1", 59 | "webpack-dev-server": "^5.2.1" 60 | }, 61 | "dependencies": { 62 | "@babel/runtime": "^7.17.9", 63 | "@hcaptcha/loader": "^2.0.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { hCaptchaLoader } from '@hcaptcha/loader'; 3 | 4 | import { getFrame, getMountElement } from './utils.js'; 5 | 6 | 7 | class HCaptcha extends React.Component { 8 | constructor (props) { 9 | super(props); 10 | 11 | /** 12 | * Internal reference to track hCaptcha API 13 | * 14 | * Required as window is relative to initialization in application 15 | * not where the script and iFrames have been loaded. 16 | */ 17 | this._hcaptcha = undefined; 18 | 19 | // API Methods 20 | this.renderCaptcha = this.renderCaptcha.bind(this); 21 | this.resetCaptcha = this.resetCaptcha.bind(this); 22 | this.removeCaptcha = this.removeCaptcha.bind(this); 23 | this.isReady = this.isReady.bind(this); 24 | this._onReady = null; 25 | 26 | // Event Handlers 27 | this.loadCaptcha = this.loadCaptcha.bind(this); 28 | this.handleOnLoad = this.handleOnLoad.bind(this); 29 | this.handleSubmit = this.handleSubmit.bind(this); 30 | this.handleExpire = this.handleExpire.bind(this); 31 | this.handleError = this.handleError.bind(this); 32 | this.handleOpen = this.handleOpen.bind(this); 33 | this.handleClose = this.handleClose.bind(this); 34 | this.handleChallengeExpired = this.handleChallengeExpired.bind(this); 35 | 36 | this.ref = React.createRef(); 37 | this.apiScriptRequested = false; 38 | this.sentryHub = null; 39 | this.captchaId = ''; 40 | 41 | this.state = { 42 | isApiReady: false, 43 | isRemoved: false, 44 | elementId: props.id, 45 | } 46 | } 47 | 48 | componentDidMount () { // Once captcha is mounted intialize hCaptcha - hCaptcha 49 | const element = getMountElement(this.props.scriptLocation); 50 | const frame = getFrame(element); 51 | this._hcaptcha = frame.window.hcaptcha || undefined; 52 | 53 | const isApiReady = typeof this._hcaptcha !== 'undefined'; 54 | 55 | /* 56 | * Check if hCaptcha has already been loaded, 57 | * If Yes, render the captcha 58 | * If No, create script tag and wait to render the captcha 59 | */ 60 | if (isApiReady) { 61 | this.setState( 62 | { 63 | isApiReady: true 64 | }, 65 | () => { 66 | this.renderCaptcha(); 67 | } 68 | ); 69 | 70 | return; 71 | } 72 | 73 | this.loadCaptcha(); 74 | } 75 | 76 | componentWillUnmount() { 77 | const hcaptcha = this._hcaptcha; 78 | const captchaId = this.captchaId; 79 | 80 | if (!this.isReady()) { 81 | return; 82 | } 83 | 84 | // Reset any stored variables / timers when unmounting 85 | hcaptcha.reset(captchaId); 86 | hcaptcha.remove(captchaId); 87 | } 88 | 89 | shouldComponentUpdate(nextProps, nextState) { 90 | // Prevent component re-rendering when these internal state variables are updated 91 | if (this.state.isApiReady !== nextState.isApiReady || this.state.isRemoved !== nextState.isRemoved) { 92 | return false; 93 | } 94 | 95 | return true; 96 | } 97 | 98 | componentDidUpdate(prevProps) { 99 | // Prop Keys that could change 100 | const keys = ['sitekey', 'size', 'theme', 'tabindex', 'languageOverride', 'endpoint']; 101 | // See if any props changed during component update 102 | const match = keys.every( key => prevProps[key] === this.props[key]); 103 | 104 | // If they have changed, remove current captcha and render a new one 105 | if (!match) { 106 | this.removeCaptcha(() => { 107 | this.renderCaptcha(); 108 | }); 109 | } 110 | } 111 | 112 | loadCaptcha() { 113 | if (this.apiScriptRequested) { 114 | return; 115 | } 116 | 117 | const { 118 | apihost, 119 | assethost, 120 | endpoint, 121 | host, 122 | imghost, 123 | languageOverride: hl, 124 | reCaptchaCompat, 125 | reportapi, 126 | sentry, 127 | custom, 128 | loadAsync, 129 | scriptLocation, 130 | scriptSource, 131 | secureApi, 132 | cleanup = true, 133 | } = this.props; 134 | const mountParams = { 135 | render: 'explicit', 136 | apihost, 137 | assethost, 138 | endpoint, 139 | hl, 140 | host, 141 | imghost, 142 | recaptchacompat: reCaptchaCompat === false? 'off' : null, 143 | reportapi, 144 | sentry, 145 | custom, 146 | loadAsync, 147 | scriptLocation, 148 | scriptSource, 149 | secureApi, 150 | cleanup 151 | }; 152 | 153 | hCaptchaLoader(mountParams) 154 | .then(this.handleOnLoad, this.handleError) 155 | .catch(this.handleError); 156 | 157 | this.apiScriptRequested = true; 158 | } 159 | 160 | renderCaptcha(onRender) { 161 | const { onReady } = this.props; 162 | const { isApiReady } = this.state; 163 | const captchaId = this.captchaId; 164 | 165 | // Prevent calling hCaptcha render on two conditions: 166 | // • API is not ready 167 | // • Component has already been mounted 168 | if (!isApiReady || captchaId) return; 169 | 170 | const renderParams = Object.assign({ 171 | "open-callback" : this.handleOpen, 172 | "close-callback" : this.handleClose, 173 | "error-callback" : this.handleError, 174 | "chalexpired-callback": this.handleChallengeExpired, 175 | "expired-callback" : this.handleExpire, 176 | "callback" : this.handleSubmit, 177 | }, this.props, { 178 | hl: this.props.hl || this.props.languageOverride, 179 | languageOverride: undefined 180 | }); 181 | 182 | const hcaptcha = this._hcaptcha; 183 | //Render hCaptcha widget and provide necessary callbacks - hCaptcha 184 | const id = hcaptcha.render(this.ref.current, renderParams); 185 | this.captchaId = id; 186 | 187 | this.setState({ isRemoved: false }, () => { 188 | onRender && onRender(); 189 | onReady && onReady(); 190 | this._onReady && this._onReady(id); 191 | }); 192 | } 193 | 194 | resetCaptcha() { 195 | const hcaptcha = this._hcaptcha; 196 | const captchaId = this.captchaId; 197 | 198 | if (!this.isReady()) { 199 | return; 200 | } 201 | // Reset captcha state, removes stored token and unticks checkbox 202 | hcaptcha.reset(captchaId) 203 | } 204 | 205 | removeCaptcha(callback) { 206 | const hcaptcha = this._hcaptcha; 207 | const captchaId = this.captchaId; 208 | 209 | if (!this.isReady()) { 210 | return; 211 | } 212 | 213 | this.setState({ isRemoved: true }, () => { 214 | this.captchaId = ''; 215 | 216 | hcaptcha.remove(captchaId); 217 | callback && callback() 218 | }); 219 | } 220 | 221 | handleOnLoad () { 222 | this.setState({ isApiReady: true }, () => { 223 | const element = getMountElement(this.props.scriptLocation); 224 | const frame = getFrame(element); 225 | 226 | this._hcaptcha = frame.window.hcaptcha; 227 | 228 | 229 | // render captcha and wait for captcha id 230 | this.renderCaptcha(() => { 231 | // trigger onLoad if it exists 232 | 233 | const { onLoad } = this.props; 234 | if (onLoad) onLoad(); 235 | }); 236 | }); 237 | } 238 | 239 | handleSubmit (event) { 240 | const { onVerify } = this.props; 241 | const { isRemoved } = this.state; 242 | const hcaptcha = this._hcaptcha; 243 | const captchaId = this.captchaId; 244 | 245 | if (typeof hcaptcha === 'undefined' || isRemoved) return 246 | 247 | const token = hcaptcha.getResponse(captchaId) //Get response token from hCaptcha widget 248 | const ekey = hcaptcha.getRespKey(captchaId) //Get current challenge session id from hCaptcha widget 249 | if (onVerify) onVerify(token, ekey) //Dispatch event to verify user response 250 | } 251 | 252 | handleExpire () { 253 | const { onExpire } = this.props; 254 | const hcaptcha = this._hcaptcha; 255 | const captchaId = this.captchaId; 256 | 257 | if (!this.isReady()) { 258 | return; 259 | } 260 | 261 | hcaptcha.reset(captchaId) // If hCaptcha runs into error, reset captcha - hCaptcha 262 | 263 | if (onExpire) onExpire(); 264 | } 265 | 266 | handleError (event) { 267 | const { onError } = this.props; 268 | const hcaptcha = this._hcaptcha; 269 | const captchaId = this.captchaId; 270 | 271 | if (this.isReady()) { 272 | // If hCaptcha runs into error, reset captcha - hCaptcha 273 | hcaptcha.reset(captchaId); 274 | } 275 | 276 | if (onError) onError(event); 277 | } 278 | 279 | isReady () { 280 | const { isApiReady, isRemoved } = this.state; 281 | 282 | return isApiReady && !isRemoved; 283 | } 284 | 285 | handleOpen () { 286 | if (!this.isReady() || !this.props.onOpen) { 287 | return; 288 | } 289 | 290 | this.props.onOpen(); 291 | } 292 | 293 | handleClose () { 294 | if (!this.isReady() || !this.props.onClose) { 295 | return; 296 | } 297 | 298 | this.props.onClose(); 299 | } 300 | 301 | handleChallengeExpired () { 302 | if (!this.isReady() || !this.props.onChalExpired) { 303 | return; 304 | } 305 | 306 | this.props.onChalExpired(); 307 | } 308 | 309 | execute (opts = null) { 310 | 311 | opts = typeof opts === 'object' ? opts : null; 312 | 313 | try { 314 | const hcaptcha = this._hcaptcha; 315 | const captchaId = this.captchaId; 316 | 317 | if (!this.isReady()) { 318 | const onReady = new Promise((resolve, reject) => { 319 | 320 | this._onReady = (id) => { 321 | try { 322 | const hcaptcha = this._hcaptcha; 323 | 324 | if (opts && opts.async) { 325 | hcaptcha.execute(id, opts).then(resolve).catch(reject); 326 | } else { 327 | resolve(hcaptcha.execute(id, opts)); 328 | } 329 | } catch (e) { 330 | reject(e); 331 | } 332 | }; 333 | }); 334 | 335 | return opts?.async ? onReady : null; 336 | } 337 | 338 | return hcaptcha.execute(captchaId, opts); 339 | } catch (error) { 340 | if (opts && opts.async) { 341 | return Promise.reject(error); 342 | } 343 | return null; 344 | } 345 | } 346 | 347 | close() { 348 | const hcaptcha = this._hcaptcha; 349 | const captchaId = this.captchaId; 350 | 351 | if (!this.isReady()) { 352 | return; 353 | } 354 | 355 | return hcaptcha.close(captchaId); 356 | } 357 | 358 | setData (data) { 359 | const hcaptcha = this._hcaptcha; 360 | const captchaId = this.captchaId; 361 | 362 | if (!this.isReady()) { 363 | return; 364 | } 365 | 366 | if (data && typeof data !== "object") { 367 | data = null; 368 | } 369 | 370 | hcaptcha.setData(captchaId, data); 371 | } 372 | 373 | getResponse() { 374 | const hcaptcha = this._hcaptcha; 375 | return hcaptcha.getResponse(this.captchaId); 376 | } 377 | 378 | getRespKey() { 379 | const hcaptcha = this._hcaptcha; 380 | return hcaptcha.getRespKey(this.captchaId) 381 | } 382 | 383 | render () { 384 | const { elementId } = this.state; 385 | return
; 386 | } 387 | } 388 | 389 | export default HCaptcha; 390 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | function getFrame(element) { 2 | const doc = (element && element.ownerDocument) || document; 3 | const win = doc.defaultView || doc.parentWindow || window; 4 | 5 | return { document: doc, window: win }; 6 | } 7 | 8 | function getMountElement(element) { 9 | return element || document.head; 10 | } 11 | 12 | export { 13 | getFrame, 14 | getMountElement 15 | }; 16 | -------------------------------------------------------------------------------- /tests/hcaptcha.mock.js: -------------------------------------------------------------------------------- 1 | export const MOCK_WIDGET_ID = 'mock-widget-id'; 2 | export const MOCK_TOKEN = 'mock-token'; 3 | export const MOCK_EKEY = 'mock-ekey'; 4 | 5 | /*global jest*/ 6 | 7 | export function getMockedHcaptcha() { 8 | return { 9 | // eslint-disable-next-line no-unused-vars 10 | setData: jest.fn((id, data) => {}), 11 | // eslint-disable-next-line no-unused-vars 12 | render: jest.fn((container, opt) => MOCK_WIDGET_ID), 13 | getResponse: jest.fn(() => MOCK_TOKEN), 14 | getRespKey: jest.fn(() => MOCK_EKEY), 15 | reset: jest.fn(), 16 | execute: jest.fn((id, opts) => { 17 | if (opts && opts.async === true) { 18 | return Promise.resolve({ response: MOCK_TOKEN, key: MOCK_EKEY }); 19 | } 20 | }), 21 | remove: jest.fn(), 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /tests/hcaptcha.spec.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import ReactTestUtils, { act } from "react-dom/test-utils"; 4 | import waitForExpect from "wait-for-expect"; 5 | 6 | import { describe, jest, it } from "@jest/globals"; 7 | 8 | import {getMockedHcaptcha, MOCK_EKEY, MOCK_TOKEN, MOCK_WIDGET_ID} from "./hcaptcha.mock"; 9 | 10 | let HCaptcha; 11 | 12 | const TEST_PROPS = { 13 | sitekey: "10000000-ffff-ffff-ffff-000000000001", 14 | theme: "light", 15 | size: "invisible", 16 | tabindex: 0, 17 | sentry: false, 18 | }; 19 | 20 | describe("hCaptcha", () => { 21 | let instance; 22 | let mockFns; 23 | 24 | beforeEach(() => { 25 | jest.isolateModules(() => { 26 | // Use node's `require` because `jest.isolateModules` cannot be async to use it with `await import()` 27 | HCaptcha = require('../src/index'); 28 | }); 29 | 30 | mockFns = { 31 | onChange: jest.fn(), 32 | onVerify: jest.fn(), 33 | onError: jest.fn(), 34 | onExpire: jest.fn(), 35 | onLoad: jest.fn(), 36 | onOpen: jest.fn(), 37 | onClose: jest.fn(), 38 | onChalExpired: jest.fn(), 39 | }; 40 | window.hcaptcha = getMockedHcaptcha(); 41 | instance = ReactTestUtils.renderIntoDocument( 42 | , 57 | ); 58 | }); 59 | 60 | it("renders into a div", () => { 61 | expect(ReactDOM.findDOMNode(instance).nodeName).toBe("DIV"); 62 | }); 63 | 64 | it("has functions", () => { 65 | expect(typeof instance.execute).toBe("function"); 66 | expect(typeof instance.resetCaptcha).toBe("function"); 67 | expect(typeof instance.getResponse).toBe("function"); 68 | expect(typeof instance.getRespKey).toBe("function"); 69 | expect(typeof instance.setData).toBe("function"); 70 | expect(instance.execute).toBeDefined(); 71 | expect(instance.resetCaptcha).toBeDefined(); 72 | expect(instance.getResponse).toBeDefined(); 73 | expect(instance.getRespKey).toBeDefined(); 74 | expect(instance.setData).toBeDefined(); 75 | }); 76 | 77 | it("can execute synchronously without arguments", () => { 78 | expect(window.hcaptcha.execute.mock.calls.length).toBe(0); 79 | instance.execute(); 80 | expect(window.hcaptcha.execute.mock.calls.length).toBe(1); 81 | expect(window.hcaptcha.execute).toBeCalledWith(MOCK_WIDGET_ID, null); 82 | }); 83 | 84 | it("can execute ignoring non-object arguments", () => { 85 | expect(window.hcaptcha.execute.mock.calls.length).toBe(0); 86 | instance.execute("foo"); 87 | expect(window.hcaptcha.execute.mock.calls.length).toBe(1); 88 | expect(window.hcaptcha.execute).toBeCalledWith(MOCK_WIDGET_ID, null); 89 | }); 90 | 91 | it("stores and calls execute after hCaptcha onload is executed", async () => { 92 | jest.spyOn(instance, 'isReady').mockReturnValueOnce(false); 93 | instance.execute(); 94 | expect(window.hcaptcha.execute.mock.calls.length).toBe(0); 95 | await instance._onReady(MOCK_WIDGET_ID); 96 | expect(window.hcaptcha.execute.mock.calls.length).toBe(1); 97 | expect(window.hcaptcha.execute).toBeCalledWith(MOCK_WIDGET_ID, null); 98 | }); 99 | 100 | it("stores the execute command and calls it after hCaptcha onload is executed", async () => { 101 | jest.spyOn(instance, 'isReady').mockReturnValueOnce(false); 102 | 103 | const onLoad = jest.fn(() => { 104 | expect(instance.captchaId).toBe(MOCK_WIDGET_ID); 105 | }); 106 | 107 | instance = ReactTestUtils.renderIntoDocument( 108 | , 113 | ); 114 | expect(window.hcaptcha.execute.mock.calls.length).toBe(0); 115 | 116 | instance.execute(); 117 | instance.handleOnLoad(); 118 | 119 | expect(window.hcaptcha.execute.mock.calls.length).toBe(1); 120 | expect(window.hcaptcha.execute).toBeCalledWith(MOCK_WIDGET_ID, null); 121 | }); 122 | 123 | it("can execute synchronously with async: false", () => { 124 | expect(window.hcaptcha.execute.mock.calls.length).toBe(0); 125 | instance.execute({ async: false }); 126 | expect(window.hcaptcha.execute.mock.calls.length).toBe(1); 127 | expect(window.hcaptcha.execute).toBeCalledWith(MOCK_WIDGET_ID, { async: false }); 128 | }); 129 | 130 | it("can execute asynchronously with async: true", async () => { 131 | expect(window.hcaptcha.execute.mock.calls.length).toBe(0); 132 | await instance.execute({ async: true }); 133 | expect(window.hcaptcha.execute.mock.calls.length).toBe(1); 134 | expect(window.hcaptcha.execute).toBeCalledWith(MOCK_WIDGET_ID, { async: true }); 135 | }); 136 | 137 | it("can asynchronously return token and key", async () => { 138 | const res = await instance.execute({ async: true }); 139 | expect(res).toMatchObject({ 140 | response: MOCK_TOKEN, 141 | key: MOCK_EKEY 142 | }) 143 | }); 144 | 145 | it("can execute synchronously without returning a promise", async () => { 146 | const resWithAsyncFalse = await instance.execute({ async: false }); 147 | const resWithoutParams = await instance.execute(); 148 | 149 | expect(resWithAsyncFalse).toBe(undefined); 150 | expect(resWithoutParams).toEqual(resWithAsyncFalse); 151 | }); 152 | 153 | it("can reset", () => { 154 | expect(window.hcaptcha.reset.mock.calls.length).toBe(0); 155 | instance.resetCaptcha(); 156 | expect(window.hcaptcha.reset.mock.calls.length).toBe(1); 157 | expect(window.hcaptcha.reset.mock.calls[0][0]).toBe(MOCK_WIDGET_ID); 158 | }); 159 | 160 | it("can remove", () => { 161 | expect(window.hcaptcha.remove.mock.calls.length).toBe(0); 162 | instance.removeCaptcha(); 163 | expect(window.hcaptcha.remove.mock.calls.length).toBe(1); 164 | expect(window.hcaptcha.remove.mock.calls[0][0]).toBe(MOCK_WIDGET_ID); 165 | }); 166 | 167 | it("can get Response", () => { 168 | expect(window.hcaptcha.getResponse.mock.calls.length).toBe(0); 169 | const res = instance.getResponse(); 170 | expect(window.hcaptcha.getResponse.mock.calls.length).toBe(1); 171 | expect(window.hcaptcha.getResponse.mock.calls[0][0]).toBe(MOCK_WIDGET_ID); 172 | expect(res).toBe(MOCK_TOKEN); 173 | }); 174 | 175 | it("can get RespKey", () => { 176 | expect(window.hcaptcha.getRespKey.mock.calls.length).toBe(0); 177 | const res = instance.getRespKey(); 178 | expect(window.hcaptcha.getRespKey.mock.calls.length).toBe(1); 179 | expect(window.hcaptcha.getRespKey.mock.calls[0][0]).toBe(MOCK_WIDGET_ID); 180 | expect(res).toBe(MOCK_EKEY); 181 | }); 182 | 183 | it("can set Data", () => { 184 | expect(window.hcaptcha.setData.mock.calls.length).toBe(0); 185 | const dataObj = { data: { nested: 1 } }; 186 | instance.setData(dataObj); 187 | expect(window.hcaptcha.setData.mock.calls.length).toBe(1); 188 | expect(window.hcaptcha.setData.mock.calls[0][0]).toBe(MOCK_WIDGET_ID); 189 | expect(window.hcaptcha.setData.mock.calls[0][1]).toBe(dataObj); 190 | }); 191 | 192 | it("should emit onLoad event if no hCaptcha ID is stored", () => { 193 | instance.captchaId = ''; 194 | expect(mockFns.onLoad.mock.calls.length).toBe(0); 195 | instance.handleOnLoad(); 196 | expect(mockFns.onLoad.mock.calls.length).toBe(1); 197 | }); 198 | 199 | it("should not emit onLoad event if hCapthcha ID is found", () => { 200 | instance.handleOnLoad(); 201 | expect(mockFns.onLoad.mock.calls.length).toBe(0); 202 | }); 203 | 204 | 205 | it("emits verify with token and eKey", () => { 206 | expect(mockFns.onVerify.mock.calls.length).toBe(0); 207 | instance.handleSubmit(); 208 | expect(mockFns.onVerify.mock.calls.length).toBe(1); 209 | expect(mockFns.onVerify.mock.calls[0][0]).toBe(MOCK_TOKEN); 210 | expect(mockFns.onVerify.mock.calls[0][1]).toBe(MOCK_EKEY); 211 | }); 212 | 213 | it("emits error and calls reset", () => { 214 | expect(mockFns.onError.mock.calls.length).toBe(0); 215 | const error = "invalid-input-response"; 216 | instance.handleError(error); 217 | expect(mockFns.onError.mock.calls.length).toBe(1); 218 | expect(mockFns.onError.mock.calls[0][0]).toBe(error); 219 | expect(window.hcaptcha.reset.mock.calls.length).toBe(1); 220 | }); 221 | 222 | it("emits expire and calls reset", () => { 223 | expect(mockFns.onExpire.mock.calls.length).toBe(0); 224 | instance.handleExpire(); 225 | expect(mockFns.onExpire.mock.calls.length).toBe(1); 226 | expect(window.hcaptcha.reset.mock.calls.length).toBe(1); 227 | }); 228 | 229 | 230 | it("el renders after api loads and a widget id is set", () => { 231 | expect(instance.captchaId).toBe(MOCK_WIDGET_ID); 232 | expect(window.hcaptcha.render.mock.calls.length).toBe(1); 233 | expect(window.hcaptcha.render.mock.calls[0][1]).toMatchObject({ 234 | sitekey: TEST_PROPS.sitekey, 235 | theme: TEST_PROPS.theme, 236 | size: TEST_PROPS.size, 237 | tabindex: TEST_PROPS.tabindex 238 | }); 239 | }); 240 | 241 | it("should set id if id prop is passed", () => { 242 | instance = ReactTestUtils.renderIntoDocument( 243 | , 248 | ); 249 | const node = ReactDOM.findDOMNode(instance); 250 | expect(node.getAttribute("id")).toBe("test-id-1"); 251 | }); 252 | 253 | it("should not set id if no id prop is passed", () => { 254 | process.env.NODE_ENV = "development"; 255 | instance = ReactTestUtils.renderIntoDocument( 256 | , 260 | ); 261 | const node = ReactDOM.findDOMNode(instance); 262 | expect(node.getAttribute("id")).toBe(null); 263 | }); 264 | 265 | it("should not set id if no id prop is passed", (done) => { 266 | 267 | const onReady = jest.fn(() => { 268 | expect(instance.captchaId).toBe(MOCK_WIDGET_ID); 269 | done(); 270 | }); 271 | 272 | instance = ReactTestUtils.renderIntoDocument( 273 | , 278 | ); 279 | }); 280 | 281 | 282 | describe("Mount hCaptcha API script", () => { 283 | 284 | const spyOnError = jest.spyOn(HTMLScriptElement.prototype, 'onerror', 'set'); 285 | 286 | beforeEach(() => { 287 | // Setup hCaptcha as undefined to load script 288 | window.hcaptcha = undefined; 289 | }); 290 | 291 | afterEach(() => { 292 | // Clean up created script tag 293 | document.querySelectorAll("head > script") 294 | .forEach(script => document.head.removeChild(script)); 295 | 296 | jest.restoreAllMocks(); 297 | }); 298 | 299 | it("emits error when script is failed", async () => { 300 | const onError = jest.fn(); 301 | 302 | spyOnError.mockImplementation((callback) => { 303 | callback('Invalid Script'); 304 | }); 305 | 306 | instance = ReactTestUtils.renderIntoDocument(); 311 | 312 | await waitForExpect(() => { 313 | expect(onError.mock.calls.length).toBe(1); 314 | expect(onError.mock.calls[0][0].message).toEqual("script-error"); 315 | }); 316 | }); 317 | 318 | it("validate src without", () => { 319 | instance = ReactTestUtils.renderIntoDocument(); 324 | 325 | const script = document.querySelector("head > script"); 326 | expect(script.src).toEqual("https://js.hcaptcha.com/1/api.js?onload=hCaptchaOnLoad&render=explicit&sentry=false"); 327 | }); 328 | 329 | it("validate src secureApi", () => { 330 | instance = ReactTestUtils.renderIntoDocument(); 336 | 337 | const script = document.querySelector("head > script"); 338 | expect(script.src).toEqual("https://js.hcaptcha.com/1/secure-api.js?onload=hCaptchaOnLoad&render=explicit&sentry=false"); 339 | }); 340 | 341 | it("validate src scriptSource", () => { 342 | instance = ReactTestUtils.renderIntoDocument(); 348 | 349 | const script = document.querySelector("head > script"); 350 | expect(script.src).toEqual("https://hcaptcha.com/1/api.js?onload=hCaptchaOnLoad&render=explicit&sentry=false"); 351 | }); 352 | 353 | it("apihost should change script src, but not be added as query", () => { 354 | const ExpectHost = "https://test.com"; 355 | 356 | instance = ReactTestUtils.renderIntoDocument(); 361 | 362 | const script = document.querySelector("head > script"); 363 | expect(script.src).toContain(ExpectHost); 364 | expect(script.src).not.toContain(`apihost=${encodeURIComponent(ExpectHost)}`); 365 | }); 366 | 367 | it("assethost should be found when prop is set", () => { 368 | const ExpectHost = "https://test.com"; 369 | 370 | instance = ReactTestUtils.renderIntoDocument(); 375 | 376 | const script = document.querySelector("head > script"); 377 | expect(script.src).toContain(`assethost=${encodeURIComponent(ExpectHost)}`); 378 | }); 379 | 380 | it("endpoint should be found when prop is set", () => { 381 | const ExpectHost = "https://test.com"; 382 | 383 | instance = ReactTestUtils.renderIntoDocument(); 388 | 389 | const script = document.querySelector("head > script"); 390 | expect(script.src).toContain(`endpoint=${encodeURIComponent(ExpectHost)}`); 391 | }); 392 | 393 | it("imghost should be found when prop is set", () => { 394 | const ExpectHost = "https://test.com"; 395 | 396 | instance = ReactTestUtils.renderIntoDocument(); 401 | 402 | const script = document.querySelector("head > script"); 403 | expect(script.src).toContain(`imghost=${encodeURIComponent(ExpectHost)}`); 404 | }); 405 | 406 | it("reportapi should be found when prop is set", () => { 407 | const ExpectHost = "https://test.com"; 408 | 409 | instance = ReactTestUtils.renderIntoDocument(); 414 | 415 | const script = document.querySelector("head > script"); 416 | expect(script.src).toContain(`reportapi=${encodeURIComponent(ExpectHost)}`); 417 | }); 418 | 419 | it("hl should be found when prop languageOverride is set", () => { 420 | instance = ReactTestUtils.renderIntoDocument(); 425 | 426 | const script = document.querySelector("head > script"); 427 | expect(script.src).toContain("hl=fr"); 428 | }); 429 | 430 | it("reCaptchaCompat should be found when prop is set to false", () => { 431 | instance = ReactTestUtils.renderIntoDocument(); 436 | 437 | const script = document.querySelector("head > script"); 438 | expect(script.src).toContain("recaptchacompat=off"); 439 | }); 440 | 441 | it("reCaptchaCompat should not be found when prop is set to anything except false", () => { 442 | instance = ReactTestUtils.renderIntoDocument(); 447 | 448 | const script = document.querySelector("head > script"); 449 | expect(script.src).not.toContain("recaptchacompat"); 450 | }); 451 | 452 | it("sentry should be found when prop is set", () => { 453 | instance = ReactTestUtils.renderIntoDocument(); 457 | 458 | const script = document.querySelector("head > script"); 459 | expect(script.src).toContain("sentry=false"); 460 | }); 461 | 462 | it("host should be found when prop is set", () => { 463 | instance = ReactTestUtils.renderIntoDocument(); 468 | 469 | const script = document.querySelector("head > script"); 470 | expect(script.src).toContain("host=test.com"); 471 | }); 472 | 473 | it("custom parameter should be in script query", () => { 474 | instance = ReactTestUtils.renderIntoDocument(); 479 | 480 | const script = document.querySelector("head > script"); 481 | expect(script.src).toContain("custom=true"); 482 | }); 483 | 484 | it("should have async set by default", () => { 485 | instance = ReactTestUtils.renderIntoDocument(); 489 | 490 | const script = document.querySelector("head > script"); 491 | expect(script.async).toBeTruthy(); 492 | }); 493 | 494 | it("should not have async set when prop loadAsync is set as false", () => { 495 | instance = ReactTestUtils.renderIntoDocument(); 500 | 501 | const script = document.querySelector("head > script"); 502 | expect(script.async).toBeFalsy(); 503 | }); 504 | 505 | it("should have async set when prop loadAsync is set as true", () => { 506 | instance = ReactTestUtils.renderIntoDocument(); 511 | 512 | const script = document.querySelector("head > script"); 513 | expect(script.async).toBeTruthy(); 514 | }); 515 | 516 | }); 517 | 518 | describe("scriptLocation", () => { 519 | 520 | beforeEach(() => { 521 | // Setup hCaptcha as undefined to load script 522 | window.hcaptcha = undefined; 523 | }); 524 | 525 | it("should append to document.head by default", () => { 526 | const instance = ReactTestUtils.renderIntoDocument(); 530 | 531 | // Manually set hCaptcha API since script does not actually load here 532 | window.hcaptcha = getMockedHcaptcha(); 533 | instance.handleOnLoad(); 534 | 535 | expect(instance._hcaptcha).toEqual(window.hcaptcha); 536 | 537 | const script = document.querySelector("head > script"); 538 | expect(script).toBeTruthy(); 539 | 540 | // clean up 541 | document.head.removeChild(script); 542 | }); 543 | 544 | it("shouldn't create multiple scripts for multiple captchas", () => { 545 | const instance0 = ReactTestUtils.renderIntoDocument(); 549 | 550 | // Manually set hCaptcha API since script does not actually load here 551 | window.hcaptcha = getMockedHcaptcha(); 552 | instance0.handleOnLoad(); 553 | 554 | const instance1 = ReactTestUtils.renderIntoDocument(); 558 | 559 | const scripts = document.querySelectorAll("head > script"); 560 | expect(scripts.length).toBe(1); 561 | 562 | expect(instance0._hcaptcha).toEqual(window.hcaptcha); 563 | expect(instance1._hcaptcha).toEqual(window.hcaptcha); 564 | 565 | // clean up 566 | const script = document.querySelector("head > script"); 567 | document.head.removeChild(script) 568 | }); 569 | 570 | it("should append script into specified DOM element", () => { 571 | const element = document.createElement('div'); 572 | element.id = "script-location"; 573 | 574 | document.body.appendChild(element); 575 | 576 | const instance = ReactTestUtils.renderIntoDocument(); 581 | 582 | // Manually set hCaptcha API since script does not actually load here 583 | window.hcaptcha = getMockedHcaptcha(); 584 | instance.handleOnLoad(); 585 | 586 | let script; 587 | script = document.querySelector("head > script"); 588 | expect(script).toBeFalsy(); 589 | 590 | script = document.querySelector("#script-location > script"); 591 | expect(script).toBeTruthy(); 592 | 593 | expect(instance._hcaptcha).toEqual(window.hcaptcha); 594 | 595 | // clean up 596 | document.body.removeChild(element) 597 | }); 598 | 599 | describe('iframe', () => { 600 | const iframe = document.createElement('iframe'); 601 | document.body.appendChild(iframe); 602 | 603 | const iframeWin = iframe.contentWindow; 604 | const iframeDoc = iframeWin.document; 605 | 606 | let instance; 607 | 608 | beforeAll(() => { 609 | delete window["hCaptchaOnLoad"]; 610 | }); 611 | 612 | afterAll(() => { 613 | // clean up, keep iFrame persistent between tests 614 | document.body.removeChild(iframe); 615 | delete iframeWin["hCaptchaOnLoad"]; 616 | }); 617 | 618 | it("should append script into supplied iFrame", () => { 619 | instance = ReactTestUtils.renderIntoDocument(); 624 | 625 | // Manually set hCaptcha API since script does not actually load here 626 | iframeWin.hcaptcha = getMockedHcaptcha(); 627 | instance.handleOnLoad(); 628 | 629 | let script; 630 | script = document.querySelector("head > script"); 631 | expect(script).toBeFalsy(); 632 | 633 | script = iframeDoc.querySelector("head > script"); 634 | expect(script).toBeTruthy(); 635 | }); 636 | 637 | it("should have hCaptchaOnLoad in iFrame window", () => { 638 | expect(iframeWin).toHaveProperty("hCaptchaOnLoad"); 639 | }); 640 | 641 | it("should load hCaptcha API in iFrame window", () => { 642 | expect(instance._hcaptcha).toEqual(iframeWin.hcaptcha); 643 | }); 644 | 645 | it("should only append script tag once for same element specified", () => { 646 | ReactTestUtils.renderIntoDocument(); 651 | 652 | const scripts = iframeDoc.querySelectorAll("head > script"); 653 | expect(scripts.length).toBe(1); 654 | }); 655 | 656 | it("should append new script tag for new element specified", () => { 657 | const iframe2 = document.createElement("iframe"); 658 | document.body.appendChild(iframe2); 659 | 660 | const iframe2Win = iframe.contentWindow; 661 | const iframe2Doc = iframe2Win.document; 662 | 663 | const instance = ReactTestUtils.renderIntoDocument(); 668 | 669 | // Manually set hCaptcha API since script does not actually load here 670 | iframe2Win.hcaptcha = getMockedHcaptcha(); 671 | instance.handleOnLoad(); 672 | 673 | const script = iframe2Doc.querySelector("head > script"); 674 | expect(script).toBeTruthy(); 675 | 676 | const scripts = iframe2Doc.querySelectorAll("head > script"); 677 | expect(scripts.length).toBe(1); 678 | 679 | expect(iframe2Win).toHaveProperty("hCaptchaOnLoad"); 680 | expect(instance._hcaptcha).toEqual(iframe2Win.hcaptcha); 681 | 682 | // clean up 683 | document.body.removeChild(iframe2); 684 | delete iframe2Win["hCaptchaOnLoad"]; 685 | }); 686 | 687 | }); 688 | 689 | }); 690 | 691 | describe('onOpen callback', () => { 692 | afterEach(() => { 693 | jest.restoreAllMocks(); 694 | }); 695 | 696 | it("should be called if the captcha is ready and the callback is provided as a prop", () => { 697 | jest.spyOn(instance, 'isReady').mockImplementation(() => true); 698 | 699 | expect(mockFns.onOpen.mock.calls.length).toBe(0); 700 | instance.handleOpen(); 701 | expect(mockFns.onOpen.mock.calls.length).toBe(1); 702 | }); 703 | 704 | it("should not be called if the captcha is not ready", () => { 705 | jest.spyOn(instance, 'isReady').mockImplementation(() => false); 706 | 707 | expect(mockFns.onOpen.mock.calls.length).toBe(0); 708 | instance.handleOpen(); 709 | expect(mockFns.onOpen.mock.calls.length).toBe(0); 710 | }); 711 | 712 | it("should not be called if not provided as a prop", () => { 713 | instance = ReactTestUtils.renderIntoDocument( 714 | , 718 | ); 719 | jest.spyOn(instance, 'isReady').mockImplementation(() => true); 720 | 721 | expect(mockFns.onOpen.mock.calls.length).toBe(0); 722 | instance.handleOpen(); 723 | expect(mockFns.onOpen.mock.calls.length).toBe(0); 724 | }); 725 | }); 726 | 727 | describe('onClose callback', () => { 728 | afterEach(() => { 729 | jest.restoreAllMocks(); 730 | }); 731 | 732 | it("should be called if the captcha is ready and the callback is provided as a prop", () => { 733 | jest.spyOn(instance, 'isReady').mockImplementation(() => true); 734 | 735 | expect(mockFns.onClose.mock.calls.length).toBe(0); 736 | instance.handleClose(); 737 | expect(mockFns.onClose.mock.calls.length).toBe(1); 738 | }); 739 | 740 | it("should not be called if the captcha is not ready", () => { 741 | jest.spyOn(instance, 'isReady').mockImplementation(() => false); 742 | 743 | expect(mockFns.onClose.mock.calls.length).toBe(0); 744 | instance.handleClose(); 745 | expect(mockFns.onClose.mock.calls.length).toBe(0); 746 | }); 747 | 748 | it("should not be called if not provided as a prop", () => { 749 | instance = ReactTestUtils.renderIntoDocument( 750 | , 754 | ); 755 | jest.spyOn(instance, 'isReady').mockImplementation(() => true); 756 | 757 | expect(mockFns.onClose.mock.calls.length).toBe(0); 758 | instance.handleClose(); 759 | expect(mockFns.onClose.mock.calls.length).toBe(0); 760 | }); 761 | }); 762 | 763 | describe('onChalExpired callback', () => { 764 | afterEach(() => { 765 | jest.restoreAllMocks(); 766 | }); 767 | 768 | it("should be called if the captcha is ready and the callback is provided as a prop", () => { 769 | jest.spyOn(instance, 'isReady').mockImplementation(() => true); 770 | 771 | expect(mockFns.onChalExpired.mock.calls.length).toBe(0); 772 | instance.handleChallengeExpired(); 773 | expect(mockFns.onChalExpired.mock.calls.length).toBe(1); 774 | }); 775 | 776 | it("should not be called if the captcha is not ready", () => { 777 | jest.spyOn(instance, 'isReady').mockImplementation(() => false); 778 | 779 | expect(mockFns.onClose.mock.calls.length).toBe(0); 780 | instance.handleClose(); 781 | expect(mockFns.onClose.mock.calls.length).toBe(0); 782 | }); 783 | 784 | it("should not be called if not provided as a prop", () => { 785 | instance = ReactTestUtils.renderIntoDocument( 786 | , 790 | ); 791 | jest.spyOn(instance, 'isReady').mockImplementation(() => true); 792 | 793 | expect(mockFns.onChalExpired.mock.calls.length).toBe(0); 794 | instance.handleChallengeExpired(); 795 | expect(mockFns.onChalExpired.mock.calls.length).toBe(0); 796 | }); 797 | }); 798 | }); 799 | -------------------------------------------------------------------------------- /tests/utils.test.js: -------------------------------------------------------------------------------- 1 | import { describe, jest, it } from "@jest/globals"; 2 | 3 | import { getFrame, getMountElement } from "../src/utils.js"; 4 | 5 | 6 | describe("getFrame", () => { 7 | 8 | it("should return the default document and window for the root application", () => { 9 | const frame = getFrame(); 10 | expect(frame.document).toEqual(document); 11 | expect(frame.window).toEqual(global); 12 | }); 13 | 14 | it("should return the root document and window for the supplied element in the root application", () => { 15 | const element = document.createElement('div'); 16 | document.body.appendChild(element); 17 | 18 | const frame = getFrame(element); 19 | expect(frame.document).toEqual(document); 20 | expect(frame.window).toEqual(global); 21 | 22 | // clean up 23 | document.body.removeChild(element); 24 | }); 25 | 26 | it("should return the corresponding frame document and window for the an element found in another document", () => { 27 | const iframe = document.createElement('iframe'); 28 | document.body.appendChild(iframe); 29 | 30 | const frameWindow = iframe.contentWindow; 31 | const frameDocument = frameWindow.document; 32 | 33 | const element = frameDocument.createElement('div'); 34 | frameDocument.body.appendChild(element); 35 | 36 | const frame = getFrame(element); 37 | expect(frame.document).toEqual(frameDocument); 38 | expect(frame.window).toEqual(frameWindow); 39 | 40 | expect(frame.document).not.toEqual(document); 41 | expect(frame.window).not.toEqual(global); 42 | 43 | // clean up 44 | document.body.removeChild(iframe); 45 | }); 46 | 47 | }); 48 | 49 | describe("getMountElement", () => { 50 | 51 | it("should return document.head by default", () => { 52 | const mountElement = getMountElement(); 53 | expect(mountElement).toEqual(document.head); 54 | }); 55 | 56 | it("should return element passed in", () => { 57 | const element = document.createElement('div'); 58 | const mountElement = getMountElement(element); 59 | expect(mountElement).toEqual(element); 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for @hcaptcha/react-hcaptcha 0.1 2 | // Project: https://github.com/hCaptcha/react-hcaptcha 3 | // Definitions by: Matt Sutkowski 4 | // Original Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | // TypeScript Version: 2.8 6 | 7 | import * as React from "react"; 8 | 9 | interface HCaptchaState { 10 | isApiReady: boolean; 11 | isRemoved: boolean; 12 | elementId: string; 13 | captchaId: string; 14 | } 15 | 16 | interface HCaptchaProps { 17 | onExpire?: () => any; 18 | onOpen?: () => any; 19 | onClose?: () => any; 20 | onChalExpired?: () => any; 21 | onError?: (event: string) => any; 22 | onVerify?: (token: string, ekey: string) => any; 23 | onLoad?: () => any; 24 | languageOverride?: string; 25 | sitekey: string; 26 | size?: "normal" | "compact" | "invisible"; 27 | theme?: "light" | "dark" | "contrast" | Object; 28 | tabIndex?: number; 29 | id?: string; 30 | reCaptchaCompat?: boolean; 31 | loadAsync?: boolean; 32 | scriptLocation?: HTMLElement | null; 33 | sentry?: boolean; 34 | cleanup?: boolean; 35 | custom?: boolean; 36 | secureApi?: boolean; 37 | scriptSource?: string; 38 | } 39 | 40 | interface ExecuteResponse { 41 | response: string; 42 | key: string; 43 | } 44 | 45 | declare class HCaptcha extends React.Component { 46 | resetCaptcha(): void; 47 | renderCaptcha(): void; 48 | removeCaptcha(): void; 49 | getRespKey(): string; 50 | getResponse(): string; 51 | setData(data: object): void; 52 | isReady(): boolean; 53 | execute(opts: { async: true, rqdata?: string }): Promise; 54 | execute(opts?: { async: false, rqdata?: string }): void; 55 | execute(opts?: { async?: boolean, rqdata?: string }): Promise | void; 56 | } 57 | 58 | export = HCaptcha; 59 | --------------------------------------------------------------------------------