Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/solid-hornets-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@prosopo/cypress-shared": patch
---

Flaky cypress tests fix

11 changes: 6 additions & 5 deletions demos/cypress-shared/cypress/e2e/correct.captcha.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import "@cypress/xpath";
import { ProsopoDatasetError } from "@prosopo/common";
import { datasetWithSolutionHashes } from "@prosopo/datasets";
import type { Captcha, CaptchaType } from "@prosopo/types";
import { checkboxClass, getWidgetElement } from "../support/commands.js";
import {
buildTestSolutions,
checkboxClass,
getWidgetElement,
} from "../support/commands.js";

const baseCaptchaType: CaptchaType = Cypress.env("CAPTCHA_TYPE") || "image";

Expand Down Expand Up @@ -55,10 +59,7 @@ describe("Captchas", () => {
});

beforeEach(() => {
const solutions = datasetWithSolutionHashes.captchas.map((captcha) => ({
captchaContentId: captcha.captchaContentId,
solution: captcha.solution,
}));
const solutions = buildTestSolutions(datasetWithSolutionHashes.captchas);

if (!solutions) {
throw new ProsopoDatasetError(
Expand Down
11 changes: 6 additions & 5 deletions demos/cypress-shared/cypress/e2e/correct.captcha.signup.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import "@cypress/xpath";
import { ProsopoDatasetError } from "@prosopo/common";
import { datasetWithSolutionHashes } from "@prosopo/datasets";
import type { Captcha, CaptchaType } from "@prosopo/types";
import { checkboxClass, getWidgetElement } from "../support/commands.js";
import {
buildTestSolutions,
checkboxClass,
getWidgetElement,
} from "../support/commands.js";

const baseCaptchaType: CaptchaType = Cypress.env("CAPTCHA_TYPE") || "image";

Expand Down Expand Up @@ -55,10 +59,7 @@ describe("Captchas", () => {
});

beforeEach(() => {
const solutions = datasetWithSolutionHashes.captchas.map((captcha) => ({
captchaContentId: captcha.captchaContentId,
solution: captcha.solution,
}));
const solutions = buildTestSolutions(datasetWithSolutionHashes.captchas);

if (!solutions) {
throw new ProsopoDatasetError(
Expand Down
51 changes: 41 additions & 10 deletions demos/cypress-shared/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,37 @@ import {
type IUserSettings,
Tier,
} from "@prosopo/types";
import { at } from "@prosopo/util";
import Chainable = Cypress.Chainable;
import { getPair } from "@prosopo/keyring";
import type { SolutionRecord } from "@prosopo/types";
import type { CaptchaWithoutId } from "@prosopo/types";

export const MAX_IMAGE_CAPTCHA_ROUNDS = 3;

// Solution record keyed by item hashes + target for stable matching across
// dataset rebuilds. We can't match by captchaContentId because buildDataset
// recomputes it via merkle trees. We must include target because multiple
// captchas can share the same images with different targets/solutions.
interface TestSolution {
itemHashes: string;
target: string;
solution: string[];
}

export function buildTestSolutions(
captchas: CaptchaWithoutId[],
): TestSolution[] {
return captchas
.filter((c) => c.solution)
.map((c) => ({
itemHashes: c.items
.map((i) => i.hash)
.sort()
.join(","),
target: c.target,
solution: (c.solution ?? []).map((s) => s.toString()),
}));
}

declare global {
namespace Cypress {
// biome-ignore lint/suspicious/noExplicitAny: TODO fix any
Expand Down Expand Up @@ -219,17 +243,24 @@ function captchaImages(): Cypress.Chainable<JQuery<HTMLElement>> {
function getSelectors(captcha: Captcha): Chainable<string[]> {
cy.wrap({ captcha })
.then(({ captcha }) => {
cy.get<SolutionRecord[]>("@solutions").then((solutions) => {
cy.get<TestSolution[]>("@solutions").then((solutions) => {
let selectors: string[] = [];
// Get the index of the captcha in the solution records array
const captchaIndex = solutions.findIndex(
(testSolution) =>
testSolution.captchaContentId === captcha.captchaContentId,
// Match by item hashes + target rather than captchaContentId, because
// buildDataset recomputes captchaContentId via merkle trees. Target is
// needed because multiple captchas share the same images with different
// targets and solutions.
const captchaItemHashes = captcha.items
.map((i) => i.hash)
.sort()
.join(",");
const match = solutions.find(
(s) =>
s.itemHashes === captchaItemHashes &&
s.target === captcha.target,
);
if (captchaIndex !== -1) {
const solution = at(solutions, captchaIndex).solution;
if (match) {
selectors = captcha.items
.filter((item) => solution.includes(item.hash))
.filter((item) => match.solution.includes(item.hash))
// create a query selector for each image that is a solution
// drop https from the urls as this is what procaptcha does (avoids mixed-content warnings, e.g. resources loaded via a mix of http / https)
.map(
Expand Down
Loading