Skip to content
Merged
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
8 changes: 6 additions & 2 deletions packages/web/src/components/CodeCaptcha.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -418,8 +418,12 @@ export function CodeCaptcha({
{/* Math Problem or Canvas Code Image */}
<div className="flex-1 flex items-center justify-center">
{currentStyle === 'math' ? (
<div className="text-center py-8">
<p className="text-sm text-gray-400 mb-2">What is:</p>
// The "What is:" label floats at the top so the NUMBER is the
// vertically-centered element — otherwise the label pushed the
// number below the panel center and the lone refresh button
// (panel-centered) sat above it.
<div className="relative w-full text-center py-8 flex items-center justify-center min-h-[5.5rem]">
<span className="absolute top-1 left-0 right-0 text-sm text-gray-400">What is:</span>
<p className="text-5xl font-bold text-teal-300 tracking-wider">
{mathProblem} = ?
</p>
Expand Down
74 changes: 74 additions & 0 deletions tests/diagnostics/captcha-layout.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { test, expect, Page } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';

/**
* CodeCaptcha (ALTCHA-style) layout diagnostic. Measures whether the refresh
* ("try different style") button is vertically centered against the challenge
* content, in BOTH the math state (no speaker button) and the text/complex
* state (speaker button present). Report-only — no app behavior changed.
*/
const OUT = path.resolve(process.cwd(), 'test-artifacts/captcha');

async function measure(page: Page) {
return page.evaluate(() => {
const refresh = document.querySelector('button[title="Try different style"]') as HTMLElement | null;
const speaker = document.querySelector('button[title="Listen to code"]') as HTMLElement | null;
// the challenge box (the bordered panel) and the math number / canvas
const num = [...document.querySelectorAll('p')].find((p) => /=\s*\?/.test(p.textContent || '')) as HTMLElement | null;
const canvas = document.querySelector('canvas') as HTMLElement | null;
const content = (num ?? canvas) as HTMLElement | null;
const box = refresh?.closest('.rounded-lg') as HTMLElement | null;
const c = (el: HTMLElement | null) => { if (!el) return null; const r = el.getBoundingClientRect(); return { top: Math.round(r.top), bottom: Math.round(r.bottom), cy: Math.round(r.top + r.height / 2), h: Math.round(r.height) }; };
return {
style: num ? 'math' : (canvas ? 'image' : 'unknown'),
hasSpeaker: !!speaker,
refresh: c(refresh),
content: c(content),
panel: c(box),
};
});
}

test.describe('captcha layout diagnostic @geometry', () => {
test('refresh button vertical centering (math vs speaker state)', async ({ page }) => {
fs.mkdirSync(OUT, { recursive: true });
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto('/signup');
await page.waitForTimeout(1500);
await page.locator('button[title="Try different style"]').first().waitFor({ timeout: 15000 });

const results: any[] = [];
// State 1: as first shown (math, no speaker)
let m = await measure(page);
await page.screenshot({ path: path.join(OUT, `state-${m.style}-speaker${m.hasSpeaker}.png`), clip: m.panel ? { x: 0, y: Math.max(0, m.panel.top - 20), width: 1280, height: m.panel.h + 40 } : undefined }).catch(() => {});
results.push(m);

// Click refresh until we land on an image/text style (speaker appears), to
// compare. Bounded so we don't loop forever if RNG keeps picking math.
for (let i = 0; i < 12 && !(results.find((r) => r.hasSpeaker)); i++) {
await page.locator('button[title="Try different style"]').click();
await page.waitForTimeout(500);
const cur = await measure(page);
if (cur.style !== results[results.length - 1].style || cur.hasSpeaker !== results[results.length - 1].hasSpeaker) {
await page.screenshot({ path: path.join(OUT, `state-${cur.style}-speaker${cur.hasSpeaker}.png`), clip: cur.panel ? { x: 0, y: Math.max(0, cur.panel.top - 20), width: 1280, height: cur.panel.h + 40 } : undefined }).catch(() => {});
results.push(cur);
}
}

const report = results.map((r) => ({
style: r.style,
hasSpeaker: r.hasSpeaker,
refreshCy: r.refresh?.cy,
contentCy: r.content?.cy,
panelCy: r.panel?.cy,
refreshVsContent: r.refresh && r.content ? r.refresh.cy - r.content.cy : null,
refreshVsPanel: r.refresh && r.panel ? r.refresh.cy - r.panel.cy : null,
}));
fs.writeFileSync(path.join(OUT, 'report.json'), JSON.stringify(report, null, 2));
// eslint-disable-next-line no-console
for (const r of report) console.log('[captcha] ' + JSON.stringify(r));

expect(report.length, 'measured at least the math state').toBeGreaterThan(0);
});
});
Loading