refactor(forms): enable approved Form A placements by default
This commit is contained in:
41 files changed
+862
-523
No files matched your search
+3
-12
@@ -2,19 +2,10 @@ PUBLIC_AIA_API_BASE=https://api.azinstitute4autism.com
|
||||
PUBLIC_COUNTERS_MODE=off
|
||||
PUBLIC_COUNTERS_LIVE_APPROVED=false
|
||||
PUBLIC_COUNTERS_MOCK_RESULT=success
|
||||
PUBLIC_AIA_FORMS_MODE=off
|
||||
PUBLIC_AIA_FORMS_LIVE_APPROVED=false
|
||||
# Optional override only: absent/empty means live on exactly 16 approved routes.
|
||||
# PUBLIC_AIA_FORMS_MODE=off
|
||||
# PUBLIC_AIA_FORMS_MODE=mock
|
||||
PUBLIC_AIA_FORMS_MOCK_RESULT=success
|
||||
# Isolated-test Form A requires this environment role and approval; it remains
|
||||
# unavailable in indexing-enabled builds even when both are set.
|
||||
PUBLIC_AIA_FORMS_ENVIRONMENT=local
|
||||
PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED=false
|
||||
# Separately authorizes the nine Form A consultation placements in an
|
||||
# isolated-test build only. It has no effect on production eligibility.
|
||||
PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED=false
|
||||
# Form A production additionally requires mode=live, LIVE_APPROVED=true and
|
||||
# environment=production. PUBLIC_ALLOW_INDEXING never authorizes a form.
|
||||
PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=false
|
||||
# Explicit, local-only Form A meeting preview; requires PUBLIC_AIA_FORMS_MODE=mock.
|
||||
PUBLIC_AIA_FORM_A_PREVIEW=false
|
||||
PUBLIC_ALLOW_INDEXING=false
|
||||
|
||||
+19
-6
@@ -582,12 +582,25 @@ The live crawl is optional and may fail in restricted network environments.
|
||||
|
||||
## Forms
|
||||
|
||||
Forms are intentionally static and do not submit anywhere. Before production:
|
||||
As of the owner decision on 2026-10-30, `npm run dev` and `npm run build`
|
||||
use live Jotform Form A on exactly 16 approved placements: 10 immediate frames
|
||||
and six deferred bilingual service drawers. No Form A flags are required.
|
||||
Public development-host forms are owner-authorized. This does not authorize cutover.
|
||||
|
||||
1. Select a form backend.
|
||||
2. Connect the forms.
|
||||
3. Add spam protection.
|
||||
4. Test validation, confirmation, and failure states.
|
||||
Use `PUBLIC_AIA_FORMS_MODE=off npm run build` for an offline artifact, or
|
||||
`PUBLIC_AIA_FORMS_MODE=mock npm run dev` for synthetic fixtures. Missing/empty
|
||||
mode means live; obsolete `isolated-test` and other invalid values produce an
|
||||
explicit configuration error. Remove old approval/environment variables from
|
||||
local settings. Indexing is independent of forms; counters remain off by default.
|
||||
|
||||
See the [current Form A guide](integrations/jotform/README.md) for the exact
|
||||
registry, safeguards, rollback and owner acceptance requirements. Form C remains
|
||||
inactive. Employee-portal private input retains its own separate gates.
|
||||
|
||||
Verify matching output with
|
||||
`node tools/phase10-artifact-verify.mjs --mode=staging --forms=live`;
|
||||
use `--mode=indexing` for indexing output and `--forms=off` or `--forms=mock`
|
||||
for those explicit builds. No actual submissions run in automated tests.
|
||||
|
||||
### Google Ads consultation-form conversion handoff
|
||||
|
||||
@@ -597,7 +610,7 @@ instructions are in
|
||||
This is documentation only: **do not install Google tags or conversion snippets**
|
||||
on health-service pages or turn on enhanced conversions until AIA's
|
||||
privacy/HIPAA and advertising-policy review explicitly approves the complete
|
||||
data flow. Count conversions only after the form API confirms success.
|
||||
data flow. Any future measurement needs separately approved success and privacy semantics; the former API-success premise is historical and no tracking is active.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -76,12 +76,11 @@ Use a private ciphertext file generated by `collect` or `rewrite`:
|
||||
```sh
|
||||
AIA_PORTAL_LOCAL_PREVIEW_APPROVED=true \
|
||||
AIA_PORTAL_CIPHERTEXT_FILE=/absolute/private/ciphertext-TIMESTAMP.json \
|
||||
PUBLIC_AIA_FORMS_ENVIRONMENT=isolated-test \
|
||||
PUBLIC_AIA_FORMS_MODE=off PUBLIC_ALLOW_INDEXING=false \
|
||||
npm run dev -- --host 127.0.0.1
|
||||
```
|
||||
|
||||
The page defaults to locked/unavailable without approved valid input. Indexing
|
||||
The 2026-10-30 revision removes dependence on the retired Form A environment role. Form mode is independent; `off` above is optional for local testing and cannot grant portal approval. The page defaults to locked/unavailable without its own explicit approval and valid private input. Indexing
|
||||
builds cannot include it. The codes never go to a server, storage or URL. No
|
||||
native form submits; Enter and Unlock use local WebCrypto. Only the matching
|
||||
stream is displayed; refresh or leaving the page locks it again.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
This directory contains the accepted CSS backup and integration guidance for AIA Form A. [`aia-form-a.css`](./aia-form-a.css) is intended for Jotform Classic Forms' **Form Designer → Styles → Inject Custom CSS** under owner-controlled configuration review. The existing repository CSS is the accepted backup; do not regenerate it from Astro styles.
|
||||
|
||||
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.
|
||||
|
||||
@@ -11,54 +11,104 @@ The current Builder theme reconciliation adds narrowly scoped `!important` decla
|
||||
## Installation and comparison
|
||||
|
||||
1. Open the approved Form A in AIA's HIPAA-enabled Jotform account. Do not change its fields or settings from this repository workflow.
|
||||
2. Keep production activation disabled while embedded-runtime, accessibility, telemetry/privacy, and release review remain pending.
|
||||
2. Preserve the approved account configuration; account changes and site cutover require their separate authorization.
|
||||
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.
|
||||
The optional Astro meeting preview requires `PUBLIC_AIA_FORMS_MODE=mock` and `PUBLIC_AIA_FORM_A_PREVIEW=true` in a non-indexing build. It does not load Jotform and is not a live submission test.
|
||||
|
||||
## Isolated Form A iframe adapter
|
||||
## Current Form A operation — superseding decision, 2026-10-30
|
||||
|
||||
The Astro adapter exists only for a separately approved, non-indexing isolated test. It can render Form A at the two contact routes and five explicitly allowlisted landing routes when **all** of the following are set at build time:
|
||||
The owner authorizes real Form A on the publicly reachable development host
|
||||
`https://aia.web3app.dev/`. Normal development and static builds use **live** Form A
|
||||
without environment flags. This intentionally supersedes the former isolated-test
|
||||
and seven-route production approval gates. It is not site-cutover authorization.
|
||||
|
||||
From `www/`:
|
||||
|
||||
```sh
|
||||
PUBLIC_AIA_FORMS_MODE=isolated-test
|
||||
PUBLIC_AIA_FORMS_ENVIRONMENT=isolated-test
|
||||
PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED=true
|
||||
PUBLIC_AIA_FORMS_LIVE_APPROVED=false
|
||||
PUBLIC_ALLOW_INDEXING=false
|
||||
npm run dev
|
||||
npm run build
|
||||
# Optional offline/restricted artifact:
|
||||
PUBLIC_AIA_FORMS_MODE=off npm run build
|
||||
# Optional synthetic fixtures:
|
||||
PUBLIC_AIA_FORMS_MODE=mock npm run dev
|
||||
```
|
||||
|
||||
It uses fixed allowlisted hosted URLs only: the contact routes use `formContext=contact` with their fixed English or Spanish language value, and the five landing routes use `formContext=landing&language=en`. `formContext` is the exact case-sensitive Jotform Unique Name supplied by the owner; it is presentation-only and does not accept visitor query values. In `isolated-test` mode, a missing gate, any other environment role, an indexing-enabled build or a route outside that seven-route allowlist renders the normal fail-closed Astro fallback. The separately documented `live` gate uses the same route allowlist and its own independent approvals.
|
||||
`PUBLIC_AIA_FORMS_MODE` is optional; missing/empty means `live`. The only accepted
|
||||
values are `live`, `off`, and `mock`. Remove obsolete `isolated-test` settings from
|
||||
local environment files; invalid explicit values stop the build with actionable
|
||||
guidance. The former live/isolated/consultation/production approvals and form
|
||||
environment role are removed, with no replacement gates. `PUBLIC_ALLOW_INDEXING`
|
||||
controls publication independently: both staging and indexing builds use the same
|
||||
16 placements. Explicit off/mock builds contain no real Form A iframe.
|
||||
|
||||
## Production-gated Form A adapter
|
||||
One typed registry in `src/utils/form-integration.ts` fixes every route, locale
|
||||
and context. No visitor query, referrer, arbitrary URL or option preselection is
|
||||
forwarded. The only origin/ID is `https://form.jotform.com/262946078314059`.
|
||||
Every URL has exactly `formContext` and `language` parameters:
|
||||
|
||||
The adapter remains off in normal builds. A production artifact may render Form A only when all four independent settings are present at build time:
|
||||
| Context | Routes | Loading |
|
||||
|---|---|---|
|
||||
| contact | `/contact` (en), `/es/contact` (es) | Immediate, 2 |
|
||||
| landing | `/lp/aba-therapy`, `/lp/aba-insurance`, `/lp/aba-ahcccs`, `/lp/aba-private-pay`, `/lp/early-intervention` (en) | Immediate, 5 |
|
||||
| consultation | `/client-consultation` (en), `/es/client-consultation` (es), `/schedule-consultation` (en) | Immediate, 3 |
|
||||
| consultation | `/aba-therapy`, `/autism-evaluations`, `/learner-social-club` (en), and their `/es/` counterparts (es) | Deferred, 6 |
|
||||
|
||||
The context is presentation-only. Physical Form B is retired. Form C and
|
||||
`/referrals` remain inactive; other routes cannot acquire Form A by setting a mode.
|
||||
The typed future Academy service copy does not publish another page.
|
||||
|
||||
Live service drawers open at four seconds OR 50% scroll, canceling the unused
|
||||
trigger. Dismissal or verified completion sets the existing shared 30-day
|
||||
suppression timestamp. The existing service CTA overrides automatic suppression;
|
||||
without JavaScript it remains a normal consultation-page link. Jotform loads only
|
||||
when the drawer opens. Off mode never automatically opens an inactive drawer.
|
||||
Mock mode retains the existing synthetic fixture workflow.
|
||||
|
||||
A verified `submission-completed` message must match the exact approved origin,
|
||||
actual activated/open iframe window, approved form ID and action. It displays a
|
||||
localized acknowledgment, waits five seconds, then uses the existing slide-out.
|
||||
Duplicate messages cannot restart the countdown. Manual close/navigation cancels
|
||||
it; suppression remains. Deliberate reopening after completion creates a fresh
|
||||
iframe. Provider-envelope acceptance still requires the owner-controlled real
|
||||
submission check; synthetic browser messages are not that acceptance.
|
||||
|
||||
The shared iframe preserves fixed responsive heights, accessible EN/ES titles,
|
||||
`referrerpolicy="no-referrer"`, and
|
||||
`sandbox="allow-forms allow-scripts allow-same-origin"`. No popup, top-navigation,
|
||||
camera, microphone, payment or geolocation permissions are added. The hosted
|
||||
fallback is outside the iframe and hidden without layout space until a 15-second
|
||||
missing-load timeout. Late load hides it; disposal cancels timers. A localized
|
||||
`noscript` fallback remains. Load does not prove application health or submission.
|
||||
No helper script, cross-origin DOM inspection, arbitrary messaging or submission
|
||||
forwarding is introduced. The official resize handler remains unimplemented and
|
||||
conditionally acceptable only after its separate security/browser review.
|
||||
|
||||
Owner-reported direct EN/ES synthetic acceptance and sampled embedded acceptance
|
||||
(English contact, Spanish contact and one ABA landing page) remain recorded as
|
||||
owner evidence. This change does not independently verify Jotform or resolve its
|
||||
Spanish consultation dropdown discrepancy, runtime telemetry, fluent-language,
|
||||
screen-reader, final HTTPS-host or operational recipient review.
|
||||
|
||||
For form rollback, explicitly set `PUBLIC_AIA_FORMS_MODE=off`, rebuild, verify
|
||||
`--forms=off`, and use the separately authorized artifact-switch procedure.
|
||||
Removing that override restores live, so removing settings is **not** rollback.
|
||||
Do not change the Jotform account as part of an Astro rollback.
|
||||
|
||||
Artifact checks (build matching output first):
|
||||
|
||||
```sh
|
||||
PUBLIC_AIA_FORMS_MODE=live
|
||||
PUBLIC_AIA_FORMS_LIVE_APPROVED=true
|
||||
PUBLIC_AIA_FORMS_ENVIRONMENT=production
|
||||
PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true
|
||||
node tools/phase10-artifact-verify.mjs --mode=staging --forms=live
|
||||
# For PUBLIC_ALLOW_INDEXING=true output:
|
||||
node tools/phase10-artifact-verify.mjs --mode=indexing --forms=live
|
||||
# For explicit off or mock output, select the matching --forms value.
|
||||
```
|
||||
|
||||
`PUBLIC_ALLOW_INDEXING=true` is required for an indexing-enabled release artifact, but it is **not** a Form A authorization setting and cannot activate a frame by itself. The production gate preserves the same fixed seven-route allowlist, URLs, `formContext`, locale parameters, fixed heights, sandbox, no-referrer policy and conditional fallback. It does not enable future Form C, service drawers, native POST forms, analytics, advertising, tracking, webhooks, API forwarding or the Jotform resize handler.
|
||||
|
||||
For a deployment rollback, set `PUBLIC_AIA_FORMS_MODE=off` or remove any one of the three approval/environment values, rebuild the static artifact, verify no Form A identifier is emitted, and switch to the reviewed artifact. This disables Form A in Astro without changing the static site or the Jotform account. Do not change Jotform notification recipients, fields or availability as part of that rollback.
|
||||
|
||||
The adapter has a localized iframe title, a fixed-height responsive frame, a `no-referrer` policy, a minimal sandbox, and a direct hosted-form fallback. The fallback is outside the iframe, initially hidden without layout space, revealed after a 15-second missing-load timeout, cleared on frame load or component disposal, and duplicated only as localized `<noscript>` content. A frame `load` cancels the timeout but is never treated as Jotform initialization, accepted submission, conversion, or proof that the cross-origin application is healthy. It loads no Jotform helper script, reads no iframe DOM, and installs no `postMessage` handler.
|
||||
|
||||
Direct-Jotform Form A synthetic EN/ES submission, native thank-you, conditional-email/language, generic staff notification, autoresponder delivery, fixed Reply-To, no-answer/no-PDF and no-webhook/integration evidence is owner-confirmed. The owner also reports successful synthetic submission and confirmation through the isolated Astro iframe on English contact, Spanish contact and one ABA landing placement. This establishes owner-reported Form A functional submission acceptance for the sampled routes; it does not replace final seven-route responsive/keyboard/accessibility acceptance, Jotform runtime/network and telemetry privacy review, deployment review or site-release authorization.
|
||||
|
||||
### Landing-route extension — 2026-10-25 / 2026-10-26
|
||||
|
||||
The owner has extended this same **isolated-test-only** adapter to `/lp/aba-therapy`, `/lp/aba-insurance`, `/lp/aba-ahcccs`, `/lp/aba-private-pay`, and `/lp/early-intervention`. Those five English landing routes use the fixed URL parameter pair `formContext=landing&language=en`; the contact routes continue to use `formContext=contact` with their fixed page locale. This supersedes their earlier Form B mapping. Physical Form B is retired; the separately documented consultation routes are Form A isolated-test-only placements.
|
||||
|
||||
The campaign adapter uses the existing campaign form surface rather than creating a second card. Its native Jotform Submit label is not overridden by Astro. The current sandbox is deliberately limited to `allow-forms allow-scripts allow-same-origin`, which is the minimum anticipated set for Jotform validation and in-frame confirmation. It intentionally does not grant popups, modals, top navigation, downloads, or a feature-policy `allow` list. Owner-authorized synthetic acceptance must confirm the real form's validation, confirmation, and any required in-frame navigation before this sandbox can be accepted for production.
|
||||
|
||||
The versioned P9-03 contract and implementation plan must record this owner decision and reconcile the superseded campaign mapping without erasing the prior decision history. Keep any documentation PR review aligned with those versioned records.
|
||||
Employee-portal ciphertext remains separately gated by its own approval, private
|
||||
input and non-indexing preview requirement. Form A modes cannot authorize it.
|
||||
|
||||
## Form context and button labels
|
||||
|
||||
@@ -71,7 +121,10 @@ The current Form A design uses a hidden `formContext` value with fixed, approved
|
||||
- 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 owner-approved Form A landing extension applies only to `/lp/aba-therapy`, `/lp/aba-insurance`, `/lp/aba-ahcccs`, `/lp/aba-private-pay`, and `/lp/early-intervention`, using its fixed `landing` context under the isolated-test gate. Physical Form B is retired; a planned Form A `consultation` context requires separate Builder approval, synthetic acceptance and an allowlist change before it can appear on other service-consultation journeys. No Form A landing placement is approved for live production activation. Do not add an external Astro button that attempts to submit a cross-origin iframe.
|
||||
All five landing routes use Form A's fixed `landing` context. The nine consultation
|
||||
placements use its fixed `consultation` context. Jotform owns native Submit labels
|
||||
and conditions. Do not add an external Astro button that attempts to submit a
|
||||
cross-origin iframe.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
@@ -84,31 +137,8 @@ The owner-approved Form A landing extension applies only to `/lp/aba-therapy`, `
|
||||
- [ ] 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.
|
||||
- [x] Owner-reported embedded synthetic submission and confirmation passed on English contact, Spanish contact and one ABA landing placement; record the exact ABA path in the owner test log before release.
|
||||
- [ ] All seven placements have final responsive, keyboard, screen-reader and fluent-language acceptance, including timeout/fallback and error expansion.
|
||||
- [ ] All sixteen placements have final responsive, keyboard, screen-reader and fluent-language acceptance, including timeout/fallback and error expansion.
|
||||
- [ ] Jotform runtime requests, telemetry and cross-origin failure behavior are reviewed with privacy/security owners.
|
||||
- [ ] 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.
|
||||
|
||||
## 2026-10-28 reconciliation
|
||||
|
||||
The current records supersede the earlier Form B mapping: all five named landing journeys use Form A under the seven-route isolated-test or separately approved production gate, and physical Form B is retired. The Form A `consultation` context is owner-reported configured and emitted only by its separate non-indexing isolated-test gate. The [two-form integration specification](../../reports/phase-9-two-form-integration-specification-2026-10-28.md) defines its fixed planning contract and the separate Form C referral work. The official Jotform JavaScript embed handler is **conditionally acceptable pending targeted verification**; it is not included in this adapter. Any future adoption must first verify its current source, origin and source-window validation, parent-page privileges, URL mutation, network requests, Firefox behavior and all supported error states. Fixed heights remain the current approved architecture until that review is separately accepted.
|
||||
|
||||
## Consultation isolated-test extension — 2026-10-29
|
||||
|
||||
Form A's owner-reported `consultation` context is implemented in Astro for a separate isolated-test path only. It uses the fixed query pair `formContext=consultation` plus `language=en` or `language=es`; the value is selected from a trusted route/locale allowlist and never from visitor input. The three standalone routes are `/client-consultation`, `/es/client-consultation`, and `/schedule-consultation`. The six service drawers are `/aba-therapy`, `/autism-evaluations`, `/learner-social-club`, `/es/aba-therapy`, `/es/autism-evaluations`, and `/es/learner-social-club`.
|
||||
|
||||
All of the following are required to render a consultation iframe:
|
||||
|
||||
```sh
|
||||
PUBLIC_AIA_FORMS_MODE=isolated-test
|
||||
PUBLIC_AIA_FORMS_ENVIRONMENT=isolated-test
|
||||
PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED=true
|
||||
PUBLIC_AIA_FORMS_LIVE_APPROVED=false
|
||||
PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED=true
|
||||
PUBLIC_ALLOW_INDEXING=false
|
||||
```
|
||||
|
||||
The service drawers use a shared bilingual component. In this mode, their visible opener is explicit and keyboard-accessible; the external iframe URL is not assigned until the visitor opens the dialog. The adapter then preserves the fixed height, minimal sandbox, no-referrer policy, localized hosted-form fallback and no-JavaScript fallback. It does not auto-open, preselect a service, use the Jotform resize handler, inspect the child document or install cross-origin messaging.
|
||||
|
||||
These nine routes are **not** part of the production resolver. The existing production gate remains exactly seven contact/landing placements, and `PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true` cannot activate a consultation route. The reported Jotform configuration requires owner browser acceptance for the visible service question, option text, requiredness, English/Spanish translation, validation, confirmation and privacy boundary. The former diagnostic-evaluation explanatory note is superseded: no such note should be added to Form A or Astro; an AIA client advocate will discuss available options in follow-up.
|
||||
Existing owner-verification scopes remain recorded in the contract/checklist. Outstanding consultation configuration acceptance, runtime/privacy, accessibility, fluent-language and site-release gates remain open. Default-live Form A is owner-authorized; this guide does not authorize Jotform account changes or site deployment.
|
||||
@@ -3,7 +3,7 @@
|
||||
**Last updated:** October 30, 2026 (UTC)
|
||||
**Repository:** `AIAMarketing/azinstitute4autism.com`
|
||||
**Working branch:** `faithful-astro-migration`
|
||||
**Last accepted technical implementation:** consolidated service drawers and employee-portal preview at `e97f0ea9b475715a1ea58110a27cdf50a2b3cb19`; the commit containing this handoff adds the focused completion/styling correction pass
|
||||
**Last pushed checkpoint:** `55adb797051473c3067a0d454515153237e9b8d6` — completion handling and portal styling. The new commit containing this handoff simplifies Form A to default-live without rewriting that history.
|
||||
**Previous accepted checkpoint:** P9-03 documentation, signed `537f9864a0a3d0806180cbeca909bf1d03200c6f`
|
||||
**Implementation checkpoint:** the commit containing this handoff update; obtain its actual SHA from Git history after the signed checkpoint is created.
|
||||
**Next milestone:** continue Phase 9 owner decisions and authorized integration batches; then Phase 10 — Final QA/Cutover Readiness.
|
||||
@@ -12,6 +12,50 @@ The landing-page migration is technically completed for checkpoint purposes.
|
||||
Editorial, accessibility, stakeholder and production-release approval remain
|
||||
separate. This handoff does not authorize deployment or integration activation.
|
||||
|
||||
## Current decision — 2026-10-30: default-live Form A
|
||||
|
||||
The owner intentionally supersedes the earlier isolated-test and seven-route
|
||||
production-gate architecture. `npm run dev` and `npm run build` now enable Form A
|
||||
on exactly **16 approved placements**, including the publicly reachable
|
||||
`aia.web3app.dev` environment. No additional approval flags or access restriction
|
||||
are required. `PUBLIC_AIA_FORMS_MODE` is optional: absent/empty means `live`;
|
||||
explicit `off` and `mock` remain available. Invalid/obsolete `isolated-test`
|
||||
settings produce an actionable configuration error. The former approval flags
|
||||
and form environment role are retired, not renamed.
|
||||
|
||||
Indexing and forms are independent. Both publication modes emit ten immediate
|
||||
frames (two contact, five landing, three consultation) and six deferred bilingual
|
||||
service-drawer frames. The bounded registry fixes ID `262946078314059`, HTTPS
|
||||
origin, context and language. Forms B/C are not activated; physical B is retired.
|
||||
|
||||
Live drawers retain four-second/50%-scroll opening, shared 30-day suppression,
|
||||
manual CTA override, deferred loading, verified completion acknowledgment for
|
||||
five seconds, animated dismissal and fresh-form reopening. Off mode does not
|
||||
automatically show inactive drawers. All iframe security/fallback behavior is
|
||||
preserved. Employee-portal preview remains independently gated by explicit local
|
||||
approval, valid private ciphertext input and non-indexing output; Form A cannot
|
||||
authorize private content. No portal private resources enter normal artifacts.
|
||||
|
||||
This owner authorization does not modify Jotform, authorize site cutover or
|
||||
complete pending consultation-language, provider-event, accessibility, runtime
|
||||
telemetry/privacy, recipient-routing or hosting acceptance. See the
|
||||
[current operating guide](../integrations/jotform/README.md) and
|
||||
[implementation and validation report](phase-9-form-a-default-live-2026-10-30.md).
|
||||
Earlier dated decisions and test results below are historical evidence, not
|
||||
current environment setup instructions.
|
||||
|
||||
## Latest verified validation — 2026-10-30 default-live revision
|
||||
|
||||
206 tests pass; Astro check has zero diagnostics. Normal default-live staging:
|
||||
128 routes, empty sitemap, zero hreflang, ten immediate plus six deferred Form A
|
||||
frames. Indexing-enabled: 127 routes, 109 sitemap URLs, 180 hreflang links, the
|
||||
same 16 placements. Explicit off/mock artifacts contain no real Form A frames.
|
||||
Guarded browser regressions preserve drawer completion/suppression and portal
|
||||
fixture behavior; 128/128 staging routes open. Link/image/blog audits have zero
|
||||
findings. See the current default-live report for exact overlapping check counts,
|
||||
network interception evidence and remaining owner acceptance. No deployment,
|
||||
Jotform mutation or new real submission was performed.
|
||||
|
||||
## Authority and checkpoint history
|
||||
|
||||
Read repository-root `AGENTS.md`, `MIGRATION_BRIEF.md` and `AUTONOMOUS-CODEX.md`,
|
||||
@@ -169,10 +213,10 @@ trusted-source comparison. No insecure retrieval was repeated.
|
||||
|
||||
### Current two-form status — 2026-10-29
|
||||
|
||||
- Form A (`262946078314059`) has a frozen seven-route production-eligible allowlist: `/contact`, `/es/contact`, the four campaign landing routes and `/lp/early-intervention`. The five landing journeys use fixed `formContext=landing&language=en`. The owner has retired physical Form B. The owner reports Form A `consultation` is configured, and the repository now has a separate nine-route, non-indexing isolated-test allowlist: `/client-consultation`, `/es/client-consultation`, `/schedule-consultation`, the three English service drawers and the three matching Spanish service drawers. Those routes require `PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED=true` in addition to every isolated-test gate and are never production-authorized by the seven-route resolver. Form C remains a separate future professional-referral form. See [the two-form integration specification](phase-9-two-form-integration-specification-2026-10-28.md).
|
||||
- Owner-confirmed direct-Jotform evidence covers synthetic EN/ES submission, native thank-you, conditional email/language, generic no-answer/no-PDF staff notification, autoresponder delivery, corrected fixed Reply-To, no webhooks/integrations, privacy scope, recipient access and retention. The owner also reports successful Astro-iframe synthetic submission and confirmation on English contact, Spanish contact and one ABA landing placement; record the exact third route before release. Functional submission acceptance is complete only for those owner-reported samples. All-seven-route accessibility, runtime-network/telemetry, deployment and site-release acceptance remain open.
|
||||
- Form A uses one 16-placement live registry: two contact, five landing, three standalone consultation and six EN/ES service drawers. Mode defaults to live; off/mock are explicit options. See the current decision above and the two-form specification.
|
||||
- Owner-confirmed direct-Jotform evidence covers synthetic EN/ES submission, native thank-you, conditional email/language, generic no-answer/no-PDF staff notification, autoresponder delivery, corrected fixed Reply-To, no webhooks/integrations, privacy scope, recipient access and retention. The owner also reports successful Astro-iframe synthetic submission and confirmation on English contact, Spanish contact and one ABA landing placement; record the exact third route before release. Functional submission acceptance is complete only for those owner-reported samples. All-sixteen-route accessibility, runtime-network/telemetry, deployment and site-release acceptance remain open.
|
||||
- The adapter retains fixed URLs, responsive fixed heights, `no-referrer`, `allow-forms allow-scripts allow-same-origin`, no helper script, and a hosted fallback that is hidden without space until a 15-second missing-load timeout. A frame load only clears that timeout; it does not prove cross-origin application health or accepted submission. The official Jotform resizing handler is conditionally acceptable pending separately authorized source, origin, privilege, Firefox and error-state verification; it is not implemented. Fixed heights remain current.
|
||||
- A separate local production gate is prepared for Form A only. It requires `PUBLIC_AIA_FORMS_MODE=live`, `PUBLIC_AIA_FORMS_LIVE_APPROVED=true`, `PUBLIC_AIA_FORMS_ENVIRONMENT=production` and `PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true`; the fixed seven-route allowlist remains enforced. `PUBLIC_ALLOW_INDEXING=true` permits an indexing-enabled release artifact but never authorizes a form. Normal/off/mock behavior remains fail-closed. The isolated consultation surfaces require an explicit user action before a service-drawer iframe gets a `src`; they retain no-referrer, minimal sandbox, fixed dimensions and the conditional hosted fallback. Form C, analytics and API forwarding remain inactive. Marketing-only notification routing is a pre-cutover operational check. Rolling back Form A means rebuilding with mode `off` or an absent approval value and switching only the static artifact; it does not alter Jotform.
|
||||
- No additional Form A approval/environment flags remain. Indexing does not authorize or disable forms. Marketing-only notification routing remains a pre-cutover operational check. Rollback requires explicit mode `off`, rebuild and artifact verification; merely removing the mode restores live. Form C, counters, analytics and API forwarding remain inactive.
|
||||
|
||||
The indexing-enabled build has 127 HTML routes/canonicals and 109 sitemap URLs.
|
||||
Normal staging has 128 `noindex,nofollow` pages because it also contains `/lp`;
|
||||
|
||||
@@ -3,6 +3,40 @@
|
||||
Date: 2026-10-22 UTC
|
||||
Status: Technical handoff prepared; production gates remain open.
|
||||
|
||||
## Current decision — 2026-10-30: default-live Form A
|
||||
|
||||
The owner intentionally supersedes the earlier isolated-test and seven-route
|
||||
production-gate architecture. `npm run dev` and `npm run build` now enable Form A
|
||||
on exactly **16 approved placements**, including the publicly reachable
|
||||
`aia.web3app.dev` environment. No additional approval flags or access restriction
|
||||
are required. `PUBLIC_AIA_FORMS_MODE` is optional: absent/empty means `live`;
|
||||
explicit `off` and `mock` remain available. Invalid/obsolete `isolated-test`
|
||||
settings produce an actionable configuration error. The former approval flags
|
||||
and form environment role are retired, not renamed.
|
||||
|
||||
Indexing and forms are independent. Both publication modes emit ten immediate
|
||||
frames (two contact, five landing, three consultation) and six deferred bilingual
|
||||
service-drawer frames. The bounded registry fixes ID `262946078314059`, HTTPS
|
||||
origin, context and language. Forms B/C are not activated; physical B is retired.
|
||||
|
||||
Live drawers retain four-second/50%-scroll opening, shared 30-day suppression,
|
||||
manual CTA override, deferred loading, verified completion acknowledgment for
|
||||
five seconds, animated dismissal and fresh-form reopening. Off mode does not
|
||||
automatically show inactive drawers. All iframe security/fallback behavior is
|
||||
preserved. Employee-portal preview remains independently gated by explicit local
|
||||
approval, valid private ciphertext input and non-indexing output; Form A cannot
|
||||
authorize private content. No portal private resources enter normal artifacts.
|
||||
|
||||
This owner authorization does not modify Jotform, authorize site cutover or
|
||||
complete pending consultation-language, provider-event, accessibility, runtime
|
||||
telemetry/privacy, recipient-routing or hosting acceptance. See the
|
||||
[current operating guide](../integrations/jotform/README.md) and
|
||||
[implementation and validation report](phase-9-form-a-default-live-2026-10-30.md).
|
||||
Earlier dated decisions and test results below are historical evidence, not
|
||||
current environment setup instructions.
|
||||
|
||||
The following acceptance matrix records the original October 22 results; see the new report for October 30 validation.
|
||||
|
||||
## Acceptance matrix
|
||||
|
||||
| Area | Evidence | Result | Human/owner gate |
|
||||
@@ -35,10 +69,12 @@ node --test tools/phase9-nonproduction.test.mjs
|
||||
node --test tools/phase10-artifact.test.mjs
|
||||
npm run build
|
||||
PUBLIC_ALLOW_INDEXING=true npm run build
|
||||
node tools/phase10-artifact-verify.mjs --mode=indexing --forms=live
|
||||
npm run build
|
||||
npm run audit:links
|
||||
npm run audit:images
|
||||
npm run audit:blog
|
||||
node tools/phase10-artifact-verify.mjs --mode=staging
|
||||
node tools/phase10-artifact-verify.mjs --mode=staging --forms=live
|
||||
node tools/phase10-release-rehearsal.mjs
|
||||
```
|
||||
|
||||
@@ -49,7 +85,7 @@ The final normal staging build was restored after the indexing build. The explic
|
||||
1. Record a locked Node/npm/toolchain and reproducible artifact manifest.
|
||||
2. Obtain sanitized Contabo/NixOS/nginx/TLS/DNS/Cloudflare facts.
|
||||
3. Verify host-local serving, canonical host, redirects, 404s, cache headers and rollback on an approved staging host.
|
||||
4. Form A has owner-reported functional acceptance and a local production gate, but still needs exact release-environment review, all-seven-route accessibility acceptance, runtime telemetry/privacy review, marketing-recipient operational review and no-deployment static-artifact verification. Its owner-reported configured `consultation` context is limited to nine isolated-test-only placements and remains outside the production resolver; physical Form B is retired; Form C remains unimplemented. Use Form A only when all four explicit build settings are approved: `PUBLIC_AIA_FORMS_MODE=live`, `PUBLIC_AIA_FORMS_LIVE_APPROVED=true`, `PUBLIC_AIA_FORMS_ENVIRONMENT=production`, and `PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true`. `PUBLIC_ALLOW_INDEXING=true` is publication behavior only.
|
||||
4. Default-live Form A is owner-authorized on exactly 16 placements, including public development hosting. No approval flags remain; indexing is independent. Final accessibility, Spanish options/provider-event, runtime privacy and marketing-recipient operational review remain open. Form C remains unimplemented. Site cutover still needs release authorization.
|
||||
5. Obtain P9-04 counter origin/credential/consent approval before a live adapter.
|
||||
6. Resolve P9-05 analytics/advertising measurement and P9-07 privacy/legal approval.
|
||||
7. Obtain P9-06 clinical/organizational and fluent-language approvals, including Arabic diagrams and Spanish HSA/FSA claims.
|
||||
@@ -59,7 +95,7 @@ The final normal staging build was restored after the indexing build. The explic
|
||||
|
||||
## Decision status
|
||||
|
||||
P9-01 through P9-10 remain **Pending decision**. No technical feasibility result in this package is an approval. Jotform, counters, analytics, advertising, forms, employee portal, redirects and hosting remain inactive.
|
||||
P9-01 through P9-10 remain **Pending decision**. No technical feasibility result in this package is an approval. Default-live Form A is explicitly owner-authorized. Counters, analytics, advertising, Form C, private portal serving and hosting activation remain separate and unauthorized by this package.
|
||||
|
||||
## Recommended checkpoint boundaries
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Phase 9 P9-09 — Contabo Deployment Readiness and Rollback Plan
|
||||
|
||||
> **Historical configuration superseded on 2026-10-30:** The default-live [Form A revision](phase-9-form-a-default-live-2026-10-30.md) replaces the former form approval/environment gates with optional live/off/mock mode on 16 placements. Earlier commands and results below describe their recorded checkpoint, not current startup requirements. No historical test result has been rerun or relabeled here.
|
||||
|
||||
Date: 2026-10-21
|
||||
Status: Planning only; no VPS, DNS, Cloudflare, nginx or TLS system was contacted or changed.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Focused consultation completion and portal presentation correction
|
||||
|
||||
> **Historical configuration superseded on 2026-10-30:** The default-live [Form A revision](phase-9-form-a-default-live-2026-10-30.md) replaces the former form approval/environment gates with optional live/off/mock mode on 16 placements. Earlier commands and results below describe their recorded checkpoint, not current startup requirements. No historical test result has been rerun or relabeled here.
|
||||
|
||||
Date: 2026-10-30 UTC. Starting branch `faithful-astro-migration`, starting HEAD
|
||||
`e97f0ea9b475715a1ea58110a27cdf50a2b3cb19`. Tracked tree and index were clean;
|
||||
no untracked paths were reported at preflight. The checkpoint is the commit
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Phase 9 — Form A consultation isolated-test integration
|
||||
|
||||
> **Historical configuration superseded on 2026-10-30:** The default-live [Form A revision](phase-9-form-a-default-live-2026-10-30.md) replaces the former form approval/environment gates with optional live/off/mock mode on 16 placements. Earlier commands and results below describe their recorded checkpoint, not current startup requirements. No historical test result has been rerun or relabeled here.
|
||||
|
||||
**Recorded:** 2026-10-29 UTC
|
||||
**Scope:** Form A (`262946078314059`) consultation context in local, non-indexing isolated-test builds
|
||||
**Status:** implemented and locally verified without a Jotform submission; owner acceptance pending
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
# Phase 9 — Default-live Form A simplification
|
||||
|
||||
Recorded: 2026-10-30 UTC. This is an owner-authorized architectural revision,
|
||||
not a site-cutover or deployment authorization.
|
||||
|
||||
## Baseline and authority
|
||||
|
||||
Started on `faithful-astro-migration` at pushed checkpoint
|
||||
`55adb797051473c3067a0d454515153237e9b8d6`
|
||||
(`fix(forms): close completed drawers and restore portal styling`). The tracked
|
||||
working tree and index were clean. This work is a new descendant checkpoint;
|
||||
the pushed commit is not amended, rebased or reset.
|
||||
|
||||
The owner explicitly authorizes working Form A on publicly reachable
|
||||
`https://aia.web3app.dev/`, with owner-controlled test notifications and cleanup.
|
||||
No extra hostname, environment-role, password or approval gate is introduced.
|
||||
Earlier isolated-test/seven-route production-gate reports retain their original
|
||||
results and now link to this superseding decision.
|
||||
|
||||
## Implementation
|
||||
|
||||
- `PUBLIC_AIA_FORMS_MODE` supports `live`, `off`, `mock`. Absent, empty or
|
||||
whitespace-only mode defaults to live. Invalid explicit values, including
|
||||
`isolated-test`, stop configuration with instructions to remove the obsolete
|
||||
value or choose a supported mode.
|
||||
- Removed the five former form approval/environment variables from active code,
|
||||
`.env.example` and current operating instructions. No replacement gate exists.
|
||||
- One typed registry fixes 16 route/locale/context triples. The ID and origin
|
||||
remain `262946078314059` and `https://form.jotform.com`. No visitor input,
|
||||
arbitrary origin, service preselection or extra query parameter is accepted.
|
||||
- Indexing is independent: both publication modes contain the same ten immediate
|
||||
frames and six deferred drawer URLs. Publication metadata and content are unchanged.
|
||||
- Native inactive/mock surfaces remain separate from the real iframe adapter.
|
||||
`/referrals` and future Form C cannot activate through live mode.
|
||||
- Live drawers retain four-second OR 50%-scroll opening, first-trigger wins,
|
||||
global 30-day dismissal/success suppression, manual CTA override, deferred
|
||||
iframe loading, native dialog focus/Escape/backdrop handling and reduced motion.
|
||||
Off mode stops automatic inactive drawers; CTA navigation remains usable.
|
||||
- Verified completion handling is unchanged: exact origin, actual activated/open
|
||||
iframe window, correct form ID/action, idempotence, localized acknowledgment,
|
||||
full five-second interval, slide-out and focus restoration. A later manual
|
||||
reopening after completion gets a fresh iframe. No load/click is success.
|
||||
- Shared iframe heights, sandbox, no-referrer, fixed URLs, 15-second conditional
|
||||
hosted fallback and localized noscript fallback remain unchanged.
|
||||
- Employee portal no longer reads the retired form environment variable. It
|
||||
still requires its own explicit approval, a valid private ciphertext input and
|
||||
explicit non-indexing preview setting. Normal builds contain no private payload.
|
||||
Decryption, sanitized rendering, styles, SOPS and private resources are unchanged.
|
||||
|
||||
## Approved placements
|
||||
|
||||
| Route | Locale | Context | Loading |
|
||||
|---|---|---|---|
|
||||
| `/contact` | en | contact | immediate |
|
||||
| `/es/contact` | es | contact | immediate |
|
||||
| `/lp/aba-therapy` | en | landing | immediate |
|
||||
| `/lp/aba-insurance` | en | landing | immediate |
|
||||
| `/lp/aba-ahcccs` | en | landing | immediate |
|
||||
| `/lp/aba-private-pay` | en | landing | immediate |
|
||||
| `/lp/early-intervention` | en | landing | immediate |
|
||||
| `/client-consultation` | en | consultation | immediate |
|
||||
| `/es/client-consultation` | es | consultation | immediate |
|
||||
| `/schedule-consultation` | en | consultation | immediate |
|
||||
| `/aba-therapy` | en | consultation | deferred drawer |
|
||||
| `/autism-evaluations` | en | consultation | deferred drawer |
|
||||
| `/learner-social-club` | en | consultation | deferred drawer |
|
||||
| `/es/aba-therapy` | es | consultation | deferred drawer |
|
||||
| `/es/autism-evaluations` | es | consultation | deferred drawer |
|
||||
| `/es/learner-social-club` | es | consultation | deferred drawer |
|
||||
|
||||
Each URL is `https://form.jotform.com/262946078314059?formContext=CONTEXT&language=LOCALE`
|
||||
with the exact fixed values above. The six deferred frames do not have `src`
|
||||
until the drawer opens. Academy copy remains typed but has no published placement.
|
||||
|
||||
## Validation
|
||||
|
||||
- Astro check: 151 files, zero errors, warnings or hints. Each real build also
|
||||
executes Astro check through the existing npm script.
|
||||
- Complete sequential test run: **206 passed, 0 failed/skipped**, including
|
||||
Markdown, publication, SEO, Library, routes, campaign, preview, Form A,
|
||||
counter/form isolation, artifact integrity and service/portal tests.
|
||||
- Initial consolidated run: 205/206. A campaign section-count assertion needed
|
||||
to exclude the nested shared iframe section while preserving the original
|
||||
seven page sections. Corrected the selector; focused rerun and full rerun pass.
|
||||
- Library/SEO off-mode presentation fixtures now explicitly select `off`, preserving
|
||||
their existing localization assertions. Publication fixtures deliberately test
|
||||
the empty/default-live mode in both publication configurations.
|
||||
- Links: 0 broken internal links. Images: 0 missing mapped images. Blog: 0 failures.
|
||||
- Invalid `PUBLIC_AIA_FORMS_MODE=isolated-test` build: rejected with actionable
|
||||
guidance, as expected. Default build restored afterward.
|
||||
- Ordinary `npm run dev`: eight representative contact, landing, consultation
|
||||
and EN/ES service routes returned default-live markup using loopback HTTP only.
|
||||
The npm launcher detached its managed server; the initial process-group cleanup
|
||||
returned ESRCH, so the identified task-owned server was stopped directly.
|
||||
- Markdown local links/fences, checklist column/ID consistency and `git diff --check`
|
||||
pass. Historical evidence is retained; no current-source reconciliation claim
|
||||
is made from an older fingerprint, and no historical JSON was regenerated.
|
||||
|
||||
| Built artifact | HTML routes | Sitemap URLs | Hreflang links | Immediate/deferred Form A | Result |
|
||||
|---|---:|---:|---:|---:|---|
|
||||
| Normal staging, default live | 128 | 0 | 0 | 10 / 6 | Pass |
|
||||
| Indexing enabled, default live | 127 | 109 | 180 | 10 / 6 | Pass |
|
||||
| Staging, explicit off | 128 | 0 | 0 | 0 / 0 | Pass |
|
||||
| Staging, explicit mock | 128 | 0 | 0 | 0 / 0 | Pass |
|
||||
|
||||
Staging remains `noindex,nofollow`; `/lp` review index appears only there.
|
||||
Indexing preserves author/campaign exclusions and translation architecture.
|
||||
Artifact unit fixtures additionally test off/mock against both publication modes,
|
||||
reject unknown destinations and premature drawer sources, and reject private
|
||||
portal payloads/resources, extra form IDs, API origins and tracking signals.
|
||||
No normal artifact contains active native lead-form actions or portal ciphertext.
|
||||
|
||||
Guarded Chromium 149.0.7827.55 evidence (temporary, not committed screenshots):
|
||||
|
||||
| Harness / artifact | Checks | Attempted Jotform GETs, intercepted locally | Evidence directory |
|
||||
|---|---:|---:|---|
|
||||
| Service/portal, default live | 47 | 37 | `/tmp/aia-service-portal-eOmGmZ` |
|
||||
| Contact/landing, default live | 35 | 38 | `/tmp/aia-form-a-iframe-5439e1` |
|
||||
| Consultation, default live | 45 | 40 | `/tmp/aia-form-a-consultation-ZzxRRB` |
|
||||
| Service/portal, off | 18 | 0 | `/tmp/aia-service-portal-IkEh6j` |
|
||||
| Contact/landing, off | 28 | 0 | `/tmp/aia-form-a-iframe-z838nC` |
|
||||
| Service/portal, mock | 18 | 0 | `/tmp/aia-service-portal-KxmUhB` |
|
||||
| Contact/landing, mock | 28 | 0 | `/tmp/aia-form-a-iframe-cetW0I` |
|
||||
|
||||
Live coverage includes 320/390/768/1440px, all 16 placements, exact URLs,
|
||||
localized titles, delayed/late-load fallback, no-JavaScript, keyboard/dialog
|
||||
behavior, trigger/suppression lifecycle, five-second verified-message handling,
|
||||
duplicate/wrong messages, fresh-frame reopening and both encrypted synthetic
|
||||
portal views. Off/mock drawer checks cover all six routes at 390/1440px; off
|
||||
CTAs navigate and do not auto-open, mock retains its synthetic drawer behavior.
|
||||
|
||||
The all-route guarded pass separately opened **128/128** staging routes with no
|
||||
failures or JavaScript exceptions. It recorded 10 approved Form A GET attempts,
|
||||
all blocked before transmission, and four blocked existing non-form resources.
|
||||
Evidence: `/tmp/aia-phase10-browser-zjwRCA`. These counts overlap the focused
|
||||
checks and are not additional unique routes. Every external request was blocked
|
||||
or fulfilled locally; zero real Jotform submissions, production API requests,
|
||||
analytics or advertising requests were sent. Existing map/resource attempts are
|
||||
recorded separately from approved Form A attempts. Real provider behavior,
|
||||
Firefox and human screen-reader acceptance were not reproduced.
|
||||
|
||||
## Operator workflow
|
||||
|
||||
From `www/`, with no Form A environment overrides:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
# Or build and inspect local static output:
|
||||
npm run build
|
||||
node tools/phase10-artifact-verify.mjs --mode=staging --forms=live
|
||||
npm run preview -- --host 127.0.0.1
|
||||
```
|
||||
|
||||
Both ordinary commands enable Form A. No deployment was performed. Remove stale
|
||||
`isolated-test` values from local `.env` files if the actionable error appears.
|
||||
Remove unused former approval/environment variables; they are not recognized gates.
|
||||
|
||||
```sh
|
||||
# Indexing changes publication only:
|
||||
PUBLIC_ALLOW_INDEXING=true npm run build
|
||||
node tools/phase10-artifact-verify.mjs --mode=indexing --forms=live
|
||||
# Explicit form rollback/offline artifact:
|
||||
PUBLIC_AIA_FORMS_MODE=off npm run build
|
||||
node tools/phase10-artifact-verify.mjs --mode=staging --forms=off
|
||||
# Synthetic local fixtures:
|
||||
PUBLIC_AIA_FORMS_MODE=mock npm run dev
|
||||
# Restore ordinary staging output:
|
||||
npm run build
|
||||
```
|
||||
|
||||
For service acceptance, use a fresh browser profile or remove only the
|
||||
`aia:service-inquiry:auto-suppressed-until` test preference. On EN/ES service pages,
|
||||
verify four-second/half-scroll opening; after dismissal, automatic opening stays
|
||||
suppressed while the existing CTA reopens manually. With JavaScript disabled or
|
||||
forms off, the CTA remains a consultation-page link. Do not clear unrelated storage.
|
||||
|
||||
## Remaining owner acceptance and boundaries
|
||||
|
||||
Owner-controlled real submissions must verify the provider's actual completion
|
||||
message and thank-you behavior. Automated fixtures validate the parent code, not
|
||||
Jotform delivery. The Spanish seven-option consultation dropdown remains a
|
||||
Builder translation/Update Options diagnostic; Astro supplies only fixed context
|
||||
and language and cannot modify the cross-origin options. No Jotform settings changed.
|
||||
|
||||
Final fluent-language, screen-reader, all-route responsive/keyboard, runtime
|
||||
telemetry/privacy, HTTPS-host and notification-recipient operational acceptance
|
||||
remain distinct from this owner-authorized default-live architecture. Historical
|
||||
owner-reported direct and sampled embedded functional acceptance is preserved.
|
||||
Form C creation/integration, counter activation, analytics and advertising remain
|
||||
separate work. Site cutover, private portal serving and infrastructure activation
|
||||
are not authorized.
|
||||
|
||||
No submissions, account mutations, API calls, deployment, push or infrastructure
|
||||
changes occurred. The accepted Jotform CSS backup is unchanged. Protected `merge-plan.md`,
|
||||
reconciliation archives and private portal collection were not read or changed.
|
||||
|
||||
## Final artifact integrity
|
||||
|
||||
Normal default-live staging restored: 410 files, aggregate SHA-256
|
||||
`cd042f9f6c27574e458223a73625f28e56b4bb964f7faf4cdb53a40acb19a4db`. This hashes only generated public output; it is not a production-release approval.
|
||||
|
||||
## Exact checkpoint path inventory
|
||||
|
||||
All paths below belong to this focused revision. Source changes remove form gates
|
||||
and decouple portal input; no content or accepted Jotform CSS was edited. The only
|
||||
new file is this report.
|
||||
|
||||
- `www/.env.example`
|
||||
- `www/README.md`
|
||||
- `www/integrations/employee-portal/README.md`
|
||||
- `www/integrations/jotform/README.md`
|
||||
- `www/reports/SESSION_HANDOFF.md`
|
||||
- `www/reports/phase-10-technical-readiness-package-2026-10-22.md`
|
||||
- `www/reports/phase-9-contabo-deployment-readiness-2026-10-21.md`
|
||||
- `www/reports/phase-9-drawer-portal-corrections-2026-10-30.md`
|
||||
- `www/reports/phase-9-form-a-consultation-isolated-test-2026-10-29.md`
|
||||
- `www/reports/phase-9-form-a-default-live-2026-10-30.md`
|
||||
- `www/reports/phase-9-jotform-implementation-plan-2026-10-18.md`
|
||||
- `www/reports/phase-9-jotform-integration-contract-2026-10-18.md`
|
||||
- `www/reports/phase-9-jotform-verification-checklist-2026-10-18.csv`
|
||||
- `www/reports/phase-9-service-portal-consolidated-2026-10-30.md`
|
||||
- `www/reports/phase-9-two-form-integration-specification-2026-10-28.md`
|
||||
- `www/src/components/ContactForm.astro`
|
||||
- `www/src/components/FormAPreview.astro`
|
||||
- `www/src/components/FormShell.astro`
|
||||
- `www/src/components/JotformIframe.astro`
|
||||
- `www/src/components/ServiceInquiryDrawer.astro`
|
||||
- `www/src/components/campaign/ConsultationForm.astro`
|
||||
- `www/src/components/campaign/EarlyInterventionPage.astro`
|
||||
- `www/src/components/pages/ContactPage.astro`
|
||||
- `www/src/components/pages/EmployeePortal.astro`
|
||||
- `www/src/scripts/service-inquiry.ts`
|
||||
- `www/src/utils/form-integration.ts`
|
||||
- `www/src/utils/portal-input.mjs`
|
||||
- `www/tools/campaign.test.mjs`
|
||||
- `www/tools/jotform-consultation-browser.mjs`
|
||||
- `www/tools/jotform-form-a-browser.mjs`
|
||||
- `www/tools/jotform-form-a-real-iframe-sizing.mjs`
|
||||
- `www/tools/jotform-form-a.test.mjs`
|
||||
- `www/tools/library.test.mjs`
|
||||
- `www/tools/phase10-artifact-verify.mjs`
|
||||
- `www/tools/phase10-artifact.test.mjs`
|
||||
- `www/tools/phase10-browser-regression.mjs`
|
||||
- `www/tools/phase9-nonproduction.test.mjs`
|
||||
- `www/tools/publication.test.mjs`
|
||||
- `www/tools/seo.test.mjs`
|
||||
- `www/tools/service-portal-browser.mjs`
|
||||
- `www/tools/service-portal.test.mjs`
|
||||
@@ -1,6 +1,38 @@
|
||||
# P9-03 source-specific implementation plan
|
||||
|
||||
Date: **2026-10-18** · Plan version **1.7 revision 2026-10-28** · Governing contract: [P9-03-JF-2026-10-18 v1.7](phase-9-jotform-integration-contract-2026-10-18.md).
|
||||
## Current decision — 2026-10-30: default-live Form A
|
||||
|
||||
The owner intentionally supersedes the earlier isolated-test and seven-route
|
||||
production-gate architecture. `npm run dev` and `npm run build` now enable Form A
|
||||
on exactly **16 approved placements**, including the publicly reachable
|
||||
`aia.web3app.dev` environment. No additional approval flags or access restriction
|
||||
are required. `PUBLIC_AIA_FORMS_MODE` is optional: absent/empty means `live`;
|
||||
explicit `off` and `mock` remain available. Invalid/obsolete `isolated-test`
|
||||
settings produce an actionable configuration error. The former approval flags
|
||||
and form environment role are retired, not renamed.
|
||||
|
||||
Indexing and forms are independent. Both publication modes emit ten immediate
|
||||
frames (two contact, five landing, three consultation) and six deferred bilingual
|
||||
service-drawer frames. The bounded registry fixes ID `262946078314059`, HTTPS
|
||||
origin, context and language. Forms B/C are not activated; physical B is retired.
|
||||
|
||||
Live drawers retain four-second/50%-scroll opening, shared 30-day suppression,
|
||||
manual CTA override, deferred loading, verified completion acknowledgment for
|
||||
five seconds, animated dismissal and fresh-form reopening. Off mode does not
|
||||
automatically show inactive drawers. All iframe security/fallback behavior is
|
||||
preserved. Employee-portal preview remains independently gated by explicit local
|
||||
approval, valid private ciphertext input and non-indexing output; Form A cannot
|
||||
authorize private content. No portal private resources enter normal artifacts.
|
||||
|
||||
This owner authorization does not modify Jotform, authorize site cutover or
|
||||
complete pending consultation-language, provider-event, accessibility, runtime
|
||||
telemetry/privacy, recipient-routing or hosting acceptance. See the
|
||||
[current operating guide](../integrations/jotform/README.md) and
|
||||
[implementation and validation report](phase-9-form-a-default-live-2026-10-30.md).
|
||||
Earlier dated decisions and test results below are historical evidence, not
|
||||
current environment setup instructions.
|
||||
|
||||
Date: **2026-10-18** · Plan version **1.9 revision 2026-10-30** · Governing contract: [P9-03-JF-2026-10-18 v1.9](phase-9-jotform-integration-contract-2026-10-18.md).
|
||||
|
||||
**Planning only. Do not execute any batch from this document without separate authorization.** Phase 9 establishes decisions and bounded implementation authority. Phase 10 retains final production integration acceptance, rollback and cutover. The user stages, signs and pushes.
|
||||
|
||||
|
||||
@@ -1,8 +1,40 @@
|
||||
# P9-03 Jotform integration contract
|
||||
|
||||
**Contract ID:** P9-03-JF-2026-10-18 · **Version:** 1.8 · **Decision revision:** 2026-10-29
|
||||
## Current decision — 2026-10-30: default-live Form A
|
||||
|
||||
**Status: Form A owner-approved for existing contexts; consultation surfaces implemented for isolated testing only; deployment and production consultation activation NOT authorized.**
|
||||
The owner intentionally supersedes the earlier isolated-test and seven-route
|
||||
production-gate architecture. `npm run dev` and `npm run build` now enable Form A
|
||||
on exactly **16 approved placements**, including the publicly reachable
|
||||
`aia.web3app.dev` environment. No additional approval flags or access restriction
|
||||
are required. `PUBLIC_AIA_FORMS_MODE` is optional: absent/empty means `live`;
|
||||
explicit `off` and `mock` remain available. Invalid/obsolete `isolated-test`
|
||||
settings produce an actionable configuration error. The former approval flags
|
||||
and form environment role are retired, not renamed.
|
||||
|
||||
Indexing and forms are independent. Both publication modes emit ten immediate
|
||||
frames (two contact, five landing, three consultation) and six deferred bilingual
|
||||
service-drawer frames. The bounded registry fixes ID `262946078314059`, HTTPS
|
||||
origin, context and language. Forms B/C are not activated; physical B is retired.
|
||||
|
||||
Live drawers retain four-second/50%-scroll opening, shared 30-day suppression,
|
||||
manual CTA override, deferred loading, verified completion acknowledgment for
|
||||
five seconds, animated dismissal and fresh-form reopening. Off mode does not
|
||||
automatically show inactive drawers. All iframe security/fallback behavior is
|
||||
preserved. Employee-portal preview remains independently gated by explicit local
|
||||
approval, valid private ciphertext input and non-indexing output; Form A cannot
|
||||
authorize private content. No portal private resources enter normal artifacts.
|
||||
|
||||
This owner authorization does not modify Jotform, authorize site cutover or
|
||||
complete pending consultation-language, provider-event, accessibility, runtime
|
||||
telemetry/privacy, recipient-routing or hosting acceptance. See the
|
||||
[current operating guide](../integrations/jotform/README.md) and
|
||||
[implementation and validation report](phase-9-form-a-default-live-2026-10-30.md).
|
||||
Earlier dated decisions and test results below are historical evidence, not
|
||||
current environment setup instructions.
|
||||
|
||||
**Contract ID:** P9-03-JF-2026-10-18 · **Version:** 1.9 · **Decision revision:** 2026-10-30
|
||||
|
||||
**Current status: default-live Form A approved on 16 placements; site deployment/cutover not authorized. Historical contract follows.**
|
||||
|
||||
This is the authoritative P9-03 planning contract for the owner decisions supplied on October 18 and revised through October 29. It supersedes conflicting *proposals* in the [October 17 inventory](phase-9-form-entry-point-inventory-2026-10-17.md), without rewriting that historical record. It does not supersede project policy, approve clinical/legal content beyond the Form A configuration scope recorded below, close the Phase 9 register, or authorize production changes. Future changes require an explicit owner decision, a version/change record and evidence links. Version 1.0 records the original planning instruction; version 1.1 adds the owner-supplied complete roster; version 1.2 supersedes the copy-based Form B proposal with three new blank forms; version 1.3 records limited owner-confirmed Form A configuration evidence; version 1.4 reconciles completed direct-Jotform testing and the seven-route isolated iframe adapter; version 1.5 records owner-reported embedded synthetic acceptance for sampled routes; version 1.6 adds the independent production-build gate without deploying it; version 1.7 retires physical Form B and specifies Form A consultation plus Form C referral planning; version 1.8 records the owner-reported consultation configuration and a separate nine-route isolated-test adapter. No future commit SHA is invented.
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ JF-04,Emails,"Notifications/autoresponders, recipient roles, subject/body/dynami
|
||||
JF-05,Thank-you behavior,"Native confirmation, redirects, answer echo, all targets, HTTP POST forwarding",Account/privacy owner; Builder; submission-data POST to Astro/Contabo OFF,"Owner-confirmed 2026-10-28 direct-Jotform tests: native thank-you behavior verified. Redirect, answer echo, POST-forwarding settings and Astro-embedded confirmation remain open",Partially verified — Form A direct native thank-you,2026-10-28,Separate batch authorization; no action authorized by this checklist
|
||||
JF-06,Integrations,"Integrations, webhooks, exports, destinations, Google/analytics, protected-data flows","Account/security/privacy owner; actual integration inventory, not cloning assumptions",Owner-confirmed 2026-10-27 ChatGPT WebUI/Jotform review: Form A has no webhooks or integrations; export/default/vendor behaviors beyond that reported review remain unverified,Verified — Form A webhooks/integrations absent,2026-10-27,Separate batch authorization; no action authorized by this checklist
|
||||
JF-07,Security and availability,"HIPAA/form/field protection, spam, availability, prefill, save/continue, access, retention, limits and duplicates",Account/privacy/security owner; settings and approved operational policy,"Owner-verified 2026-10-27: privacy approval covers Form A PHI protection, generic no-PHI notification, no PDF attachment, no webhooks/integrations, recipient access and retention/deletion; other security/availability controls remain unverified","Partially verified — Form A approved configuration, access and retention scope",2026-10-27,Separate batch authorization; no action authorized by this checklist
|
||||
JF-08,Embedding,"Approved HTTPS origin, hosted/iframe URLs, dimensions/mobile, scripts, referrer, locale and fallback",Account/hosting/accessibility owners; exported code and later authorized runtime evidence,"Repository evidence 2026-10-28: fixed Form A origin/URLs, no-referrer policy, minimal sandbox, localized conditional hosted fallback and seven-route isolated-test gate. Owner reports embedded synthetic submission/confirmation on EN contact, ES contact and one ABA landing placement; exact third route and all-seven-route keyboard/responsive/runtime-network acceptance remain open",Partially verified — owner-reported sampled embedded acceptance plus adapter/fallback,2026-10-28,Separate batch authorization; no action authorized by this checklist
|
||||
JF-09,Controlled synthetic acceptance,"Actual EN/ES synthetic submission, notifications, success/failure, redirects and privacy boundary",Later separately authorized isolated form/environment; never real patient data,"Owner-confirmed 2026-10-28 direct-Jotform EN/ES and sampled Astro-iframe synthetic submissions with native thank-you and notification/autoresponder delivery. Functional submission acceptance is complete for the reported samples; exact ABA landing route, all-seven-route runtime/privacy and production-release acceptance remain open",Owner-verified — Form A functional submission samples,2026-10-28,Separate batch authorization; no action authorized by this checklist
|
||||
JF-08,Embedding,"Approved HTTPS origin, hosted/iframe URLs, dimensions/mobile, scripts, referrer, locale and fallback",Account/hosting/accessibility owners; exported code and later authorized runtime evidence,"Repository evidence 2026-10-28: fixed Form A origin/URLs, no-referrer policy, minimal sandbox, localized conditional hosted fallback and seven-route isolated-test gate. Owner reports embedded synthetic submission/confirmation on EN contact, ES contact and one ABA landing placement; exact third route and all-seven-route keyboard/responsive/runtime-network acceptance remain open; 2026-10-30 supersession: 16 placements are default-live by owner authorization, with remaining all-route runtime/accessibility acceptance separate",Partially verified — owner-reported sampled embedded acceptance plus adapter/fallback,2026-10-28,Separate batch authorization; no action authorized by this checklist
|
||||
JF-09,Controlled synthetic acceptance,"Actual EN/ES synthetic submission, notifications, success/failure, redirects and privacy boundary",Later separately authorized isolated form/environment; never real patient data,"Owner-confirmed 2026-10-28 direct-Jotform EN/ES and sampled Astro-iframe synthetic submissions with native thank-you and notification/autoresponder delivery. Functional submission acceptance is complete for the reported samples; exact ABA landing route, all-seven-route runtime/privacy and production-release acceptance remain open; 2026-10-30 supersession: 16 placements are default-live by owner authorization, with remaining all-route runtime/accessibility acceptance separate",Owner-verified — Form A functional submission samples,2026-10-28,Separate batch authorization; no action authorized by this checklist
|
||||
OA-01,Intake/operations,"Minimum fields, required flags/contact preference consistency, follow-up/callback expectations, Tour/fundraising scope",Intake/operations,Dated owner approval with scope and sanitized supporting evidence,Pending/Unverified,,Separate batch authorization; no action authorized by this checklist
|
||||
OA-02,Clinical leadership,"Eligibility, service/evaluation availability, private-pay Social Club distinction, referral boundary and approved claims",Clinical leadership,Dated owner approval with scope and sanitized supporting evidence,Pending/Unverified,,Separate batch authorization; no action authorized by this checklist
|
||||
OA-03,Jotform account owner,"JF-01–08 for Form A consultation and Form C, Form C blank-form creation, actual configuration/ID and preservation of all existing forms",Jotform account owner,Dated owner approval with scope and sanitized supporting evidence,Pending/Unverified,,Separate batch authorization; no action authorized by this checklist
|
||||
@@ -16,7 +16,7 @@ OA-05,Privacy/legal/security,"Protected fields, generic email/headers/recipients
|
||||
OA-06,Marketing,Deferred attribution categories and redesigned successful-submission semantics,Marketing,Dated owner approval with scope and sanitized supporting evidence,Pending/Unverified,,Separate batch authorization; no action authorized by this checklist
|
||||
OA-07,Hosting owner,"Static/API separation, compile/deploy gates, CSP/referrer/log policy, preview isolation and fallback topology",Hosting owner,Dated owner approval with scope and sanitized supporting evidence,Pending/Unverified,,Separate batch authorization; no action authorized by this checklist
|
||||
OA-08,Release owner,"Phase 10 integration activation, go/no-go, artifact/vendor rollback and deployment",Release owner,Dated owner approval with scope and sanitized supporting evidence,Pending/Unverified,,Separate batch authorization; no action authorized by this checklist
|
||||
FA-PRD-01,Form A production build gate,"Confirm live mode, existing live approval, production environment role and independent Form A production approval are all set only in the intended release environment; verify no Form C or unrelated surface activates",Release + hosting owner,"Repository configuration resolver and production-artifact test; no deployed environment configured",Implemented locally — deployment verification pending,2026-10-28,Phase 10 deployment authorization and sanitized environment review
|
||||
FA-CONS-01,Form A consultation context,"Verify fixed `consultation` presentation value, required service-interest question, five approved choices, conditional email/phone behavior, EN/ES translations and no additional collection; no diagnostic-evaluation explanatory note",Account + operations + clinical + privacy owners,"Owner reports connected-app configuration; sanitized Builder configuration and approved synthetic test evidence still required; no ID/condition inferred by repository",Owner-reported configured — browser acceptance pending,2026-10-29,No Jotform mutation by repository; isolated-test route acceptance is separate from production authorization
|
||||
FC-01,Form C referral initiation,"Create only after authorization, then verify minimum professional fields, no patient data instruction, PHI controls, notification/retention and protected-workflow handoff",Account + operations + privacy owners,"Sanitized Form C Builder configuration and approved synthetic test evidence",Pending/Unverified,,Separate creation and synthetic-test authorization required
|
||||
FA-PRD-01,Form A mode and placement contract,Superseded 2026-10-30: live is default on 16 fixed placements; explicit off/mock isolate forms; indexing is independent; invalid modes fail clearly,Release + hosting owner,Owner authorizes public development forms; repository registry/artifact/browser tests; no hosting deployment performed,Owner-authorized architecture — runtime/site-release acceptance separate,2026-10-30,No additional Form A build approval flags; site cutover requires separate authorization
|
||||
FA-CONS-01,Form A consultation context,"Verify fixed `consultation` presentation value, required service-interest question, five approved choices, conditional email/phone behavior, EN/ES translations and no additional collection; no diagnostic-evaluation explanatory note",Account + operations + clinical + privacy owners,Owner reports connected-app configuration; sanitized Builder configuration and approved synthetic test evidence still required; no ID/condition inferred by repository,Owner-reported configured — browser acceptance pending,2026-10-29,Default-live consultation placements owner-authorized 2026-10-30; Jotform runtime/language acceptance and site cutover remain separate
|
||||
FC-01,Form C referral initiation,"Create only after authorization, then verify minimum professional fields, no patient data instruction, PHI controls, notification/retention and protected-workflow handoff",Account + operations + privacy owners,Sanitized Form C Builder configuration and approved synthetic test evidence,Pending/Unverified,,Separate creation and synthetic-test authorization required
|
||||
INV-01,Supplied roster provenance and remaining verification,Complete 27-ID roster received and uniqueness checked; verify uncertain generic titles and relevant operational classification with account owner without inferring usage or retrieving submissions,Jotform account owner,Owner-supplied read-only connector roster; contract v1.2 section 3 lists all 27 unique IDs; individual roster no longer outstanding; new-form configuration and usage verification pending,Pending/Unverified,,Read-only metadata review authorization; no account mutation
|
||||
|
@@ -1,5 +1,7 @@
|
||||
# Consolidated service-drawer and employee-portal development
|
||||
|
||||
> **Historical configuration superseded on 2026-10-30:** The default-live [Form A revision](phase-9-form-a-default-live-2026-10-30.md) replaces the former form approval/environment gates with optional live/off/mock mode on 16 placements. Earlier commands and results below describe their recorded checkpoint, not current startup requirements. No historical test result has been rerun or relabeled here.
|
||||
|
||||
Date: 2026-10-30 UTC. Starting branch: `faithful-astro-migration`.
|
||||
Starting HEAD: `ff80ee516f0185023af1f29937e29dfb30d31f47`.
|
||||
Tracked tree and index were clean; only owner-provided `prompt.txt` was untracked.
|
||||
|
||||
@@ -1,28 +1,60 @@
|
||||
# Phase 9 — Two-form integration specification
|
||||
|
||||
**Recorded:** 2026-10-28 UTC; revised 2026-10-29 UTC
|
||||
## Current decision — 2026-10-30: default-live Form A
|
||||
|
||||
**Scope:** Form A consultation isolated-test integration and future Form C referral initiation
|
||||
**Status:** owner-reported Jotform consultation configuration; Astro routes limited to isolated testing; no production activation authorized
|
||||
The owner intentionally supersedes the earlier isolated-test and seven-route
|
||||
production-gate architecture. `npm run dev` and `npm run build` now enable Form A
|
||||
on exactly **16 approved placements**, including the publicly reachable
|
||||
`aia.web3app.dev` environment. No additional approval flags or access restriction
|
||||
are required. `PUBLIC_AIA_FORMS_MODE` is optional: absent/empty means `live`;
|
||||
explicit `off` and `mock` remain available. Invalid/obsolete `isolated-test`
|
||||
settings produce an actionable configuration error. The former approval flags
|
||||
and form environment role are retired, not renamed.
|
||||
|
||||
Indexing and forms are independent. Both publication modes emit ten immediate
|
||||
frames (two contact, five landing, three consultation) and six deferred bilingual
|
||||
service-drawer frames. The bounded registry fixes ID `262946078314059`, HTTPS
|
||||
origin, context and language. Forms B/C are not activated; physical B is retired.
|
||||
|
||||
Live drawers retain four-second/50%-scroll opening, shared 30-day suppression,
|
||||
manual CTA override, deferred loading, verified completion acknowledgment for
|
||||
five seconds, animated dismissal and fresh-form reopening. Off mode does not
|
||||
automatically show inactive drawers. All iframe security/fallback behavior is
|
||||
preserved. Employee-portal preview remains independently gated by explicit local
|
||||
approval, valid private ciphertext input and non-indexing output; Form A cannot
|
||||
authorize private content. No portal private resources enter normal artifacts.
|
||||
|
||||
This owner authorization does not modify Jotform, authorize site cutover or
|
||||
complete pending consultation-language, provider-event, accessibility, runtime
|
||||
telemetry/privacy, recipient-routing or hosting acceptance. See the
|
||||
[current operating guide](../integrations/jotform/README.md) and
|
||||
[implementation and validation report](phase-9-form-a-default-live-2026-10-30.md).
|
||||
Earlier dated decisions and test results below are historical evidence, not
|
||||
current environment setup instructions.
|
||||
|
||||
**Recorded:** 2026-10-28 UTC; revised 2026-10-30 UTC
|
||||
|
||||
**Scope:** Default-live Form A integration and future Form C referral initiation
|
||||
**Status:** 16 default-live placements owner-authorized; Jotform language/runtime acceptance and site cutover remain separate
|
||||
|
||||
## Decision revision
|
||||
|
||||
The owner has replaced the former three-physical-form plan with two physical Jotform forms:
|
||||
|
||||
- **Form A — General Enquiries, landing enquiries and family service consultations.** The existing Form A ID remains `262946078314059`; its approved `contact` and `landing` contexts and seven current Astro placements are unchanged.
|
||||
- **Form A — General Enquiries, landing enquiries and family service consultations.** The existing Form A ID remains `262946078314059`; its approved `contact`, `landing` and `consultation` contexts cover 16 current Astro placements.
|
||||
- **Form C — Professional Referral Initiation.** It remains a separate future blank form with no assigned Jotform ID.
|
||||
|
||||
**Form B is retired as a physical form.** Historical Form B decisions remain in the versioned contract and plan. The repository may keep `service` as a logical visitor-intent classification, but it will map to Form A's planned `consultation` context after the required Builder configuration and acceptance.
|
||||
**Form B is retired as a physical form.** Historical Form B decisions remain in the versioned contract and plan. The repository may keep `service` as a logical visitor-intent classification, but it maps to Form A's `consultation` context; remaining Builder/runtime acceptance does not constitute a new code gate.
|
||||
|
||||
This revision does not enable a new route, add an iframe placement, modify Form A, create Form C, add a query parameter, alter a canonical URL, promise an appointment, or change the frozen seven-route Form A production allowlist.
|
||||
The October 30 decision enables all 16 existing placements by default without creating routes, modifying Jotform, creating Form C, changing query parameters or canonicals, or promising appointments.
|
||||
|
||||
## Form A contexts and planned consultation contract
|
||||
|
||||
| Context | Status | Intended audience | Required route state |
|
||||
|---|---|---|---|
|
||||
| `contact` | Existing, owner-tested | General contact enquiries | Current two-contact-route gate only |
|
||||
| `landing` | Existing, owner-tested | Five named campaign landing journeys | Current five-landing-route gate only |
|
||||
| `consultation` | Configured in Jotform; isolated-test Astro placements only | Family service consultations | Owner browser acceptance; no production route authorization |
|
||||
| `contact` | Existing, owner-tested | General contact enquiries | Two approved live placements |
|
||||
| `landing` | Existing, owner-tested | Five named campaign landing journeys | Five approved live placements |
|
||||
| `consultation` | Configured in Jotform; nine live Astro placements | Family service consultations | Owner runtime acceptance remains separate |
|
||||
|
||||
The context value is presentation-only. It is not an authorization, attribution, security, analytics or visitor-controlled value. `formContext` remains the known case-sensitive hidden-field Unique Name; no other Jotform field ID or condition name is inferred here.
|
||||
|
||||
@@ -60,48 +92,48 @@ Form C remains a separate future physical Jotform created from a blank form. Its
|
||||
|
||||
The visible form must instruct referrers not to submit patient names, dates of birth, diagnosis narratives, medical records or other patient-identifying information. Detailed referrals remain in the separately approved protected workflow. No Form C ID, hosted URL, iframe, language route or conditional logic is assigned by this document.
|
||||
|
||||
## Isolated-test route and component matrix
|
||||
## Current consultation route and component matrix
|
||||
|
||||
The following nine placements are implemented only for an explicitly authorized, non-indexing isolated-test build. Normal staging, mock, indexing-enabled and production builds retain their existing inactive/off/mock behavior. The seven existing Form A production-eligible placements remain a separate frozen allowlist.
|
||||
The following nine placements join the original seven in one live registry. Both indexing and non-indexing builds enable them by default. Explicit off/mock builds contain no real Form A frames.
|
||||
|
||||
| Route or entry point | Locale | Current component / behavior | Future physical form and context | Dependencies |
|
||||
|---|---|---|---|---|
|
||||
| `/client-consultation` | EN | `FormShell.astro` through `PageLayout.astro`; immediate isolated-test iframe | Form A / `consultation` | Owner Jotform visibility/requiredness/confirmation acceptance |
|
||||
| `/es/client-consultation` | ES | Same component; immediate isolated-test iframe | Form A / `consultation` | Spanish Jotform labels, errors and locale acceptance |
|
||||
| `/schedule-consultation` | EN | `FormShell.astro` through `PageLayout.astro`; immediate isolated-test iframe | Form A / `consultation` | No-booking copy and owner acceptance |
|
||||
| `/aba-therapy` drawer | EN | `ServiceInquiryDrawer.astro`; explicit user-triggered isolated-test drawer | Form A / `consultation` | Owner acceptance and dialog accessibility review |
|
||||
| `/autism-evaluations` drawer | EN | Same component; explicit user-triggered isolated-test drawer | Form A / `consultation` | Clinical/operations copy and owner acceptance |
|
||||
| `/learner-social-club` drawer | EN | Same component; explicit user-triggered isolated-test drawer | Form A / `consultation` | Program copy and owner acceptance |
|
||||
| `/es/aba-therapy` drawer | ES | Same shared drawer; explicit user-triggered isolated-test drawer | Form A / `consultation` | Spanish Jotform and fluent-language acceptance |
|
||||
| `/es/autism-evaluations` drawer | ES | Same shared drawer; explicit user-triggered isolated-test drawer | Form A / `consultation` | Spanish Jotform and fluent-language acceptance |
|
||||
| `/es/learner-social-club` drawer | ES | Same shared drawer; explicit user-triggered isolated-test drawer | Form A / `consultation` | Spanish Jotform and fluent-language acceptance |
|
||||
| `/client-consultation` | EN | `FormShell.astro` through `PageLayout.astro`; immediate live iframe | Form A / `consultation` | Owner Jotform visibility/requiredness/confirmation acceptance |
|
||||
| `/es/client-consultation` | ES | Same component; immediate live iframe | Form A / `consultation` | Spanish Jotform labels, errors and locale acceptance |
|
||||
| `/schedule-consultation` | EN | `FormShell.astro` through `PageLayout.astro`; immediate live iframe | Form A / `consultation` | No-booking copy and owner acceptance |
|
||||
| `/aba-therapy` drawer | EN | `ServiceInquiryDrawer.astro`; live automatic/manual drawer | Form A / `consultation` | Owner acceptance and dialog accessibility review |
|
||||
| `/autism-evaluations` drawer | EN | Same component; live automatic/manual drawer | Form A / `consultation` | Clinical/operations copy and owner acceptance |
|
||||
| `/learner-social-club` drawer | EN | Same component; live automatic/manual drawer | Form A / `consultation` | Program copy and owner acceptance |
|
||||
| `/es/aba-therapy` drawer | ES | Same shared drawer; live automatic/manual drawer | Form A / `consultation` | Spanish Jotform and fluent-language acceptance |
|
||||
| `/es/autism-evaluations` drawer | ES | Same shared drawer; live automatic/manual drawer | Form A / `consultation` | Spanish Jotform and fluent-language acceptance |
|
||||
| `/es/learner-social-club` drawer | ES | Same shared drawer; live automatic/manual drawer | Form A / `consultation` | Spanish Jotform and fluent-language acceptance |
|
||||
| `/referrals` | EN | `FormShell.astro`, `logicalForm=referral`; inactive | Form C / future referral initiation | Create and verify Form C; no patient data in initial form |
|
||||
|
||||
English and Spanish service-page CTAs already route visitors to the client consultation routes. They are navigation dependencies, not independent Form A iframe placements. The same is true of homepage, services, FAQ and library consultation CTAs that resolve to those pages.
|
||||
English and Spanish service-page CTAs open their authorized live drawer with JavaScript, retaining their consultation-page navigation destination otherwise. They are not additional iframe placements. The same is true of homepage, services, FAQ and library consultation CTAs that resolve to those pages.
|
||||
|
||||
The current Tour CTA in `src/content/pages/en/tour.mdx` points to `/client-consultation`. The planned consultation service list contains no tour choice. Intake/operations must decide whether Tour keeps that destination, moves to the existing general-contact/tour journey, or receives a separately approved context before any code or CTA change. This specification makes no Tour-route change.
|
||||
|
||||
## Owner and Jotform actions before consultation activation
|
||||
## Remaining owner and Jotform acceptance actions
|
||||
|
||||
1. Confirm that the configured Form A `consultation` context has not changed the approved `contact` or `landing` journeys; record the rollback method for the Builder configuration.
|
||||
2. Verify the exact hidden context value, service question, five display options, requiredness, EN/ES translations, validation messages and confirmation language in Jotform. Do not invent fields or accept query-controlled context values.
|
||||
3. Confirm that no child/clinical/payment/location/booking fields, webhooks, integrations, answer-bearing notifications, PDF attachments, analytics or forwarding are introduced for the consultation context.
|
||||
4. Obtain intake/operations, clinical and privacy approval for the field set and approved service options. Obtain fluent English/Spanish review for all added presentation and error text.
|
||||
5. Run separately authorized synthetic Jotform and Astro-iframe acceptance tests with no real patient data. Cover both locales, all service choices, validation/error states, no-JavaScript and keyboard behavior, confirmation, notifications, responsive sizes and direct hosted fallback.
|
||||
6. Only then authorize a production allowlist and production-gate update. The current seven Form A routes remain the only production-eligible real iframes until that implementation is independently reviewed.
|
||||
6. Record acceptance separately from the owner-authorized default-live 16-route registry. Site deployment/cutover still requires release authorization; no further Form A build flags are required.
|
||||
7. For Form C, separately authorize creation from a blank form, then verify its minimum fields, PHI protections, recipients, retention, hosted/iframe behavior and protected-workflow handoff before mapping `/referrals`.
|
||||
|
||||
## Retained gates
|
||||
|
||||
This decision does not close P9-01, P9-03, P9-05, P9-06, P9-07, P9-09 or P9-10. Form A's existing owner-reported direct and sampled embedded acceptance does not prove the new consultation context. Final real screen-reader, all-route responsive, runtime telemetry/privacy, hosting, notification-recipient and release-owner acceptance remain open.
|
||||
|
||||
## 2026-10-29 supersession and implementation addendum
|
||||
## Historical 2026-10-29 supersession and implementation addendum (gate superseded above)
|
||||
|
||||
The owner reports that the Form A `consultation` context and its conditional rules have been configured through the connected Jotform app. The repository now permits the nine matrix placements only when every isolated-test gate is present, including `PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED=true`. Three standalone pages load the fixed consultation URL immediately; six English/Spanish service drawers retain no iframe `src` until the visitor explicitly opens them. This does not change the frozen seven-route production allowlist.
|
||||
|
||||
The earlier version of this specification required a conditional Autism Diagnostic Evaluation explanation. That requirement is superseded: no conditional explanatory note is required in Form A or Astro. The historical decision is preserved by this dated addendum; the current contract is the client-advocate conversation described above.
|
||||
|
||||
## 2026-10-30 consolidated drawer behavior revision
|
||||
## Historical 2026-10-30 consolidated drawer behavior revision (mode superseded above)
|
||||
|
||||
The later owner prompt supersedes explicit-only opening for the six EN/ES
|
||||
service drawers. Authorized consultation-test drawers now open after four seconds
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
const { lang = 'en', logicalForm = 'general' } = Astro.props;
|
||||
const text = formMessages(lang);
|
||||
const title = Astro.props.title ?? text.contactHeading;
|
||||
const config = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE, import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED);
|
||||
const config = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE);
|
||||
const mode = formModeForRuntime(config);
|
||||
const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_RESULT);
|
||||
---
|
||||
|
||||
@@ -10,7 +10,7 @@ interface Props {
|
||||
|
||||
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 config = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE);
|
||||
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}`;
|
||||
|
||||
@@ -8,18 +8,12 @@ const { lang = 'en', showBackground = false, route = '' } = Astro.props;
|
||||
const text = formMessages(lang);
|
||||
const title = Astro.props.title ?? text.heading;
|
||||
const logicalForm: LogicalForm = Astro.props.logicalForm ?? (Astro.props.title === 'Physician & Insurer Referrals' ? 'referral' : 'service');
|
||||
const config = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE, import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED);
|
||||
const config = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE);
|
||||
const mode = formModeForRuntime(config);
|
||||
const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_RESULT);
|
||||
const formAIframe = logicalForm === 'service'
|
||||
? resolveFormAIframe({
|
||||
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
||||
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
||||
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED,
|
||||
consultationTestApproved: import.meta.env.PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED,
|
||||
productionApproved: import.meta.env.PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED,
|
||||
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
||||
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
||||
route,
|
||||
locale: lang,
|
||||
})
|
||||
|
||||
@@ -27,7 +27,6 @@ const headingId = `${idBase}-heading`;
|
||||
aria-label={hideHeading ? config.title : undefined}
|
||||
data-jotform-form-a
|
||||
data-form-mode={config.mode}
|
||||
data-form-environment={config.environmentRole}
|
||||
data-logical-form={config.context === 'consultation' ? 'service' : 'general'}
|
||||
data-form-context={config.context}
|
||||
data-jotform-source="approved-form-a"
|
||||
|
||||
@@ -122,24 +122,18 @@ const serviceCopy = serviceInquiryCopy[service][lang];
|
||||
const drawerId = `service-inquiry-${lang}-${service}`;
|
||||
const titleId = `${drawerId}-title`;
|
||||
const noteId = `${drawerId}-note`;
|
||||
const config = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE, import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED);
|
||||
const config = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE);
|
||||
const mode = formModeForRuntime(config);
|
||||
const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_RESULT);
|
||||
const formAIframe = resolveFormAIframe({
|
||||
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
||||
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
||||
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED,
|
||||
consultationTestApproved: import.meta.env.PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED,
|
||||
productionApproved: import.meta.env.PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED,
|
||||
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
||||
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
||||
route,
|
||||
locale: lang,
|
||||
});
|
||||
const consultationTestEnabled = formAIframe.enabled && formAIframe.context === 'consultation';
|
||||
const consultationEnabled = formAIframe.enabled && formAIframe.context === 'consultation';
|
||||
---
|
||||
|
||||
{consultationTestEnabled && formAIframe.url && <>
|
||||
{consultationEnabled && formAIframe.url && <>
|
||||
<noscript>
|
||||
<p class="service-inquiry-drawer__noscript">
|
||||
{copy.noScript} <a href={formAIframe.url} target="_blank" rel="noopener noreferrer">{copy.hostedLink}</a>
|
||||
@@ -151,7 +145,7 @@ const consultationTestEnabled = formAIframe.enabled && formAIframe.context === '
|
||||
id={drawerId}
|
||||
class="service-inquiry-drawer"
|
||||
data-service-inquiry-drawer
|
||||
data-consultation-test={consultationTestEnabled ? 'true' : undefined}
|
||||
data-consultation-enabled={consultationEnabled ? 'true' : undefined}
|
||||
data-service={service}
|
||||
aria-labelledby={titleId}
|
||||
>
|
||||
@@ -162,12 +156,12 @@ const consultationTestEnabled = formAIframe.enabled && formAIframe.context === '
|
||||
</button>
|
||||
|
||||
<header class="service-inquiry-drawer__intro">
|
||||
<h2 id={titleId} data-service-inquiry-title tabindex="-1">{consultationTestEnabled ? serviceCopy.heading : serviceName}</h2>
|
||||
<p>{consultationTestEnabled ? serviceCopy.introduction : copy.legacyStatus}</p>
|
||||
{consultationTestEnabled && <p class="service-inquiry-drawer__privacy">{copy.privacy}</p>}
|
||||
<h2 id={titleId} data-service-inquiry-title tabindex="-1">{consultationEnabled ? serviceCopy.heading : serviceName}</h2>
|
||||
<p>{consultationEnabled ? serviceCopy.introduction : copy.legacyStatus}</p>
|
||||
{consultationEnabled && <p class="service-inquiry-drawer__privacy">{copy.privacy}</p>}
|
||||
</header>
|
||||
|
||||
{consultationTestEnabled
|
||||
{consultationEnabled
|
||||
? <div class="service-inquiry-drawer__iframe">
|
||||
<p data-service-inquiry-completed hidden role="status" aria-live="polite" aria-atomic="true">
|
||||
{lang === 'es' ? '¡Gracias! Hemos recibido su mensaje.' : 'Thank you! Your message has been received.'}
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 formConfig = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE);
|
||||
const formPreview = formAPreviewEnabled(import.meta.env.PUBLIC_AIA_FORM_A_PREVIEW, import.meta.env.PUBLIC_ALLOW_INDEXING, formModeForRuntime(formConfig));
|
||||
const campaignRoute = sourcePage === 'general'
|
||||
? '/lp/aba-therapy'
|
||||
@@ -24,12 +24,6 @@ const campaignRoute = sourcePage === 'general'
|
||||
const iframeDescription = 'Use this form to request a conversation. An AIA team member can explain the next steps for your family.';
|
||||
const formAIframe = resolveFormAIframe({
|
||||
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
||||
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
||||
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED,
|
||||
consultationTestApproved: import.meta.env.PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED,
|
||||
productionApproved: import.meta.env.PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED,
|
||||
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
||||
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
||||
route: campaignRoute,
|
||||
locale: 'en',
|
||||
});
|
||||
|
||||
@@ -4,12 +4,6 @@ import { resolveFormAIframe } from '../../utils/form-integration';
|
||||
|
||||
const formAIframe = resolveFormAIframe({
|
||||
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
||||
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
||||
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED,
|
||||
consultationTestApproved: import.meta.env.PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED,
|
||||
productionApproved: import.meta.env.PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED,
|
||||
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
||||
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
||||
route: '/lp/early-intervention',
|
||||
locale: 'en',
|
||||
});
|
||||
|
||||
@@ -12,17 +12,11 @@ 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 formConfig = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE);
|
||||
const formPreview = formAPreviewEnabled(import.meta.env.PUBLIC_AIA_FORM_A_PREVIEW, import.meta.env.PUBLIC_ALLOW_INDEXING, formModeForRuntime(formConfig));
|
||||
const route = lang === 'es' ? '/es/contact' : '/contact';
|
||||
const formAIframe = resolveFormAIframe({
|
||||
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
||||
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
||||
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED,
|
||||
consultationTestApproved: import.meta.env.PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED,
|
||||
productionApproved: import.meta.env.PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED,
|
||||
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
||||
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
||||
route,
|
||||
locale: lang,
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import Button from '../Button.astro';
|
||||
const payload = readPortalInput({
|
||||
approved: import.meta.env.AIA_PORTAL_LOCAL_PREVIEW_APPROVED,
|
||||
indexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
||||
environment: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
||||
file: import.meta.env.AIA_PORTAL_CIPHERTEXT_FILE,
|
||||
});
|
||||
const serialized = payload ? JSON.stringify(payload).replaceAll('<', '\\u003c') : 'null';
|
||||
|
||||
@@ -18,7 +18,7 @@ export function initializeServiceInquiryDrawers() {
|
||||
document.querySelectorAll<HTMLDialogElement>('[data-service-inquiry-drawer]').forEach((dialog) => {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
const authorized = dialog.dataset.consultationTest === 'true';
|
||||
const authorized = dialog.dataset.consultationEnabled === 'true';
|
||||
const closeButton = dialog.querySelector<HTMLButtonElement>('[data-service-inquiry-close]');
|
||||
const triggers = [...document.querySelectorAll<HTMLAnchorElement>('[data-service-inquiry-trigger]')]
|
||||
.filter((anchor) => anchor.dataset.serviceInquiryTrigger === dialog.dataset.service);
|
||||
@@ -121,8 +121,8 @@ export function initializeServiceInquiryDrawers() {
|
||||
observer?.observe(document.documentElement);
|
||||
checkScroll();
|
||||
}
|
||||
} else if (!suppressed()) {
|
||||
// Retain the existing inactive/mock demonstration timer outside test mode.
|
||||
} else if (dialog.querySelector('[data-form-mode="mock"]') && !suppressed()) {
|
||||
// Retain the existing synthetic demonstration timer only in mock mode.
|
||||
timer = window.setTimeout(() => open(), 10000);
|
||||
}
|
||||
addEventListener('storage', (event) => { if (event.key === suppressionKey && suppressed()) stopAuto(); }, { signal });
|
||||
|
||||
@@ -1,47 +1,32 @@
|
||||
export type FormMode = 'off' | 'mock' | 'isolated-test' | 'live';
|
||||
export type FormMode = 'live' | 'off' | 'mock';
|
||||
export type LogicalForm = 'general' | 'service' | 'referral';
|
||||
export type MockFormResult = 'success' | 'failed' | 'unavailable';
|
||||
|
||||
export interface FormModeConfig {
|
||||
mode: FormMode;
|
||||
reason: 'default-off' | 'mock' | 'isolated-test' | 'live-approved' | 'invalid-mode' | 'live-not-approved';
|
||||
reason: 'default-live' | 'live' | 'off' | 'mock';
|
||||
}
|
||||
|
||||
export function resolveFormMode(rawMode: string | undefined, liveApproved: string | undefined): FormModeConfig {
|
||||
export function resolveFormMode(rawMode: string | undefined): FormModeConfig {
|
||||
const mode = rawMode?.trim().toLowerCase();
|
||||
if (!mode) return { mode: 'off', reason: 'default-off' };
|
||||
if (mode === 'mock') return { mode: 'mock', reason: 'mock' };
|
||||
if (mode === 'isolated-test') return { mode: 'isolated-test', reason: 'isolated-test' };
|
||||
if (mode !== 'live') return { mode: 'off', reason: 'invalid-mode' };
|
||||
if (liveApproved?.trim().toLowerCase() !== 'true') return { mode: 'off', reason: 'live-not-approved' };
|
||||
return { mode: 'live', reason: 'live-approved' };
|
||||
if (!mode) return { mode: 'live', reason: 'default-live' };
|
||||
if (mode === 'live' || mode === 'off' || mode === 'mock') return { mode, reason: mode };
|
||||
throw new Error('Invalid PUBLIC_AIA_FORMS_MODE. Remove the obsolete setting (including isolated-test) or choose live, off, or mock.');
|
||||
}
|
||||
|
||||
/** Native fixtures never become live; only the bounded Form A adapter can. */
|
||||
export function formModeForRuntime(config: FormModeConfig): 'off' | 'mock' {
|
||||
// The isolated iframe adapter has its own fixed Form A route allowlist.
|
||||
// Keep all other lead-form surfaces and every live setting fail-closed.
|
||||
return config.mode === 'mock' ? 'mock' : 'off';
|
||||
}
|
||||
|
||||
export type FormAIframeReason =
|
||||
| 'enabled'
|
||||
| 'mode-not-authorized'
|
||||
| 'isolated-test-not-approved'
|
||||
| 'consultation-test-not-approved'
|
||||
| 'live-approval-not-disabled'
|
||||
| 'production-approval-not-granted'
|
||||
| 'invalid-environment-role'
|
||||
| 'indexing-enabled'
|
||||
| 'route-not-authorized'
|
||||
| 'locale-not-authorized';
|
||||
export type FormAIframeReason = 'enabled' | 'mode-not-authorized' | 'route-not-authorized' | 'locale-not-authorized';
|
||||
|
||||
export interface FormAIframeConfig {
|
||||
enabled: boolean;
|
||||
reason: FormAIframeReason;
|
||||
route: FormAIframeRoute | string;
|
||||
locale: 'en' | 'es' | string;
|
||||
mode?: 'isolated-test' | 'live';
|
||||
environmentRole?: 'isolated-test' | 'production';
|
||||
mode?: 'live';
|
||||
context?: FormAIframeContext;
|
||||
url?: string;
|
||||
title?: string;
|
||||
@@ -51,25 +36,13 @@ export interface FormAIframeConfig {
|
||||
|
||||
export interface ResolveFormAIframeOptions {
|
||||
rawMode: string | undefined;
|
||||
liveApproved: string | undefined;
|
||||
isolatedTestApproved: string | undefined;
|
||||
consultationTestApproved: string | undefined;
|
||||
productionApproved: string | undefined;
|
||||
environmentRole: string | undefined;
|
||||
allowIndexing: string | undefined;
|
||||
route: string;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The owner-approved Form A identifier is deliberately only made into an
|
||||
* iframe URL after every explicit isolated-test or production gate has
|
||||
* passed. It is never enabled by an API URL, hostname, or publication flag.
|
||||
*/
|
||||
/** Fixed owner-approved identity. Never accept visitor-supplied destinations. */
|
||||
export const formAIframeOrigin = 'https://form.jotform.com' as const;
|
||||
export const formAIframeId = '262946078314059' as const;
|
||||
const isolatedTestEnvironmentRole = 'isolated-test';
|
||||
const productionEnvironmentRole = 'production';
|
||||
|
||||
export type FormAIframeContext = 'contact' | 'landing' | 'consultation';
|
||||
export type FormAIframeRoute =
|
||||
@@ -96,13 +69,7 @@ export interface FormAIframePlacement {
|
||||
context: FormAIframeContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the complete fixed allowlist for the Form A adapter in both
|
||||
* explicitly approved modes.
|
||||
* Campaign placements are an owner-approved override of their earlier
|
||||
* logical Form B mapping. The shared compact landing presentation uses the
|
||||
* same fixed `landing` context; no route may supply its own context or URL.
|
||||
*/
|
||||
/** Complete owner-approved registry, independent of indexing or hostname. */
|
||||
export const formAIframePlacements: ReadonlyArray<FormAIframePlacement> = [
|
||||
{ route: '/contact', locale: 'en', context: 'contact' },
|
||||
{ route: '/es/contact', locale: 'es', context: 'contact' },
|
||||
@@ -111,15 +78,6 @@ export const formAIframePlacements: ReadonlyArray<FormAIframePlacement> = [
|
||||
{ route: '/lp/aba-ahcccs', locale: 'en', context: 'landing' },
|
||||
{ route: '/lp/aba-private-pay', locale: 'en', context: 'landing' },
|
||||
{ route: '/lp/early-intervention', locale: 'en', context: 'landing' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Consultation placements are a separate, non-production allowlist. They
|
||||
* cannot be activated by the production approval gate, even when every
|
||||
* production value is true. Service drawers defer assigning their iframe src
|
||||
* until the authorized drawer opens.
|
||||
*/
|
||||
export const formAConsultationTestPlacements: ReadonlyArray<FormAIframePlacement> = [
|
||||
{ route: '/client-consultation', locale: 'en', context: 'consultation' },
|
||||
{ route: '/es/client-consultation', locale: 'es', context: 'consultation' },
|
||||
{ route: '/schedule-consultation', locale: 'en', context: 'consultation' },
|
||||
@@ -131,8 +89,6 @@ export const formAConsultationTestPlacements: ReadonlyArray<FormAIframePlacement
|
||||
{ route: '/es/learner-social-club', locale: 'es', context: 'consultation' },
|
||||
];
|
||||
|
||||
const isTrue = (value: string | undefined) => value?.trim().toLowerCase() === 'true';
|
||||
|
||||
function formAIframeCopy(locale: 'en' | 'es', context: FormAIframeContext) {
|
||||
const consultation = context === 'consultation';
|
||||
return locale === 'es'
|
||||
@@ -150,17 +106,16 @@ function formAIframeCopy(locale: 'en' | 'es', context: FormAIframeContext) {
|
||||
|
||||
/** Builds a fixed, allowlisted URL; it accepts neither visitor data nor URL parameters. */
|
||||
export function buildFormAIframeUrl(locale: 'en' | 'es', context: FormAIframeContext): string {
|
||||
if (!['en', 'es'].includes(locale) || !['contact', 'landing', 'consultation'].includes(context)) {
|
||||
throw new Error('Unsupported Form A locale or context');
|
||||
}
|
||||
const url = new URL(`/${formAIframeId}`, formAIframeOrigin);
|
||||
url.searchParams.set('formContext', context);
|
||||
url.searchParams.set('language', locale);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the real Form A iframe only through one of two explicit gates:
|
||||
* a non-indexing isolated test, or a separately approved production build.
|
||||
* No API URL, hostname, indexing setting, or form ID can activate it alone.
|
||||
*/
|
||||
/** Enables Form A only for a fixed route/locale pairing in live mode. */
|
||||
export function resolveFormAIframe(options: ResolveFormAIframeOptions): FormAIframeConfig {
|
||||
const disabled = (reason: Exclude<FormAIframeReason, 'enabled'>): FormAIframeConfig => ({
|
||||
enabled: false,
|
||||
@@ -168,45 +123,17 @@ export function resolveFormAIframe(options: ResolveFormAIframeOptions): FormAIfr
|
||||
route: options.route,
|
||||
locale: options.locale,
|
||||
});
|
||||
const config = resolveFormMode(options.rawMode, options.liveApproved);
|
||||
const environmentRole = options.environmentRole?.trim().toLowerCase();
|
||||
let mode: 'isolated-test' | 'live';
|
||||
let approvedEnvironment: 'isolated-test' | 'production';
|
||||
|
||||
if (config.mode === 'isolated-test') {
|
||||
if (options.liveApproved?.trim().toLowerCase() !== 'false') return disabled('live-approval-not-disabled');
|
||||
if (!isTrue(options.isolatedTestApproved)) return disabled('isolated-test-not-approved');
|
||||
if (environmentRole !== isolatedTestEnvironmentRole) return disabled('invalid-environment-role');
|
||||
if (isTrue(options.allowIndexing)) return disabled('indexing-enabled');
|
||||
mode = 'isolated-test';
|
||||
approvedEnvironment = 'isolated-test';
|
||||
} else if (config.mode === 'live') {
|
||||
if (!isTrue(options.productionApproved)) return disabled('production-approval-not-granted');
|
||||
if (environmentRole !== productionEnvironmentRole) return disabled('invalid-environment-role');
|
||||
mode = 'live';
|
||||
approvedEnvironment = 'production';
|
||||
} else {
|
||||
return disabled('mode-not-authorized');
|
||||
}
|
||||
|
||||
const standardPlacement = formAIframePlacements.find((candidate) => candidate.route === options.route);
|
||||
const consultationPlacement = formAConsultationTestPlacements.find((candidate) => candidate.route === options.route);
|
||||
const placement = mode === 'isolated-test'
|
||||
? standardPlacement ?? consultationPlacement
|
||||
: standardPlacement;
|
||||
if (resolveFormMode(options.rawMode).mode !== 'live') return disabled('mode-not-authorized');
|
||||
const placement = formAIframePlacements.find((candidate) => candidate.route === options.route);
|
||||
if (!placement) return disabled('route-not-authorized');
|
||||
if (options.locale !== placement.locale) return disabled('locale-not-authorized');
|
||||
if (placement.context === 'consultation' && (options.allowIndexing !== 'false' || !isTrue(options.consultationTestApproved))) {
|
||||
return disabled('consultation-test-not-approved');
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
reason: 'enabled',
|
||||
route: options.route,
|
||||
locale: placement.locale,
|
||||
mode,
|
||||
environmentRole: approvedEnvironment,
|
||||
mode: 'live',
|
||||
context: placement.context,
|
||||
url: buildFormAIframeUrl(placement.locale, placement.context),
|
||||
...formAIframeCopy(placement.locale, placement.context),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Build-only reader; never imported by a browser script.
|
||||
import fs from 'node:fs';
|
||||
import { validatePortalPayload } from './portal-crypto.mjs';
|
||||
export function readPortalInput({ approved, indexing, environment, file }) {
|
||||
if (approved !== 'true' || indexing !== 'false' || environment !== 'isolated-test' || !file) return undefined;
|
||||
export function readPortalInput({ approved, indexing, file }) {
|
||||
if (approved !== 'true' || indexing !== 'false' || !file) return undefined;
|
||||
try {
|
||||
if (fs.statSync(file).size > 12_000_000) return undefined;
|
||||
return validatePortalPayload(JSON.parse(fs.readFileSync(file, 'utf8')));
|
||||
|
||||
+31
-16
@@ -81,20 +81,31 @@ test('photos, responsive variants and licensed fonts are self-hosted', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('inactive lead presentation has no native submission surface or live integration runtime', () => {
|
||||
test('campaign panels use bounded default live frames or explicitly inactive controls', () => {
|
||||
for (const page of pages) {
|
||||
const $ = html(page);
|
||||
assert.equal($('form').length, 0);
|
||||
assert.equal($('#lead-form').attr('data-forms-mode'), 'off');
|
||||
assert.equal($('#lead-form [name]:not([disabled])').length, 0);
|
||||
assert.equal($('#lead-form button').attr('type'), 'button');
|
||||
assert.ok($('#lead-form button').is('[disabled]'));
|
||||
const live = $('[data-jotform-form-a]').length === 1;
|
||||
if (!live) {
|
||||
assert.equal($('#lead-form').attr('data-forms-mode'), 'off');
|
||||
assert.equal($('#lead-form [name]:not([disabled])').length, 0);
|
||||
assert.equal($('#lead-form button').attr('type'), 'button');
|
||||
assert.ok($('#lead-form button').is('[disabled]'));
|
||||
} else {
|
||||
assert.equal($('[data-jotform-form-a]').attr('data-form-mode'), 'live');
|
||||
assert.equal($('iframe').attr('src'), 'https://form.jotform.com/262946078314059?formContext=landing&language=en');
|
||||
assert.equal($('#lead-form').length, 0);
|
||||
}
|
||||
assert.equal($('[data-form-status-notice], #campaign-form-status').length, 0);
|
||||
assert.equal($('#lead-form a[href^="tel:"]').length, 1);
|
||||
assert.equal($('#lead-form a[href="/privacy-policy"]').length, 1);
|
||||
assert.ok($('.consultation a[href^="tel:"]').length);
|
||||
assert.ok($('a[href="/privacy-policy"]').length);
|
||||
if (!live) {
|
||||
assert.equal($('#lead-form a[href^="tel:"]').length, 1);
|
||||
assert.equal($('#lead-form a[href="/privacy-policy"]').length, 1);
|
||||
}
|
||||
assert.doesNotMatch($('#lead-form').text(), /(?:online )?(?:forms?|requests?|submissions?) (?:are |is )?(?:unavailable|not enabled)|detailed medical|diagnostic|insurance-member/i);
|
||||
assert.equal($('#lead-form p:empty').length, 0);
|
||||
assert.equal($('iframe').length, 0);
|
||||
assert.equal($('iframe').length, live ? 1 : 0);
|
||||
assert.equal($('script[src]').length, 0);
|
||||
assert.doesNotMatch($('script').text(), /gtag|googletagmanager|sessionStorage|fetch\(|utm_|gclid|postMessage/);
|
||||
}
|
||||
@@ -108,8 +119,8 @@ test('campaign contact paths remain without unavailable-form messaging', () => {
|
||||
assert.doesNotMatch(heroNote, /online forms are not enabled/i);
|
||||
assert.ok($('.hero .form-note a[href^="tel:"]').length);
|
||||
assert.doesNotMatch($('#lead-form').text(), /By submitting this form/i);
|
||||
assert.ok($('#lead-form a[href^="tel:"]').length);
|
||||
assert.ok($('#lead-form a[href="/privacy-policy"]').length);
|
||||
assert.ok($('a[href^="tel:"]').length);
|
||||
assert.ok($('a[href="/privacy-policy"]').length);
|
||||
assert.doesNotMatch($('#lead-form').text(), /(?:online )?(?:forms?|requests?|submissions?) (?:are |is )?(?:unavailable|not enabled)/i);
|
||||
assert.doesNotMatch($('.consultation__contact').text(), /Online (?:forms|requests|submissions)/i);
|
||||
}
|
||||
@@ -146,16 +157,20 @@ test('early-intervention preserves its own publication identity and source secti
|
||||
assert.equal($('h1').length, 1);
|
||||
assert.equal($('main').length, 1);
|
||||
assert.equal($('link[rel="canonical"]').attr('href'), 'https://www.azinstitute4autism.com/lp/early-intervention');
|
||||
assert.equal($('.qr-landing section').length, 7);
|
||||
assert.equal($('.qr-landing section:not([data-jotform-form-a])').length, 7);
|
||||
assert.equal($('.qr-benefit-card').length, 3);
|
||||
assert.equal($('.qr-process-step').length, 3);
|
||||
assert.equal($('form, input[type="hidden"], script[src]').length, 0);
|
||||
assert.equal($('#consultation-heading').text(), 'Contact AIA about early intervention');
|
||||
assert.equal($('#consultation input').length, 3);
|
||||
assert.equal($('#consultation input:disabled').length, 3);
|
||||
assert.equal($('#consultation').attr('data-forms-mode'), 'off');
|
||||
assert.equal($('#consultation [data-jotform-form-a]').length, 0);
|
||||
assert.equal($('#consultation button').attr('type'), 'button');
|
||||
const live = $('#consultation [data-jotform-form-a]').length === 1;
|
||||
assert.equal($('#consultation').attr('data-forms-mode'), live ? 'live' : 'off');
|
||||
if (live) {
|
||||
assert.equal($('#consultation input').length, 0);
|
||||
assert.equal($('#consultation iframe').attr('src'), 'https://form.jotform.com/262946078314059?formContext=landing&language=en');
|
||||
} else {
|
||||
assert.equal($('#consultation input:disabled').length, 3);
|
||||
assert.equal($('#consultation button').attr('type'), 'button');
|
||||
}
|
||||
assert.ok($('.qr-call-card a[href="tel:+14806877099"]').length);
|
||||
assert.equal($('#consultation-unavailable, [data-form-status-notice]').length, 0);
|
||||
assert.equal($('img[src^="https:"]').length, 0);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Guarded consultation-only Form A browser regression.
|
||||
* Run after an explicitly gated isolated-test build. Every Jotform request is
|
||||
* Run after a normal live build. Every Jotform request is
|
||||
* fulfilled or aborted locally; no Jotform request is transmitted.
|
||||
*/
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -95,8 +95,7 @@ try {
|
||||
const url = expectedUrl(expected.locale);
|
||||
assert.equal(await adapter.count(), 1, expected.route);
|
||||
assert.equal(await adapter.getAttribute('data-form-context'), 'consultation', expected.route);
|
||||
assert.equal(await adapter.getAttribute('data-form-mode'), 'isolated-test', expected.route);
|
||||
assert.equal(await adapter.getAttribute('data-form-environment'), 'isolated-test', expected.route);
|
||||
assert.equal(await adapter.getAttribute('data-form-mode'), 'live', expected.route);
|
||||
assert.equal(await iframe.getAttribute('title'), expected.locale === 'es' ? 'Formulario de consulta de AIA' : 'AIA consultation form', expected.route);
|
||||
assert.equal(await iframe.getAttribute('referrerpolicy'), 'no-referrer', expected.route);
|
||||
assert.equal(await iframe.getAttribute('sandbox'), 'allow-forms allow-scripts allow-same-origin', expected.route);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* run this against a loopback-only Astro preview. Every external request is
|
||||
* intercepted and aborted before it can leave the browser.
|
||||
*
|
||||
* FORM_A_IFRAME_BROWSER_MODE=isolated-test \
|
||||
* FORM_A_IFRAME_BROWSER_MODE=live \
|
||||
* PLAYWRIGHT_MODULE=/path/to/playwright/index.mjs \
|
||||
* CHROMIUM_EXECUTABLE=/path/to/chromium node tools/jotform-form-a-browser.mjs
|
||||
*/
|
||||
@@ -18,12 +18,11 @@ 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 mode = process.env.FORM_A_IFRAME_BROWSER_MODE ?? 'off';
|
||||
if (!['off', 'mock', 'isolated-test', 'production'].includes(mode)) throw new Error(`Unsupported Form A browser mode: ${mode}`);
|
||||
const mode = process.env.FORM_A_IFRAME_BROWSER_MODE ?? 'live';
|
||||
if (!['off', 'mock', 'live'].includes(mode)) throw new Error(`Unsupported Form A browser mode: ${mode}`);
|
||||
|
||||
const expectedIframe = mode === 'isolated-test' || mode === 'production';
|
||||
const expectedFormMode = mode === 'production' ? 'live' : 'isolated-test';
|
||||
const expectedEnvironment = mode === 'production' ? 'production' : 'isolated-test';
|
||||
const expectedIframe = mode === 'live';
|
||||
const expectedFormMode = 'live';
|
||||
const routes = [
|
||||
{ route: '/contact', locale: 'en', context: 'contact', screenshot: 'contact' },
|
||||
{ route: '/es/contact', locale: 'es', context: 'contact', screenshot: 'es-contact' },
|
||||
@@ -94,7 +93,6 @@ try {
|
||||
assert.equal(await iframe.getAttribute('referrerpolicy'), 'no-referrer', route);
|
||||
assert.equal(await iframe.getAttribute('sandbox'), 'allow-forms allow-scripts allow-same-origin', route);
|
||||
assert.equal(await page.locator(`[data-jotform-form-a][data-form-mode="${expectedFormMode}"]`).count(), 1, route);
|
||||
assert.equal(await page.locator(`[data-jotform-form-a][data-form-environment="${expectedEnvironment}"]`).count(), 1, route);
|
||||
assert.equal(await page.locator(`[data-jotform-form-a][data-form-context="${formContext}"]`).count(), 1, route);
|
||||
const frameBox = await iframe.boundingBox();
|
||||
const expectedFrameHeight = formContext === 'contact'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* focuses, clicks, or submits an iframe control. All other external origins,
|
||||
* non-read methods, downloads, and WebSockets are blocked and recorded.
|
||||
*
|
||||
* Build the isolated-test artifact first, then run:
|
||||
* Build the ordinary live artifact first, then run:
|
||||
* FORM_A_IFRAME_REAL_LOAD_APPROVED=true \
|
||||
* FORM_A_IFRAME_JOTFORM_ASSETS_APPROVED=true \
|
||||
* PLAYWRIGHT_MODULE=/path/to/playwright/index.mjs \
|
||||
|
||||
@@ -1,167 +1,67 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
buildFormAIframeUrl,
|
||||
formAConsultationTestPlacements,
|
||||
formAIframeId,
|
||||
formAIframeOrigin,
|
||||
formAIframePlacements,
|
||||
formModeForRuntime,
|
||||
resolveFormAIframe,
|
||||
resolveFormMode,
|
||||
} from '../src/utils/form-integration.ts';
|
||||
import { buildFormAIframeUrl, formAIframeId, formAIframeOrigin, formAIframePlacements,
|
||||
formModeForRuntime, resolveFormAIframe, resolveFormMode } from '../src/utils/form-integration.ts';
|
||||
|
||||
const isolated = (overrides = {}) => ({
|
||||
rawMode: 'isolated-test',
|
||||
liveApproved: 'false',
|
||||
isolatedTestApproved: 'true',
|
||||
consultationTestApproved: 'false',
|
||||
productionApproved: 'false',
|
||||
environmentRole: 'isolated-test',
|
||||
allowIndexing: 'false',
|
||||
route: '/contact',
|
||||
locale: 'en',
|
||||
...overrides,
|
||||
});
|
||||
const expected = [
|
||||
['/contact', 'en', 'contact'], ['/es/contact', 'es', 'contact'],
|
||||
...['aba-therapy','aba-insurance','aba-ahcccs','aba-private-pay','early-intervention'].map(s => ['/lp/' + s, 'en', 'landing']),
|
||||
['/client-consultation', 'en', 'consultation'], ['/es/client-consultation', 'es', 'consultation'], ['/schedule-consultation', 'en', 'consultation'],
|
||||
...['en','es'].flatMap(lang => ['aba-therapy','autism-evaluations','learner-social-club'].map(s => [(lang === 'es' ? '/es/' : '/') + s, lang, 'consultation'])),
|
||||
];
|
||||
|
||||
const consultation = (overrides = {}) => isolated({
|
||||
consultationTestApproved: 'true',
|
||||
route: '/client-consultation',
|
||||
locale: 'en',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const production = (overrides = {}) => ({
|
||||
rawMode: 'live',
|
||||
liveApproved: 'true',
|
||||
isolatedTestApproved: 'false',
|
||||
consultationTestApproved: 'true',
|
||||
productionApproved: 'true',
|
||||
environmentRole: 'production',
|
||||
allowIndexing: 'true',
|
||||
route: '/contact',
|
||||
locale: 'en',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test('Form A iframe uses only fixed, approved route parameters', () => {
|
||||
test('default and explicit live enable exactly sixteen fixed route/locale/context placements', () => {
|
||||
assert.equal(formAIframeOrigin, 'https://form.jotform.com');
|
||||
assert.equal(formAIframeId, '262946078314059');
|
||||
assert.equal(
|
||||
buildFormAIframeUrl('en', 'contact'),
|
||||
'https://form.jotform.com/262946078314059?formContext=contact&language=en',
|
||||
);
|
||||
assert.equal(
|
||||
buildFormAIframeUrl('es', 'contact'),
|
||||
'https://form.jotform.com/262946078314059?formContext=contact&language=es',
|
||||
);
|
||||
assert.equal(
|
||||
buildFormAIframeUrl('en', 'landing'),
|
||||
'https://form.jotform.com/262946078314059?formContext=landing&language=en',
|
||||
);
|
||||
assert.equal(
|
||||
buildFormAIframeUrl('es', 'consultation'),
|
||||
'https://form.jotform.com/262946078314059?formContext=consultation&language=es',
|
||||
);
|
||||
const english = resolveFormAIframe(isolated());
|
||||
const spanish = resolveFormAIframe(isolated({ route: '/es/contact', locale: 'es' }));
|
||||
assert.equal(english.fallbackText, 'If the embedded form does not load,');
|
||||
assert.equal(english.fallbackLinkText, 'open the form in a new tab.');
|
||||
assert.equal(spanish.fallbackText, 'Si el formulario integrado no se carga,');
|
||||
assert.equal(spanish.fallbackLinkText, 'abra el formulario en una pestaña nueva.');
|
||||
assert.deepEqual(formAIframePlacements.map((placement) => placement.route), [
|
||||
'/contact',
|
||||
'/es/contact',
|
||||
'/lp/aba-therapy',
|
||||
'/lp/aba-insurance',
|
||||
'/lp/aba-ahcccs',
|
||||
'/lp/aba-private-pay',
|
||||
'/lp/early-intervention',
|
||||
]);
|
||||
assert.deepEqual(formAConsultationTestPlacements.map((placement) => placement.route), [
|
||||
'/client-consultation',
|
||||
'/es/client-consultation',
|
||||
'/schedule-consultation',
|
||||
'/aba-therapy',
|
||||
'/autism-evaluations',
|
||||
'/learner-social-club',
|
||||
'/es/aba-therapy',
|
||||
'/es/autism-evaluations',
|
||||
'/es/learner-social-club',
|
||||
]);
|
||||
});
|
||||
|
||||
test('Form A iframe requires every isolated-test or production approval gate', () => {
|
||||
const enabled = resolveFormAIframe(isolated());
|
||||
assert.equal(enabled.enabled, true);
|
||||
assert.equal(enabled.reason, 'enabled');
|
||||
assert.equal(enabled.title, 'AIA general enquiries form');
|
||||
assert.equal(enabled.mode, 'isolated-test');
|
||||
assert.equal(enabled.environmentRole, 'isolated-test');
|
||||
assert.equal(resolveFormAIframe(isolated({ rawMode: undefined })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(isolated({ rawMode: 'mock' })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(isolated({ liveApproved: undefined })).reason, 'live-approval-not-disabled');
|
||||
assert.equal(resolveFormAIframe(isolated({ liveApproved: 'true' })).reason, 'live-approval-not-disabled');
|
||||
assert.equal(resolveFormAIframe(isolated({ isolatedTestApproved: 'false' })).reason, 'isolated-test-not-approved');
|
||||
assert.equal(resolveFormAIframe(isolated({ environmentRole: 'staging' })).reason, 'invalid-environment-role');
|
||||
assert.equal(resolveFormAIframe(isolated({ allowIndexing: 'true' })).reason, 'indexing-enabled');
|
||||
assert.equal(resolveFormAIframe(isolated({ route: '/lp/early-intervention' })).url, 'https://form.jotform.com/262946078314059?formContext=landing&language=en');
|
||||
assert.equal(resolveFormAIframe(isolated({ route: '/es/contact', locale: 'en' })).reason, 'locale-not-authorized');
|
||||
assert.equal(resolveFormAIframe(isolated({ route: '/es/contact', locale: 'es' })).url, 'https://form.jotform.com/262946078314059?formContext=contact&language=es');
|
||||
const campaign = resolveFormAIframe(isolated({ route: '/lp/aba-therapy' }));
|
||||
assert.equal(campaign.url, 'https://form.jotform.com/262946078314059?formContext=landing&language=en');
|
||||
assert.equal(campaign.context, 'landing');
|
||||
const live = resolveFormAIframe(production());
|
||||
assert.equal(live.enabled, true);
|
||||
assert.equal(live.mode, 'live');
|
||||
assert.equal(live.environmentRole, 'production');
|
||||
assert.equal(live.url, 'https://form.jotform.com/262946078314059?formContext=contact&language=en');
|
||||
assert.equal(resolveFormAIframe(production({ productionApproved: undefined })).reason, 'production-approval-not-granted');
|
||||
assert.equal(resolveFormAIframe(production({ productionApproved: 'false' })).reason, 'production-approval-not-granted');
|
||||
assert.equal(resolveFormAIframe(production({ environmentRole: 'isolated-test' })).reason, 'invalid-environment-role');
|
||||
assert.equal(resolveFormAIframe(production({ liveApproved: 'false' })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(production({ rawMode: 'off' })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(production({ route: '/referrals' })).reason, 'route-not-authorized');
|
||||
assert.equal(resolveFormAIframe(production({ route: '/es/contact', locale: 'es' })).url, 'https://form.jotform.com/262946078314059?formContext=contact&language=es');
|
||||
assert.equal(formModeForRuntime(resolveFormMode('isolated-test', undefined)), 'off');
|
||||
assert.equal(formModeForRuntime(resolveFormMode('live', 'true')), 'off');
|
||||
});
|
||||
|
||||
test('consultation is isolated-test-only and requires its additional explicit approval', () => {
|
||||
const enabled = resolveFormAIframe(consultation());
|
||||
assert.equal(enabled.enabled, true);
|
||||
assert.equal(enabled.context, 'consultation');
|
||||
assert.equal(enabled.title, 'AIA consultation form');
|
||||
assert.equal(enabled.url, 'https://form.jotform.com/262946078314059?formContext=consultation&language=en');
|
||||
|
||||
const spanish = resolveFormAIframe(consultation({ route: '/es/client-consultation', locale: 'es' }));
|
||||
assert.equal(spanish.enabled, true);
|
||||
assert.equal(spanish.title, 'Formulario de consulta de AIA');
|
||||
assert.equal(spanish.url, 'https://form.jotform.com/262946078314059?formContext=consultation&language=es');
|
||||
|
||||
for (const placement of formAConsultationTestPlacements) {
|
||||
const config = resolveFormAIframe(consultation({ route: placement.route, locale: placement.locale }));
|
||||
assert.equal(config.enabled, true, placement.route);
|
||||
assert.equal(config.context, 'consultation', placement.route);
|
||||
assert.equal(config.url, buildFormAIframeUrl(placement.locale, 'consultation'), placement.route);
|
||||
assert.deepEqual(formAIframePlacements.map(p => [p.route,p.locale,p.context]), expected);
|
||||
assert.equal(new Set(formAIframePlacements.map(p => p.route)).size, 16);
|
||||
for (const rawMode of [undefined, '', ' ', 'live', 'LIVE']) for (const [route, locale, context] of expected) {
|
||||
const config = resolveFormAIframe({ rawMode, route, locale });
|
||||
assert.equal(config.enabled, true, route);
|
||||
assert.equal(config.context, context);
|
||||
assert.equal(config.mode, 'live');
|
||||
assert.equal(config.url, `https://form.jotform.com/262946078314059?formContext=${context}&language=${locale}`);
|
||||
assert.equal(formModeForRuntime(resolveFormMode(rawMode)), 'off', 'native surfaces remain inactive');
|
||||
}
|
||||
|
||||
assert.equal(resolveFormAIframe(isolated({ route: '/client-consultation' })).reason, 'consultation-test-not-approved');
|
||||
assert.equal(resolveFormAIframe(consultation({ consultationTestApproved: undefined })).reason, 'consultation-test-not-approved');
|
||||
assert.equal(resolveFormAIframe(consultation({ consultationTestApproved: 'yes' })).reason, 'consultation-test-not-approved');
|
||||
assert.equal(resolveFormAIframe(consultation({ rawMode: 'mock' })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(consultation({ liveApproved: 'true' })).reason, 'live-approval-not-disabled');
|
||||
assert.equal(resolveFormAIframe(consultation({ isolatedTestApproved: 'false' })).reason, 'isolated-test-not-approved');
|
||||
assert.equal(resolveFormAIframe(consultation({ environmentRole: 'staging' })).reason, 'invalid-environment-role');
|
||||
assert.equal(resolveFormAIframe(consultation({ allowIndexing: 'true' })).reason, 'indexing-enabled');
|
||||
for (const value of [undefined, '', 'invalid']) assert.equal(resolveFormAIframe(consultation({ allowIndexing: value })).enabled, false);
|
||||
assert.equal(resolveFormAIframe(consultation({ route: '/es/aba-therapy', locale: 'en' })).reason, 'locale-not-authorized');
|
||||
assert.equal(resolveFormAIframe(production({ route: '/client-consultation' })).reason, 'route-not-authorized');
|
||||
assert.equal(resolveFormAIframe(production({ route: '/client-consultation', consultationTestApproved: 'true' })).reason, 'route-not-authorized');
|
||||
});
|
||||
|
||||
test('the adapter preserves seven production routes and supports the separate bilingual consultation test surface', () => {
|
||||
test('invalid and obsolete mode values fail clearly instead of silently disabling forms', () => {
|
||||
for (const rawMode of ['isolated-test', 'production', 'invalid', 'false']) {
|
||||
assert.throws(() => resolveFormMode(rawMode), /PUBLIC_AIA_FORMS_MODE.*Remove.*live, off, or mock/);
|
||||
assert.throws(() => resolveFormAIframe({ rawMode, route: '/contact', locale: 'en' }), /PUBLIC_AIA_FORMS_MODE/);
|
||||
}
|
||||
});
|
||||
|
||||
test('off and mock reject real frames on every approved placement', () => {
|
||||
for (const rawMode of ['off', 'mock']) for (const [route, locale] of expected) {
|
||||
const config = resolveFormAIframe({ rawMode, route, locale });
|
||||
assert.equal(config.enabled, false);
|
||||
assert.equal(config.url, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
test('registry rejects unknown routes, mismatched locales and arbitrary URL parameters', () => {
|
||||
for (const route of ['/referrals', '/tour', '/ar/contact', '/contact/', '/contact?formContext=landing', 'https://evil.invalid']) {
|
||||
assert.equal(resolveFormAIframe({ rawMode: undefined, route, locale: 'en' }).reason, 'route-not-authorized');
|
||||
}
|
||||
for (const [route, locale] of expected) {
|
||||
assert.equal(resolveFormAIframe({ rawMode: undefined, route, locale: locale === 'en' ? 'es' : 'en' }).reason, 'locale-not-authorized');
|
||||
}
|
||||
assert.throws(() => buildFormAIframeUrl('ar', 'contact'), /Unsupported/);
|
||||
assert.throws(() => buildFormAIframeUrl('en', 'untrusted'), /Unsupported/);
|
||||
});
|
||||
|
||||
test('EN/ES consultation and contact titles/fallbacks remain localized', () => {
|
||||
const en = resolveFormAIframe({ rawMode: undefined, route: '/client-consultation', locale: 'en' });
|
||||
const es = resolveFormAIframe({ rawMode: undefined, route: '/es/client-consultation', locale: 'es' });
|
||||
assert.equal(en.title, 'AIA consultation form');
|
||||
assert.equal(es.title, 'Formulario de consulta de AIA');
|
||||
assert.equal(es.fallbackLinkText, 'abra el formulario de consulta en una pestaña nueva.');
|
||||
assert.equal(resolveFormAIframe({rawMode: undefined,route:'/es/contact',locale:'es'}).fallbackText,'Si el formulario integrado no se carga,');
|
||||
});
|
||||
|
||||
test('the shared adapter preserves layout, bilingual drawers, sandbox, fallback and dimensions', () => {
|
||||
const contact = fs.readFileSync('src/components/pages/ContactPage.astro', 'utf8');
|
||||
const adapter = fs.readFileSync('src/components/JotformIframe.astro', 'utf8');
|
||||
const shell = fs.readFileSync('src/components/FormShell.astro', 'utf8');
|
||||
@@ -173,11 +73,8 @@ test('the adapter preserves seven production routes and supports the separate bi
|
||||
assert.match(contact, /resolveFormAIframe/);
|
||||
assert.match(contact, /route = lang === 'es' \? '\/es\/contact' : '\/contact'/);
|
||||
assert.match(contact, /JotformIframe/);
|
||||
assert.match(contact, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
|
||||
assert.match(contact, /PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED/);
|
||||
assert.match(campaign, /resolveFormAIframe/);
|
||||
assert.match(campaign, /JotformIframe/);
|
||||
assert.match(campaign, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
|
||||
assert.match(campaign, /variant="embedded"/);
|
||||
assert.match(campaign, /\/lp\/aba-therapy/);
|
||||
assert.match(campaign, /const iframeDescription = 'Use this form to request a conversation\. An AIA team member can explain the next steps for your family\.'/);
|
||||
@@ -186,14 +83,10 @@ test('the adapter preserves seven production routes and supports the separate bi
|
||||
assert.match(earlyIntervention, /resolveFormAIframe/);
|
||||
assert.match(earlyIntervention, /route: '\/lp\/early-intervention'/);
|
||||
assert.match(earlyIntervention, /JotformIframe/);
|
||||
assert.match(earlyIntervention, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
|
||||
assert.match(earlyIntervention, /PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED/);
|
||||
assert.match(campaign, /PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED/);
|
||||
assert.match(shell, /resolveFormAIframe/);
|
||||
assert.match(shell, /PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED/);
|
||||
assert.match(shell, /JotformIframe/);
|
||||
assert.match(drawer, /lang: SupportedLocale/);
|
||||
assert.match(drawer, /data-consultation-test/);
|
||||
assert.match(drawer, /data-consultation-enabled/);
|
||||
assert.match(drawer, /serviceCopy.heading/);
|
||||
assert.match(drawer, /deferUntilActivated/);
|
||||
assert.match(fs.readFileSync('src/scripts/service-inquiry.ts', 'utf8'), /aia:jotform-activate/);
|
||||
@@ -203,7 +96,6 @@ test('the adapter preserves seven production routes and supports the separate bi
|
||||
assert.match(adapter, /referrerpolicy="no-referrer"/);
|
||||
assert.match(adapter, /sandbox="allow-forms allow-scripts allow-same-origin"/);
|
||||
assert.match(adapter, /data-form-mode=\{config\.mode\}/);
|
||||
assert.match(adapter, /data-form-environment=\{config\.environmentRole\}/);
|
||||
assert.match(adapter, /data-jotform-iframe-frame/);
|
||||
assert.match(adapter, /data-jotform-iframe-url/);
|
||||
assert.match(adapter, /data-jotform-deferred/);
|
||||
|
||||
@@ -495,7 +495,7 @@ before(async () => {
|
||||
await build({root:${JSON.stringify(temporary)},configFile:${JSON.stringify(path.relative(temporary, path.join(project, 'astro.config.mjs')))},logLevel:'silent',vite:{cacheDir:${JSON.stringify(path.join(temporary, '.vite'))}}});`;
|
||||
for (const mode of ['staging', 'indexing']) {
|
||||
execFileSync(process.execPath, ['--input-type=module', '-e', runner], {
|
||||
cwd: temporary, env: { ...process.env, PUBLIC_ALLOW_INDEXING: mode === 'indexing' ? 'true' : 'false' },
|
||||
cwd: temporary, env: { ...process.env, PUBLIC_AIA_FORMS_MODE: 'off', PUBLIC_ALLOW_INDEXING: mode === 'indexing' ? 'true' : 'false' },
|
||||
timeout: 90_000, maxBuffer: 4 * 1024 * 1024
|
||||
});
|
||||
const html = new Map();
|
||||
|
||||
@@ -36,7 +36,7 @@ function hasTerminalSlash(url) {
|
||||
return pathname !== '/' && pathname.endsWith('/');
|
||||
}
|
||||
|
||||
const formAProductionUrls = new Map([
|
||||
const formAContactLandingUrls = new Map([
|
||||
['/contact', 'https://form.jotform.com/262946078314059?formContext=contact&language=en'],
|
||||
['/es/contact', 'https://form.jotform.com/262946078314059?formContext=contact&language=es'],
|
||||
['/lp/aba-therapy', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
@@ -61,7 +61,7 @@ const formAConsultationDrawerUrls = new Map([
|
||||
['/es/learner-social-club', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
]);
|
||||
|
||||
const formAIsolatedStaticUrls = new Map([...formAProductionUrls, ...formAConsultationStaticUrls]);
|
||||
const formAStaticUrls = new Map([...formAContactLandingUrls, ...formAConsultationStaticUrls]);
|
||||
|
||||
function iframeSources(html) {
|
||||
// Astro correctly escapes query separators in serialized HTML attributes.
|
||||
@@ -81,7 +81,7 @@ function formAAdapters(html) {
|
||||
function unsafeSignals(html, allowApprovedFormA = false) {
|
||||
const signals = [];
|
||||
if (!allowApprovedFormA && /<iframe\b[^>]*\bsrc=["'][^"']*jotform/i.test(html)) signals.push('active Jotform iframe');
|
||||
if (!allowApprovedFormA && /262946078314059/i.test(html)) signals.push('isolated-test Form A identifier');
|
||||
if (!allowApprovedFormA && /262946078314059/i.test(html)) signals.push('disabled Form A identifier');
|
||||
if (/<script\b[^>]*\bsrc=["'][^"']*(?:jotform|googletagmanager|google-analytics|doubleclick)/i.test(html)) signals.push('active vendor tracking/form script');
|
||||
if (!allowApprovedFormA && /data-(?:form|forms)-mode=["']live["']/i.test(html)) signals.push('live form mode');
|
||||
if (/api\.azinstitute4autism\.com/i.test(html)) signals.push('production API origin');
|
||||
@@ -89,12 +89,13 @@ function unsafeSignals(html, allowApprovedFormA = false) {
|
||||
return signals;
|
||||
}
|
||||
|
||||
export async function verifyArtifact(root, mode = 'staging') {
|
||||
const formAEnabled = mode === 'isolated-test' || mode === 'production';
|
||||
const production = mode === 'production';
|
||||
const stagingPolicy = mode === 'staging' || mode === 'isolated-test';
|
||||
const staticFormAUrls = production ? formAProductionUrls : mode === 'isolated-test' ? formAIsolatedStaticUrls : new Map();
|
||||
const deferredFormAUrls = mode === 'isolated-test' ? formAConsultationDrawerUrls : new Map();
|
||||
export async function verifyArtifact(root, mode = 'staging', forms = 'live') {
|
||||
if (!['staging', 'indexing'].includes(mode)) throw new Error('Artifact mode must be staging or indexing; form availability is selected separately with --forms=live|off|mock.');
|
||||
if (!['live', 'off', 'mock'].includes(forms)) throw new Error('Artifact forms must be live, off, or mock.');
|
||||
const formAEnabled = forms === 'live';
|
||||
const stagingPolicy = mode === 'staging';
|
||||
const staticFormAUrls = formAEnabled ? formAStaticUrls : new Map();
|
||||
const deferredFormAUrls = formAEnabled ? formAConsultationDrawerUrls : new Map();
|
||||
const htmlFiles = (await fg('**/*.html', { cwd: root, absolute: true })).sort();
|
||||
const routes = [];
|
||||
const canonicalMap = new Map();
|
||||
@@ -133,13 +134,21 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
}
|
||||
});
|
||||
const adapters = formAAdapters(html);
|
||||
for (const url of iframeSources(html)) {
|
||||
if (url !== expectedFormAUrl) errors.push(`${route}: unauthorized iframe source`);
|
||||
}
|
||||
for (const url of deferredIframeSources(html)) {
|
||||
if (url !== expectedDeferredFormAUrl) errors.push(`${route}: unauthorized deferred iframe source`);
|
||||
}
|
||||
if (expectedFormAUrl && deferredIframeSources(html).length) errors.push(`${route}: unexpected deferred frame`);
|
||||
if (expectedDeferredFormAUrl && iframeSources(html).length) errors.push(`${route}: drawer must not load before activation`);
|
||||
if (/<script[^>]*data-portal-payload[^>]*>\s*(?!null\s*<)[\s\S]*?\"streams\"[\s\S]*?<\/script>/i.test(html)) errors.push(`${route}: private portal ciphertext is not an approved release artifact`);
|
||||
if (!title) errors.push(`${route}: missing title`);
|
||||
if (!canonical) errors.push(`${route}: missing canonical`);
|
||||
if (!robots) errors.push(`${route}: missing robots directive`);
|
||||
if (trailingSlashUrls.length) errors.push(`${route}: root-relative URL(s) end in a slash: ${trailingSlashUrls.join(', ')}`);
|
||||
if (stagingPolicy && robots !== 'noindex,nofollow') errors.push(`${route}: staging robots is ${robots ?? 'missing'}`);
|
||||
if (production && robots === 'noindex,nofollow') errors.push(`${route}: production artifact is noindex`);
|
||||
if (!stagingPolicy && robots === 'noindex,nofollow') errors.push(`${route}: production artifact is noindex`);
|
||||
if (canonical) {
|
||||
const prior = canonicalMap.get(canonical);
|
||||
if (prior) errors.push(`${route}: duplicate canonical ${canonical} (also ${prior})`);
|
||||
@@ -150,18 +159,16 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
if (formAEnabled) {
|
||||
if (expectedFormAUrl) {
|
||||
if (jotformFrames.length !== 1 || jotformFrames[0] !== expectedFormAUrl) errors.push(`${route}: missing or invalid approved Form A iframe URL`);
|
||||
const expectedMode = production ? 'live' : 'isolated-test';
|
||||
const expectedEnvironment = production ? 'production' : 'isolated-test';
|
||||
if (adapters.length !== 1 || !adapters[0].includes(`data-form-mode="${expectedMode}"`) || !adapters[0].includes(`data-form-environment="${expectedEnvironment}"`)) {
|
||||
errors.push(`${route}: Form A adapter lacks the approved ${expectedMode} ${expectedEnvironment} gate`);
|
||||
if (adapters.length !== 1 || !adapters[0].includes('data-form-mode="live"')) {
|
||||
errors.push(`${route}: Form A adapter lacks live mode`);
|
||||
}
|
||||
formAIframes += jotformFrames.length;
|
||||
} else if (expectedDeferredFormAUrl) {
|
||||
if (jotformFrames.length || deferredJotformFrames.length !== 1 || deferredJotformFrames[0] !== expectedDeferredFormAUrl) {
|
||||
errors.push(`${route}: missing or invalid deferred consultation Form A iframe URL`);
|
||||
}
|
||||
if (adapters.length !== 1 || !adapters[0].includes('data-form-mode="isolated-test"') || !adapters[0].includes('data-form-environment="isolated-test"') || !adapters[0].includes('data-jotform-deferred="true"')) {
|
||||
errors.push(`${route}: deferred consultation adapter lacks the approved isolated-test gate`);
|
||||
if (adapters.length !== 1 || !adapters[0].includes('data-form-mode="live"') || !adapters[0].includes('data-jotform-deferred="true"')) {
|
||||
errors.push(`${route}: deferred consultation adapter lacks the live deferred configuration`);
|
||||
}
|
||||
deferredFormAIframes += deferredJotformFrames.length;
|
||||
} else if (jotformFrames.length || deferredJotformFrames.length || adapters.length || /262946078314059/i.test(html)) {
|
||||
@@ -205,14 +212,15 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
if (formAEnabled) {
|
||||
const expectedFormAFiles = new Set([...staticFormAUrls.keys(), ...deferredFormAUrls.keys()].map((route) => route === '/' ? 'index.html' : `${route.slice(1)}/index.html`));
|
||||
const unexpectedFormAFiles = formAResourceFiles.filter((file) => !expectedFormAFiles.has(file));
|
||||
if (unexpectedFormAFiles.length) errors.push(`Form A identifier appears outside approved production routes: ${unexpectedFormAFiles.join(', ')}`);
|
||||
if (unexpectedFormAFiles.length) errors.push(`Form A identifier appears outside approved routes: ${unexpectedFormAFiles.join(', ')}`);
|
||||
if (formAIframes !== staticFormAUrls.size) errors.push(`${mode} artifact has ${formAIframes} approved static Form A iframe(s), expected ${staticFormAUrls.size}`);
|
||||
if (deferredFormAIframes !== deferredFormAUrls.size) errors.push(`${mode} artifact has ${deferredFormAIframes} approved deferred Form A iframe(s), expected ${deferredFormAUrls.size}`);
|
||||
} else if (formAResourceFiles.length) {
|
||||
errors.push(`isolated-test Form A identifier appears in artifact: ${formAResourceFiles.join(', ')}`);
|
||||
errors.push(`disabled Form A identifier appears in artifact: ${formAResourceFiles.join(', ')}`);
|
||||
}
|
||||
return {
|
||||
mode,
|
||||
forms,
|
||||
htmlRoutes: htmlFiles.length,
|
||||
sitemapUrls: sitemapRoutes.length,
|
||||
hreflangLinks,
|
||||
@@ -232,8 +240,8 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)) {
|
||||
const mode = process.argv.find((arg) => arg.startsWith('--mode='))?.slice('--mode='.length) ?? 'staging';
|
||||
const root = process.argv.find((arg) => arg.startsWith('--root='))?.slice('--root='.length) ?? 'dist';
|
||||
if (!['staging', 'indexing', 'isolated-test', 'production'].includes(mode)) throw new Error(`Unsupported mode: ${mode}`);
|
||||
const result = await verifyArtifact(path.resolve(root), mode);
|
||||
const forms = process.argv.find((arg) => arg.startsWith('--forms='))?.slice('--forms='.length) ?? 'live';
|
||||
const result = await verifyArtifact(path.resolve(root), mode, forms);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
if (!result.passed) process.exitCode = 1;
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { verifyArtifact } from './phase10-artifact-verify.mjs';
|
||||
|
||||
async function fixture(mode = 'staging') {
|
||||
async function fixture(mode = 'staging', forms = 'live') {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'aia-phase10-artifact-'));
|
||||
const productionFrames = [
|
||||
const contactLandingFrames = [
|
||||
['contact/index.html', 'https://form.jotform.com/262946078314059?formContext=contact&language=en'],
|
||||
['es/contact/index.html', 'https://form.jotform.com/262946078314059?formContext=contact&language=es'],
|
||||
['lp/aba-therapy/index.html', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
@@ -29,24 +29,21 @@ async function fixture(mode = 'staging') {
|
||||
['es/autism-evaluations/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
['es/learner-social-club/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||
];
|
||||
const pages = mode === 'staging'
|
||||
? [['index.html', 'noindex,nofollow', ''] , ['lp/index.html', 'noindex,nofollow', '']]
|
||||
: mode === 'isolated-test'
|
||||
? [
|
||||
...productionFrames.map(([file, frame]) => [file, 'noindex,nofollow', `<section data-jotform-form-a data-form-mode="isolated-test" data-form-environment="isolated-test"><iframe src="${frame}"></iframe></section>`]),
|
||||
...consultationFrames.map(([file, frame]) => [file, 'noindex,nofollow', `<section data-jotform-form-a data-form-mode="isolated-test" data-form-environment="isolated-test"><iframe src="${frame}"></iframe></section>`]),
|
||||
...consultationDrawers.map(([file, frame]) => [file, 'noindex,nofollow', `<section data-jotform-form-a data-form-mode="isolated-test" data-form-environment="isolated-test" data-jotform-deferred="true"><iframe data-jotform-iframe-url="${frame}"></iframe></section>`]),
|
||||
['lp/index.html', 'noindex,nofollow', ''],
|
||||
]
|
||||
: mode === 'production'
|
||||
? productionFrames.map(([file, frame]) => [file, 'index,follow', `<section data-jotform-form-a data-form-mode="live" data-form-environment="production"><iframe src="${frame}"></iframe></section>`])
|
||||
: [['index.html', 'index,follow', '']];
|
||||
const robots = mode === 'staging' ? 'noindex,nofollow' : 'index,follow';
|
||||
const pages = [['index.html', robots, '']];
|
||||
if (mode === 'staging') pages.push(['lp/index.html', robots, '']);
|
||||
if (forms === 'live') {
|
||||
pages.push(...[...contactLandingFrames,...consultationFrames].map(([file, frame]) =>
|
||||
[file, robots, `<section data-jotform-form-a data-form-mode="live"><iframe src="${frame}"></iframe></section>`]));
|
||||
pages.push(...consultationDrawers.map(([file, frame]) =>
|
||||
[file, robots, `<section data-jotform-form-a data-form-mode="live" data-jotform-deferred="true"><iframe data-jotform-iframe-url="${frame}"></iframe></section>`]));
|
||||
}
|
||||
for (const [file, robots, body] of pages) {
|
||||
const target = path.join(root, file);
|
||||
await fs.mkdir(path.dirname(target), { recursive: true });
|
||||
await fs.writeFile(target, `<!doctype html><html><head><title>${file}</title><meta name="robots" content="${robots}"><link rel="canonical" href="https://www.azinstitute4autism.com/${file}"></head><body><main><h1>${file}</h1>${body}</main></body></html>`);
|
||||
}
|
||||
await fs.writeFile(path.join(root, 'sitemap.xml'), ['staging', 'isolated-test'].includes(mode) ? '<urlset></urlset>' : '<urlset><url><loc>https://www.azinstitute4autism.com/</loc></url></urlset>');
|
||||
await fs.writeFile(path.join(root, 'sitemap.xml'), mode === 'staging' ? '<urlset></urlset>' : '<urlset><url><loc>https://www.azinstitute4autism.com/</loc></url></urlset>');
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -64,28 +61,33 @@ test('indexing artifact requires sitemap and excludes review index', async () =>
|
||||
assert.equal(result.reviewIndex, false);
|
||||
});
|
||||
|
||||
test('isolated-test artifact permits ten static and six user-activated consultation Form A placements under staging safeguards', async () => {
|
||||
const result = await verifyArtifact(await fixture('isolated-test'), 'isolated-test');
|
||||
assert.equal(result.passed, true);
|
||||
assert.equal(result.formAIframes, 10);
|
||||
assert.equal(result.deferredFormAIframes, 6);
|
||||
assert.equal(result.sitemapUrls, 0);
|
||||
assert.equal(result.reviewIndex, true);
|
||||
test('live forms have ten immediate and six deferred placements independently of publication mode', async () => {
|
||||
for (const mode of ['staging','indexing']) {
|
||||
const result = await verifyArtifact(await fixture(mode), mode);
|
||||
assert.equal(result.passed, true, result.errors.join('; '));
|
||||
assert.equal(result.formAIframes, 10);
|
||||
assert.equal(result.deferredFormAIframes, 6);
|
||||
assert.equal(result.reviewIndex, mode === 'staging');
|
||||
}
|
||||
});
|
||||
|
||||
test('production artifact permits exactly the seven approved Form A iframe placements', async () => {
|
||||
const result = await verifyArtifact(await fixture('production'), 'production');
|
||||
assert.equal(result.passed, true);
|
||||
assert.equal(result.formAIframes, 7);
|
||||
assert.equal(result.reviewIndex, false);
|
||||
test('off and mock artifacts contain no real frames independently of publication mode', async () => {
|
||||
for (const mode of ['staging','indexing']) for (const forms of ['off','mock']) {
|
||||
const root = await fixture(mode, forms);
|
||||
const result = await verifyArtifact(root, mode, forms);
|
||||
assert.equal(result.passed, true);
|
||||
assert.equal(result.formAIframes + result.deferredFormAIframes, 0);
|
||||
await fs.appendFile(path.join(root, 'index.html'), '<iframe src="https://form.jotform.com/262946078314059"></iframe>');
|
||||
assert.equal((await verifyArtifact(root,mode,forms)).passed,false);
|
||||
}
|
||||
});
|
||||
|
||||
test('production artifact rejects an invalid Form A URL', async () => {
|
||||
const root = await fixture('production');
|
||||
test('indexing artifact rejects an invalid Form A URL', async () => {
|
||||
const root = await fixture('indexing');
|
||||
const contact = path.join(root, 'contact/index.html');
|
||||
const html = await fs.readFile(contact, 'utf8');
|
||||
await fs.writeFile(contact, html.replace('formContext=contact&language=en', 'formContext=landing&language=en'));
|
||||
const result = await verifyArtifact(root, 'production');
|
||||
const result = await verifyArtifact(root, 'indexing');
|
||||
assert.equal(result.passed, false);
|
||||
assert.ok(result.errors.some((error) => error.includes('invalid approved Form A iframe URL')));
|
||||
});
|
||||
@@ -98,7 +100,7 @@ test('staging artifact rejects active native form actions', async () => {
|
||||
assert.ok(result.errors.some((error) => error.includes('native form action')));
|
||||
});
|
||||
|
||||
test('artifact rejects live form mode and production API origin', async () => {
|
||||
test('artifact rejects production API origin even when Form A is approved', async () => {
|
||||
const root = await fixture('staging');
|
||||
await fs.appendFile(path.join(root, 'index.html'), '<div data-form-mode="live">https://api.azinstitute4autism.com</div>');
|
||||
const result = await verifyArtifact(root, 'staging');
|
||||
@@ -106,12 +108,12 @@ test('artifact rejects live form mode and production API origin', async () => {
|
||||
assert.ok(result.errors.some((error) => error.includes('unsafe artifact signals')));
|
||||
});
|
||||
|
||||
test('artifact rejects the isolated-test Form A identifier in any shipped text asset', async () => {
|
||||
test('artifact rejects Form A identity outside the exact approved HTML routes', async () => {
|
||||
const root = await fixture('staging');
|
||||
await fs.writeFile(path.join(root, 'assets.js'), 'https://form.jotform.com/262946078314059?formContext=contact');
|
||||
const result = await verifyArtifact(root, 'staging');
|
||||
assert.equal(result.passed, false);
|
||||
assert.ok(result.errors.some((error) => error.includes('isolated-test Form A identifier')));
|
||||
assert.ok(result.errors.some((error) => error.includes('outside approved routes')));
|
||||
});
|
||||
|
||||
test('artifact rejects root-relative URLs with terminal slashes', async () => {
|
||||
@@ -140,3 +142,22 @@ test('release verification rejects private portal resources and encrypted payloa
|
||||
assert.ok(result.errors.some((error) => error.includes('private portal ciphertext')));
|
||||
assert.ok(result.errors.some((error) => error.includes('private/legacy')));
|
||||
});
|
||||
|
||||
|
||||
test('artifact rejects extra routes, wrong origins and prematurely loaded drawers', async () => {
|
||||
const root = await fixture();
|
||||
await fs.appendFile(path.join(root,'index.html'), '<iframe src="https://form.jotform.com/262946078314059?formContext=contact&language=en"></iframe>');
|
||||
assert.equal((await verifyArtifact(root)).passed,false);
|
||||
const clean = await fixture();
|
||||
const file = path.join(clean,'aba-therapy/index.html');
|
||||
await fs.writeFile(file,(await fs.readFile(file,'utf8')).replace('data-jotform-iframe-url=', 'src='));
|
||||
assert.ok((await verifyArtifact(clean)).errors.some(e => e.includes('before activation')));
|
||||
const other = await fixture();
|
||||
await fs.appendFile(path.join(other,'index.html'), '<iframe src="https://unapproved.invalid/form"></iframe>');
|
||||
assert.ok((await verifyArtifact(other)).errors.some(e => e.includes('unauthorized iframe')));
|
||||
});
|
||||
|
||||
test('obsolete artifact modes are rejected with separate publication/form guidance', async () => {
|
||||
await assert.rejects(verifyArtifact('/unused','isolated-test'), /--forms=live\|off\|mock/);
|
||||
await assert.rejects(verifyArtifact('/unused','staging','invalid'), /live, off, or mock/);
|
||||
});
|
||||
@@ -13,7 +13,7 @@ const moduleName = process.env.PLAYWRIGHT_MODULE
|
||||
? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href : 'playwright';
|
||||
const executablePath = process.env.CHROMIUM_EXECUTABLE;
|
||||
const output = await fs.mkdtemp('/tmp/aia-phase10-browser-');
|
||||
const result = { startedAt: new Date().toISOString(), routeCount: 0, opened: 0, failures: [], pageErrors: [], consoleErrors: [], blocked: [], productionAttempts: [], screenshots: [], status: 'failed' };
|
||||
const result = { startedAt: new Date().toISOString(), routeCount: 0, opened: 0, failures: [], pageErrors: [], consoleErrors: [], blocked: [], productionAttempts: [], approvedFormAAttempts: [], screenshots: [], status: 'failed' };
|
||||
let browser;
|
||||
let preview;
|
||||
|
||||
@@ -59,7 +59,17 @@ try {
|
||||
if (url.origin !== origin || !['GET', 'HEAD'].includes(request.method())) {
|
||||
const attempt = { origin: url.origin, path: url.pathname, method: request.method() };
|
||||
result.blocked.push(attempt);
|
||||
if (isProductionLike(url)) result.productionAttempts.push(attempt);
|
||||
const parent = request.frame().parentFrame();
|
||||
const parentPath = parent ? new URL(parent.url()).pathname : '';
|
||||
const locale = parentPath.startsWith('/es/') ? 'es' : 'en';
|
||||
const contacts = ['/contact', '/es/contact'];
|
||||
const landings = ['/lp/aba-therapy', '/lp/aba-insurance', '/lp/aba-ahcccs', '/lp/aba-private-pay', '/lp/early-intervention'];
|
||||
const consultations = ['/client-consultation', '/es/client-consultation', '/schedule-consultation', '/aba-therapy', '/autism-evaluations', '/learner-social-club', '/es/aba-therapy', '/es/autism-evaluations', '/es/learner-social-club'];
|
||||
const formContext = contacts.includes(parentPath) ? 'contact' : landings.includes(parentPath) ? 'landing' : consultations.includes(parentPath) ? 'consultation' : undefined;
|
||||
const approved = request.method() === 'GET' && request.isNavigationRequest() && formContext &&
|
||||
url.href === `https://form.jotform.com/262946078314059?formContext=${formContext}&language=${locale}`;
|
||||
if (approved) result.approvedFormAAttempts.push(attempt);
|
||||
else if (isProductionLike(url)) result.productionAttempts.push(attempt);
|
||||
return route.abort();
|
||||
}
|
||||
return route.continue();
|
||||
|
||||
@@ -24,13 +24,13 @@ test('counter configuration defaults off and requires explicit approval', () =>
|
||||
assert.equal(resolveCounterConfig('mock', undefined, undefined).mode, 'mock');
|
||||
});
|
||||
|
||||
test('form configuration defaults off and live remains fail-closed', () => {
|
||||
assert.deepEqual(resolveFormMode(undefined, undefined), { mode: 'off', reason: 'default-off' });
|
||||
assert.equal(resolveFormMode('invalid', undefined).mode, 'off');
|
||||
assert.equal(resolveFormMode('live', 'false').mode, 'off');
|
||||
assert.equal(resolveFormMode('live', 'true').mode, 'live');
|
||||
assert.equal(formModeForRuntime(resolveFormMode('live', 'true')), 'off');
|
||||
assert.equal(resolveFormMode('mock', undefined).mode, 'mock');
|
||||
test('forms default live while native fixtures stay off and invalid settings throw', () => {
|
||||
assert.deepEqual(resolveFormMode(undefined), { mode: 'live', reason: 'default-live' });
|
||||
assert.throws(() => resolveFormMode('invalid'), /PUBLIC_AIA_FORMS_MODE/);
|
||||
assert.equal(resolveFormMode('live').mode, 'live');
|
||||
assert.equal(formModeForRuntime(resolveFormMode('live')), 'off');
|
||||
assert.equal(resolveFormMode('off').mode, 'off');
|
||||
assert.equal(resolveFormMode('mock').mode, 'mock');
|
||||
assert.equal(resolveMockFormResult(undefined), 'success');
|
||||
assert.equal(resolveMockFormResult('failed'), 'failed');
|
||||
assert.equal(resolveMockFormResult('unavailable'), 'unavailable');
|
||||
|
||||
@@ -336,7 +336,7 @@ await build({root:${JSON.stringify(temporary)},configFile:${JSON.stringify(path.
|
||||
}
|
||||
}
|
||||
execFileSync(process.execPath, ['--input-type=module', '-e', runner], {
|
||||
cwd: temporary, env: { ...process.env, PUBLIC_ALLOW_INDEXING: mode === 'staging' ? 'false' : 'true' },
|
||||
cwd: temporary, env: { ...process.env, PUBLIC_AIA_FORMS_MODE: '', PUBLIC_ALLOW_INDEXING: mode === 'staging' ? 'false' : 'true' },
|
||||
timeout: 60_000, maxBuffer: 2 * 1024 * 1024
|
||||
});
|
||||
const html = new Map();
|
||||
|
||||
@@ -39,7 +39,7 @@ async function writeEntry(collection, lang, slug, data = {}) {
|
||||
|
||||
function build(indexing = false) {
|
||||
return execFileSync(process.execPath, ['--input-type=module', '-e', runner], {
|
||||
cwd: temporary, env: { ...process.env, PUBLIC_ALLOW_INDEXING: String(indexing), TZ: 'America/Phoenix' },
|
||||
cwd: temporary, env: { ...process.env, PUBLIC_AIA_FORMS_MODE: 'off', PUBLIC_ALLOW_INDEXING: String(indexing), TZ: 'America/Phoenix' },
|
||||
timeout: 60_000, maxBuffer: 2 * 1024 * 1024, stdio: 'pipe'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Guarded consultation-only Form A browser regression.
|
||||
* Run after an explicitly gated isolated-test build. Every Jotform request is
|
||||
* Run after a normal live build. Every Jotform request is
|
||||
* fulfilled or aborted locally; no Jotform request is transmitted.
|
||||
*/
|
||||
import { randomBytes } from 'node:crypto';
|
||||
@@ -14,6 +14,8 @@ import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
|
||||
const forms = process.argv.find(a => a.startsWith('--forms='))?.split('=')[1] ?? 'live';
|
||||
assert.ok(['live', 'off', 'mock'].includes(forms));
|
||||
const root = fileURLToPath(new URL('../', import.meta.url));
|
||||
const moduleName = process.env.PLAYWRIGHT_MODULE
|
||||
? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href
|
||||
@@ -31,7 +33,7 @@ const routes = [
|
||||
{ route: '/es/learner-social-club', locale: 'es', drawer: true, screenshot: 'es-learner-social-club' },
|
||||
];
|
||||
const widths = [320, 390, 768, 1440];
|
||||
const evidence = { routes, widths, checks: 0, blocked: [], jotformAttempts: [], screenshots: [], fallbackChecks: [], status: 'failed' };
|
||||
const evidence = { forms, routes, widths, checks: 0, blocked: [], jotformAttempts: [], screenshots: [], fallbackChecks: [], 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;
|
||||
@@ -93,7 +95,7 @@ try {
|
||||
return route.abort();
|
||||
});
|
||||
};
|
||||
if (!process.argv.includes('--portal-only') && !process.argv.includes('--correction-only')) {
|
||||
if (forms === 'live' && !process.argv.includes('--portal-only') && !process.argv.includes('--correction-only')) {
|
||||
for (const width of widths) {
|
||||
for (const target of routes.filter((r) => r.drawer)) {
|
||||
const context = await browser.newContext({ viewport: { width, height: 844 }, serviceWorkers: 'block', reducedMotion: 'reduce' });
|
||||
@@ -196,7 +198,7 @@ try {
|
||||
evidence.checks++;
|
||||
await backContext.close();
|
||||
}
|
||||
if (!process.argv.includes('--portal-only')) {
|
||||
if (forms === 'live' && !process.argv.includes('--portal-only')) {
|
||||
// Completion messages originate from a locally fulfilled cross-origin frame.
|
||||
// No fixture submits anything; malformed browser envelopes are injected only in tests.
|
||||
for (const locale of ['en', 'es']) {
|
||||
@@ -282,6 +284,35 @@ try {
|
||||
await ctx.close();
|
||||
}
|
||||
}
|
||||
if (forms !== 'live') {
|
||||
for (const width of [390, 1440]) for (const target of routes.filter(r => r.drawer)) {
|
||||
const ctx = await browser.newContext({ viewport: { width, height: 900 }, serviceWorkers: 'block', reducedMotion: 'reduce' });
|
||||
await guard(ctx);
|
||||
const p = await ctx.newPage();
|
||||
await p.clock.install({ time: new Date('2026-10-30T12:00:00Z') });
|
||||
await p.clock.pauseAt(new Date('2026-10-30T12:00:01Z'));
|
||||
await p.goto(origin + target.route, { waitUntil: 'domcontentloaded' });
|
||||
assert.equal(await p.locator('iframe[data-jotform-iframe-frame]').count(), 0);
|
||||
await p.evaluate(() => scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' }));
|
||||
await p.clock.runFor(11000);
|
||||
const dialog = p.locator('[data-service-inquiry-drawer]');
|
||||
if (forms === 'off') {
|
||||
assert.equal(await dialog.evaluate(d => d.open), false, 'off never auto-opens');
|
||||
const destination = target.locale === 'es' ? '/es/client-consultation' : '/client-consultation';
|
||||
const cta = p.locator('main a[href="' + destination + '"]').first();
|
||||
assert.ok(await cta.count());
|
||||
await cta.click();
|
||||
await p.waitForURL(origin + destination);
|
||||
} else {
|
||||
assert.equal(await dialog.evaluate(d => d.open), true, 'existing synthetic drawer remains available');
|
||||
assert.ok(await dialog.locator('[data-form-mode="mock"]').count());
|
||||
assert.equal(await dialog.locator('form').count(), 0);
|
||||
}
|
||||
assert.equal(evidence.jotformAttempts.length, 0);
|
||||
evidence.checks++;
|
||||
await ctx.close();
|
||||
}
|
||||
}
|
||||
// Actual portal UI with synthetic encrypted response fixtures, never real private source.
|
||||
for (const width of widths) {
|
||||
const ctx = await browser.newContext({ viewport: { width, height: 844 }, serviceWorkers: 'block' }); await guard(ctx, true);
|
||||
|
||||
@@ -58,9 +58,9 @@ test('four service configurations are bilingual without adding an Academy route'
|
||||
for (const variants of Object.values(serviceInquiryCopy)) for (const copy of Object.values(variants)) {
|
||||
assert.ok(copy.heading && copy.introduction); assert.ok(!copy.introduction.includes('—'));
|
||||
}
|
||||
const { formAConsultationTestPlacements } = await import('../src/utils/form-integration.ts');
|
||||
assert.equal(formAConsultationTestPlacements.length, 9);
|
||||
assert.ok(!formAConsultationTestPlacements.some((p) => p.route.includes('preparatory')));
|
||||
const { formAIframePlacements } = await import('../src/utils/form-integration.ts');
|
||||
assert.equal(formAIframePlacements.filter(p => p.context === 'consultation').length, 9);
|
||||
assert.ok(!formAIframePlacements.some((p) => p.route.includes('preparatory')));
|
||||
});
|
||||
test('legacy independent crypto fixture and two-view separation round-trip', async () => {
|
||||
const pass = randomBytes(32).toString('base64'), other = randomBytes(32).toString('base64');
|
||||
@@ -108,9 +108,14 @@ test('portal input requires every local gate and rejects missing or malformed ci
|
||||
const pass = randomBytes(24).toString('base64');
|
||||
const streams = await Promise.all(['one','two'].map((id) => encryptPortalStream(id, '<p>Synthetic</p>', pass)));
|
||||
const file = path.join(dir, 'fixture.json'); await fs.writeFile(file, JSON.stringify({ alg: portalAlgorithm, streams }), { mode: 0o600 });
|
||||
const options = { approved: 'true', indexing: 'false', environment: 'isolated-test', file };
|
||||
const options = { approved: 'true', indexing: 'false', file };
|
||||
assert.ok(readPortalInput(options));
|
||||
for (const changed of [{ approved: undefined }, { approved: 'false' }, { indexing: 'true' }, { indexing: undefined }, { environment: 'production' }, { file: '/missing-fixture' }]) assert.equal(readPortalInput({ ...options, ...changed }), undefined);
|
||||
// Form modes cannot substitute for the independent private-input approval.
|
||||
for (const mode of ['live', 'off', 'mock']) {
|
||||
assert.ok(readPortalInput({ ...options, mode }));
|
||||
assert.equal(readPortalInput({ indexing: 'false', file, mode }), undefined);
|
||||
}
|
||||
for (const changed of [{ approved: undefined }, { approved: 'false' }, { indexing: 'true' }, { indexing: undefined }, { file: '/missing-fixture' }]) assert.equal(readPortalInput({ ...options, ...changed }), undefined);
|
||||
await fs.writeFile(file, '<h1>Not encrypted</h1>'); assert.equal(readPortalInput(options), undefined);
|
||||
} finally { await fs.rm(dir, { recursive: true }); }
|
||||
});
|
||||
Reference in new issue
Block a user