Happy path
-
Guest is on the public event page. Clicks the registration CTA.
Modal opens (uses
ui-modal). Browser URL gains#registerhash so back-button closes the modal cleanly. -
Modal renders the form.
Required fields by default: first name, last name, email. Custom questions configured by the organizer (EF-040) appear below the built-in fields. Each access type's specific fields render here too. The submit button label matches the access type's transaction type ("Register" for free, "Continue to checkout" for paid).
-
Guest fills the form. Browser autofill works.
Standard
autocompleteattributes on each field:given-name,family-name,email. Browser autofill populates these correctly without triggering validation errors (per ui-text-input contract). Tab order is: first name → last name → email → custom questions → submit. -
Guest submits.
Form validates client-side. If valid, POST
/v1/public/events/:id/registrations. Loading state shows on the submit button. Form fields lock during submission. -
Success.
Modal transitions to a confirmation state INSIDE the modal (no page reload). Confirmation card shows "You're registered for <Event name>" with calendar links (EF-058), confirmation email note, and a close button. Closing the modal returns to the public event page with the URL hash reverted.
Failure modes
Validation: required field missing
Trigger: guest submits with empty first name.
Per ui-form contract: inline errors on each missing field, focus moves to first errored field, no toast. Error message says what + how-to-fix: "First name is required." Submit button is NOT disabled (so guest can retry after fixing).
Recovery: Guest fills the field, submits again.
Validation: email format invalid
Trigger: guest types "alice@" or "alice@example" (missing TLD).
Inline error: "Please enter a complete email address." Pattern uses RFC 5321-compatible regex; not strict (allows quoted strings, IP-literal domains for edge cases). Validation runs on blur AND on submit, never on initial autofill (per ui-text-input contract).
Recovery: Fix email, resubmit.
Browser back-button while modal open
Trigger: guest opened modal, partially filled, hit browser back.
URL hash #register means back closes the modal cleanly without leaving the page. Form input is preserved IF the guest re-opens within the same session — using sessionStorage scoped to the URL. They don't lose what they typed. Closing the modal does NOT submit.
Recovery: Re-open modal; input restored.
Autofill triggers validation prematurely
Trigger: guest has saved login info; autofill fires on modal open and triggers validation, showing errors before the guest has typed anything.
Per ui-text-input contract: validation does NOT run on autofill events. The first validation pass is on submit OR on field blur (which only fires after autofill is done). Errors don't appear during the autofill cascade.
Recovery: Component contract handles it; harness asserts.
Mobile keyboard covers the submit button
Trigger: guest is on mobile, focused in the email field; keyboard opens; submit button is now hidden behind the keyboard.
The modal is a full-screen sheet on mobile (per ui-modal contract). Submit button is sticky to the bottom of the modal body, with the body scrolling above it when the keyboard pushes things up. Guest can always reach submit without dismissing the keyboard.
Recovery: Sticky-bottom submit; nothing more to do.
Network failure on submit
Trigger: guest clicked submit; connection dropped.
Per ui-form contract: form-error-banner appears above the fields with retry hint. Form input preserved. Retry includes Idempotency-Key so the server dedupes if the original landed.
Recovery: Retry. Idempotent.
Capacity reached mid-fill (FCFS race)
Trigger: guest started filling 2 minutes ago; another guest registered the last spot 30 seconds ago. This guest hits submit.
Server returns 409 with code CAPACITY_FULL. Modal shows: "Registration filled while you were typing. Would you like to join the waitlist?" with the guest's filled values pre-populated for a one-click waitlist conversion. Guest's effort is not lost.
Recovery: Convert to waitlist with one click.
Two-tab race — same guest registering twice in parallel
Trigger: guest opened the page in two tabs (e.g., bookmarked + clicked email link). Submitted in both.
Server uses email-as-uniqueness-key per event_id. The second submission returns 200 with body indicating the existing registration. Guest sees the same confirmation as the first — "You're registered." Backend does NOT create a duplicate row. Both tabs reach the same confirmation state.
Recovery: Idempotent server.
Server returns 500
Trigger: server crashed mid-registration.
Per ui-form contract: error banner with request_id from response header. Form preserved. Retry sends Idempotency-Key. Tone is calm: "Couldn't complete registration. Try again, or reference req_abc123 if you contact support."
Recovery: Retry; support has the ID.
Custom question with conditional logic
Trigger: organizer configured a question that's only required if a previous answer is "Yes" (EF-040 conditional questions).
Conditional fields appear/disappear based on the parent question. Required-only-when-shown — validation respects the condition. The harness asserts: answer "No" to parent, observe conditional field absent (per visibility-teeth: actually display:none is fine here because the field is intentionally not part of the form when not relevant). Answer "Yes," conditional field appears AND becomes required.
Recovery: Form respects conditional logic.
Custom question text is malicious / very long
Trigger: organizer pasted 5000 characters of HTML into a question label.
Question label is rendered as plain text (HTML entities escaped) with a max-display of 500 characters and a "Show more" if truncated. Server-side validation caps custom-question text at 1000 chars per question; longer text is truncated at the catalog level.
Recovery: Truncated at catalog AND render layers.
Confirmation calendar links fail to load
Trigger: confirmation state shows but the calendar links from EF-058 aren't generated (transient backend issue).
Per EF-058: links are generated synchronously in the confirmation render, OR (if async-generated) the confirmation card shows a loading state with "Calendar links will be available in a moment." that hydrates when ready. Confirmation is NOT blocked on calendar link availability.
Recovery: Calendar links hydrate; meanwhile guest can save the page.
Edge cases
Guest already registered
If a guest with the same email tries to register again, server returns 200 with the existing registration. Modal shows: "You're already registered for <Event name> (since <date>)" with calendar links. NOT an error — they're confirmed.
Free event with payment access type also exists
Some events have multiple access types (free + paid VIP). The modal includes an access-type picker at the top if >1 type is available. Guest picks; form fields adjust accordingly.
Embed-friendly modal (organizer embeds the form on their own site)
Out of scope for this branch. Embedding is a separate EF capability. The branch's modal assumes it's on the canonical event page.
Mobile keyboard with predictive input
Email field uses type="email" + inputmode="email" so the mobile keyboard shows the @ key prominently. Predictive text doesn't auto-correct email addresses.
Page evaluation
| Surface | Discoverability | Error UX | Layout | Orientation |
|---|---|---|---|---|
| Modal · open | Modal title matches access type ("Register for <Event>" or "Buy tickets for <Event>"). | Errors per ui-form contract; inline + summary if 2+. | ui-modal full-screen sheet on mobile, max-width 540px on desktop. | Modal title is the focused element on open; aria-modal=true. |
| Modal · submitting | Submit button shows spinner + busy state. | Form fields locked (readonly) during submit. | No layout shift when busy state activates. | aria-live="polite" announces "Submitting registration…" |
| Modal · confirmation state | "You're registered" past-tense; calendar buttons visible. | If post-success errors appear (e.g., calendar link generation fails), they don't block the confirmation message. | Calendar buttons stack vertically on mobile. | Heading shifts from "Register" to "Confirmed!" — clear state transition. |
| Modal · capacity-full mid-submit | Waitlist conversion CTA is the primary action with the guest's input pre-populated. | Calm tone: "Registration filled while you were typing." not "Failed." | Waitlist context visible immediately. | Guest's input doesn't disappear. |
Acceptance signals
- POST
/v1/public/events/:id/registrationsreturns 200 (or 200 with already-registered body) with aregistration_id. - D1 row exists in
event_registrationswith the submitted email + first/last name + access_type_id. - D1 row exists in
event_guestslinked to the new registration withstatus='confirmed'. - Confirmation email queued in notification_outbox (per EF-052 confirmation association).
- Modal transitions to confirmation state without page reload.
- URL hash returns to
#(no#register) on modal close. - document.title remains the event page's title throughout (modal does not change document.title).
- No console errors at severity ≥
warn. - Idempotency-Key header sent on retry attempts.
Stable test attributes
Branch-specific only; trunk and component attributes inherited.
Visibility teeth. Each attribute must be present AND effectively visible when the relevant state is active.
| data-test | Where | Purpose |
|---|---|---|
register-modal | Modal portal (uses ui-modal) | Registration modal instance |
register-form | Inside register-modal | The registration form (uses ui-form) |
register-access-type-picker | Inside register-form | Access type chooser; absent for single-type events |
register-field-first-name | Inside register-form | First name input |
register-field-last-name | Inside register-form | Last name input |
register-field-email | Inside register-form | Email input |
register-custom-question | Inside register-form | Each custom question (multiple instances) |
register-submit-cta | Inside register-form | Submit button; label varies by access type |
register-confirmation | Inside register-modal after success | Confirmation card replacing the form |
register-confirmation-h1 | Inside register-confirmation | "You're registered" past-tense heading |
register-confirmation-calendar | Inside register-confirmation | Calendar buttons region (uses EF-058 attributes) |
register-already-registered | Inside register-modal · already-registered state | Body when guest already has a registration |
register-capacity-full | Inside register-modal · capacity-full state | "Filled while you were typing" with waitlist CTA |
register-waitlist-convert-cta | Inside register-capacity-full | One-click waitlist conversion |
register-network-error-banner | Top of register-form (uses ui-form-error-banner) | Reused from ui-form contract |
register-modal-close | Inside register-modal (uses ui-modal-close) | Reused from ui-modal contract |
Agent test plan
Inherits public-event-page trunk preconditions. Component contract chaining applies for ui-modal (register-modal instance), ui-form (register-form), ui-text-input (each register-field-*).
Probe list
- modal-opens-on-cta-click: from public-event-page, click register-cta, assert register-modal visible AND URL hash = #register
- modal-renders-form-fields: assert register-field-first-name, register-field-last-name, register-field-email all visible
- happy-path-submit: fill all fields with valid data, click register-submit-cta, assert API POST returns 200 AND register-confirmation visible AND register-confirmation-calendar visible
- email-validation-incomplete: fill "alice@", blur, assert ui-text-input-error visible with text matching "complete email"
- back-button-closes-modal: open modal, fill, browser back, assert modal closed AND URL hash empty AND form input preserved in sessionStorage
- back-then-reopen-restores-input: continuation, click register-cta again, assert input restored
- autofill-no-validation: simulate browser autofill, assert ui-text-input-error absent on all fields
- mobile-sticky-submit: at 375x667, focus email field (keyboard simulated), assert register-submit-cta still visible (not occluded by simulated keyboard)
- network-failure-with-retry: stub POST to abort, submit, assert ui-form-error-banner visible AND form input preserved; cleanup retry, assert Idempotency-Key header sent
- capacity-full-mid-fill: stub POST to 409 CAPACITY_FULL, submit, assert register-capacity-full visible AND register-waitlist-convert-cta visible AND form input still rendered
- waitlist-conversion: continuation, click register-waitlist-convert-cta, assert API POST to waitlist endpoint AND register-confirmation visible
- two-tab-race: stub POST to 200 with already-registered body on second call, submit twice, assert both reach register-confirmation
- 500-error-with-request-id: stub POST to 500 with X-Request-Id, assert ui-form-error-banner contains "req_"
- already-registered: navigate with existing-guest-email pre-filled, submit, assert register-already-registered visible (NOT register-confirmation — distinct state)
- conditional-field-shows-when-yes: answer parent question "Yes", assert conditional register-custom-question visible AND required
- conditional-field-hides-when-no: answer "No", assert conditional field absent (display:none acceptable; field is intentionally not in form)
- malicious-question-text-escaped: render with question label containing HTML, assert no actual HTML elements rendered (text content matches sanitized output)
- calendar-links-async-loading-state: stub calendar generation to delay 2s, assert confirmation appears WITHOUT calendar links AND a loading state visible