react-turnstile
react-turnstile copied to clipboard
🐛 Bug: react-turstile is not compatible with NextJS and Jest
Bug Report Checklist
- [x] I have tried restarting my IDE and the issue persists.
- [x] I have pulled the latest
mainbranch of the repository. - [x] I have searched for related issues and found none that matched my issue.
Expected
I can use Jest to write tests that renders a contact form that has the turnstile widget embedded.
Actual
I have a simple component that looks like this:
"use client";
import { sendContactMessage } from "@/lib/actions";
import { useEffect, useRef, useState } from "react";
import { Turnstile, TurnstileInstance } from "@marsidev/react-turnstile";
export type MessageType = "absent" | "success" | "error";
export type MessageParams = {
messageType: MessageType;
messages: string[]
};
export const DefaultMessageParams: MessageParams = {
messageType: "absent",
messages: []
};
export default function ContactForm(messageParams: MessageParams) {
const messageType = messageParams.messageType;
const messages = messageParams.messages
const isSuccess = messageType === "success";
const isError = messageType === "error";
const [showSuccessToast, setShowSuccessToast] = useState(isSuccess);
const [showErrorToast, setShowErrorToast] = useState(isError);
const refTurnstile = useRef<TurnstileInstance>(null);
const [canSubmit, setCanSubmit] = useState(false);
const [consentGiven, setConsentGiven] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setShowSuccessToast(false);
setShowErrorToast(false);
}, 5000);
return () => clearTimeout(timer);
}, []);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
await new Promise((resolve) => setTimeout(resolve, 500));
refTurnstile.current?.reset();
setCanSubmit(false);
}
return (
<div>
<form action={sendContactMessage} onSubmit={handleSubmit}>
<div className="w-full rounded-lg border border-base-200 overflow-hidden">
<Turnstile
ref={refTurnstile}
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
onSuccess={() => setCanSubmit(true)}
options={{ theme: "light", size: "flexible" }}
/>
</div>
<label className="floating-label">
<span>Vorname</span>
<input
className="input input-xl mt-10 w-full"
name="givenName"
type="text"
placeholder="Vorname"
/>
</label>
<label className="floating-label">
<span>Nachname</span>
<input
className="input input-xl w-full mt-10"
name="name"
type="text"
placeholder="Nachname"
/>
</label>
<label className="floating-label">
<span>Email</span>
<input
className="input input-xl w-full mt-10 validator"
type="email"
name="email"
autoComplete="on"
required
placeholder="[email protected]"
/>
<div className="validator-hint">Bitte gib eine gültige E-Mail Adresse an</div>
</label>
<label className="floating-label">
<span className="mt-6">Deine Nachricht an uns</span>
<textarea
className="textarea textarea-xl w-full h-58 mt-6"
name="message"
required
placeholder="Deine Nachricht an uns"
/>
</label>
<label className="label">
<input
className="checkbox mt-6"
name="consent"
type="checkbox"
required
onChange={(e) => setConsentGiven(e.target.checked)}
/>
<span className="ml-4 mt-4 text-wrap text-sm">
Zur Weiterverarbeitung bin ich mit den Nutzungsbedingungen und
Datenschutzrichtlinien einverstanden.
</span>
</label>
<button
className="btn btn-xl mt-6 mb-6"
type="submit"
disabled={(consentGiven && canSubmit) === false}
>
Absenden
</button>
</form>
{showSuccessToast && (
<div className="toast">
<div className="alert alert-success">
<span className="text-lg">
{
messages.map((message, key) => (
<p key={key}>{message}</p>
))
}
</span>
</div>
</div>
)}
{showErrorToast && (
<div className="toast">
<div className="alert alert-error">
<span className="text-lg">
{
messages.map((message, key) => (
<p key={key}>{message}</p>
))
}
</span>
</div>
</div>
)}
</div>
);
}
And a jest test like this:
/**
* @jest-environment jsdom
*/
import ContactForm, { DefaultMessageParams, MessageParams } from '../contact-form';
import { render, screen } from '@testing-library/react';
import userEvent from "@testing-library/user-event";
import '@testing-library/jest-dom';
describe("ContactForm", () => {
beforeEach(() => {
render(<ContactForm {...DefaultMessageParams} />);
});
describe("rendering", () => {
it("should verify that givenName is present", () => {
expect(screen.getByLabelText("Vorname")).toBeInTheDocument();
});
});
})
});
Running this tests leads to:
FAIL app/components/ui/__tests__/contact-form.tsx
● Test suite failed to run
Jest encountered an unexpected token
Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.
Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.
By default "node_modules" folder is ignored by transformers.
Here's what you can do:
• If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
• If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation, specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/configuration
For information about custom transformations, see:
https://jestjs.io/docs/code-transformation
Details:
/home/fs/projects/aixo.com/website-next-js/node_modules/@marsidev/react-turnstile/dist/index.js:2
import{forwardRef as pe,useCallback as Q,useEffect as h,useImperativeHandle as fe,useMemo as me,useRef as D,useState as Z}from"react";import{forwardRef as ce}from"react";import{jsx as ue}from"react/jsx-runtime";var ae=({as:n="div",...a},s)=>ue(n,{...a,ref:s}),J=ce(ae);import{useEffect as le,useState as de}from"react";var K="https://challenges.cloudflare.com/turnstile/v0/api.js",b="cf-turnstile-script",U="cf-turnstile",P="onloadTurnstileCallback",M=n=>!!document.getElementById(n),X=({render:n="explicit",onLoadCallbackName:a=P,scriptOptions:{nonce:s="",defer:e=!0,async:m=!0,id:v="",appendTo:g,onError:T,crossOrigin:w=""}={}})=>{let E=v||b;if(M(E))return;let i=document.createElement("script");if(i.id=E,i.src=`${K}?onload=${a}&render=${n}`,document.querySelector(`script[src="${i.src}"]`))return;i.defer=!!e,i.async=!!m,s&&(i.nonce=s),w&&(i.crossOrigin=w),T&&(i.onerror=T,delete window[a]),(g==="body"?document.body:document.getElementsByTagName("head")[0]).appendChild(i)},f={normal:{width:300,height:65},compact:{width:150,height:140},invisible:{width:0,height:0,overflow:"hidden"},flexible:{minWidth:300,width:"100%",height:65},interactionOnly:{width:"fit-content",height:"auto",display:"flex"}};function G(n){if(n!=="invisible"&&n!=="interactionOnly")return n}function z(n=b){let[a,s]=de(!1);return le(()=>{let e=()=>{M(n)&&s(!0)},m=new MutationObserver(e);return m.observe(document,{childList:!0,subtree:!0}),e(),()=>{m.disconnect()}},[n]),a}import{jsx as xe}from"react/jsx-runtime";var _="unloaded",ee,Te=new Promise((n,a)=>{ee={resolve:n,reject:a},_==="ready"&&n(void 0)}),we=(n=P)=>(_==="unloaded"&&(_="loading",window[n]=()=>{ee.resolve(),_="ready",delete window[n]}),Te),Ee=pe((n,a)=>{let{scriptOptions:s,options:e={},siteKey:m,onWidgetLoad:v,onSuccess:g,onExpire:T,onError:w,onBeforeInteractive:E,onAfterInteractive:i,onUnsupported:I,onTimeout:k,onLoadScript:W,id:te,style:re,as:ne="div",injectScript:$=!0,rerenderOnCallbackChange:o=!1,...oe}=n,c=e.size,j=Q(()=>typeof c>"u"?{}:e.execution==="execute"?f.invisible:e.appearance==="interaction-only"?f.interactionOnly:f[c],[e.execution,c,e.appearance]),[ie,R]=Z(j()),u=D(null),[x,B]=Z(!1),r=D(),L=D(!1),H=te||U,d=D({onSuccess:g,onError:w,onExpire:T,onBeforeInteractive:E,onAfterInteractive:i,onUnsupported:I,onTimeout:k});h(()=>{o||(d.current={onSuccess:g,onError:w,onExpire:T,onBeforeInteractive:E,onAfterInteractive:i,onUnsupported:I,onTimeout:k})});let O=s?.id||b,A=z(O),V=s?.onLoadCallbackName||P,se=e.appearance||"always",C=me(()=>({sitekey:m,action:e.action,cData:e.cData,theme:e.theme||"auto",language:e.language||"auto",tabindex:e.tabIndex,"response-field":e.responseField,"response-field-name":e.responseFieldName,size:G(c),retry:e.retry||"auto","retry-interval":e.retryInterval||8e3,"refresh-expired":e.refreshExpired||"auto","refresh-timeout":e.refreshTimeout||"auto",execution:e.execution||"render",appearance:e.appearance||"always","feedback-enabled":e.feedbackEnabled||!0,callback:t=>{L.current=!0,o?g?.(t):d.current.onSuccess?.(t)},"error-callback":o?w:(...t)=>d.current.onError?.(...t),"expired-callback":o?T:(...t)=>d.current.onExpire?.(...t),"before-interactive-callback":o?E:(...t)=>d.current.onBeforeInteractive?.(...t),"after-interactive-callback":o?i:(...t)=>d.current.onAfterInteractive?.(...t),"unsupported-callback":o?I:(...t)=>d.current.onUnsupported?.(...t),"timeout-callback":o?k:(...t)=>d.current.onTimeout?.(...t)}),[e.action,e.appearance,e.cData,e.execution,e.language,e.refreshExpired,e.responseField,e.responseFieldName,e.retry,e.retryInterval,e.tabIndex,e.theme,e.feedbackEnabled,e.refreshTimeout,m,c,o,o?g:null,o?w:null,o?T:null,o?E:null,o?i:null,o?I:null,o?k:null]),y=Q(()=>typeof window<"u"&&!!window.turnstile,[]);return h(function(){$&&!x&&X({onLoadCallbackName:V,scriptOptions:{...s,id:O}})},[$,x,s,O]),h(function(){_!=="ready"&&we(V).then(()=>B(!0)).catch(console.error)},[]),h(function(){if(!u.current||!x)return;let l=!1;return(async()=>{if(l||!u.current)return;let F=window.turnstile.render(u.current,C);r.current=F,r.current&&v?.(r.current)})(),()=>{l=!0,r.current&&(window.turnstile.remove(r.current),L.current=!1)}},[H,x,C]),fe(a,()=>{let{turnstile:t}=window;return{getResponse(){if(!t?.getResponse||!r.current||!y()){console.warn("Turnstile has not been loaded");return}return t.getResponse(r.current)},async getResponsePromise(l=3e4,Y=100){return new Promise((F,N)=>{let p,q=async()=>{if(L.current&&window.turnstile&&r.current)try{let S=window.turnstile.getResponse(r.current);return p&&clearTimeout(p),S?F(S):N(new Error("No response received"))}catch(S){return p&&clearTimeout(p),console.warn("Failed to get response",S),N(new Error("Failed to get response"))}p||(p=setTimeout(()=>{p&&clearTimeout(p),N(new Error("Timeout"))},l)),await new Promise(S=>setTimeout(S,Y)),await q()};q()})},reset(){if(!t?.reset||!r.current||!y()){console.warn("Turnstile has not been loaded");return}e.execution==="execute"&&R(f.invisible);try{L.current=!1,t.reset(r.current)}catch(l){console.warn(`Failed to reset Turnstile widget ${r}`,l)}},remove(){if(!t?.remove||!r.current||!y()){console.warn("Turnstile has not been loaded");return}R(f.invisible),L.current=!1,t.remove(r.current),r.current=null},render(){if(!t?.render||!u.current||!y()||r.current){console.warn("Turnstile has not been loaded or container not found");return}let l=t.render(u.current,C);return r.current=l,r.current&&v?.(r.current),e.execution!=="execute"&&R(c?f[c]:{}),l},execute(){if(e.execution!=="execute"){console.warn('Execution mode is not set to "execute"');return}if(!t?.execute||!u.current||!r.current||!y()){console.warn("Turnstile has not been loaded or container not found");return}t.execute(u.current,C),R(c?f[c]:{})},isExpired(){return!t?.isExpired||!r.current||!y()?(console.warn("Turnstile has not been loaded"),!1):t.isExpired(r.current)}}},[r,e.execution,c,C,u,y,x,v]),h(()=>{A&&!x&&window.turnstile&&B(!0)},[x,A]),h(()=>{R(j())},[e.execution,c,se]),h(()=>{!A||typeof W!="function"||W()},[A]),xe(J,{ref:u,as:ne,id:H,style:{...ie,...re},...oe})});Ee.displayName="Turnstile";export{U as DEFAULT_CONTAINER_ID,P as DEFAULT_ONLOAD_NAME,b as DEFAULT_SCRIPT_ID,K as SCRIPT_URL,Ee as Turnstile};
^^^^^^
SyntaxError: Cannot use import statement outside a module
21 | const messages = messageParams.messages
22 |
> 23 | const isSuccess = messageType === "success";
| ^
24 | const isError = messageType === "error";
25 |
26 | const [showSuccessToast, setShowSuccessToast] = useState(isSuccess);
at Runtime.createScriptFromCode (../node_modules/jest-runtime/build/index.js:1318:40)
at Object.<anonymous> (components/ui/contact-form.tsx:23:25)
at Object.<anonymous> (components/ui/__tests__/contact-form.tsx:8:62)
It seems, that Jest cannot handle the imports.
Package Version
1.3.1
Browsers
Other
Additional Info
The tests ran fine without embedding the Turnstile component.
Hello, our library ships esm-only output. That may be the issue. Did you checked this?