feat(forms): add local Form A presentation preview

This commit is contained in:
DeCentN2Madness committed 2026-10-25 13:04:00 -07:00
1 parent b102a061fe
commit 7a6cab29db
13 files changed
+983 -8

No files matched your search

+2
View File
@@ -5,4 +5,6 @@ PUBLIC_COUNTERS_MOCK_RESULT=success
PUBLIC_AIA_FORMS_MODE=off
PUBLIC_AIA_FORMS_LIVE_APPROVED=false
PUBLIC_AIA_FORMS_MOCK_RESULT=success
# Explicit, local-only Form A meeting preview; requires PUBLIC_AIA_FORMS_MODE=mock.
PUBLIC_AIA_FORM_A_PREVIEW=false
PUBLIC_ALLOW_INDEXING=false
+46
View File
@@ -0,0 +1,46 @@
# AIA Form A Jotform styling and conditional-button guide
This directory contains the nonproduction visual handoff for the new AIA Form A. [`aia-form-a.css`](./aia-form-a.css) is intended for Jotform Classic Forms' **Form Designer → Styles → Inject Custom CSS** after the form is created and its settings are approved.
The CSS translates the existing AIA form treatment: Rubik typography, blue labels and text, `#fcf9f5` inputs with `#edeef3` borders, the `#ffbd59` filled button, the `#ff914d` hover/focus border, four-pixel corners, visible validation errors, and strong keyboard focus. The Astro page supplies the heading, introduction, privacy-policy link, and surrounding card. The stylesheet therefore removes the Jotform surface rather than adding another header, footer, or nested card.
The selectors target Jotform Classic Forms identifiers such as `#form`, `.form-all`, `.form-line`, `.form-label`, `.form-sub-label`, `.form-required`, `.form-textbox`, `.form-dropdown`, `.form-textarea`, `.form-error-message`, and `.form-submit-button`. Jotform's final generated DOM must be checked in the account before relying on pixel-level parity. The stylesheet does not hide required indicators, error messages, honeypots, or focus outlines, and it does not request fonts or other external assets.
## Installation and comparison
1. Create or open the approved blank Form A in AIA's HIPAA-enabled Jotform account.
2. Keep the form disabled while field, PHI, notification, conditional-logic, and language review is pending.
3. Paste the contents of `aia-form-a.css` into Jotform's custom CSS area.
4. Compare the Jotform preview with the Astro Form A mock at 390px and 1440px. Check the form surface, label rhythm, input height, button fill, outlined hover/focus state, error state, and mobile reflow.
5. Check both English and Spanish configurations. Confirm translated labels, required indicators, validation messages, confirmation copy, and the accessible iframe title supplied by the parent Astro integration.
6. Test the form in a real embedded iframe and on the direct hosted-form fallback. Confirm the iframe has a practical mobile height, does not create horizontal overflow, and does not trap keyboard focus.
The current Astro meeting preview remains `off` by default and can only be shown in a non-indexing mock build. It does not load Jotform and is not a live submission test.
## Form context and button labels
The current Form A design uses a hidden `formContext` value with fixed, approved values such as `contact` and `landing`. Jotform does not provide a native conditional rule that changes one Submit button's accessible label safely in every context. If owner-approved context-specific labels are required, use multiple native Jotform Submit buttons and Jotform's conditional visibility rules:
- Add one native Submit button for each approved context/language label.
- Show exactly one intended Submit button for the current, validated `formContext`.
- Keep every button a real Jotform Submit control; do not use CSS `::before`/`::after` text as a label.
- Verify that every visible variant submits through the same approved form and preserves the honeypot and validation behavior.
- Make the no-context and invalid-context states fail closed: no ambiguous extra button and no unintended submission path.
- Verify keyboard order, focus visibility, screen-reader names, English/Spanish labels, and mobile layout for every variant.
The four campaign pages remain mapped to Form B in the production architecture. Their current Form A mock override is temporary meeting-preview behavior only. Do not add an external Astro button that attempts to submit a cross-origin iframe.
## Verification checklist
- [ ] Form A is disabled while configuration is reviewed.
- [ ] Exact question types, required flags, choices, conditional logic, and `formContext` values are approved.
- [ ] English and Spanish labels, validation, confirmation, and button variants are reviewed by fluent-language owners.
- [ ] The hidden honeypot remains present, effective, and excluded from user-facing visual changes.
- [ ] Exactly one native Submit button is visible for every valid context.
- [ ] Invalid or missing context does not expose an unintended submission path.
- [ ] No PHI-bearing answer, PDF, recipient, redirect, webhook, or HTTP POST forwarding is inherited without privacy/security approval.
- [ ] The iframe title, loading/error state, keyboard behavior, and mobile height are accepted.
- [ ] Synthetic testing uses only approved synthetic values and a separately authorized test form.
- [ ] No Jotform, Contabo, analytics, advertising, or production API request is made by off/mock builds.
JF-01 through JF-09, privacy/security review, operations approval, fluent-language review, and production activation remain pending. This guide does not authorize Jotform account changes or live integration.
+192
View File
@@ -0,0 +1,192 @@
/*
* AIA Form A — Jotform Classic Forms custom CSS
*
* Paste into Jotform Form Designer > Styles > Inject Custom CSS after
* creating the approved Form A. These selectors are scoped to Jotform's
* Classic Forms structure and do not replace Jotform validation or controls.
* Validate the final generated form DOM before activation; no account access
* or pixel-perfect Jotform comparison was performed for this artifact.
*/
/* The Astro page supplies the surrounding heading, copy, privacy link and
card. Keep the iframe's own surface quiet so it does not create a second
card inside the page's existing form container. */
#form,
#form .form-all {
background: #fff;
border: 0;
box-shadow: none;
color: #254080;
font-family: Rubik, Arial, sans-serif;
max-width: 100%;
}
#form .form-all {
margin: 0;
padding: 0;
}
#form .form-line {
background: transparent;
border: 0;
margin: 0;
padding: 0 0 1rem;
}
#form .form-label,
#form .form-label-left,
#form .form-label-right,
#form .form-label-top {
color: #254080;
font-size: .9rem;
font-weight: 700;
line-height: 1.5;
margin: 0 0 .4rem;
padding: 0;
}
#form .form-sub-label {
color: #254080;
font-size: .78rem;
line-height: 1.5;
}
#form .form-required {
color: #a21c28;
margin-left: .2rem;
}
#form input.form-textbox,
#form input.form-textbox.form-phone,
#form input.form-textbox.form-email,
#form select.form-dropdown,
#form textarea.form-textarea {
background: #fcf9f5;
border: 1px solid #edeef3;
border-radius: 4px;
box-sizing: border-box;
color: #254080;
font: inherit;
line-height: 1.5;
min-height: 2.9rem;
padding: .7rem .8rem;
width: 100%;
}
#form textarea.form-textarea {
min-height: 7rem;
resize: vertical;
}
#form input.form-textbox:focus-visible,
#form select.form-dropdown:focus-visible,
#form textarea.form-textarea:focus-visible,
#form input[type='radio']:focus-visible + label,
#form input[type='checkbox']:focus-visible + label,
#form .form-submit-button:focus-visible {
outline: 3px solid #115bbd;
outline-offset: 3px;
}
#form .form-line.form-line-error input.form-textbox,
#form .form-line.form-line-error select.form-dropdown,
#form .form-line.form-line-error textarea.form-textarea {
border-color: #a21c28;
outline: 2px solid #a21c28;
outline-offset: 1px;
}
#form .form-error-message,
#form .form-line-error .form-error-message {
color: #a21c28;
font-size: .85rem;
font-weight: 700;
line-height: 1.5;
margin: .45rem 0 0;
}
#form .form-radio-item,
#form .form-checkbox-item {
color: #254080;
line-height: 1.5;
margin: 0 1rem .5rem 0;
}
#form .form-radio,
#form .form-checkbox {
accent-color: #254080;
}
#form .form-buttons-wrapper {
background: transparent;
border: 0;
margin: .25rem 0 0;
padding: 0;
text-align: left;
}
#form .form-submit-button,
#form button[type='submit'] {
background: #ffbd59;
border: 2px solid #ffbd59;
border-radius: 4px;
box-shadow: none;
color: #254080;
cursor: pointer;
font: inherit;
font-weight: 400;
line-height: 1.5;
min-height: 3rem;
padding: .65rem 1.2rem;
transition: .3s linear all;
width: 100%;
}
#form .form-submit-button:hover,
#form .form-submit-button:focus-visible,
#form button[type='submit']:hover,
#form button[type='submit']:focus-visible {
background: transparent;
border-color: #ff914d;
color: #254080;
}
#form .form-submit-button:disabled,
#form button[type='submit']:disabled {
cursor: not-allowed;
opacity: .65;
}
/* Jotform error and required content must remain visible. Do not hide these
classes in a campaign-specific override. */
#form .form-validation-error,
#form .form-required,
#form .form-error-message {
visibility: visible;
}
@media (max-width: 640px) {
#form .form-line,
#form .form-label,
#form .form-label-left,
#form .form-label-right,
#form .form-label-top {
width: 100%;
}
#form .form-all {
width: 100%;
}
#form .form-submit-button,
#form button[type='submit'] {
min-height: 3rem;
}
}
@media (prefers-reduced-motion: reduce) {
#form .form-submit-button,
#form button[type='submit'] {
transition: none;
}
}
@@ -0,0 +1,121 @@
# Phase 9 Form A Meeting Preview — 2026-10-22
## Scope and baseline
This report records a local, non-submitting preview of the new Jotform Form A visitor journey. The repository remained on `faithful-astro-migration` at `ce17728e0b9a0e148941cfa47b2d074859b24456` when this work began. The Git index was empty, and no production configuration or Jotform account setting was changed.
The preview is available only when all of the following are true:
```text
PUBLIC_AIA_FORMS_MODE=mock
PUBLIC_AIA_FORM_A_PREVIEW=true
PUBLIC_ALLOW_INDEXING is not true
```
Default builds remain `off`. `live` remains fail-closed because no approved live adapter or production form identifier is present. Indexing-enabled builds omit the preview and retain the ordinary inactive fallback.
## Placements and fields
The full Form A preview is rendered on `/contact` and `/es/contact`. It contains the required name, required phone, conditionally required email, preferred contact method, optional message, sensitive-information caution, and a local demonstration control. It intentionally does not include an enquiry-category field. Spanish labels, including the dynamic email-requirement text and internal-preview label, are supplied through the existing locale message architecture.
The compact preview is rendered on `/lp/aba-therapy`, `/lp/aba-insurance`, `/lp/aba-ahcccs`, and `/lp/aba-private-pay`. It contains only name, phone, conditional email, and preferred contact method. It has no message, enquiry-category, or sensitive-information notice. `/lp/early-intervention` was not changed and remains outside this meeting-preview override.
The contact buttons use `Submit` and `Enviar`. Campaign buttons use the approved page CTA: `Request a Free Consultation`, `Request Insurance Verification`, `Request a Benefits Check`, or `Discuss Private-Pay Options`. The production route contract still assigns all four campaign pages to Form B; Form A is a temporary mock-only review override.
Final copy cleanup was verified on 2026-10-23 UTC. In mock mode, each compact campaign preview has one short status line beneath its controls: “Preview only — nothing is submitted.” The same line retains the telephone and privacy-policy links. In off mode, the four campaign panels contain no form-availability notice; a plain support line retains the telephone and privacy-policy links. The compact panels contain no free-text field and therefore contain no sensitive-information disclaimer. Early Intervention likewise contains no free-text field, disclaimer, or unavailable-form notice; its existing telephone CTAs, hours, clinic address, and directions remain available. The hero and separate clinic-contact block retain their contact information without repeating form status.
## Visual fidelity
The mock now uses the established AIA form values from the existing Astro components and tokens: `#ffbd59` button fill and border, transparent hover/focus fill, `#ff914d` hover/focus border, `#254080` text, four-pixel corners, `#fcf9f5` input backgrounds, and `#edeef3` input borders. Keyboard focus retains a separate three-pixel outline. The compact variant relies on the campaign page's existing white form container instead of introducing a nested card surface.
`integrations/jotform/aia-form-a.css` translates these values to narrowly scoped Jotform Classic Forms selectors. `integrations/jotform/README.md` documents installation, iframe/mobile comparison, multilingual verification, honeypot checks, and the multiple-native-Submit-button pattern needed for context-specific accessible labels. The Jotform selectors still require verification against the form's actual generated DOM.
The component does not contain a form element, action, iframe, Jotform URL, form ID, fetch call, persistence mechanism, logging call, email operation, analytics event, or advertising event. The control is `type="button"`; its success state explicitly says that no information was sent or saved. The preview's fields are disabled in the ordinary/off build.
## Validation behavior
Chromium verifies that:
- Name, phone, and preferred contact are required.
- Selecting Email makes the email field required and validates its format.
- Phone preference permits the optional email field to remain empty.
- The local success state is shown only after the local checks pass.
- English and Spanish labels render in their respective pages.
- Every placement uses its approved contextual button text as actual accessible text.
- The rendered button begins with `#ffbd59` fill and border, changes to a transparent fill with `#ff914d` border on hover/focus, retains `#254080` text, and keeps a visible focus outline.
- The sensitive-information caution appears only with the contact-page message field.
- No native `<form>` or iframe is emitted by the preview.
- The six preview routes have one main H1 and no horizontal overflow at 390px and 1440px.
- JavaScript-disabled `/contact` and `/lp/aba-therapy` retain the preview content and an explicit local-preview/no-send notice.
- All five landing pages contain no unavailable/not-enabled form wording and no sensitive-information disclaimer without a free-text field.
- All five landing pages retain non-submitting controls and avoid horizontal overflow at 390px and 1440px in both off and mock builds.
## Guarded browser evidence
Command:
```bash
PLAYWRIGHT_MODULE=/home/alice/projects/wrk/aia/www/aia-aba-landing-pages/node_modules/playwright/index.mjs \
CHROMIUM_EXECUTABLE=/etc/profiles/per-user/alice/bin/chromium \
node tools/form-a-preview-browser.mjs
```
The final mock run completed 16 checks: 14 route/viewport checks (the two Contact pages, four campaign pages, and Early Intervention at 390px and 1440px) and two JavaScript-disabled checks. A separate off-mode run completed ten checks across all five landing pages at the same two widths. Both passed with zero failures and zero production API/form/analytics attempts. The mock run intercepted nine automatic Leaflet requests to `unpkg.com` from the Contact-page map; they were recorded as blocked external resources and did not transmit. No Jotform, AIA API, Google Analytics, Google Tag Manager, Google Ads, or other production request was observed or attempted.
The first preview pass is preserved as the before set in `/tmp/aia-form-a-preview-furat7/`. The final copy-cleanup evidence is in `/tmp/aia-form-a-preview-3YPMT1/`:
| Evidence | Before SHA-256 | After SHA-256 |
|---|---|---|
| `contact-390.png` | `8add0699084e6bced38a8cb0e1c3946da01c2af8695c68d436448fb6108aa1db` | `2a53669580143e0ce0f5fe284bd93b2023bf899e676e4863165e7930ed08b4fc` |
| `contact-1440.png` | `1955196fe16620a5d6e358b4d512989b391389cb3bf02e292f23364202dbb142` | `9d0433eeb76c3f120fd7f75cf8af3edc7e08835736946edbc23312bafc359d64` |
| `campaign-390.png` | `2ed1f939fe8c82a7c282e2568bd174bbe3f7bdf9dd9c3ac5e0107120439a9c0e` | `b63a2dbd9c4b1bf8266021044084a414673ffd6500e2f9a45d24034f77008c84` |
| `campaign-1440.png` | `e352eacd860530ec75cc887a2784dca7287d8520b351e0ed16bda4a340b9e597` | `138c5030e406af1f7689f9ba39fb17a3faf2339c077f86320ea8a9cb56f0bf67` |
The final mock result is `/tmp/aia-form-a-preview-3YPMT1/results.json` with SHA-256 `177711789606a776a747432b6f94d7c9281ac65a8580d20f4075bace933e10a7`. The off-mode result is `/tmp/aia-form-a-preview-e5FUCA/results.json` with SHA-256 `99ff3cb2837f650b72afc37f4c5ff0ad0927bb6117ebeb29af406b72fb1cd8ff`.
### Follow-up validation — 2026-10-25
The final implementation review corrected the Spanish internal-preview label and dynamic email-requirement text. A guarded Chromium mock run completed 16 checks across `/contact`, `/es/contact`, the four campaign pages, and Early Intervention at 390px and 1440px, plus two JavaScript-disabled checks. A separate off-mode run completed ten checks across the five landing pages at the same widths. Both runs passed with zero failures and zero Jotform, AIA API, analytics, advertising, or other production attempts. The Contact-page map made nine automatic Leaflet requests to `unpkg.com` during the mock run; the test guard blocked them before transmission.
## Automated validation
The following checks passed:
| Check | Result |
|---|---:|
| `npm run check` | 133 files, 0 diagnostics |
| `npm run test:markdown` | 20 passed |
| `npm run test:publication` | 33 passed |
| `npm run test:seo` | 35 passed |
| `npm run test:library` | 53 passed |
| `npm run test:routes` | 10 passed |
| Phase 9, Form A, and campaign tests | 21 passed |
| `npm run audit:links` | 0 broken internal links |
| `npm run audit:images` | 0 missing mapped images |
| `npm run audit:blog` | 0 audit failures |
| Normal build | 128 pages |
| Indexing-enabled build with preview flag set | 127 pages; preview omitted |
| `phase10-artifact-verify.mjs --mode=staging` | passed; 128 HTML routes, 0 sitemap URLs, 0 hreflang links |
The final normal staging output was restored after the indexing-enabled verification. Its six target pages contain no active Form A preview marker and retain `noindex,nofollow`. The temporary `/lp` review index remains a staging-only route.
## Files changed
- `src/components/FormAPreview.astro` — new full/compact local preview component.
- `src/components/pages/ContactPage.astro` — explicit gated full preview placement.
- `src/components/campaign/ConsultationForm.astro` — explicit gated compact preview placement for the four Form B campaign journeys.
- `src/utils/form-integration.ts` — non-indexing mock-only Form A preview gate.
- `src/utils/form-messages.ts` — English and Spanish preview labels and states.
- `.env.example` — default-disabled preview flag documentation.
- `tools/form-a-preview.test.mjs` — source-level safety and placement regression checks.
- `tools/form-a-preview-browser.mjs` — guarded Chromium, responsive, validation, no-JavaScript, and network checks.
- `tools/phase9-nonproduction.test.mjs` — preview-gate regression coverage.
- `integrations/jotform/aia-form-a.css` — scoped Jotform Classic Forms visual handoff.
- `integrations/jotform/README.md` — installation and conditional-button verification guide.
- `reports/phase-9-form-a-meeting-preview-2026-10-22.md` — this evidence report.
## Remaining approval requirements
This is suitable for an internal Clinical Director demonstration only. It is not a live intake path. The owner still must complete the new Form A Jotform configuration review, including minimum fields, required flags, English/Spanish behavior, PHI protection, notification and autoresponder contents, confirmation behavior, recipient access, retention, embedding, and synthetic acceptance. The Form A logical mapping remains distinct from the four campaign pages' production Form B mapping. No live Jotform ID or URL was added.
P9-03 privacy/security approval, P9-05 analytics and advertising approval, P9-07 privacy/legal approval, and Phase 10 production authorization remain open. The local preview does not establish HIPAA compliance, operational readiness, or successful submission handling.
+9 -1
View File
@@ -25,7 +25,8 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
<label>{text.email}<input type="email" name="email" autocomplete="email" disabled={mode === 'off'} /></label>
<label>{text.phone}<input type="tel" name="phone" autocomplete="tel" disabled={mode === 'off'} /></label>
<label>{text.service}<select name="service" disabled={mode === 'off'}>{text.options.map((label) => <option>{label}</option>)}</select></label>
<label>{text.message}<textarea name="message" rows="5" disabled={mode === 'off'}></textarea></label>
<label>{text.message}<textarea name="message" rows="5" aria-describedby="contact-sensitive-note" disabled={mode === 'off'}></textarea></label>
<p id="contact-sensitive-note" class="contact-form-card__caution" role="note">{text.formAPreview.caution}</p>
<Button type="button" data-mock-submit disabled={mode === 'off'} aria-describedby="contact-form-note" class="contact-form-card__submit">{text.submit}</Button>
<p id="contact-form-note" class="contact-form-card__note" data-form-status>{mode === 'mock' ? text.mock : <>{text.disabled} <ContactObfuscation phone="(480) 687-7099" hrefValue="+14806877099" obfuscatedText="9907-786 (084)" />.</>}</p>
</div>
@@ -99,4 +100,11 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
margin: 0;
text-align: center;
}
.contact-form-card__caution {
color: var(--color-muted);
font-size: .85rem;
line-height: 1.65;
margin: -.25rem 0 0;
}
</style>
+279
View File
@@ -0,0 +1,279 @@
---
import { formAPreviewEnabled, formModeForRuntime, resolveFormMode } from '../utils/form-integration';
import { formMessages } from '../utils/form-messages';
interface Props {
lang?: 'en' | 'es';
variant?: 'full' | 'compact';
submitText?: string;
}
const { lang = 'en', variant = 'full', submitText } = Astro.props;
const text = formMessages(lang).formAPreview;
const config = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE, import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED);
const mode = formModeForRuntime(config);
const enabled = formAPreviewEnabled(import.meta.env.PUBLIC_AIA_FORM_A_PREVIEW, import.meta.env.PUBLIC_ALLOW_INDEXING, mode);
const prefix = `form-a-${variant}`;
const buttonLabel = submitText ?? text.submit;
---
<div
class:list={['form-a-preview', `form-a-preview--${variant}`]}
role="group"
aria-labelledby={`${prefix}-heading`}
data-form-a-preview
data-form-a-variant={variant}
data-form-a-preview-enabled={String(enabled)}
data-form-mode={mode}
data-required-error={text.requiredError}
data-email-error={text.emailError}
data-email-required={text.emailRequired}
data-email-optional={text.optional}
data-success={text.success}
>
<p class="form-a-preview__eyebrow">{text.internalPreview}</p>
<h3 id={`${prefix}-heading`}>{text.heading}</h3>
{variant === 'full' && <p class="form-a-preview__note">{text.previewNote}</p>}
<div class="form-a-preview__fields">
<div class="form-a-preview__field form-a-preview__field--full">
<label for={`${prefix}-name`}>{text.name}<span aria-hidden="true"> *</span></label>
<input id={`${prefix}-name`} name="name" autocomplete="name" required disabled={!enabled} />
</div>
<div class="form-a-preview__field">
<label for={`${prefix}-phone`}>{text.phone}<span aria-hidden="true"> *</span></label>
<input id={`${prefix}-phone`} name="phone" type="tel" autocomplete="tel" required disabled={!enabled} />
</div>
<div class="form-a-preview__field">
<label for={`${prefix}-email`}>{text.email} <span id={`${prefix}-email-requirement`} class="form-a-preview__optional">({text.optional})</span></label>
<input id={`${prefix}-email`} name="email" type="email" autocomplete="email" aria-describedby={`${prefix}-email-requirement`} disabled={!enabled} />
</div>
<div class="form-a-preview__field form-a-preview__field--full">
<label for={`${prefix}-preferred`}>{text.preferredContact}<span aria-hidden="true"> *</span></label>
<select id={`${prefix}-preferred`} name="preferredContact" required disabled={!enabled}>
<option value="">{text.choose}</option>
<option value="phone">{text.phoneOption}</option>
<option value="email">{text.emailOption}</option>
</select>
</div>
{variant === 'full' && <div class="form-a-preview__field form-a-preview__field--full">
<label for={`${prefix}-message`}>{text.message} <span class="form-a-preview__optional">({text.optional})</span></label>
<textarea id={`${prefix}-message`} name="message" rows="4" disabled={!enabled}></textarea>
<p class="form-a-preview__caution" role="note">{text.caution}</p>
</div>}
</div>
<button class="form-a-preview__submit" type="button" data-preview-submit disabled={!enabled}>{buttonLabel}</button>
{variant === 'compact' ? (
<p class="form-a-preview__status" data-preview-status data-form-status-notice role="status" aria-live="polite">
{text.previewOnly} <a href="tel:+14806877099">{text.callAia}</a>. <a href="/privacy-policy">{text.privacyPolicy}</a>.
</p>
) : (
<p class="form-a-preview__status" data-preview-status role="status" aria-live="polite">{enabled ? text.previewNote : text.noScript}</p>
)}
{variant === 'full' && <noscript><p class="form-a-preview__noscript">{text.noScript}</p></noscript>}
</div>
{enabled && <script>
const preview = document.querySelector<HTMLElement>('[data-form-a-preview][data-form-a-preview-enabled="true"]');
if (preview) {
const preferred = preview.querySelector<HTMLSelectElement>('select[name="preferredContact"]');
const email = preview.querySelector<HTMLInputElement>('input[name="email"]');
const requirement = preview.querySelector<HTMLElement>('[id$="-email-requirement"]');
const button = preview.querySelector<HTMLButtonElement>('[data-preview-submit]');
const status = preview.querySelector<HTMLElement>('[data-preview-status]');
const initialStatus = status?.innerHTML ?? '';
const updateEmailRequirement = () => {
const required = preferred?.value === 'email';
if (email) {
email.required = required;
email.setAttribute('aria-required', String(required));
}
if (requirement) {
const label = required ? preview.dataset.emailRequired : preview.dataset.emailOptional;
requirement.textContent = `(${label ?? ''})`;
}
};
preferred?.addEventListener('change', updateEmailRequirement);
updateEmailRequirement();
button?.addEventListener('click', () => {
const name = preview.querySelector<HTMLInputElement>('input[name="name"]');
const phone = preview.querySelector<HTMLInputElement>('input[name="phone"]');
const fields = [name, phone, preferred].filter(Boolean) as Array<HTMLInputElement | HTMLSelectElement>;
fields.forEach((field) => field.removeAttribute('aria-invalid'));
email?.removeAttribute('aria-invalid');
const missing = fields.filter((field) => !field.value.trim());
const invalidEmail = preferred?.value === 'email' && (!email?.value.trim() || !email.validity.valid);
if (missing.length || invalidEmail) {
missing.forEach((field) => field.setAttribute('aria-invalid', 'true'));
if (invalidEmail) email?.setAttribute('aria-invalid', 'true');
if (status) status.textContent = invalidEmail ? (preview.dataset.emailError ?? '') : (preview.dataset.requiredError ?? '');
(missing[0] ?? email)?.focus();
return;
}
if (status) {
if (preview.dataset.formAVariant === 'compact') status.innerHTML = initialStatus;
else status.textContent = preview.dataset.success ?? 'Demo only: no information was sent or saved.';
}
});
}
</script>}
<style>
.form-a-preview {
color: var(--color-primary, #254080);
}
.form-a-preview--full {
background: white;
border-radius: 4px;
box-shadow: 0 4px 4px rgb(81 81 81 / 4%), 0 4px 16px rgb(81 81 81 / 8%);
margin: 0 auto;
max-width: 600px;
padding: 1.5rem;
}
.form-a-preview__eyebrow {
color: var(--color-muted, #65718a);
font-size: .72rem;
font-weight: 700;
letter-spacing: .08em;
margin: 0 0 .35rem;
text-transform: uppercase;
}
.form-a-preview h3 {
margin: 0 0 .4rem;
}
.form-a-preview__note,
.form-a-preview__caution,
.form-a-preview__status,
.form-a-preview__noscript {
color: var(--color-muted, #65718a);
font-size: .85rem;
line-height: 1.65;
}
.form-a-preview__fields {
display: grid;
gap: 1rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: 1.25rem;
}
.form-a-preview__field {
display: grid;
gap: .4rem;
min-width: 0;
}
.form-a-preview__field--full {
grid-column: 1 / -1;
}
.form-a-preview label {
font-weight: 700;
}
.form-a-preview input,
.form-a-preview select,
.form-a-preview textarea {
background: var(--color-background, #fcf9f5);
border: 1px solid var(--color-border, #edeef3);
border-radius: 4px;
color: var(--color-primary, #254080);
font: inherit;
min-height: 2.9rem;
padding: .7rem .8rem;
width: 100%;
}
.form-a-preview textarea {
resize: vertical;
}
.form-a-preview input:focus-visible,
.form-a-preview select:focus-visible,
.form-a-preview textarea:focus-visible,
.form-a-preview__submit:focus-visible {
outline: 3px solid var(--color-focus, #115bbd);
outline-offset: 3px;
}
.form-a-preview [aria-invalid="true"] {
border-color: #a21c28;
outline: 2px solid #a21c28;
outline-offset: 1px;
}
.form-a-preview__optional {
color: var(--color-muted, #65718a);
font-size: .82em;
font-weight: 400;
}
.form-a-preview__caution {
color: var(--color-muted, #254080);
font-size: .85rem;
line-height: 1.65;
margin: .8rem 0 0;
}
.form-a-preview__submit {
background: var(--color-accent, #ffbd59);
border: 2px solid var(--color-accent, #ffbd59);
border-radius: 4px;
color: var(--color-primary, #254080);
cursor: pointer;
font: inherit;
font-weight: 700;
min-height: 3rem;
padding: .65rem 1.2rem;
transition: .3s linear all;
width: 100%;
}
.form-a-preview__submit:hover,
.form-a-preview__submit:focus-visible {
background: transparent;
border-color: var(--color-accent-strong, #ff914d);
color: var(--color-primary, #254080);
}
.form-a-preview__status {
margin: .75rem 0 0;
}
.form-a-preview__status a {
color: inherit;
text-decoration: underline;
text-underline-offset: .15em;
}
.form-a-preview__noscript {
margin: .75rem 0 0;
}
.form-a-preview--compact {
background: transparent;
padding: 0;
}
.form-a-preview--compact .form-a-preview__fields {
grid-template-columns: 1fr;
}
@media (max-width: 640px) {
.form-a-preview--full .form-a-preview__fields {
grid-template-columns: 1fr;
}
.form-a-preview--full .form-a-preview__field--full {
grid-column: auto;
}
}
</style>
@@ -1,5 +1,7 @@
---
import Icon from './Icon.astro';
import FormAPreview from '../FormAPreview.astro';
import { formAPreviewEnabled, formModeForRuntime, resolveFormMode } from '../../utils/form-integration';
interface Props {
sourcePage: 'general' | 'commercial' | 'ahcccs' | 'private_pay';
title: string;
@@ -9,6 +11,8 @@ interface Props {
const { sourcePage, title, intro, buttonText } = Astro.props;
const phone = import.meta.env.PUBLIC_AIA_PHONE || '480-687-7099';
const tel = phone.replace(/[^0-9+]/g, '');
const formConfig = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE, import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED);
const formPreview = formAPreviewEnabled(import.meta.env.PUBLIC_AIA_FORM_A_PREVIEW, import.meta.env.PUBLIC_ALLOW_INDEXING, formModeForRuntime(formConfig));
---
<section class="section consultation" id="consultation" aria-labelledby="consultation-heading">
<div class="container consultation__panel grid-2">
@@ -18,9 +22,9 @@ const tel = phone.replace(/[^0-9+]/g, '');
<p class="lede">{intro}</p>
</div>
<div class="form-wrap">
<!-- TODO: Replace this inactive presentation with the approved Jotform integration under P9-03. -->
<!-- TODO: Verify privacy, spam protection and notifications before production activation. -->
<div id="lead-form" role="group" aria-labelledby="campaign-form-heading" data-forms-mode="off">
{formPreview ? <FormAPreview lang="en" variant="compact" submitText={buttonText} /> : <div id="lead-form" role="group" aria-labelledby="campaign-form-heading" data-forms-mode="off">
<!-- TODO: Replace this inactive presentation with the approved Jotform integration under P9-03. -->
<!-- TODO: Verify privacy, spam protection and notifications before production activation. -->
<h3 id="campaign-form-heading">Start with your contact details</h3>
<p class="form-note form-intro">Use this form to request a conversation. An AIA team member can explain the next steps for your family.</p>
<input disabled type="hidden" name="sourcePage" value={sourcePage} />
@@ -53,7 +57,7 @@ const tel = phone.replace(/[^0-9+]/g, '');
<div id="form-error" class="form-error" role="alert" tabindex="-1"></div>
<button class="button" type="button" disabled>{buttonText}<Icon name="arrow" /></button>
<p class="form-note">Questions? <a href={`tel:${tel}`}>Call AIA at {phone}</a>. Review AIA's <a href="/privacy-policy">privacy policy</a>.</p>
</div>
</div>}
</div>
<div class="consultation__details">
<div class="consultation__contact">
+5 -1
View File
@@ -1,6 +1,8 @@
---
import ContactForm from '../ContactForm.astro';
import FormAPreview from '../FormAPreview.astro';
import { formMessages } from '../../utils/form-messages';
import { formAPreviewEnabled, formModeForRuntime, resolveFormMode } from '../../utils/form-integration';
interface Props {
title: string;
@@ -9,6 +11,8 @@ interface Props {
const { title, lang } = Astro.props;
const text = formMessages(lang);
const formConfig = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE, import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED);
const formPreview = formAPreviewEnabled(import.meta.env.PUBLIC_AIA_FORM_A_PREVIEW, import.meta.env.PUBLIC_ALLOW_INDEXING, formModeForRuntime(formConfig));
---
<section class="contact-page">
@@ -16,7 +20,7 @@ const text = formMessages(lang);
<div class="contact-page__copy">
<slot />
</div>
<ContactForm title={title} lang={lang} />
{formPreview ? <FormAPreview lang={lang as 'en' | 'es'} variant="full" submitText={lang === 'es' ? 'Enviar' : 'Submit'} /> : <ContactForm title={title} lang={lang} />}
</div>
</section>
+15
View File
@@ -21,6 +21,21 @@ export function formModeForRuntime(config: FormModeConfig): 'off' | 'mock' {
return config.mode === 'mock' ? 'mock' : 'off';
}
/**
* The Form A meeting preview is deliberately narrower than the general mock
* mode: it is available only for an explicitly requested, non-indexing mock
* build. This keeps the review UI out of default and production-shaped output.
*/
export function formAPreviewEnabled(
rawPreview: string | undefined,
allowIndexing: string | undefined,
runtimeMode: 'off' | 'mock'
): boolean {
return rawPreview?.trim().toLowerCase() === 'true'
&& allowIndexing?.trim().toLowerCase() !== 'true'
&& runtimeMode === 'mock';
}
export function resolveMockFormResult(rawResult: string | undefined): MockFormResult {
const result = rawResult?.trim().toLowerCase();
return result === 'failed' || result === 'unavailable' ? result : 'success';
+28 -2
View File
@@ -10,7 +10,20 @@ const messages = {
mock: 'Demo only: this local fixture never sends information.', mockSuccess: 'Demo only: the local fixture completed without sending anything.', mockFailed: 'Demo only: the local fixture simulated a failed submission.', mockUnavailable: 'Demo only: the local fixture simulated an unavailable service.',
map: 'Map showing Arizona Institute for Autism in Scottsdale',
tile: 'OpenStreetMap tile showing area around Arizona Institute for Autism in Scottsdale, AZ',
zoomIn: 'Zoom in', zoomOut: 'Zoom out', marker: 'Arizona Institute for Autism'
zoomIn: 'Zoom in', zoomOut: 'Zoom out', marker: 'Arizona Institute for Autism',
formAPreview: {
internalPreview: 'Internal meeting preview', heading: 'General enquiries preview',
name: 'Your name', phone: 'Phone number', email: 'Email address',
preferredContact: 'Preferred contact method', phoneOption: 'Phone', emailOption: 'Email',
message: 'How can we help?', caution: 'Please avoid entering detailed medical, diagnostic, or insurance-member information here. An AIA team member can gather any needed details through the appropriate follow-up process.',
submit: 'Submit', optional: 'optional', emailRequired: 'required for email contact',
choose: 'Choose one', previewNote: 'Internal preview only: nothing is sent, saved or emailed.',
previewOnly: 'Preview only — nothing is submitted.', callAia: 'Call AIA at (480) 687-7099', privacyPolicy: 'Privacy policy',
noScript: 'This local preview needs JavaScript for its demonstration controls. No information is sent.',
requiredError: 'Please complete your name, phone number and preferred contact method.',
emailError: 'Please enter an email address when Email is your preferred contact method.',
success: 'Demo only: Form A preview completed locally. No information was sent or saved.'
}
},
es: {
heading: 'Programe su consulta gratuita', eyebrow: 'Comience aquí',
@@ -22,7 +35,20 @@ const messages = {
mock: 'Solo demostración: esta prueba local nunca envía información.', mockSuccess: 'Solo demostración: la prueba local terminó sin enviar información.', mockFailed: 'Solo demostración: la prueba local simuló un envío fallido.', mockUnavailable: 'Solo demostración: la prueba local simuló un servicio no disponible.',
map: 'Mapa de la ubicación de Arizona Institute for Autism en Scottsdale',
tile: 'Mapa de OpenStreetMap de los alrededores de Arizona Institute for Autism en Scottsdale, Arizona',
zoomIn: 'Acercar', zoomOut: 'Alejar', marker: 'Arizona Institute for Autism'
zoomIn: 'Acercar', zoomOut: 'Alejar', marker: 'Arizona Institute for Autism',
formAPreview: {
internalPreview: 'Vista previa para reunión interna', heading: 'Vista previa de consultas generales',
name: 'Su nombre', phone: 'Número de teléfono', email: 'Correo electrónico',
preferredContact: 'Método de contacto preferido', phoneOption: 'Teléfono', emailOption: 'Correo electrónico',
message: '¿Cómo podemos ayudarle?', caution: 'Evite ingresar aquí información médica o diagnóstica detallada, o información del miembro del seguro. Un miembro del equipo de AIA puede recopilar los detalles necesarios mediante el proceso de seguimiento adecuado.',
submit: 'Enviar', optional: 'opcional', emailRequired: 'obligatorio para contacto por correo',
choose: 'Elija una opción', previewNote: 'Solo para revisión interna: no se envía, guarda ni comparte información.',
previewOnly: 'Solo vista previa: no se envía nada.', callAia: 'Llame a AIA al (480) 687-7099', privacyPolicy: 'Política de privacidad',
noScript: 'Esta vista previa local necesita JavaScript para sus controles de demostración. No se envía información.',
requiredError: 'Complete su nombre, número de teléfono y método de contacto preferido.',
emailError: 'Ingrese un correo electrónico cuando el correo sea su método de contacto preferido.',
success: 'Solo demostración: la vista previa del Formulario A terminó localmente. No se envió ni guardó información.'
}
}
};
+210
View File
@@ -0,0 +1,210 @@
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import http from 'node:http';
import net from 'node:net';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { setTimeout as delay } from 'node:timers/promises';
const root = fileURLToPath(new URL('../', import.meta.url));
const moduleName = process.env.PLAYWRIGHT_MODULE ? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href : 'playwright';
const expectedMode = process.env.FORM_A_BROWSER_MODE === 'off' ? 'off' : 'mock';
const campaignRoutes = ['/lp/aba-therapy', '/lp/aba-insurance', '/lp/aba-ahcccs', '/lp/aba-private-pay'];
const earlyInterventionRoute = '/lp/early-intervention';
const routes = expectedMode === 'off' ? [...campaignRoutes, earlyInterventionRoute] : ['/contact', '/es/contact', ...campaignRoutes, earlyInterventionRoute];
const output = await fs.mkdtemp('/tmp/aia-form-a-preview-');
const evidence = { mode: expectedMode, routes, widths: [390, 1440], checks: 0, failures: [], blocked: [], productionAttempts: [], screenshots: [], status: 'failed' };
const reserve = net.createServer();
await new Promise((resolve, reject) => { reserve.once('error', reject); reserve.listen(0, '127.0.0.1', resolve); });
const port = reserve.address().port;
await new Promise((resolve) => reserve.close(resolve));
const origin = `http://127.0.0.1:${port}`;
const isProduction = (url) => /(?:jotform|api\.azinstitute4autism\.com|google-analytics|googletagmanager|doubleclick)/i.test(url.hostname + url.pathname);
let browser;
let preview;
try {
const { chromium } = await import(moduleName);
browser = await chromium.launch(process.env.CHROMIUM_EXECUTABLE ? { headless: true, executablePath: process.env.CHROMIUM_EXECUTABLE } : { headless: true });
preview = spawn(process.execPath, ['node_modules/astro/bin/astro.mjs', 'preview', '--host', '127.0.0.1', '--port', String(port)], { cwd: root, env: { ...process.env, ASTRO_TELEMETRY_DISABLED: '1' }, stdio: 'ignore' });
const reachable = () => new Promise((resolve) => {
const request = http.get(origin + '/', (response) => { response.resume(); resolve(response.statusCode === 200); });
request.setTimeout(500, () => request.destroy());
request.on('error', () => resolve(false));
});
for (let i = 0; i < 60 && !(await reachable()); i++) await delay(250);
assert.equal(await reachable(), true);
for (const width of evidence.widths) {
const context = await browser.newContext({ viewport: { width, height: width < 500 ? 844 : 900 }, serviceWorkers: 'block' });
await context.routeWebSocket('**/*', (socket) => socket.close());
await context.route('**/*', (route) => {
const request = route.request();
const url = new URL(request.url());
if (url.origin !== origin || !['GET', 'HEAD'].includes(request.method())) {
const item = { origin: url.origin, path: url.pathname, method: request.method() };
evidence.blocked.push(item);
if (isProduction(url)) evidence.productionAttempts.push(item);
return route.abort();
}
return route.continue();
});
const page = await context.newPage();
for (const route of routes) {
evidence.checks++;
try {
const response = await page.goto(origin + route, { waitUntil: 'domcontentloaded', timeout: 15000 });
assert.equal(response?.status(), 200);
if (route === earlyInterventionRoute) {
assert.equal(await page.locator('[data-form-a-preview]').count(), 0);
assert.equal(await page.locator('#consultation[data-forms-mode="off"]').count(), 1);
assert.equal(await page.locator('#consultation button[type="button"][disabled]').count(), 1);
assert.equal(await page.locator('[data-form-status-notice], #consultation-unavailable').count(), 0);
assert.doesNotMatch(await page.locator('.qr-landing').textContent(), /(?:online )?(?:forms?|requests?|submissions?) (?:are |is )?(?:unavailable|not enabled)|detailed medical|diagnostic|insurance-member/i);
assert.ok(await page.locator('a[href="tel:+14806877099"]').count() >= 1);
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
continue;
}
if (expectedMode === 'off') {
assert.equal(await page.locator('[data-form-a-preview]').count(), 0);
assert.equal(await page.locator('#lead-form[data-forms-mode="off"]').count(), 1);
assert.equal(await page.locator('[data-form-status-notice], #campaign-form-status').count(), 0);
assert.equal(await page.locator('#lead-form a[href^="tel:"]').count(), 1);
assert.equal(await page.locator('#lead-form a[href="/privacy-policy"]').count(), 1);
assert.doesNotMatch(await page.locator('#lead-form').textContent(), /(?:online )?(?:forms?|requests?|submissions?) (?:are |is )?(?:unavailable|not enabled)|detailed medical|diagnostic|insurance-member/i);
assert.equal(await page.locator('#lead-form button[type="button"][disabled]').count(), 1);
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
continue;
}
const compact = route.startsWith('/lp/');
const fields = compact ? ['name', 'phone', 'email', 'preferredContact'] : ['name', 'phone', 'email', 'preferredContact', 'message'];
const result = await page.evaluate(({ fields: expected, compact: isCompact }) => ({
enabled: document.querySelector('[data-form-a-preview]')?.dataset.formAPreviewEnabled,
names: [...document.querySelectorAll('[data-form-a-preview] [name]')].map((el) => el.getAttribute('name')),
forms: document.querySelectorAll('form').length,
iframe: document.querySelectorAll('iframe').length,
h1: document.querySelectorAll('main h1').length,
width: document.documentElement.scrollWidth,
expectedCount: expected.length,
isCompact
}), { fields, compact });
assert.equal(result.enabled, 'true');
assert.deepEqual(result.names, fields);
assert.equal(result.forms, 0);
assert.equal(result.iframe, 0);
assert.equal(result.h1, 1);
assert.ok(result.width <= width + 1, `overflow ${result.width}`);
const preview = page.locator('[data-form-a-preview]');
if (route === '/es/contact') {
assert.match(await preview.textContent(), /Su nombre|Método de contacto preferido/);
assert.doesNotMatch(await preview.textContent(), /Your name|Preferred contact method/);
}
const expectedButtonText = route === '/contact' ? 'Submit' : route === '/es/contact' ? 'Enviar' : {
'/lp/aba-therapy': 'Request a Free Consultation',
'/lp/aba-insurance': 'Request Insurance Verification',
'/lp/aba-ahcccs': 'Request a Benefits Check',
'/lp/aba-private-pay': 'Discuss Private-Pay Options'
}[route];
assert.equal(await preview.locator('[data-preview-status]').count(), 1);
assert.equal(await preview.locator('.form-a-preview__caution').count(), compact ? 0 : 1);
if (compact) {
assert.equal(await page.locator('[data-form-status-notice]').count(), 1);
assert.equal((await page.locator('[data-form-status-notice]').textContent()).match(/Preview only — nothing is submitted\./g)?.length, 1);
assert.equal(await page.locator('[data-form-status-notice] a[href="tel:+14806877099"]').count(), 1);
assert.equal(await page.locator('[data-form-status-notice] a[href="/privacy-policy"]').count(), 1);
assert.doesNotMatch(await page.locator('#consultation').textContent(), /(?:online )?(?:forms?|requests?|submissions?) (?:are |is )?(?:unavailable|not enabled)|detailed medical|diagnostic|insurance-member/i);
}
const button = preview.locator('[data-preview-submit]');
assert.equal((await button.textContent()).trim(), expectedButtonText);
const filledStyle = await button.evaluate((element) => {
const style = getComputedStyle(element);
return { background: style.backgroundColor, border: style.borderColor, borderWidth: style.borderWidth, color: style.color };
});
assert.equal(filledStyle.background, 'rgb(255, 189, 89)');
assert.equal(filledStyle.border, 'rgb(255, 189, 89)');
assert.equal(filledStyle.borderWidth, '2px');
assert.equal(filledStyle.color, 'rgb(37, 64, 128)');
await button.hover();
await delay(350);
const hoverStyle = await button.evaluate((element) => {
const style = getComputedStyle(element);
return { background: style.backgroundColor, border: style.borderColor };
});
assert.equal(hoverStyle.background, 'rgba(0, 0, 0, 0)');
assert.equal(hoverStyle.border, 'rgb(255, 145, 77)');
await button.focus();
assert.notEqual(await button.evaluate((element) => getComputedStyle(element).outlineWidth), '0px');
await button.click();
assert.doesNotMatch(await preview.locator('[data-preview-status]').textContent(), /completed locally|terminó localmente/);
await preview.locator('input[name="name"]').fill('Demo Reviewer');
await preview.locator('input[name="phone"]').fill('480-555-0100');
await preview.locator('select[name="preferredContact"]').selectOption('phone');
await button.click();
if (compact) assert.match(await preview.locator('[data-preview-status]').textContent(), /Preview only — nothing is submitted/);
else assert.match(await preview.locator('[data-preview-status]').textContent(), /completed locally|terminó localmente/);
await preview.locator('select[name="preferredContact"]').selectOption('email');
if (route === '/es/contact') {
assert.equal(
(await preview.locator('[id$="-email-requirement"]').textContent())?.trim(),
'(obligatorio para contacto por correo)'
);
}
await button.click();
await assert.match(await preview.locator('[data-preview-status]').textContent(), /email|correo/i);
await preview.locator('input[name="email"]').fill('demo@example.test');
await button.click();
if (compact) assert.match(await preview.locator('[data-preview-status]').textContent(), /Preview only — nothing is submitted/);
else assert.match(await preview.locator('[data-preview-status]').textContent(), /completed locally|terminó localmente/);
if ((route === '/contact' || route === '/lp/aba-therapy') && (width === 390 || width === 1440)) {
const file = `${route === '/contact' ? 'contact' : 'campaign'}-${width}.png`;
await page.screenshot({ path: path.join(output, file), fullPage: true });
evidence.screenshots.push({ route, width, file });
}
} catch (error) {
evidence.failures.push({ route, width, error: String(error.message) });
}
}
await context.close();
}
if (expectedMode === 'mock') {
const noJsContext = await browser.newContext({ viewport: { width: 390, height: 844 }, javaScriptEnabled: false, serviceWorkers: 'block' });
await noJsContext.route('**/*', (route) => {
const request = route.request();
const url = new URL(request.url());
if (url.origin !== origin || !['GET', 'HEAD'].includes(request.method())) {
const item = { origin: url.origin, path: url.pathname, method: request.method(), noJs: true };
evidence.blocked.push(item);
if (isProduction(url)) evidence.productionAttempts.push(item);
return route.abort();
}
return route.continue();
});
for (const route of ['/contact', '/lp/aba-therapy']) {
evidence.checks++;
const page = await noJsContext.newPage();
const response = await page.goto(origin + route, { waitUntil: 'domcontentloaded', timeout: 15000 });
assert.equal(response?.status(), 200);
assert.equal(await page.locator('[data-form-a-preview]').count(), 1);
assert.equal(await page.locator('form').count(), 0);
const bodyText = await page.locator('body').textContent();
if (route === '/contact') assert.match(bodyText, /JavaScript|JavaScript is required|Se requiere JavaScript/);
else assert.match(bodyText, /Preview only — nothing is submitted/);
}
await noJsContext.close();
}
evidence.status = evidence.failures.length || evidence.productionAttempts.length ? 'failed' : 'passed';
} catch (error) {
evidence.error = String(error.message);
} finally {
await browser?.close();
if (preview && preview.exitCode === null) {
preview.kill('SIGTERM');
await Promise.race([new Promise((resolve) => preview.once('exit', resolve)), delay(3000)]);
if (preview.exitCode === null) preview.kill('SIGKILL');
}
evidence.finishedAt = new Date().toISOString();
await fs.writeFile(path.join(output, 'results.json'), JSON.stringify(evidence, null, 2) + '\n');
console.log(JSON.stringify({ ...evidence, output }, null, 2));
if (evidence.status !== 'passed') process.exitCode = 1;
}
+59
View File
@@ -0,0 +1,59 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import test from 'node:test';
import { formAPreviewEnabled } from '../src/utils/form-integration.ts';
const read = (file) => fs.readFileSync(file, 'utf8');
test('Form A preview is explicit and has no live destination in source', () => {
const source = read('src/components/FormAPreview.astro');
assert.match(source, /data-form-a-preview/);
assert.match(source, /type="button"/);
assert.match(source, /submitText/);
assert.match(source, /text\.internalPreview/);
assert.match(source, /dataset\.emailRequired/);
assert.match(source, /border: 2px solid var\(--color-accent/);
assert.doesNotMatch(source, /jotform\.com|api\.azinstitute4autism\.com|fetch\(/i);
assert.equal(formAPreviewEnabled('true', 'false', 'mock'), true);
assert.equal(formAPreviewEnabled('true', 'true', 'mock'), false);
});
test('Form A preview placements are explicit and compact previews omit message cautions', () => {
const contact = read('src/components/pages/ContactPage.astro');
const contactForm = read('src/components/ContactForm.astro');
const campaign = read('src/components/campaign/ConsultationForm.astro');
const preview = read('src/components/FormAPreview.astro');
const messages = read('src/utils/form-messages.ts');
const early = read('src/components/campaign/EarlyInterventionPage.astro');
assert.match(contact, /FormAPreview/);
assert.match(contact, /submitText=\{lang === 'es' \? 'Enviar' : 'Submit'\}/);
assert.match(campaign, /FormAPreview/);
assert.match(campaign, /submitText=\{buttonText\}/);
assert.doesNotMatch(early, /FormAPreview/);
assert.doesNotMatch(preview, /name="inquiryType"/);
assert.match(preview, /variant === 'full'.*form-a-preview__caution/s);
assert.match(contactForm, /textarea[^>]+aria-describedby="contact-sensitive-note"/);
assert.match(contactForm, /text\.formAPreview\.caution/);
assert.match(messages, /Preview only — nothing is submitted/);
assert.match(messages, /Vista previa para reunión interna/);
assert.doesNotMatch(campaign, /detailed medical|diagnostic|insurance-member/i);
assert.doesNotMatch(early, /(?:online )?(?:forms?|requests?|submissions?) (?:are |is )?(?:unavailable|not enabled)|detailed medical|diagnostic|insurance-member/i);
});
test('default environment keeps the preview disabled', () => {
assert.match(read('.env.example'), /PUBLIC_AIA_FORM_A_PREVIEW=false/);
assert.equal(formAPreviewEnabled('false', 'false', 'off'), false);
});
test('Jotform handoff preserves AIA form tokens and accessible native controls', () => {
const css = read('integrations/jotform/aia-form-a.css');
const guide = read('integrations/jotform/README.md');
for (const value of ['#ffbd59', '#ff914d', '#254080', '#fcf9f5', '#edeef3']) assert.match(css, new RegExp(value));
assert.match(css, /#form \.form-submit-button/);
assert.match(css, /:focus-visible/);
assert.doesNotMatch(css, /https?:\/\/|@import|url\(/i);
assert.match(guide, /formContext/);
assert.match(guide, /multiple native Jotform Submit buttons/i);
assert.match(guide, /honeypot/i);
assert.match(guide, /cross-origin iframe/i);
});
+9
View File
@@ -6,6 +6,7 @@ import {
resolveCounterConfig,
} from '../src/utils/counter-mode.ts';
import {
formAPreviewEnabled,
formModeForRuntime,
formRouteMapping,
logicalFormForRoute,
@@ -36,6 +37,14 @@ test('form configuration defaults off and live remains fail-closed', () => {
assert.equal(resolveMockFormResult('anything-else'), 'success');
});
test('Form A meeting preview requires explicit mock mode and remains staging-only', () => {
assert.equal(formAPreviewEnabled(undefined, 'false', 'mock'), false);
assert.equal(formAPreviewEnabled('true', undefined, 'off'), false);
assert.equal(formAPreviewEnabled('true', 'true', 'mock'), false);
assert.equal(formAPreviewEnabled('true', 'false', 'mock'), true);
assert.equal(formAPreviewEnabled('TRUE', 'FALSE', 'mock'), true);
});
test('form mapping covers nine native instances and five landing journeys', () => {
assert.equal(formRouteMapping.filter((entry) => entry.kind === 'native-instance').length, 9);
assert.equal(formRouteMapping.filter((entry) => entry.kind === 'landing-journey').length, 5);