The opposite of every other lab: here the skill is knowing what not to touch. This signup form hides
trap fields a human never sees — a naïve script that fills every input trips them and gets rejected. There's
also a time-trap: submit faster than a human could and you're flagged as a bot. Fill only the
visible fields, pause a moment, and submit.
Signup form with anti-bot traps
POST /newPOST /submit
Click New form to start (this also starts the time-trap clock), fill the three visible fields,
wait a moment, then submit. Toggle Reveal traps to see the hidden fields for learning.
1
The form
No active form — click “New form”.
2
Submit
✓ Account created
✗ Rejected
Current Execution
HONEYPOT
No request executed yet.
Status
Not run yet
Response body
Not run yet
No history yet.
A honeypot is a form field that exists purely to catch bots — a human filling out this form in a real browser will never see it, so it should always arrive empty. A script that blindly fills "every input on the page" fills it anyway, and that's the tell.
Why hidden fields work
Simple bots and scrapers often locate every <input> on a page and populate it without checking whether a person could actually see or reach it. A honeypot field exploits that shortcut: it's real, valid HTML sitting in the DOM, concealed only by CSS. A human never interacts with it because they never see it; a naive automated fill routine has no such restraint.
The three concealment techniques used in this lab
display:nonehp-website / "Website"
The classic approach — display: none removes the element from layout and the accessibility tree entirely. Nothing is rendered; a screen reader skips it too.
Off-screen positioninghp-company / "Company URL"
position: absolute; left: -9999px pushes the field far outside the visible viewport. It still occupies DOM layout space technically, but no user will ever scroll to -9999px to find it.
A 1px × 1px box with clip-based clipping — the technique normally used for accessible screen-reader-only text, repurposed here to hide a trap. It's on-screen in the DOM but visually imperceptible.
Invisible inputs, in general
All three of the above are variations on the same idea: an <input> that is technically present and technically fillable, but invisible or unreachable to a real user. Playwright's default actionability checks (and Selenium's visibility checks) refuse to interact with such elements without an explicit override — which is exactly the signal worth noticing: if your test needs { force: true } or a visibility bypass to fill a field, that's usually a sign the field isn't meant to be filled at all. Not all three concealment techniques resist automation equally, either — the off-screen and clip-based traps can still be forced (they're focusable, just unreachable visually), but a display:none field can never receive focus in a real browser at all, so even a forced .fill() silently leaves it empty; deliberately populating one for a negative test needs a direct value assignment (page.evaluate(...) or a JS executor) instead.
The time-trap
A second, independent defense: the server stamps the moment a form is issued (POST /new) and checks the elapsed time when it's submitted. Submit faster than a human plausibly could — the exact threshold is returned to you as minFillMs in the /new response, so always read it rather than hardcoding a guess — and the submission is rejected even if every visible field and every trap were handled correctly. It catches bots that skip the traps but still submit instantly.
1
User opens the page
The Honeypot Lab loads. No form exists yet — nothing to fill, nothing to submit.
↓
2
Server creates the form — POST /new
Issues a single-use formToken, starts the time-trap clock server-side, and returns honeypotFields (the trap field names) plus minFillMs — the platform hands you what to avoid.
↓
3
Hidden traps are generated
Website, Company URL, and Confirm email render into the DOM, concealed by the three CSS techniques above. They're real inputs; they're just not visible.
↓
4
Visible form renders
Full name, Email, and Password appear as the only fields a real user perceives.
↓
5
A human fills the visible fields only
Because that's all they can see. The traps never receive input — there's nothing to click into.
↓
6
A naive bot fills every input
A script that blindly iterates every <input> on the page, visible or not, populates the traps too — this is exactly the mistake this lab teaches you to avoid.
↓
7
Server validates — POST /submit
Checks run in a fixed order: any trap field non-empty → rejected immediately. Otherwise, elapsed time since /new → rejected if under minFillMs. Otherwise, the real fields are validated (required, valid email, password length).
↓
8
Reject or Accept
Any failure returns success: false with a reason (honeypot_filled, too_fast, missing_fields, invalid_email, weak_password) and a human-readable message. Only a submission that clears every check returns success: true and burns the token.
Where honeypots show up
✓Contact forms
✓Registration / signup forms
✓Newsletter subscriptions
✓Password reset pages
✓Marketing lead-gen forms
✓Internal enterprise portals
Why honeypots over CAPTCHA
A honeypot adds zero friction — a real user never even knows it's there, never solves a puzzle, never waits for an image to load. It's also essentially free to run (a hidden field and a timestamp check, no third-party service, no image generation), which is why many teams reach for it before anything more elaborate.
Advantages
No user-facing friction or UX cost
Cheap to build and run
Effective against unsophisticated, blind auto-fill bots
Easy to combine with other checks
Limitations
A bot that actually checks computed CSS visibility can dodge the trap
Headless browsers with real rendering reduce its effectiveness
Does nothing against a human directly operating the bot
No protection alone against targeted, motivated abuse
Defense in depth
In production, honeypots are rarely the only layer — they're the cheapest one, stacked underneath others: CAPTCHA for suspicious traffic, rate limiting to cap submission velocity per IP/session, device fingerprinting to spot headless/automated environments, behavioral analysis (mouse movement, typing cadence) to distinguish humans from scripts, and a WAF or dedicated bot-detection service in front of everything. A honeypot catches the cheap, blind bots for free, so the more expensive layers only need to deal with what's left.
API vs. browser automation here
API Automation can drive the full POST /new → POST /submit round trip directly — including a clean negative test (deliberately send a value for a honeypotFields name) — without a browser at all, because the trap field names are handed to you in the /new response. What it can NOT do is prove your production form actually conceals those fields from a real user (CSS display:none vs. off-screen vs. clip-based hiding). That's a DOM/rendering question, so verifying the concealment itself — and the "only fill what's visible" technique a real bot-avoidance script needs — requires Playwright or Selenium.
Loading example…
Loading example…
Loading example…
Loading example…
Loading example…
Loading example…
Loading example…
Loading example…
Loading example…
Loading example…
Loading example…
Loading example…
✓
Never "fill every textbox"
A generic loop over every <input> on the page is exactly the anti-pattern this lab exists to catch — it's what trips the honeypot. Target fields explicitly by testid/role instead.
✓
Prefer accessible, explicit locators
getByTestId / getByRole / getByLabel over blind DOM traversal — locators tied to what a user can actually perceive naturally skip what they can't.
✓
Assert hidden fields stay empty
A regression test isn't just "did it succeed" — assert the trap inputs' values are still '' after your fill routine runs, proving your selectors never touched them.
✓
Cover both outcomes
Test the success path (visible fields only, human-like pause) AND the rejection paths (a trap filled, submitted too fast) — a lab, and a real form, is only proven safe if both are verified.
✓
Verify the rejection reason, not just the status
success: false alone isn't enough — assert reason (honeypot_filled / too_fast / missing_fields / ...) and message match the scenario you triggered, so a regression in the wrong check still fails your suite.
✓
Verify the timing restriction explicitly
Don't just always wait "long enough" — write one test that submits immediately and asserts a too_fast rejection, so a regression that removes the time-trap server-side gets caught.
✓
Verify trap field names, don't hardcode them
This lab returns honeypotFields in the /new response specifically so tests can read the current trap names dynamically rather than hardcoding 'website'/'company_url'/'confirm_email' — a safer pattern if the backend ever renames or adds a trap.