cypress: handle Clerk verification method step before OTP

This commit is contained in:
Kunal
2026-02-08 14:32:12 +00:00
parent ca5c5a2eea
commit 76dc011459

View File

@@ -56,8 +56,9 @@ Cypress.Commands.add("loginWithClerkOtp", () => {
const emailSelector = const emailSelector =
'input[type="email"], input[name="identifier"], input[autocomplete="email"]'; 'input[type="email"], input[name="identifier"], input[autocomplete="email"]';
const otpSelector = const otpSelector =
'input[autocomplete="one-time-code"], input[name*="code"], input[inputmode="numeric"]'; 'input[autocomplete="one-time-code"], input[name*="code"], input[name^="code"], input[name^="code."], input[inputmode="numeric"]';
const continueSelector = 'button[type="submit"], button'; const continueSelector = 'button[type="submit"], button';
const methodSelector = /email|code|otp|send code|verification|verify|use email/i;
const fillEmailStep = (email: string) => { const fillEmailStep = (email: string) => {
cy.get(emailSelector, { timeout: 20_000 }) cy.get(emailSelector, { timeout: 20_000 })
@@ -65,13 +66,46 @@ Cypress.Commands.add("loginWithClerkOtp", () => {
.clear() .clear()
.type(email, { delay: 10 }); .type(email, { delay: 10 });
cy.get(continueSelector) cy.contains(continueSelector, /continue|sign in|send|next/i, { timeout: 20_000 })
.contains(/continue|sign in|send|next/i) .should("be.visible")
.click({ force: true }); .click({ force: true });
}; };
const maybeSelectEmailCodeMethod = () => {
cy.get("body").then(($body) => {
const hasOtp = $body.find(otpSelector).length > 0;
if (hasOtp) return;
const candidates = $body
.find("button,a")
.toArray()
.filter((el) => methodSelector.test((el.textContent || "").trim()));
if (candidates.length > 0) {
cy.wrap(candidates[0]).click({ force: true });
}
});
};
const waitForOtpOrMethod = () => {
cy.get("body", { timeout: 60_000 }).should(($body) => {
const hasOtp = $body.find(otpSelector).length > 0;
const hasMethod = $body
.find("button,a")
.toArray()
.some((el) => methodSelector.test((el.textContent || "").trim()));
expect(
hasOtp || hasMethod,
"waiting for OTP input or verification method UI",
).to.equal(true);
});
};
const fillOtpAndSubmit = (otp: string) => { const fillOtpAndSubmit = (otp: string) => {
cy.get(otpSelector, { timeout: 20_000 }).first().clear().type(otp, { delay: 10 }); waitForOtpOrMethod();
maybeSelectEmailCodeMethod();
cy.get(otpSelector, { timeout: 60_000 }).first().clear().type(otp, { delay: 10 });
cy.get("body").then(($body) => { cy.get("body").then(($body) => {
const hasSubmit = $body const hasSubmit = $body
@@ -79,40 +113,72 @@ Cypress.Commands.add("loginWithClerkOtp", () => {
.toArray() .toArray()
.some((el) => /verify|continue|sign in|confirm/i.test(el.textContent || "")); .some((el) => /verify|continue|sign in|confirm/i.test(el.textContent || ""));
if (hasSubmit) { if (hasSubmit) {
cy.get(continueSelector) cy.contains(continueSelector, /verify|continue|sign in|confirm/i, { timeout: 20_000 })
.contains(/verify|continue|sign in|confirm/i) .should("be.visible")
.click({ force: true }); .click({ force: true });
} }
}); });
}; };
// Clerk SignIn can start on our app origin and then redirect to Clerk-hosted UI. // Clerk SignIn can start on our app origin and then redirect to Clerk-hosted UI.
// We do email step first, then decide where the OTP step lives based on the *current* origin. // Do email step first, then decide where the OTP step lives based on the *current* origin.
fillEmailStep(opts.email); fillEmailStep(opts.email);
cy.location("origin", { timeout: 20_000 }).then((origin) => { cy.location("origin", { timeout: 60_000 }).then((origin) => {
const current = normalizeOrigin(origin); const current = normalizeOrigin(origin);
if (current === opts.clerkOrigin) { if (current === opts.clerkOrigin) {
cy.origin( cy.origin(
opts.clerkOrigin, opts.clerkOrigin,
{ args: { otp: opts.otp } }, { args: { otp: opts.otp } },
({ otp }) => { ({ otp }) => {
cy.get( const otpSelector =
'input[autocomplete="one-time-code"], input[name*="code"], input[inputmode="numeric"]', 'input[autocomplete="one-time-code"], input[name*="code"], input[name^="code"], input[name^="code."], input[inputmode="numeric"]';
{ timeout: 20_000 }, const continueSelector = 'button[type="submit"], button';
) const methodSelector = /email|code|otp|send code|verification|verify|use email/i;
.first()
.clear() const maybeSelectEmailCodeMethod = () => {
.type(otp, { delay: 10 }); cy.get("body").then(($body) => {
const hasOtp = $body.find(otpSelector).length > 0;
if (hasOtp) return;
const candidates = $body
.find("button,a")
.toArray()
.filter((el) => methodSelector.test((el.textContent || "").trim()));
if (candidates.length > 0) {
cy.wrap(candidates[0]).click({ force: true });
}
});
};
const waitForOtpOrMethod = () => {
cy.get("body", { timeout: 60_000 }).should(($body) => {
const hasOtp = $body.find(otpSelector).length > 0;
const hasMethod = $body
.find("button,a")
.toArray()
.some((el) => methodSelector.test((el.textContent || "").trim()));
expect(
hasOtp || hasMethod,
"waiting for OTP input or verification method UI",
).to.equal(true);
});
};
waitForOtpOrMethod();
maybeSelectEmailCodeMethod();
cy.get(otpSelector, { timeout: 60_000 }).first().clear().type(otp, { delay: 10 });
cy.get("body").then(($body) => { cy.get("body").then(($body) => {
const hasSubmit = $body const hasSubmit = $body
.find('button[type="submit"], button') .find(continueSelector)
.toArray() .toArray()
.some((el) => /verify|continue|sign in|confirm/i.test(el.textContent || "")); .some((el) => /verify|continue|sign in|confirm/i.test(el.textContent || ""));
if (hasSubmit) { if (hasSubmit) {
cy.get('button[type="submit"], button') cy.contains(continueSelector, /verify|continue|sign in|confirm/i, { timeout: 20_000 })
.contains(/verify|continue|sign in|confirm/i) .should("be.visible")
.click({ force: true }); .click({ force: true });
} }
}); });