Skip to content

Commit

Permalink
Wrapped the widget into WebComponent with Shadow DOM (#1544)
Browse files Browse the repository at this point in the history
Co-authored-by: prosoponator[bot] <dev@prosopo.io>
Co-authored-by: Chris <forgetso86@gmail.com>
  • Loading branch information
3 people authored Nov 27, 2024
1 parent 3308a68 commit 1903bd4
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 35 deletions.
4 changes: 2 additions & 2 deletions demos/cypress-shared/cypress/e2e/captcha.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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");
});
Expand Down
12 changes: 4 additions & 8 deletions demos/cypress-shared/cypress/e2e/correct.captcha.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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");
});
Expand All @@ -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
Expand All @@ -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");
});
});
});
Expand Down
9 changes: 6 additions & 3 deletions demos/cypress-shared/cypress/e2e/correct.captcha.signup.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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");
});
Expand Down Expand Up @@ -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");
Expand Down
52 changes: 36 additions & 16 deletions demos/cypress-shared/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,38 @@ declare global {
// biome-ignore lint/suspicious/noExplicitAny: TODO fix any
interface Chainable<Subject = any> {
clickIAmHuman(): Cypress.Chainable<Captcha[]>;

captchaImages(): Cypress.Chainable<JQuery<HTMLElement>>;

clickCorrectCaptchaImages(
captcha: Captcha,
): Chainable<JQuery<HTMLElement>>;

getSelectors(captcha: Captcha): Cypress.Chainable<string[]>;

clickNextButton(): Cypress.Chainable<void>;

elementExists(element: string): Chainable<Subject>;
}
}
}

export const checkboxClass = '[type="checkbox"]';

export function getWidgetElement(
selector: string,
options: object = {},
): Chainable<JQuery<HTMLElement>> {
options = { ...options, includeShadowDom: true };

return cy.get(selector, options);
}

function clickIAmHuman(): Cypress.Chainable<Captcha[]> {
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 })
Expand Down Expand Up @@ -69,19 +84,24 @@ function clickIAmHuman(): Cypress.Chainable<Captcha[]> {
}

function captchaImages(): Cypress.Chainable<JQuery<HTMLElement>> {
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) {
Expand Down Expand Up @@ -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 });
}
Expand All @@ -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);
}

Expand Down
52 changes: 46 additions & 6 deletions packages/procaptcha-bundle/src/util/renderLogic.tsx
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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 +=
'<style>:host{all:initial!important;}:host *{font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";}</style>';
wrapperShadow.innerHTML +=
"" !== customCss ? `<style>${customCss}</style>` : "";

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(<ProcaptchaPow config={config} callbacks={callbacks} />);
root = createRoot(shadowRoot, { identifierPrefix });
root.render(
<CacheProvider value={emotionCache}>
<ProcaptchaPow config={config} callbacks={callbacks} />
</CacheProvider>,
);
break;
case "image":
console.log("rendering image");
root = createRoot(element, { identifierPrefix });
root.render(<Procaptcha config={config} callbacks={callbacks} />);
root = createRoot(shadowRoot, { identifierPrefix });
root.render(
<CacheProvider value={emotionCache}>
<Procaptcha config={config} callbacks={callbacks} />
</CacheProvider>,
);
break;
default:
console.log("rendering frictionless");
root = createRoot(element, { identifierPrefix });
root = createRoot(shadowRoot, { identifierPrefix });
root.render(
<ProcaptchaFrictionless config={config} callbacks={callbacks} />,
<CacheProvider value={emotionCache}>
<ProcaptchaFrictionless config={config} callbacks={callbacks} />
</CacheProvider>,
);
break;
}
Expand Down

0 comments on commit 1903bd4

Please sign in to comment.