From 1903bd49dd984d5f6096c3a0eb9ae0f3de4ad7b0 Mon Sep 17 00:00:00 2001 From: Maxim Akimov <61589446+light-source@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:55:08 +0200 Subject: [PATCH] Wrapped the widget into WebComponent with Shadow DOM (#1544) Co-authored-by: prosoponator[bot] Co-authored-by: Chris --- .../cypress-shared/cypress/e2e/captcha.cy.ts | 4 +- .../cypress/e2e/correct.captcha.cy.ts | 12 ++--- .../cypress/e2e/correct.captcha.signup.cy.ts | 9 ++-- .../cypress/support/commands.ts | 52 +++++++++++++------ .../src/util/renderLogic.tsx | 52 ++++++++++++++++--- 5 files changed, 94 insertions(+), 35 deletions(-) diff --git a/demos/cypress-shared/cypress/e2e/captcha.cy.ts b/demos/cypress-shared/cypress/e2e/captcha.cy.ts index 54aab99fd..961bbacc6 100644 --- a/demos/cypress-shared/cypress/e2e/captcha.cy.ts +++ b/demos/cypress-shared/cypress/e2e/captcha.cy.ts @@ -23,7 +23,7 @@ import { type IUserSettings, } from "@prosopo/types"; import { at } from "@prosopo/util"; -import { checkboxClass } from "../support/commands.js"; +import { checkboxClass, getWidgetElement } from "../support/commands.js"; describe("Captchas", () => { before(async () => { @@ -69,7 +69,7 @@ describe("Captchas", () => { // visit the base URL specified on command line when running cypress return cy.visit(Cypress.env("default_page")).then(() => { - cy.get(checkboxClass).should("be.visible"); + getWidgetElement(checkboxClass).should("be.visible"); // wrap the solutions to make them available to the tests cy.wrap(solutions).as("solutions"); }); diff --git a/demos/cypress-shared/cypress/e2e/correct.captcha.cy.ts b/demos/cypress-shared/cypress/e2e/correct.captcha.cy.ts index 866ff90c7..adcd0452a 100644 --- a/demos/cypress-shared/cypress/e2e/correct.captcha.cy.ts +++ b/demos/cypress-shared/cypress/e2e/correct.captcha.cy.ts @@ -22,7 +22,7 @@ import { type Captcha, type IUserSettings, } from "@prosopo/types"; -import { checkboxClass } from "../support/commands.js"; +import { checkboxClass, getWidgetElement } from "../support/commands.js"; describe("Captchas", () => { before(async () => { @@ -69,7 +69,7 @@ describe("Captchas", () => { // visit the base URL specified on command line when running cypress return cy.visit(Cypress.env("default_page")).then(() => { - cy.get(checkboxClass).should("be.visible"); + getWidgetElement(checkboxClass).should("be.visible"); // wrap the solutions to make them available to the tests cy.wrap(solutions).as("solutions"); }); @@ -90,9 +90,7 @@ describe("Captchas", () => { cy.clickNextButton(); }); }); - cy.get("input[type='checkbox']").then((checkboxes) => { - cy.wrap(checkboxes).first().should("not.be.checked"); - }); + getWidgetElement(checkboxClass).first().should("not.be.checked"); }); // check the logs by going through all recorded calls @@ -115,9 +113,7 @@ describe("Captchas", () => { }) .then(() => { // Get inputs of type checkbox - cy.get("input[type='checkbox']").then((checkboxes) => { - cy.wrap(checkboxes).first().should("be.checked"); - }); + getWidgetElement(checkboxClass).first().should("be.checked"); }); }); }); diff --git a/demos/cypress-shared/cypress/e2e/correct.captcha.signup.cy.ts b/demos/cypress-shared/cypress/e2e/correct.captcha.signup.cy.ts index 93e1d965e..520513e73 100644 --- a/demos/cypress-shared/cypress/e2e/correct.captcha.signup.cy.ts +++ b/demos/cypress-shared/cypress/e2e/correct.captcha.signup.cy.ts @@ -22,7 +22,7 @@ import { type Captcha, type IUserSettings, } from "@prosopo/types"; -import { checkboxClass } from "../support/commands.js"; +import { checkboxClass, getWidgetElement } from "../support/commands.js"; describe("Captchas", () => { before(async () => { @@ -69,7 +69,7 @@ describe("Captchas", () => { // visit the base URL specified on command line when running cypress return cy.visit(Cypress.env("default_page")).then(() => { - cy.get(checkboxClass).should("be.visible"); + getWidgetElement(checkboxClass).should("be.visible"); // wrap the solutions to make them available to the tests cy.wrap(solutions).as("solutions"); }); @@ -100,7 +100,10 @@ describe("Captchas", () => { cy.wait("@postSolution"); // Get checked checkboxes - cy.get("input[type='checkbox']:checked").should("have.length.gte", 1); + getWidgetElement(`${checkboxClass}:checked`).should( + "have.length.gte", + 1, + ); const uniqueId = `test${Cypress._.random(0, 1e6)}`; cy.get('input[type="password"]').type("password"); diff --git a/demos/cypress-shared/cypress/support/commands.ts b/demos/cypress-shared/cypress/support/commands.ts index d1543e2bf..9fcfc657b 100644 --- a/demos/cypress-shared/cypress/support/commands.ts +++ b/demos/cypress-shared/cypress/support/commands.ts @@ -22,23 +22,38 @@ declare global { // biome-ignore lint/suspicious/noExplicitAny: TODO fix any interface Chainable { clickIAmHuman(): Cypress.Chainable; + captchaImages(): Cypress.Chainable>; + clickCorrectCaptchaImages( captcha: Captcha, ): Chainable>; + getSelectors(captcha: Captcha): Cypress.Chainable; + clickNextButton(): Cypress.Chainable; + elementExists(element: string): Chainable; } } } export const checkboxClass = '[type="checkbox"]'; + +export function getWidgetElement( + selector: string, + options: object = {}, +): Chainable> { + options = { ...options, includeShadowDom: true }; + + return cy.get(selector, options); +} + function clickIAmHuman(): Cypress.Chainable { cy.intercept("POST", "**/prosopo/provider/client/captcha/**").as( "getCaptcha", ); - cy.get(checkboxClass, { timeout: 12000 }).first().click(); + getWidgetElement(checkboxClass, { timeout: 12000 }).first().click(); return cy .wait("@getCaptcha", { timeout: 36000 }) @@ -69,19 +84,24 @@ function clickIAmHuman(): Cypress.Chainable { } function captchaImages(): Cypress.Chainable> { - return cy - .xpath("//p[contains(text(),'all containing')]", { timeout: 4000 }) - .should("be.visible") - .parent() - .parent() - .parent() - .parent() - .children() - .next() - .children() - .first() - .children() - .as("captchaImages"); + return getWidgetElement("p").then(($p) => { + const $pWithText = $p.filter((index, el) => { + return Cypress.$(el).text().includes("all containing"); + }); + + cy.wrap($pWithText) + .should("be.visible") + .parent() + .parent() + .parent() + .parent() + .children() + .next() + .children() + .first() + .children() + .as("captchaImages"); + }); } function getSelectors(captcha: Captcha) { @@ -121,7 +141,7 @@ function clickCorrectCaptchaImages( cy.getSelectors(captcha).then((selectors: string[]) => { console.log("captchaId", captcha.captchaId, "selectors", selectors); // Click the correct images - cy.get(selectors.join(", ")).then((elements) => { + getWidgetElement(selectors.join(", ")).then((elements) => { if (elements.length > 0) { cy.wrap(elements).click({ multiple: true }); } @@ -137,7 +157,7 @@ function clickNextButton() { "postSolution", ); // Go to the next captcha or submit solution - cy.get('button[data-cy="button-next"]').click({ force: true }); + getWidgetElement('button[data-cy="button-next"]').click({ force: true }); cy.wait(0); } diff --git a/packages/procaptcha-bundle/src/util/renderLogic.tsx b/packages/procaptcha-bundle/src/util/renderLogic.tsx index 086904e42..07feac6ab 100644 --- a/packages/procaptcha-bundle/src/util/renderLogic.tsx +++ b/packages/procaptcha-bundle/src/util/renderLogic.tsx @@ -1,3 +1,5 @@ +import createCache from "@emotion/cache"; +import { CacheProvider } from "@emotion/react"; // Copyright 2021-2024 Prosopo (UK) Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,37 +28,75 @@ import { setValidChallengeLength } from "./timeout.js"; const identifierPrefix = "procaptcha-"; +function makeShadowRoot( + element: Element, + renderOptions?: ProcaptchaRenderOptions, +): ShadowRoot { + // todo maybe introduce customCSS in renderOptions. + const customCss = ""; + + const wrapperElement = document.createElement("prosopo-procaptcha"); + + const wrapperShadow = wrapperElement.attachShadow({ mode: "open" }); + wrapperShadow.innerHTML += + ''; + wrapperShadow.innerHTML += + "" !== customCss ? `` : ""; + + element.appendChild(wrapperElement); + + return wrapperShadow; +} + export const renderLogic = ( elements: Element[], config: ProcaptchaClientConfigOutput, renderOptions?: ProcaptchaRenderOptions, ) => { const roots: Root[] = []; + for (const element of elements) { const callbacks = getDefaultCallbacks(element); + const shadowRoot = makeShadowRoot(element, renderOptions); setUserCallbacks(renderOptions, callbacks, element); setTheme(renderOptions, element, config); setValidChallengeLength(renderOptions, element, config); setLanguage(renderOptions, element, config); + const emotionCache = createCache({ + key: "procaptcha", + prepend: true, + container: shadowRoot, + }); + let root: Root | null = null; switch (renderOptions?.captchaType) { case "pow": console.log("rendering pow"); - root = createRoot(element, { identifierPrefix }); - root.render(); + root = createRoot(shadowRoot, { identifierPrefix }); + root.render( + + + , + ); break; case "image": console.log("rendering image"); - root = createRoot(element, { identifierPrefix }); - root.render(); + root = createRoot(shadowRoot, { identifierPrefix }); + root.render( + + + , + ); break; default: console.log("rendering frictionless"); - root = createRoot(element, { identifierPrefix }); + root = createRoot(shadowRoot, { identifierPrefix }); root.render( - , + + + , ); break; }