Add widget test

Mainly for bug fixed in 5df983a5c4
This commit is contained in:
Taras Kushnir
2025-10-10 16:04:49 +03:00
parent b9ebb5830b
commit fe1b40b99d
11 changed files with 299 additions and 17 deletions
@@ -1,10 +1,11 @@
name: eslint
name: widget
on:
push:
branches:
- '**'
paths:
- 'widget/js/**'
- 'widget/test/**'
- '.github/workflows/eslint.yml'
# - 'web/js/**'
- '!node_modules/**'
@@ -23,8 +24,7 @@ permissions:
# pull-requests: read
jobs:
golangci:
name: lint
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -40,3 +40,7 @@ jobs:
- name: Run eslint for widget
run: npm run lint
working-directory: ./widget
- name: Run widget tests
run: npm run test
working-directory: ./widget
+2
View File
@@ -27,6 +27,8 @@ web/static/*
widget/static/
widget/lib/*.js
widget/lib/*.js.map
widget/test/bundle.test.js
widget/test/bundle.test.js.map
pkg/db/migrations/postgres/000000_sqlc_fix.sql
pc.env
+3
View File
@@ -12,6 +12,9 @@ test-unit:
test-unit-cover:
@env GOFLAGS="-mod=vendor" CGO_ENABLED=0 go test -short -coverprofile=coverage_unit.cov -coverpkg=$(shell go list ./... | paste -sd, -) ./...
test-widget-unit:
cd widget && env STAGE="$(STAGE)" npm run test
bench-unit:
env GOFLAGS="-mod=vendor" CGO_ENABLED=0 go test -bench=. -benchtime=20s -short ./...
+1 -1
View File
@@ -7,7 +7,7 @@
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/PrivateCaptcha/PrivateCaptcha)
![CI](https://github.com/PrivateCaptcha/PrivateCaptcha/actions/workflows/ci.yaml/badge.svg) ![Go lint](https://github.com/PrivateCaptcha/PrivateCaptcha/actions/workflows/golangci-lint.yml/badge.svg) ![JS lint](https://github.com/PrivateCaptcha/PrivateCaptcha/actions/workflows/eslint.yml/badge.svg)
![CI](https://github.com/PrivateCaptcha/PrivateCaptcha/actions/workflows/ci.yaml/badge.svg) ![Go lint](https://github.com/PrivateCaptcha/PrivateCaptcha/actions/workflows/golangci-lint.yml/badge.svg) ![JS lint](https://github.com/PrivateCaptcha/PrivateCaptcha/actions/workflows/widget.yml/badge.svg)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=PrivateCaptcha_PrivateCaptcha&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=PrivateCaptcha_PrivateCaptcha)
[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=PrivateCaptcha_PrivateCaptcha&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=PrivateCaptcha_PrivateCaptcha)
+31
View File
@@ -0,0 +1,31 @@
import { build, transform } from 'esbuild';
import { readFile } from "fs/promises"
import inlineWorkerPlugin from 'esbuild-plugin-inline-worker';
let CSSMinifyPlugin = {
name: "CSSMinifyPlugin",
setup(build) {
build.onLoad({ filter: /\.css$/ }, async (args) => {
const f = await readFile(args.path)
const css = await transform(f, { loader: "css", minify: false })
return { loader: "text", contents: css.code }
})
}
}
build({
entryPoints: ['./test/widget.test.js'],
bundle: true,
outfile: './test/bundle.test.js',
format: 'esm',
loader: { '.css': 'text' },
external: ['node:test', 'node:assert', 'happy-dom'],
plugins: [
CSSMinifyPlugin,
inlineWorkerPlugin({
minify: false
}),
],
minify: false,
sourcemap: true,
}).catch(() => process.exit(1));
+1 -1
View File
@@ -1,7 +1,7 @@
'use strict';
import { ProgressRing } from './progress.js';
import { SafeHTMLElement } from "./utils";
import { SafeHTMLElement } from "./utils.js";
import styles from "./styles.css" with { type: 'css' };
import * as i18n from './strings.js';
import * as errors from './errors.js';
+1 -1
View File
@@ -1,6 +1,6 @@
'use strict';
import { SafeHTMLElement } from "./utils";
import { SafeHTMLElement } from "./utils.js";
export class ProgressRing extends SafeHTMLElement {
constructor() {
+7 -2
View File
@@ -90,12 +90,17 @@ export class CaptchaWidget {
defaultField = "g-recaptcha-response";
}
let sitekey = "";
if (options.hasOwnProperty('sitekey')) {
sitekey = options.sitekey;
}
this._options = Object.assign({
startMode: this._element.dataset["startMode"] || "auto",
debug: this._element.dataset["debug"],
fieldName: this._element.dataset["solutionField"] || defaultField,
puzzleEndpoint: this._element.dataset["puzzleEndpoint"] || defaultEndpoint,
sitekey: this._element.dataset["sitekey"] || "",
sitekey: sitekey || this._element.dataset["sitekey"] || "",
displayMode: this._element.dataset["displayMode"] || "widget",
lang: this._element.dataset["lang"] || "en",
theme: this._element.dataset["theme"] || "light",
@@ -131,7 +136,7 @@ export class CaptchaWidget {
try {
this.setState(STATE_LOADING);
this.trace('fetching puzzle');
this.trace(`fetching puzzle. sitekey=${sitekey}`);
const puzzleData = await getPuzzle(this._options.puzzleEndpoint, sitekey);
this._puzzle = new Puzzle(puzzleData);
if (this._puzzle && this._puzzle.isZero()) { this._errorCode = errors.ERROR_ZERO_PUZZLE; }
+150 -7
View File
@@ -15,7 +15,8 @@
"devDependencies": {
"esbuild": "^0.25.0",
"esbuild-plugin-inline-worker": "^0.1.1",
"eslint": "^9.23.0"
"eslint": "^9.23.0",
"happy-dom": "^12.0.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
@@ -730,9 +731,9 @@
"integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
@@ -808,6 +809,12 @@
"node": ">= 8"
}
},
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -831,6 +838,18 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
@@ -1205,6 +1224,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/happy-dom": {
"version": "12.10.3",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-12.10.3.tgz",
"integrity": "sha512-JzUXOh0wdNGY54oKng5hliuBkq/+aT1V3YpTM+lrN/GoLQTANZsMaIvmHiHe612rauHvPJnDZkZ+5GZR++1Abg==",
"dev": true,
"dependencies": {
"css.escape": "^1.5.1",
"entities": "^4.5.0",
"iconv-lite": "^0.6.3",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^2.0.0",
"whatwg-mimetype": "^3.0.0"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -1214,6 +1247,18 @@
"node": ">=8"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -1511,6 +1556,12 @@
"node": ">=4"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -1586,6 +1637,36 @@
"punycode": "^2.1.0"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
"dev": true,
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -2013,9 +2094,9 @@
"integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
@@ -2076,6 +2157,12 @@
"which": "^2.0.1"
}
},
"css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true
},
"debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -2091,6 +2178,12 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true
},
"esbuild": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
@@ -2359,12 +2452,35 @@
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true
},
"happy-dom": {
"version": "12.10.3",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-12.10.3.tgz",
"integrity": "sha512-JzUXOh0wdNGY54oKng5hliuBkq/+aT1V3YpTM+lrN/GoLQTANZsMaIvmHiHe612rauHvPJnDZkZ+5GZR++1Abg==",
"dev": true,
"requires": {
"css.escape": "^1.5.1",
"entities": "^4.5.0",
"iconv-lite": "^0.6.3",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^2.0.0",
"whatwg-mimetype": "^3.0.0"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
},
"ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2590,6 +2706,12 @@
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -2644,6 +2766,27 @@
"punycode": "^2.1.0"
}
},
"webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dev": true
},
"whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
"dev": true,
"requires": {
"iconv-lite": "0.6.3"
}
},
"whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
"dev": true
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+3 -2
View File
@@ -6,11 +6,12 @@
"devDependencies": {
"esbuild": "^0.25.0",
"esbuild-plugin-inline-worker": "^0.1.1",
"eslint": "^9.23.0"
"eslint": "^9.23.0",
"happy-dom": "^12.0.0"
},
"scripts": {
"build": "node esbuild.config.mjs",
"test": "echo \"Error: no test specified\" && exit 1",
"test": "node esbuild.test.config.mjs && node --test test/bundle.test.js",
"lint": "eslint js/**/*.js"
},
"dependencies": {
+93
View File
@@ -0,0 +1,93 @@
import test from 'node:test';
import assert from 'node:assert';
import { Window } from 'happy-dom';
const window = new Window({
url: 'https://localhost:8080'
});
global.window = window;
global.document = window.document;
global.HTMLElement = window.HTMLElement;
global.CustomEvent = window.CustomEvent;
global.CSSStyleSheet = window.CSSStyleSheet;
// we have to mock worker too
global.Worker = class Worker {
constructor() {
this.onmessage = null;
this.onerror = null;
}
postMessage(data) {
setTimeout(() => {
if (data.command === 'init') {
this.onmessage?.({ data: { command: 'init' } });
} else if (data.command === 'solve') {
// Simulate immediate solution for zero puzzle
this.onmessage?.({
data: {
command: 'solve',
argument: {
id: BigInt(data.argument.id || 0),
solution: new Uint8Array(8),
wasm: false
}
}
});
}
}, 10);
}
terminate() { }
};
test('CaptchaWidget execute() fires finished event and callback', async (t) => {
document.body.innerHTML = `
<form>
<div class="private-captcha"
data-sitekey="test_key"
data-finished-callback="testFinishedCallback">
</div>
</form>
`;
let callbackCalled = false;
global.window.testFinishedCallback = (widget) => {
callbackCalled = true;
};
const { CaptchaWidget } = await import('../js/widget.js');
const element = document.querySelector('.private-captcha');
assert.ok(element, 'Should find captcha element');
const widget = new CaptchaWidget(element, {
sitekey: 'aaaaaaaabbbbccccddddeeeeeeeeeeee',
debug: true
});
// Set up event listener
let eventFired = false;
element.addEventListener('privatecaptcha:finish', (event) => {
eventFired = true;
assert.ok(event.detail.widget, 'Event should include widget in detail');
assert.strictEqual(event.detail.element, element, 'Event should include element in detail');
});
widget.execute();
// Wait for async operations to complete (assumes zero puzzle mock above)
await new Promise(resolve => setTimeout(resolve, 150));
assert.strictEqual(eventFired, true, 'privatecaptcha:finish event should be fired');
assert.strictEqual(callbackCalled, true, 'Finished callback should be called');
// Verify solution is available
const solution = widget.solution();
assert.ok(solution, 'Widget should have a solution');
assert.ok(typeof solution === 'string', 'Solution should be a string');
console.log('✓ Widget execute test passed');
});