feat(forms): add isolated Form A consultation testing
This commit is contained in:
25 files changed
+970
-177
No files matched your search
@@ -9,6 +9,9 @@ PUBLIC_AIA_FORMS_MOCK_RESULT=success
|
|||||||
# unavailable in indexing-enabled builds even when both are set.
|
# unavailable in indexing-enabled builds even when both are set.
|
||||||
PUBLIC_AIA_FORMS_ENVIRONMENT=local
|
PUBLIC_AIA_FORMS_ENVIRONMENT=local
|
||||||
PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED=false
|
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
|
# Form A production additionally requires mode=live, LIVE_APPROVED=true and
|
||||||
# environment=production. PUBLIC_ALLOW_INDEXING never authorizes a form.
|
# environment=production. PUBLIC_ALLOW_INDEXING never authorizes a form.
|
||||||
PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=false
|
PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=false
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ Direct-Jotform Form A synthetic EN/ES submission, native thank-you, conditional-
|
|||||||
|
|
||||||
### Landing-route extension — 2026-10-25 / 2026-10-26
|
### 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; other service-consultation journeys are planned Form A `consultation` targets and remain inactive.
|
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 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.
|
||||||
|
|
||||||
@@ -92,4 +92,23 @@ JF-01 through JF-09, privacy/security review, operations approval, fluent-langua
|
|||||||
|
|
||||||
## 2026-10-28 reconciliation
|
## 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. Other service-consultation journeys are planned Form A `consultation` targets; this context is not configured or emitted. 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.
|
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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# AIA Astro Migration — Living Session Handoff
|
# AIA Astro Migration — Living Session Handoff
|
||||||
|
|
||||||
**Last updated:** October 28, 2026 (UTC)
|
**Last updated:** October 29, 2026 (UTC)
|
||||||
**Repository:** `AIAMarketing/azinstitute4autism.com`
|
**Repository:** `AIAMarketing/azinstitute4autism.com`
|
||||||
**Working branch:** `faithful-astro-migration`
|
**Working branch:** `faithful-astro-migration`
|
||||||
**Last accepted technical implementation:** Phase 9 bounded landing-page integration (presentation and inactive lead UI only; human decisions and production activation pending)
|
**Last accepted technical implementation:** Phase 9 bounded landing-page integration (presentation and inactive lead UI only; human decisions and production activation pending)
|
||||||
@@ -167,12 +167,12 @@ trusted-source comparison. No insecure retrieval was repeated.
|
|||||||
Earlier October 20 reconciliations remain superseded local evidence outside
|
Earlier October 20 reconciliations remain superseded local evidence outside
|
||||||
the checkpoint.
|
the checkpoint.
|
||||||
|
|
||||||
### Current two-form status — 2026-10-28
|
### Current two-form status — 2026-10-29
|
||||||
|
|
||||||
- Form A (`262946078314059`) is implemented only in a non-indexing, seven-route `isolated-test` adapter: `/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: future family service journeys are planned for an unconfigured Form A `consultation` context, while Form C remains a separate future professional-referral form. The seven-route Form A allowlist is frozen until Builder configuration, synthetic acceptance and a separately authorized route change are complete. See [the two-form integration specification](phase-9-two-form-integration-specification-2026-10-28.md).
|
- 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.
|
- 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.
|
||||||
- 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.
|
- 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/isolated behavior remains fail-closed, and Form C, future consultation surfaces, service drawers, 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.
|
- 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.
|
||||||
|
|
||||||
The indexing-enabled build has 127 HTML routes/canonicals and 109 sitemap URLs.
|
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`;
|
Normal staging has 128 `noindex,nofollow` pages because it also contains `/lp`;
|
||||||
|
|||||||
@@ -49,7 +49,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.
|
1. Record a locked Node/npm/toolchain and reproducible artifact manifest.
|
||||||
2. Obtain sanitized Contabo/NixOS/nginx/TLS/DNS/Cloudflare facts.
|
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.
|
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 planned `consultation` context is unconfigured; 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. 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.
|
||||||
5. Obtain P9-04 counter origin/credential/consent approval before a live adapter.
|
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.
|
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.
|
7. Obtain P9-06 clinical/organizational and fluent-language approvals, including Arabic diagrams and Spanish HSA/FSA claims.
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Recorded: 2026-10-14 (source review began October 13). Prepared during Phase 7;
|
|||||||
## P9-03 — Form/API ownership
|
## P9-03 — Form/API ownership
|
||||||
|
|
||||||
- **Status:** Pending decision
|
- **Status:** Pending decision
|
||||||
- **Current implementation:** Form A has seven separately gated placements; all other shared form controls remain disabled with no backend. Physical Form B is retired; Form A `consultation` and Form C remain future work.
|
- **Current implementation:** Form A has seven production-eligible placements and nine separately gated, isolated-test-only consultation placements; all other shared form controls remain disabled with no backend. Physical Form B is retired; Form C remains future work. The consultation surfaces are not production-authorized.
|
||||||
- **Evidence:** [two-form integration specification](phase-9-two-form-integration-specification-2026-10-28.md), [../src/components/FormShell.astro](../src/components/FormShell.astro)
|
- **Evidence:** [two-form integration specification](phase-9-two-form-integration-specification-2026-10-28.md), [../src/components/FormShell.astro](../src/components/FormShell.astro)
|
||||||
- **Options:** Approved direct Jotform Form A consultation and separate Form C; no Contabo backend for protected lead data.
|
- **Options:** Approved direct Jotform Form A consultation and separate Form C; no Contabo backend for protected lead data.
|
||||||
- **Risk if unresolved:** Lost inquiries, accidental collection, unsafe retention or ambiguous failure.
|
- **Risk if unresolved:** Lost inquiries, accidental collection, unsafe retention or ambiguous failure.
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Phase 9 — Form A consultation isolated-test integration
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
## Purpose and boundary
|
||||||
|
|
||||||
|
This batch retires no additional physical form: Form A serves the owner-reported configured `consultation` context and Form C remains the future professional-referral form. It does not activate a production consultation route, modify the Jotform account, submit a form, route answers through Astro or Contabo, or add analytics, advertising, conversion tracking, webhooks, cookies, query forwarding, service preselection, child/clinical/payment/booking collection, a Jotform helper, cross-origin DOM access, or `postMessage` handling.
|
||||||
|
|
||||||
|
The pre-existing production resolver remains frozen at seven Form A contact/landing paths. The following nine paths are a separate isolated-test allowlist:
|
||||||
|
|
||||||
|
| Surface | Route | Locale | Fixed context and browser behavior |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| Standalone | `/client-consultation` | EN | `formContext=consultation&language=en`; iframe receives its fixed source during render |
|
||||||
|
| Standalone | `/es/client-consultation` | ES | `formContext=consultation&language=es`; iframe receives its fixed source during render |
|
||||||
|
| Standalone | `/schedule-consultation` | EN | `formContext=consultation&language=en`; iframe receives its fixed source during render |
|
||||||
|
| Service drawer | `/aba-therapy` | EN | Explicit opener assigns the fixed EN source only after the native dialog opens |
|
||||||
|
| Service drawer | `/autism-evaluations` | EN | Explicit opener assigns the fixed EN source only after the native dialog opens |
|
||||||
|
| Service drawer | `/learner-social-club` | EN | Explicit opener assigns the fixed EN source only after the native dialog opens |
|
||||||
|
| Service drawer | `/es/aba-therapy` | ES | Explicit opener assigns the fixed ES source only after the native dialog opens |
|
||||||
|
| Service drawer | `/es/autism-evaluations` | ES | Explicit opener assigns the fixed ES source only after the native dialog opens |
|
||||||
|
| Service drawer | `/es/learner-social-club` | ES | Explicit opener assigns the fixed ES source only after the native dialog opens |
|
||||||
|
|
||||||
|
Each URL comes from the typed, fixed route/locale/context mapping. No route may derive the form ID, language, context or parameter values from visitor input, queries, referrers or page labels.
|
||||||
|
|
||||||
|
## Activation contract
|
||||||
|
|
||||||
|
All six values are required for the nine consultation surfaces:
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Missing, invalid, contradictory or indexing-enabled values fail closed. The production values `PUBLIC_AIA_FORMS_MODE=live`, `PUBLIC_AIA_FORMS_LIVE_APPROVED=true`, `PUBLIC_AIA_FORMS_ENVIRONMENT=production` and `PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true` continue to permit only `/contact`, `/es/contact`, the four named ABA landing pages and `/lp/early-intervention`. The consultation-test flag cannot expand that production list.
|
||||||
|
|
||||||
|
The adapter retains `referrerpolicy="no-referrer"`, `sandbox="allow-forms allow-scripts allow-same-origin"`, fixed context-sensitive dimensions, localized accessible titles, its conditional 15-second hosted-form fallback and localized no-JavaScript fallback. A frame `load` only stops the fallback timer; it is not evidence of Jotform initialization, an accepted submission, a conversion or a healthy cross-origin application.
|
||||||
|
|
||||||
|
## Shared bilingual drawer behavior
|
||||||
|
|
||||||
|
`ServiceInquiryDrawer.astro` now uses one typed English/Spanish implementation. In authorized consultation-test mode it removes the legacy child-information surface, renders a visible keyboard-accessible opener and uses a native dialog with a localized close name. The drawer does not auto-open. Opening assigns the already validated source once, starts the adapter timer and focuses the close control. Escape, close-button and backdrop paths use the existing dialog behavior and restore focus to the trigger. Closing and reopening does not create another iframe or reset its source. The rendered parent supplies localized instructions and a no-JavaScript hosted path; Jotform's child text remains a separate owner language-acceptance item.
|
||||||
|
|
||||||
|
The owner superseded the earlier diagnostic-evaluation explanatory-note requirement. The implementation adds no conditional note. AIA's client advocate will discuss available options and coordination with qualified diagnostic providers in the later conversation. The parent and Form A must not imply current in-clinic diagnostic availability, a diagnosis or a completed diagnostic-provider hire.
|
||||||
|
|
||||||
|
## Verification record
|
||||||
|
|
||||||
|
- `npm run check`: passed with 0 errors, 0 warnings and 0 hints.
|
||||||
|
- Focused resolver, artifact and nonproduction tests: 21 passing tests.
|
||||||
|
- Guarded browser verification uses loopback Astro preview, blocks every external request before transmission and supplies only a local Jotform fixture. It verifies all nine routes at 320, 390, 768 and 1440px; static rendering and deferred drawer loads are recorded separately. Result details, screenshot directory and the final validation/audit outcomes are added to this report after the complete regression pass.
|
||||||
|
|
||||||
|
No automated check submits form data or treats a frame load as a conversion. The guarded tests establish parent-page routing, gating, accessibility hooks and request timing only; they cannot verify the actual Jotform dropdown, conditions, localized child UI, validation, confirmation, notification or telemetry behavior.
|
||||||
|
|
||||||
|
## Remaining acceptance
|
||||||
|
|
||||||
|
The Jotform owner and applicable operations, clinical, privacy/security and English/Spanish reviewers must still accept the actual consultation dropdown's visibility, requiredness, five display choices, condition behavior, translations, validation, confirmation and notification boundary. Required browser acceptance includes real embedded loading, responsive/error expansion, keyboard traversal across the iframe boundary, no-JavaScript hosted fallback, real screen-reader behavior and Jotform runtime/telemetry privacy review. A separate owner decision and production allowlist change are required before any consultation surface is eligible for production. Form C remains uncreated and unmapped.
|
||||||
|
|
||||||
|
### Completed local evidence
|
||||||
|
|
||||||
|
The guarded consultation browser runner passed **45 checks**: nine routes at 320, 390, 768 and 1440px, plus nine no-JavaScript checks. It recorded 36 full-page screenshots in `/tmp/aia-form-a-consultation-NUdKYz` and one held-request timeout check. The local request guard intercepted 40 attempted Jotform GETs, all to one of the two fixed consultation URLs; it fulfilled ordinary checks from a local fixture and held one timeout request. No request was transmitted to Jotform. It observed no non-GET/HEAD request, production API request, analytics request, advertising request or WebSocket request. This is parent-page browser evidence only, not a test of the real Jotform child application.
|
||||||
|
|
||||||
|
The original seven Form A routes were also rerun with the guarded isolated-test browser runner: **35 checks** across the seven paths at the same four widths plus no-JavaScript and fallback states. The resulting request ledger contains only intercepted GET attempts to the fixed Form A URLs and pre-existing Leaflet resources; no request was transmitted and no non-read request was attempted.
|
||||||
|
|
||||||
|
Focused resolver/artifact/nonproduction tests passed **21/21**. The existing Form A preview stylesheet test was reconciled to the owner-maintained `.form-all` Jotform selector and passed **4/4** without modifying `integrations/jotform/aia-form-a.css`. Campaign tests passed **11/11**. Core suites passed: Markdown 20/20, publication 33/33, SEO 35/35, Library 53/53, routes 10/10 and Phase 8 contrast 5/5. `astro check` reported zero diagnostics. Normal staging, mock, indexing-enabled, production-gated and isolated-test artifacts all passed the artifact verifier:
|
||||||
|
|
||||||
|
| Artifact mode | HTML routes | Sitemap URLs | hreflang links | Form A frames | Deferred drawer frames |
|
||||||
|
|---|---:|---:|---:|---:|---:|
|
||||||
|
| Normal staging/off | 128 | 0 | 0 | 0 | 0 |
|
||||||
|
| Mock | 128 | 0 | 0 | 0 | 0 |
|
||||||
|
| Indexing-enabled/off | 127 | 109 | 180 | 0 | 0 |
|
||||||
|
| Production-gated | 127 | 109 | 180 | 7 | 0 |
|
||||||
|
| Consultation isolated-test | 128 | 0 | 0 | 10 | 6 |
|
||||||
|
|
||||||
|
In the isolated artifact, ten static frames are the seven frozen contact/landing paths plus the three standalone consultation pages. The six service-drawer adapters retain an approved source only as deferred data until a visitor opens the dialog. The final normal staging build was restored: 128 `noindex,nofollow` pages, an empty sitemap, zero hreflang links and zero Form A frames. Link, image and blog audits reported 0 broken internal links, 0 missing mapped page images and 0 blog-audit failures.
|
||||||
@@ -45,7 +45,15 @@ The official Jotform resize/embed handler is **conditionally acceptable pending
|
|||||||
- Privacy/security owners must review Jotform runtime requests and telemetry, embed/referrer behavior and deployment configuration.
|
- Privacy/security owners must review Jotform runtime requests and telemetry, embed/referrer behavior and deployment configuration.
|
||||||
- Hosting owners must accept the HTTPS host, CSP/referrer/log policy and production environment gate.
|
- Hosting owners must accept the HTTPS host, CSP/referrer/log policy and production environment gate.
|
||||||
- Marketing notification routing remains a pre-cutover operational check; it is not changed by this record.
|
- Marketing notification routing remains a pre-cutover operational check; it is not changed by this record.
|
||||||
- Form A `consultation` remains unconfigured and unverified; physical Form B is retired; Form C remains unimplemented and unverified.
|
- Form A `consultation` is owner-reported configured and now has nine isolated-test-only Astro placements. Its Jotform visibility, requiredness, translations, submission/confirmation and all-route presentation remain unverified by this repository; physical Form B is retired; Form C remains unimplemented and unverified.
|
||||||
- Phase 10 requires a separate release-owner go/no-go and rollback rehearsal.
|
- Phase 10 requires a separate release-owner go/no-go and rollback rehearsal.
|
||||||
|
|
||||||
No Jotform account settings, recipient routing, submissions, analytics, advertising, API behavior, hosting configuration or deployment was changed while recording this evidence.
|
No Jotform account settings, recipient routing, submissions, analytics, advertising, API behavior, hosting configuration or deployment was changed while recording this evidence.
|
||||||
|
|
||||||
|
## 2026-10-29 consultation-context addendum
|
||||||
|
|
||||||
|
The owner reports that Form A now has a `consultation` context with the required service-interest dropdown. The repository has implemented nine **isolated-test-only** Astro placements: three standalone consultation pages and six English/Spanish service drawers. This addendum does not represent a real Jotform submission, confirmation or Builder review by the repository.
|
||||||
|
|
||||||
|
The new routes require the separate `PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED=true` flag together with every isolated-test gate. They remain absent from normal staging, mock, indexing-enabled, production-gated and deployed artifacts. The existing seven Form A contact/landing production-eligible routes are unchanged.
|
||||||
|
|
||||||
|
The current decision supersedes the earlier plan for a conditional Autism Diagnostic Evaluation explanation. Do not display that note. The AIA client advocate will discuss evaluation options and coordination with qualified diagnostic providers in follow-up. Owner acceptance is still required for the actual dropdown's visibility, requiredness, choices, translations, conditional behavior, embedded submission/confirmation, error behavior and all nine route presentations.
|
||||||
@@ -4,7 +4,7 @@ Date: **2026-10-18** · Plan version **1.7 revision 2026-10-28** · Governing co
|
|||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
> **Form A reconciliation — 2026-10-28 (current implementation evidence, no production release approval):** Form A — **AIA | Website | General Enquiries** (`262946078314059`) has owner-confirmed direct EN/ES synthetic submission, native thank-you, generic no-answer/no-PDF staff notification, autoresponder delivery, fixed hidden Reply-To, no-webhook/integration, conditional-email and language evidence. The owner also reports successful isolated Astro-iframe synthetic submission and confirmation on English contact, Spanish contact and one ABA landing placement; record that third route explicitly before release rather than inferring it. This completes functional submission acceptance for the reported samples, not seven-route responsive, accessibility, telemetry/privacy, deployment or site-release acceptance. Its hidden presentation field is **Advanced → Field Details → Unique Name = `formContext`** (exact case). The current Astro adapter is implemented only for gated, non-indexing isolated tests: `/contact`, `/es/contact`, `/lp/aba-therapy`, `/lp/aba-insurance`, `/lp/aba-ahcccs`, `/lp/aba-private-pay`, and `/lp/early-intervention`; the five landing journeys now use fixed `formContext=landing`, superseding their earlier Form B mapping. Physical Form B is retired. Form A `consultation` and Form C remain future work; their Jotform field keys and IDs must not be guessed.
|
> **Form A reconciliation — 2026-10-28 (current implementation evidence, no production release approval):** Form A — **AIA | Website | General Enquiries** (`262946078314059`) has owner-confirmed direct EN/ES synthetic submission, native thank-you, generic no-answer/no-PDF staff notification, autoresponder delivery, fixed hidden Reply-To, no-webhook/integration, conditional-email and language evidence. The owner also reports successful isolated Astro-iframe synthetic submission and confirmation on English contact, Spanish contact and one ABA landing placement; record that third route explicitly before release rather than inferring it. This completes functional submission acceptance for the reported samples, not seven-route responsive, accessibility, telemetry/privacy, deployment or site-release acceptance. Its hidden presentation field is **Advanced → Field Details → Unique Name = `formContext`** (exact case). The current Astro adapter is implemented only for gated, non-indexing isolated tests: `/contact`, `/es/contact`, `/lp/aba-therapy`, `/lp/aba-insurance`, `/lp/aba-ahcccs`, `/lp/aba-private-pay`, and `/lp/early-intervention`; the five landing journeys now use fixed `formContext=landing`, superseding their earlier Form B mapping. Physical Form B is retired. Form A `consultation` now has a separate nine-route isolated-test adapter; Form C remains future work. Jotform field keys and IDs must not be guessed.
|
||||||
|
|
||||||
> **Production-gate addendum — 2026-10-28:** The local adapter now also supports the same seven Form A placements only when `PUBLIC_AIA_FORMS_MODE=live`, `PUBLIC_AIA_FORMS_LIVE_APPROVED=true`, `PUBLIC_AIA_FORMS_ENVIRONMENT=production`, and `PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true` are all set. This is a build-time configuration contract, not a deployed environment or site-release authorization. It leaves Form C, drawers and every unrelated lead surface in their off/mock path. An indexing-enabled release artifact is compatible with the gate, but `PUBLIC_ALLOW_INDEXING` never enables a form.
|
> **Production-gate addendum — 2026-10-28:** The local adapter now also supports the same seven Form A placements only when `PUBLIC_AIA_FORMS_MODE=live`, `PUBLIC_AIA_FORMS_LIVE_APPROVED=true`, `PUBLIC_AIA_FORMS_ENVIRONMENT=production`, and `PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true` are all set. This is a build-time configuration contract, not a deployed environment or site-release authorization. It leaves Form C, drawers and every unrelated lead surface in their off/mock path. An indexing-enabled release artifact is compatible with the gate, but `PUBLIC_ALLOW_INDEXING` never enables a form.
|
||||||
|
|
||||||
@@ -24,9 +24,9 @@ The confirmation is limited. Recipient access and Form A retention/deletion cont
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| F-001 | `/contact` | `en/contact.mdx` | `pages/ContactPage.astro` → `ContactForm.astro` | A EN |
|
| F-001 | `/contact` | `en/contact.mdx` | `pages/ContactPage.astro` → `ContactForm.astro` | A EN |
|
||||||
| F-002 | `/es/contact` | `es/contact.mdx` | Same | A ES |
|
| F-002 | `/es/contact` | `es/contact.mdx` | Same | A ES |
|
||||||
| F-003 | `/client-consultation` | `en/client-consultation.md` | `FormShell.astro` via PageLayout | Form A / planned `consultation` EN |
|
| F-003 | `/client-consultation` | `en/client-consultation.md` | `FormShell.astro` via PageLayout | Form A / `consultation` EN, isolated-test only |
|
||||||
| F-004 | `/es/client-consultation` | `es/client-consultation.md` | Same | Form A / planned `consultation` ES |
|
| F-004 | `/es/client-consultation` | `es/client-consultation.md` | Same | Form A / `consultation` ES, isolated-test only |
|
||||||
| F-005 | `/schedule-consultation` | `en/schedule-consultation.mdx` | Same | Form A / planned `consultation` EN |
|
| F-005 | `/schedule-consultation` | `en/schedule-consultation.mdx` | Same | Form A / `consultation` EN, isolated-test only |
|
||||||
| F-006 | `/referrals` | `en/referrals.mdx` | Same; preserve `#form-title` | C EN |
|
| F-006 | `/referrals` | `en/referrals.mdx` | Same; preserve `#form-title` | C EN |
|
||||||
| F-007 | `/aba-therapy` | `en/aba-therapy.mdx` | `ServiceInquiryDrawer.astro` | Form A / planned `consultation`; drawer redesign |
|
| F-007 | `/aba-therapy` | `en/aba-therapy.mdx` | `ServiceInquiryDrawer.astro` | Form A / planned `consultation`; drawer redesign |
|
||||||
| F-008 | `/autism-evaluations` | `en/autism-evaluations.mdx` | Same | Form A / planned `consultation`; referral-assistance approval |
|
| F-008 | `/autism-evaluations` | `en/autism-evaluations.mdx` | Same | Form A / planned `consultation`; referral-assistance approval |
|
||||||
@@ -34,11 +34,11 @@ The confirmation is limited. Recipient access and Form A retention/deletion cont
|
|||||||
|
|
||||||
Nine instances = 7 EN + 2 ES, three implementations. Eleven GET search instances remain separate. No Arabic enquiry form or Spanish referral route is created. EN/ES-capable logical forms do not imply new published routes.
|
Nine instances = 7 EN + 2 ES, three implementations. Eleven GET search instances remain separate. No Arabic enquiry form or Spanish referral route is created. EN/ES-capable logical forms do not imply new published routes.
|
||||||
|
|
||||||
The five imported landing-page journeys—`/lp/aba-therapy`, `/lp/aba-insurance`, `/lp/aba-ahcccs`, `/lp/aba-private-pay` and `/lp/early-intervention`—are current gated Form A isolated-test placements with fixed `formContext=landing&language=en`. This supersedes their earlier Form B mapping. Physical Form B is retired; other service-consultation routes are planned Form A `consultation` targets and are not current iframe placements. The landing panels are not additional native form implementations, and Form A is absent from off/mock/indexing output.
|
The five imported landing-page journeys—`/lp/aba-therapy`, `/lp/aba-insurance`, `/lp/aba-ahcccs`, `/lp/aba-private-pay` and `/lp/early-intervention`—are current gated Form A isolated-test placements with fixed `formContext=landing&language=en`. This supersedes their earlier Form B mapping. Physical Form B is retired; the nine documented consultation routes are current isolated-test-only Form A placements and are never production-eligible. The landing panels are not additional native form implementations, and Form A is absent from off/mock/indexing output.
|
||||||
|
|
||||||
### Discrepancies and supersession notes
|
### Discrepancies and supersession notes
|
||||||
|
|
||||||
1. October 17 proposed possible event/fundraising consolidation into general enquiries. October 18 explicitly preserves the Event Submission Form separately; fundraising still needs approval. Tour currently points to `/client-consultation`, a planned Form A `consultation` route whose service list has no tour choice; its CTA disposition requires an intake/operations decision.
|
1. October 17 proposed possible event/fundraising consolidation into general enquiries. October 18 explicitly preserves the Event Submission Form separately; fundraising still needs approval. Tour currently points to `/client-consultation`, a Form A `consultation` isolated-test route whose service list has no tour choice; its CTA disposition requires an intake/operations decision.
|
||||||
2. October 17 proposed one chosen contact channel. Form A retains its existing conditional email/phone behavior; the planned consultation context adds only service interest. Operations must approve any additional field or contact-preference change; do not implement the old alternative.
|
2. October 17 proposed one chosen contact channel. Form A retains its existing conditional email/phone behavior; the planned consultation context adds only service interest. Operations must approve any additional field or contact-preference change; do not implement the old alternative.
|
||||||
3. The older report allowed date/time/source notification extensions after review. Initial contract now explicitly defers them; use only the exact generic template after notification approval.
|
3. The older report allowed date/time/source notification extensions after review. Initial contract now explicitly defers them; use only the exact generic template after notification approval.
|
||||||
4. The older inventory knew no confirmed lead source. The supplied October 18 evidence preserves `250727122848156` as a **historical structural reference only**. The revised owner decision rejects cloning or repurposing it; it is not a validated schema or active embed destination.
|
4. The older inventory knew no confirmed lead source. The supplied October 18 evidence preserves `250727122848156` as a **historical structural reference only**. The revised owner decision rejects cloning or repurposing it; it is not a validated schema or active embed destination.
|
||||||
@@ -99,7 +99,7 @@ For off mode prefer non-form fallback markup without editable lead controls or a
|
|||||||
|
|
||||||
Mock fixtures must be synthetic and inert to external requests, including Enter and native submission paths; Playwright aborts unapproved requests before transmission. Fixture URLs/assets must not appear in the production route manifest. Live descriptors must not leak into off/mock emitted bundles through shared imports—scan generated HTML, scripts and preload hints.
|
Mock fixtures must be synthetic and inert to external requests, including Enter and native submission paths; Playwright aborts unapproved requests before transmission. Fixture URLs/assets must not appear in the production route manifest. Live descriptors must not leak into off/mock emitted bundles through shared imports—scan generated HTML, scripts and preload hints.
|
||||||
|
|
||||||
Parent component preserves approved page design, explanatory text, iframe title, hosted fallback and responsive width/height. The Form A adapter currently uses owner-verified fixed `language=en|es` and `formContext=contact|landing` values only; `consultation` remains unconfigured and must not be emitted; it forwards no visitor data. Cross-origin child labels/errors must be verified in Jotform; Astro locale and CSS do not localize/fix them. The first-party fallback remains hidden without layout space, appears only after a 15-second missing-load timeout, and has a localized noscript version. Loading/error UI must not claim it can inspect the child DOM. A frame load is not proof of availability, application initialization or accepted submission.
|
Parent component preserves approved page design, explanatory text, iframe title, hosted fallback and responsive width/height. The Form A adapter uses fixed `language=en|es` and `formContext=contact|landing|consultation` values selected only by its typed allowlists; it forwards no visitor data. `consultation` is emitted only by the separately approved non-indexing isolated-test gate and never by the production resolver. Cross-origin child labels/errors must be verified in Jotform; Astro locale and CSS do not localize/fix them. The first-party fallback remains hidden without layout space, appears only after a 15-second missing-load timeout, and has a localized noscript version. Loading/error UI must not claim it can inspect the child DOM. A frame load is not proof of availability, application initialization or accepted submission.
|
||||||
|
|
||||||
Drawer decision is a prerequisite for replacing its live content: retain timer only if approved and incapable of loading vendor resources without the approved trigger; otherwise use an explicitly approved open/load interaction or full-page fallback. Preserve Escape/initial focus/restoration and test cross-origin focus boundaries. No wholesale dialog replacement.
|
Drawer decision is a prerequisite for replacing its live content: retain timer only if approved and incapable of loading vendor resources without the approved trigger; otherwise use an explicitly approved open/load interaction or full-page fallback. Preserve Escape/initial focus/restoration and test cross-origin focus boundaries. No wholesale dialog replacement.
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ Source changes require a newly named offline reconciliation using explicit final
|
|||||||
|
|
||||||
First recommended authorized batch: **Batch A**, final Form A consultation/Form C specifications and owner field approval, using the now-complete owner-supplied 27-asset roster as historical context only. First safe **code** batch is C's off/mock contact slice after its explicit authorization; it can avoid external account changes while B is pending, but cannot invent Jotform settings.
|
First recommended authorized batch: **Batch A**, final Form A consultation/Form C specifications and owner field approval, using the now-complete owner-supplied 27-asset roster as historical context only. First safe **code** batch is C's off/mock contact slice after its explicit authorization; it can avoid external account changes while B is pending, but cannot invent Jotform settings.
|
||||||
|
|
||||||
The companion CSV records limited owner-confirmed Form A portions of JF-01, JF-04, JF-06, JF-07 and OA-05. Pending facts still include actual question IDs/types/defaults/options, conditional routing, real translations/locale URLs, default email behavior, thank-you/POST behavior, blank-form isolation and supported success signaling. Form A recipient access and retention/deletion are owner-verified; detailed role and retention-policy records remain outside the repository. Form A exists, but its `consultation` context has no route or production activation approval; Form C remains pending and physical Form B is retired. Tour/event/fundraising separation must be retained.
|
The companion CSV records limited owner-confirmed Form A portions of JF-01, JF-04, JF-06, JF-07 and OA-05. Pending facts still include actual question IDs/types/defaults/options, conditional routing, real translations/locale URLs, default email behavior, thank-you/POST behavior, blank-form isolation and supported success signaling. Form A recipient access and retention/deletion are owner-verified; detailed role and retention-policy records remain outside the repository. Form A `consultation` has nine isolated-test routes but no production activation approval; Form C remains pending and physical Form B is retired. Tour/event/fundraising separation must be retained.
|
||||||
|
|
||||||
Any approval record must identify approver role, date, exact form/environment/version, sanitized evidence and scope. An account label ENABLED, a BAA, a successful static build or a generic vendor help article cannot close these gates. Keep the real screen-reader, authorized visual/non-Chromium, fluent-language, clinical, financial and policy/source-provenance requirements from SESSION_HANDOFF.
|
Any approval record must identify approver role, date, exact form/environment/version, sanitized evidence and scope. An account label ENABLED, a BAA, a successful static build or a generic vendor help article cannot close these gates. Keep the real screen-reader, authorized visual/non-Chromium, fluent-language, clinical, financial and policy/source-provenance requirements from SESSION_HANDOFF.
|
||||||
|
|
||||||
@@ -246,3 +246,30 @@ Version 1.5, 2026-10-28: records owner-reported embedded synthetic submission an
|
|||||||
Version 1.6, 2026-10-28: adds the exact production-gate configuration for Form A and its seven fixed placements. It does not activate an environment, change the Jotform account, map Forms B/C, or authorize a production release.
|
Version 1.6, 2026-10-28: adds the exact production-gate configuration for Form A and its seven fixed placements. It does not activate an environment, change the Jotform account, map Forms B/C, or authorize a production release.
|
||||||
|
|
||||||
Version 1.7, 2026-10-28: records the owner-approved two-physical-form architecture. Physical Form B is retired; Form A gains a planned, unconfigured `consultation` context and Form C remains a future referral form. The exact future route/component matrix, service-interest options, evaluation explanation and pre-activation owner actions are in [the two-form integration specification](phase-9-two-form-integration-specification-2026-10-28.md). No source component, current seven-route gate, Jotform configuration or deployment changed.
|
Version 1.7, 2026-10-28: records the owner-approved two-physical-form architecture. Physical Form B is retired; Form A gains a planned, unconfigured `consultation` context and Form C remains a future referral form. The exact future route/component matrix, service-interest options, evaluation explanation and pre-activation owner actions are in [the two-form integration specification](phase-9-two-form-integration-specification-2026-10-28.md). No source component, current seven-route gate, Jotform configuration or deployment changed.
|
||||||
|
|
||||||
|
## Version 1.8 addendum — 2026-10-29 consultation isolated-test implementation
|
||||||
|
|
||||||
|
The owner reports that Form A's `consultation` context is configured in Jotform. Astro implements it for a separate, non-indexing isolated-test build only; the repository has not inspected the configured Builder rules or submitted data. It extends `FormAIframeContext` to the fixed value `consultation` and uses the existing trusted Form A URL builder. No Jotform field identifier, service-option value or condition identifier is represented in source.
|
||||||
|
|
||||||
|
| Placement class | Routes | Locale and activation behavior |
|
||||||
|
|---|---|---|
|
||||||
|
| Standalone consultation | `/client-consultation`, `/es/client-consultation`, `/schedule-consultation` | Fixed `formContext=consultation`; EN for the first and third routes, ES for the second |
|
||||||
|
| Service drawer | `/aba-therapy`, `/autism-evaluations`, `/learner-social-club` | EN. An accessible button opens the native dialog; only then does the adapter assign its fixed iframe URL. |
|
||||||
|
| Service drawer | `/es/aba-therapy`, `/es/autism-evaluations`, `/es/learner-social-club` | Same shared drawer and behavior in ES, with a fixed `language=es` URL and localized parent controls/fallback. |
|
||||||
|
|
||||||
|
Every consultation placement requires all of the following build-time values:
|
||||||
|
|
||||||
|
```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 original seven contact/landing placements are the complete production allowlist. `PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true`, whether alone or with the consultation flag, does not authorize these nine routes. Off, mock, normal staging, indexing-enabled and production builds emit no consultation iframe. For the six drawers, the static artifact contains a deferred adapter; its fixed source is assigned only after the visitor opens the dialog, so static iframe counts and on-demand browser requests must be reported separately.
|
||||||
|
|
||||||
|
The shared `ServiceInquiryDrawer.astro` keeps legacy off/mock behavior scoped outside the isolated consultation test. In the authorized test it removes the legacy child-information surface, uses localized English/Spanish drawer copy, relies on the native dialog's close/Escape/backdrop/focus-restoration behavior, and provides a localized no-JavaScript hosted-form path. It does not auto-open or load Jotform merely because the closed drawer is present. The fixed iframe keeps `referrerpolicy="no-referrer"`, `sandbox="allow-forms allow-scripts allow-same-origin"`, its 15-second hosted fallback, no Jotform helper, no cross-origin DOM access and no `postMessage` handler.
|
||||||
|
|
||||||
|
The previously proposed conditional Autism Diagnostic Evaluation explanation is superseded. The consultation form must not add that note or imply in-clinic diagnostic availability. The AIA client advocate will explain available options and coordination with qualified diagnostic providers in the later conversation. Required owner acceptance remains: rendered service question/choices/conditions, English/Spanish translations and validation, synthetic submission/confirmation, notification boundary, responsive/error sizing, keyboard/dialog behavior, real screen-reader review, telemetry/privacy review and any future production route authorization.
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# P9-03 Jotform integration contract
|
# P9-03 Jotform integration contract
|
||||||
|
|
||||||
**Contract ID:** P9-03-JF-2026-10-18 · **Version:** 1.7 · **Decision revision:** 2026-10-28
|
**Contract ID:** P9-03-JF-2026-10-18 · **Version:** 1.8 · **Decision revision:** 2026-10-29
|
||||||
|
|
||||||
**Status: Form A owner-approved for existing contexts; two-form consultation direction documented; deployment and new-route activation NOT authorized.**
|
**Status: Form A owner-approved for existing contexts; consultation surfaces implemented for isolated testing only; deployment and production consultation activation NOT authorized.**
|
||||||
|
|
||||||
This is the authoritative P9-03 planning contract for the owner decisions supplied on October 18 and revised through October 28. 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. No future commit SHA is invented.
|
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.
|
||||||
|
|
||||||
Companions: [source-specific implementation plan](phase-9-jotform-implementation-plan-2026-10-18.md), [two-form integration specification](phase-9-two-form-integration-specification-2026-10-28.md), [verification and approval ledger](phase-9-jotform-verification-checklist-2026-10-18.csv).
|
Companions: [source-specific implementation plan](phase-9-jotform-implementation-plan-2026-10-18.md), [two-form integration specification](phase-9-two-form-integration-specification-2026-10-28.md), [verification and approval ledger](phase-9-jotform-verification-checklist-2026-10-18.csv).
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ Evidence classes must remain distinct:
|
|||||||
- AIA uses Jotform Gold with HIPAA enabled and a signed Jotform BAA; 27 of 100 form slots are occupied. Existing clinical/operational workflows must remain intact.
|
- AIA uses Jotform Gold with HIPAA enabled and a signed Jotform BAA; 27 of 100 form slots are occupied. Existing clinical/operational workflows must remain intact.
|
||||||
- **All public lead submissions go directly to Jotform.** No Contabo submission proxy, PHI-bearing webhook, completed form-data forwarding or clinical-document transfer to Contabo.
|
- **All public lead submissions go directly to Jotform.** No Contabo submission proxy, PHI-bearing webhook, completed form-data forwarding or clinical-document transfer to Contabo.
|
||||||
- Prefer iframe presentation, preserving Astro design and locale behavior; use a hosted form fallback where embedding is unsuitable.
|
- Prefer iframe presentation, preserving Astro design and locale behavior; use a hosted form fallback where embedding is unsuitable.
|
||||||
- Use two physical forms: **A General Enquiries, landing enquiries and family service consultations**, and **C Professional Referral Initiation**. Form A retains its existing `contact` and `landing` contexts; `consultation` is planned only. `service` may remain a logical visitor-intent classification, not a separate physical form.
|
- Use two physical forms: **A General Enquiries, landing enquiries and family service consultations**, and **C Professional Referral Initiation**. Form A retains its existing `contact` and `landing` contexts; owner-reported Builder configuration now includes `consultation`, which Astro may render only through its separate non-indexing isolated-test gate. `service` may remain a logical visitor-intent classification, not a separate physical form.
|
||||||
- **Physical Form B is retired.** Do not create, clone or repurpose a separate service-consultation form. Form C alone remains a future blank-form creation task; the former proposal to use a working copy of **AIA Lead Generator | plain** remains superseded.
|
- **Physical Form B is retired.** Do not create, clone or repurpose a separate service-consultation form. Form C alone remains a future blank-form creation task; the former proposal to use a working copy of **AIA Lead Generator | plain** remains superseded.
|
||||||
- The historical source ID `250727122848156` remains reference evidence only. It must not be modified, cloned, disabled, deleted, repurposed or used as an embed destination under this decision. Existing clinical, agreement, billing, event, HR, administrative and CentralReach workflows remain independent.
|
- The historical source ID `250727122848156` remains reference evidence only. It must not be modified, cloned, disabled, deleted, repurposed or used as an embed destination under this decision. Existing clinical, agreement, billing, event, HR, administrative and CentralReach workflows remain independent.
|
||||||
- No appointment booking in initial consultation forms. Staff contact the family to arrange follow-up.
|
- No appointment booking in initial consultation forms. Staff contact the family to arrange follow-up.
|
||||||
@@ -150,8 +150,7 @@ Names below are domain semantics only, **not Jotform question IDs, uniqueName va
|
|||||||
| A | preferred_contact | Optional; define valid choices and phone/email consistency |
|
| A | preferred_contact | Optional; define valid choices and phone/email consistency |
|
||||||
| A | message | Omit by default; add only with explicit approval |
|
| A | message | Omit by default; add only with explicit approval |
|
||||||
| A | source_code | Deferred; absent initially |
|
| A | source_code | Deferred; absent initially |
|
||||||
| A / consultation | service_interest | Required controlled choice after Builder/clinical/privacy approval; no ID or option value inferred |
|
| A / consultation | service_interest | Owner-reported configured required controlled choice; Builder properties, translations, conditions and runtime acceptance remain pending; no ID or option value inferred |
|
||||||
| A / consultation | evaluation_explanation | Conditional referral-assistance explanation only; no claim of in-clinic diagnostic availability |
|
|
||||||
| B (retired) | adult_name | Historical proposal only; not a physical-form requirement |
|
| B (retired) | adult_name | Historical proposal only; not a physical-form requirement |
|
||||||
| B (retired) | adult_email | Historical proposal only |
|
| B (retired) | adult_email | Historical proposal only |
|
||||||
| B (retired) | adult_phone | Historical proposal only |
|
| B (retired) | adult_phone | Historical proposal only |
|
||||||
@@ -187,17 +186,17 @@ Cross-checked against F-001–F-009 in the local CSV, current source and eligibl
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| F-001 | `/contact` | EN | `src/components/ContactForm.astro:17`, via `components/pages/ContactPage.astro:19` | A |
|
| F-001 | `/contact` | EN | `src/components/ContactForm.astro:17`, via `components/pages/ContactPage.astro:19` | A |
|
||||||
| F-002 | `/es/contact` | ES | Same, explicit locale | A ES |
|
| F-002 | `/es/contact` | ES | Same, explicit locale | A ES |
|
||||||
| F-003 | `/client-consultation` | EN | `src/components/FormShell.astro:12`, `layouts/PageLayout.astro:15,82` | Form A / planned `consultation` |
|
| F-003 | `/client-consultation` | EN | `src/components/FormShell.astro:12`, `layouts/PageLayout.astro:15,82` | Form A / `consultation`, isolated-test only |
|
||||||
| F-004 | `/es/client-consultation` | ES | Same, explicit locale | Form A / planned `consultation` |
|
| F-004 | `/es/client-consultation` | ES | Same, explicit locale | Form A / `consultation`, isolated-test only |
|
||||||
| F-005 | `/schedule-consultation` | EN | Same FormShell | Form A / planned `consultation` |
|
| F-005 | `/schedule-consultation` | EN | Same FormShell | Form A / `consultation`, isolated-test only |
|
||||||
| F-006 | `/referrals` | EN | Same FormShell; CTA `#form-title` | C |
|
| F-006 | `/referrals` | EN | Same FormShell; CTA `#form-title` | C |
|
||||||
| F-007 | `/aba-therapy` | EN | `src/components/ServiceInquiryDrawer.astro:71`, `layouts/PageLayout.astro:81` | Form A / planned `consultation`; drawer redesign pending |
|
| F-007 | `/aba-therapy` | EN | `src/components/ServiceInquiryDrawer.astro`, `layouts/PageLayout.astro` | Form A / `consultation`, isolated-test-only explicit drawer |
|
||||||
| F-008 | `/autism-evaluations` | EN | Same drawer | Form A / planned `consultation` only after referral-assistance/copy approval |
|
| F-008 | `/autism-evaluations` | EN | Same drawer | Form A / `consultation`, isolated-test-only explicit drawer |
|
||||||
| F-009 | `/learner-social-club` | EN | Same drawer | Form A / planned `consultation`; program approval pending |
|
| F-009 | `/learner-social-club` | EN | Same drawer | Form A / `consultation`, isolated-test-only explicit drawer |
|
||||||
|
|
||||||
Three implementations, nine instances: seven EN, two ES, zero AR. Eleven GET search instances are not lead forms and must not be disabled or routed to Jotform. No new Arabic form or translated publication is authorized. Spanish service pages currently use Spanish consultation CTAs, not drawers. Arabic visitors retain established English fallbacks and clear language notices. The two physical forms prefer EN/ES capability, but no Spanish referral page or new route is implied.
|
The October 17 inventory remains three implementations and nine original instances: seven EN, two ES, zero AR. The October 29 isolated-test extension adds three explicitly opened Spanish service drawers, without creating a Spanish referral route or changing Arabic fallbacks. Eleven GET search instances are not lead forms and must not be disabled or routed to Jotform.
|
||||||
|
|
||||||
The five imported landing-page journeys—`/lp/aba-therapy`, `/lp/aba-insurance`, `/lp/aba-ahcccs`, `/lp/aba-private-pay` and `/lp/early-intervention`—are owner-approved **Form A** isolated-test placements, each using fixed `formContext=landing&language=en`. This supersedes their earlier Form B mapping. Physical Form B is retired; other service-consultation journeys are future Form A `consultation` targets and are not yet iframe placements. The five panels do not add five native form implementations, and Form A remains unavailable in normal, indexing-enabled and production-shaped artifacts.
|
The five imported landing-page journeys—`/lp/aba-therapy`, `/lp/aba-insurance`, `/lp/aba-ahcccs`, `/lp/aba-private-pay` and `/lp/early-intervention`—are owner-approved **Form A** isolated-test placements, each using fixed `formContext=landing&language=en`. This supersedes their earlier Form B mapping. Physical Form B is retired. The nine documented consultation routes are Form A isolated-test placements only; no production consultation placement is authorized. The five panels do not add five native form implementations, and Form A remains unavailable in normal, indexing-enabled and production-shaped artifacts.
|
||||||
|
|
||||||
Tour's actual CTA currently goes to `/client-consultation`, a future Form A `consultation` destination. Its service list has no tour choice, so the Tour disposition remains an intake/operations decision, **not a completed route change**. Fundraising awaits owner confirmation. Historical event requests remain separate; do not reinterpret the historical Eventbrite link as the account's Event Submission Form without evidence. `/client-forms` retains existing clinical, agreement, Sign and CentralReach links. The six repository-linked Jotform destinations are not automatically matched to the four named account assets.
|
Tour's actual CTA currently goes to `/client-consultation`, a future Form A `consultation` destination. Its service list has no tour choice, so the Tour disposition remains an intake/operations decision, **not a completed route change**. Fundraising awaits owner confirmation. Historical event requests remain separate; do not reinterpret the historical Eventbrite link as the account's Event Submission Form without evidence. `/client-forms` retains existing clinical, agreement, Sign and CentralReach links. The six repository-linked Jotform destinations are not automatically matched to the four named account assets.
|
||||||
|
|
||||||
@@ -229,7 +228,7 @@ Suggested semantic setting: `PUBLIC_AIA_FORMS_MODE`, exact values `off`, `mock`,
|
|||||||
|---|---|
|
|---|---|
|
||||||
| off | No active lead form/empty POST action, live iframe, Jotform preconnect/helper/network request, or analytics. Render approved localized contact fallback; no native-submit bypass on inactive lead UI |
|
| off | No active lead form/empty POST action, live iframe, Jotform preconnect/helper/network request, or analytics. Render approved localized contact fallback; no native-submit bypass on inactive lead UI |
|
||||||
| mock | Synthetic local-only fixture, no external endpoint or real data; safe guarded Playwright use; no Jotform ID required |
|
| mock | Synthetic local-only fixture, no external endpoint or real data; safe guarded Playwright use; no Jotform ID required |
|
||||||
| isolated-test | Form A only: requires `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`, and a fixed seven-route allowlist. It is not a production mode. |
|
| isolated-test | Form A only: requires `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`, and `PUBLIC_ALLOW_INDEXING=false`. The frozen seven-route Form A allowlist is eligible under those gates. The separate nine-route consultation allowlist additionally requires `PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED=true`; it is never production-eligible. |
|
||||||
| live | Form A only: `PUBLIC_AIA_FORMS_MODE=live`, `PUBLIC_AIA_FORMS_LIVE_APPROVED=true`, `PUBLIC_AIA_FORMS_ENVIRONMENT=production`, and `PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true` are all required. The fixed seven-route allowlist, fixed origin/URLs, locale and context still apply. Missing/invalid values fail closed. `PUBLIC_ALLOW_INDEXING` is independent: it must be `true` for an indexing-enabled release but cannot enable Form A alone. Form C and all other lead surfaces remain off. |
|
| live | Form A only: `PUBLIC_AIA_FORMS_MODE=live`, `PUBLIC_AIA_FORMS_LIVE_APPROVED=true`, `PUBLIC_AIA_FORMS_ENVIRONMENT=production`, and `PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true` are all required. The fixed seven-route allowlist, fixed origin/URLs, locale and context still apply. Missing/invalid values fail closed. `PUBLIC_ALLOW_INDEXING` is independent: it must be `true` for an indexing-enabled release but cannot enable Form A alone. Form C and all other lead surfaces remain off. |
|
||||||
|
|
||||||
Off/mock output must not contain **new lead embed IDs/URLs or the historical `250727122848156` reference ID** in HTML, scripts, preload hints or client bundles. Existing clinical/Sign links intentionally remain in the site; they already contain production destinations. Preserving them conflicts with a blanket “no production ID anywhere” assertion, so this contract's new-embed restriction is scoped to the new lead integration. Existing user-initiated clinical navigation is not permission for automatic loading or test submissions. Do not claim the whole site has zero vendor destinations.
|
Off/mock output must not contain **new lead embed IDs/URLs or the historical `250727122848156` reference ID** in HTML, scripts, preload hints or client bundles. Existing clinical/Sign links intentionally remain in the site; they already contain production destinations. Preserving them conflicts with a blanket “no production ID anywhere” assertion, so this contract's new-embed restriction is scoped to the new lead integration. Existing user-initiated clinical navigation is not permission for automatic loading or test submissions. Do not claim the whole site has zero vendor destinations.
|
||||||
@@ -259,7 +258,7 @@ Jotform's [HIPAA email guidance](https://www.jotform.com/help/504-how-to-use-not
|
|||||||
|
|
||||||
## 9. Blank-form creation safety and success measurement
|
## 9. Blank-form creation safety and success measurement
|
||||||
|
|
||||||
No form creation or original mutation is authorized now. A future configuration batch may add Form A `consultation` and create one disabled blank Form C only after explicit Jotform mutation authorization naming the scope, account, reviewers, environment and stopping conditions. The new forms must be configured from the approved minimum field contracts, with PHI protection and generic notification defaults reviewed before any synthetic submission. Do not clone, copy, import or repurpose an existing form, and never copy submissions.
|
No form creation or original mutation is authorized now. The owner reports that Form A `consultation` is configured; its Builder and runtime acceptance remains pending. A future configuration batch may create one disabled blank Form C only after explicit Jotform mutation authorization naming the scope, account, reviewers, environment and stopping conditions. The new form must be configured from the approved minimum field contract, with PHI protection and generic notification defaults reviewed before any synthetic submission. Do not clone, copy, import or repurpose an existing form, and never copy submissions.
|
||||||
|
|
||||||
The historical [cloning guide](https://www.jotform.com/help/27-how-to-clone-an-existing-form-from-your-account/) remains evidence for why the owner rejected inheritance: copied forms can carry email settings, recipients and workflow/thank-you behavior, with account-specific treatment of integrations and protection requiring verification. It is not an implementation path. The old source ID `250727122848156` remains reference-only and must be rejected by any future configuration validator. Account capacity does not establish creation permission.
|
The historical [cloning guide](https://www.jotform.com/help/27-how-to-clone-an-existing-form-from-your-account/) remains evidence for why the owner rejected inheritance: copied forms can carry email settings, recipients and workflow/thank-you behavior, with account-specific treatment of integrations and protection requiring verification. It is not an implementation path. The old source ID `250727122848156` remains reference-only and must be rejected by any future configuration validator. Account capacity does not establish creation permission.
|
||||||
|
|
||||||
@@ -309,3 +308,13 @@ Version 1.5, 2026-10-28: records the owner's successful embedded synthetic submi
|
|||||||
Version 1.6, 2026-10-28: adds the locally tested Form A production-build gate. It requires `live`, the existing live approval, `production` environment role and a new independent Form A production approval; it permits an indexing-enabled build but does not derive authorization from indexing. It leaves normal/off/mock/isolated behavior unchanged, maps no Forms B/C or service drawers, and does not deploy or change Jotform.
|
Version 1.6, 2026-10-28: adds the locally tested Form A production-build gate. It requires `live`, the existing live approval, `production` environment role and a new independent Form A production approval; it permits an indexing-enabled build but does not derive authorization from indexing. It leaves normal/off/mock/isolated behavior unchanged, maps no Forms B/C or service drawers, and does not deploy or change Jotform.
|
||||||
|
|
||||||
Version 1.7, 2026-10-28: records the owner-approved two-physical-form direction. Form B is retired as a physical Jotform form; Form A gains a planned, unconfigured `consultation` context and Form C remains a separate future referral form. The seven current Form A placements and all gates remain unchanged. No Builder, route, iframe, account or deployment change occurred.
|
Version 1.7, 2026-10-28: records the owner-approved two-physical-form direction. Form B is retired as a physical Jotform form; Form A gains a planned, unconfigured `consultation` context and Form C remains a separate future referral form. The seven current Form A placements and all gates remain unchanged. No Builder, route, iframe, account or deployment change occurred.
|
||||||
|
|
||||||
|
### Version 1.8, 2026-10-29 — consultation isolated-test adapter
|
||||||
|
|
||||||
|
The owner reports that Form A's fixed `formContext=consultation` presentation and its required service-interest dropdown have been configured in Jotform. The repository did not inspect the Builder or submit a form. Exact visibility, requiredness, English/Spanish translations, conditional behavior and submission/confirmation behavior remain owner-acceptance items.
|
||||||
|
|
||||||
|
Astro now exposes that context only through a separate non-indexing isolated-test allowlist. The three standalone placements are `/client-consultation` (EN), `/es/client-consultation` (ES), and `/schedule-consultation` (EN). The six service-drawer placements are `/aba-therapy`, `/autism-evaluations`, `/learner-social-club`, `/es/aba-therapy`, `/es/autism-evaluations`, and `/es/learner-social-club`. The standalone pages use fixed, localized consultation URLs. The drawer pages hold the same fixed URL without an iframe `src` until an explicit visitor action. All nine require `PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED=true` in addition to the existing isolated-test gates. They are excluded from off, mock, normal staging, indexing-enabled and production builds.
|
||||||
|
|
||||||
|
This change leaves the existing seven-route Form A production allowlist unchanged. It does not authorize a production consultation route, activate Form C, add a service-specific preselection, add child/clinical/payment/booking collection, add a Jotform helper, modify Jotform, or add analytics, advertising, Contabo forwarding, tracking or a conversion signal.
|
||||||
|
|
||||||
|
The earlier requirement to show a conditional Autism Diagnostic Evaluation explanation is superseded. No diagnostic-evaluation explanatory note is to be added in Jotform or Astro. An AIA client advocate will discuss available options and coordination with qualified diagnostic providers during the subsequent conversation. This does not imply current in-clinic diagnostic availability, a diagnosis, or a completed diagnostic-provider hire.
|
||||||
@@ -17,6 +17,6 @@ OA-06,Marketing,Deferred attribution categories and redesigned successful-submis
|
|||||||
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-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
|
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-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, evaluation explanation, conditional email/phone behavior, EN/ES translations and no additional collection",Account + operations + clinical + privacy owners,"Sanitized Builder configuration and approved synthetic test evidence; no ID/condition inferred by repository",Pending/Unverified,,Separate mutation and synthetic-test authorization required
|
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
|
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
|
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,9 +1,9 @@
|
|||||||
# Phase 9 — Two-form integration specification
|
# Phase 9 — Two-form integration specification
|
||||||
|
|
||||||
**Recorded:** 2026-10-28 UTC
|
**Recorded:** 2026-10-28 UTC; revised 2026-10-29 UTC
|
||||||
|
|
||||||
**Scope:** future Form A consultation configuration and future Form C referral initiation
|
**Scope:** Form A consultation isolated-test integration and future Form C referral initiation
|
||||||
**Status:** owner direction documented; no Jotform or Astro route activation authorized
|
**Status:** owner-reported Jotform consultation configuration; Astro routes limited to isolated testing; no production activation authorized
|
||||||
|
|
||||||
## Decision revision
|
## Decision revision
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ This revision does not enable a new route, add an iframe placement, modify Form
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `contact` | Existing, owner-tested | General contact enquiries | Current two-contact-route gate only |
|
| `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 |
|
| `landing` | Existing, owner-tested | Five named campaign landing journeys | Current five-landing-route gate only |
|
||||||
| `consultation` | Planned; not configured or emitted | Family service consultations | Builder configuration, synthetic acceptance and a separately approved Astro allowlist |
|
| `consultation` | Configured in Jotform; isolated-test Astro placements only | Family service consultations | Owner browser acceptance; no production route authorization |
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -42,11 +42,7 @@ The consultation context reuses the current Form A adult contact and preferred c
|
|||||||
|
|
||||||
The values in the right column are planning labels, not Jotform option values or IDs. Operations and the Jotform owner must approve the exact Builder labels, translations, requiredness and conditions.
|
The values in the right column are planning labels, not Jotform option values or IDs. Operations and the Jotform owner must approve the exact Builder labels, translations, requiredness and conditions.
|
||||||
|
|
||||||
When **Autism Diagnostic Evaluation** is selected, show this owner-approved explanation:
|
Do not add a conditional explanatory note when **Autism Diagnostic Evaluation** is selected. An AIA client advocate will explain available options and coordination with qualified diagnostic providers during the subsequent conversation. The consultation form must not imply that AIA currently provides an in-clinic diagnostic evaluation, offers a diagnosis, or has completed a diagnostic-professional hire.
|
||||||
|
|
||||||
> AIA can help families connect with qualified providers who offer autism diagnostic evaluations. Diagnostic evaluations are not currently available at our clinic.
|
|
||||||
|
|
||||||
This is referral-assistance language. It does not state or imply that AIA currently provides an in-clinic diagnostic evaluation, offers a diagnosis, or has completed a diagnostic-professional hire.
|
|
||||||
|
|
||||||
Do not add child name, date of birth, age, diagnosis, medical history, records, insurance member ID, payment, location or booking questions to the initial consultation context. Do not make any additional consultation field required without a separately documented operations, clinical and privacy decision. Whether an existing optional free-text message remains visible requires the existing Form A privacy treatment; it is not an additional required consultation field under this specification.
|
Do not add child name, date of birth, age, diagnosis, medical history, records, insurance member ID, payment, location or booking questions to the initial consultation context. Do not make any additional consultation field required without a separately documented operations, clinical and privacy decision. Whether an existing optional free-text message remains visible requires the existing Form A privacy treatment; it is not an additional required consultation field under this specification.
|
||||||
|
|
||||||
@@ -64,18 +60,21 @@ 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.
|
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.
|
||||||
|
|
||||||
## Future route and component matrix
|
## Isolated-test route and component matrix
|
||||||
|
|
||||||
The following is a future integration plan. Every listed consultation surface continues to use its current inactive/off/mock behavior until Form A's consultation configuration is approved, synthetically tested and added through a separately authorized code change.
|
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.
|
||||||
|
|
||||||
| Route or entry point | Locale | Current component / behavior | Future physical form and context | Dependencies |
|
| Route or entry point | Locale | Current component / behavior | Future physical form and context | Dependencies |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `/client-consultation` | EN | `FormShell.astro` through `PageLayout.astro`; inactive logical `service` form | Form A / `consultation` | Builder fields, EN copy, new allowlist and synthetic acceptance |
|
| `/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; inactive logical `service` form | Form A / `consultation` | Spanish Form A labels, errors, evaluation explanation and locale test |
|
| `/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`; inactive logical `service` form | Form A / `consultation` | Builder configuration, no-booking copy review and 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`; inactive legacy child-data drawer | Form A / `consultation` | Separate drawer/copy redesign, clinical approval and dialog accessibility test |
|
| `/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 drawer; inactive legacy evaluation variant | Form A / `consultation` | Required referral-assistance wording and clinical approval |
|
| `/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 drawer; inactive legacy Social Club variant | Form A / `consultation` | Program-eligibility/copy approval and dialog accessibility test |
|
| `/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 |
|
||||||
| `/referrals` | EN | `FormShell.astro`, `logicalForm=referral`; inactive | Form C / future referral initiation | Create and verify Form C; no patient data in initial form |
|
| `/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 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.
|
||||||
@@ -84,14 +83,20 @@ The current Tour CTA in `src/content/pages/en/tour.mdx` points to `/client-consu
|
|||||||
|
|
||||||
## Owner and Jotform actions before consultation activation
|
## Owner and Jotform actions before consultation activation
|
||||||
|
|
||||||
1. Confirm that extending the active Form A with a `consultation` context will not change the approved `contact` or `landing` journeys; record the rollback method for the Builder configuration.
|
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. Configure the exact hidden context value, service question, option labels, requiredness, conditional evaluation explanation, EN/ES translations, validation messages and confirmation language in Jotform. Do not invent fields or accept query-controlled context values.
|
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.
|
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, the evaluation explanation and the approved service options. Obtain fluent English/Spanish review for all added presentation and error text.
|
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.
|
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 new fixed Astro allowlist and production-gate update. The current seven Form A routes remain the only permitted real iframes until that implementation is independently reviewed.
|
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.
|
||||||
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`.
|
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
|
## 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.
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -1,20 +1,41 @@
|
|||||||
---
|
---
|
||||||
import Button from './Button.astro';
|
import Button from './Button.astro';
|
||||||
import ContactObfuscation from './ContactObfuscation.astro';
|
import ContactObfuscation from './ContactObfuscation.astro';
|
||||||
|
import JotformIframe from './JotformIframe.astro';
|
||||||
import { formMessages } from '../utils/form-messages';
|
import { formMessages } from '../utils/form-messages';
|
||||||
import { formModeForRuntime, resolveFormMode, resolveMockFormResult, type LogicalForm } from '../utils/form-integration';
|
import { formModeForRuntime, resolveFormAIframe, resolveFormMode, resolveMockFormResult, type LogicalForm } from '../utils/form-integration';
|
||||||
const { lang = 'en', showBackground = false } = Astro.props;
|
const { lang = 'en', showBackground = false, route = '' } = Astro.props;
|
||||||
const text = formMessages(lang);
|
const text = formMessages(lang);
|
||||||
const title = Astro.props.title ?? text.heading;
|
const title = Astro.props.title ?? text.heading;
|
||||||
const logicalForm: LogicalForm = Astro.props.logicalForm ?? (Astro.props.title === 'Physician & Insurer Referrals' ? 'referral' : 'service');
|
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, import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED);
|
||||||
const mode = formModeForRuntime(config);
|
const mode = formModeForRuntime(config);
|
||||||
const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_RESULT);
|
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,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
const consultationIframeEnabled = formAIframe?.enabled && formAIframe.context === 'consultation';
|
||||||
|
const consultationHeading = lang === 'es' ? 'Comience una conversación' : 'Start a conversation';
|
||||||
|
const consultationDescription = lang === 'es'
|
||||||
|
? 'Use este formulario para solicitar una conversación. Un defensor de clientes de AIA puede explicarle los próximos pasos.'
|
||||||
|
: 'Use this form to request a conversation. An AIA client advocate can explain the next steps.';
|
||||||
---
|
---
|
||||||
<section class:list={['form-section', { 'form-section-background': showBackground }]} aria-labelledby="form-title">
|
<section class:list={['form-section', { 'form-section-background': showBackground }]} aria-labelledby="form-title">
|
||||||
<div class="container form-grid">
|
<div class="container form-grid">
|
||||||
<div><p class="eyebrow">{text.eyebrow}</p><h2 id="form-title">{title}</h2><p>{text.intro}</p></div>
|
<div><p class="eyebrow">{text.eyebrow}</p><h2 id="form-title">{title}</h2><p>{text.intro}</p></div>
|
||||||
<div class="form" role="group" data-aia-form data-form-mode={mode} data-form-mode-reason={config.reason} data-logical-form={logicalForm} data-mock-result={mockResult} data-mock-success={text.mockSuccess} data-mock-failed={text.mockFailed} data-mock-unavailable={text.mockUnavailable}>
|
{formAIframe && consultationIframeEnabled
|
||||||
|
? <JotformIframe config={formAIframe} heading={consultationHeading} description={consultationDescription} />
|
||||||
|
: <div class="form" role="group" data-aia-form data-form-mode={mode} data-form-mode-reason={config.reason} data-logical-form={logicalForm} data-mock-result={mockResult} data-mock-success={text.mockSuccess} data-mock-failed={text.mockFailed} data-mock-unavailable={text.mockUnavailable}>
|
||||||
<label>{text.name}<input name="name" autocomplete="name" disabled={mode === 'off'} /></label>
|
<label>{text.name}<input name="name" autocomplete="name" disabled={mode === 'off'} /></label>
|
||||||
<label>{text.email}<input type="email" name="email" autocomplete="email" disabled={mode === 'off'} /></label>
|
<label>{text.email}<input type="email" name="email" autocomplete="email" disabled={mode === 'off'} /></label>
|
||||||
<label>{text.phone}<input type="tel" name="phone" autocomplete="tel" disabled={mode === 'off'} /></label>
|
<label>{text.phone}<input type="tel" name="phone" autocomplete="tel" disabled={mode === 'off'} /></label>
|
||||||
@@ -22,7 +43,7 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
<label>{text.message}<textarea name="message" rows="5" disabled={mode === 'off'}></textarea></label>
|
<label>{text.message}<textarea name="message" rows="5" disabled={mode === 'off'}></textarea></label>
|
||||||
<Button type="button" data-mock-submit disabled={mode === 'off'} aria-describedby="form-note">{text.submit}</Button>
|
<Button type="button" data-mock-submit disabled={mode === 'off'} aria-describedby="form-note">{text.submit}</Button>
|
||||||
<p id="form-note" class="form-note" data-form-status>{mode === 'mock' ? text.mock : <>{text.disabled} <ContactObfuscation phone="(480) 687-7099" hrefValue="+14806877099" obfuscatedText="9907-786 (084)" />.</>}</p>
|
<p id="form-note" class="form-note" data-form-status>{mode === 'mock' ? text.mock : <>{text.disabled} <ContactObfuscation phone="(480) 687-7099" hrefValue="+14806877099" obfuscatedText="9907-786 (084)" />.</>}</p>
|
||||||
</div>
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -5,32 +5,40 @@ interface Props {
|
|||||||
config: FormAIframeConfig;
|
config: FormAIframeConfig;
|
||||||
heading: string;
|
heading: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
variant?: 'standalone' | 'embedded';
|
variant?: 'standalone' | 'embedded' | 'drawer';
|
||||||
|
deferUntilActivated?: boolean;
|
||||||
|
hideHeading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { config, heading, description, variant = 'standalone' } = Astro.props;
|
const { config, heading, description, variant = 'standalone', deferUntilActivated = false, hideHeading = false } = Astro.props;
|
||||||
|
|
||||||
if (!config.enabled || !config.url || !config.title || !config.fallbackText || !config.fallbackLinkText) {
|
if (!config.enabled || !config.url || !config.title || !config.fallbackText || !config.fallbackLinkText) {
|
||||||
throw new Error('JotformIframe requires an enabled, fully validated Form A iframe configuration.');
|
throw new Error('JotformIframe requires an enabled, fully validated Form A iframe configuration.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const idBase = `jotform-form-a-${config.route.replaceAll('/', '-').replaceAll(/^-+|-+$/g, '')}-${config.context}`;
|
||||||
|
const headingId = `${idBase}-heading`;
|
||||||
---
|
---
|
||||||
|
|
||||||
<section
|
<section
|
||||||
class:list={['jotform-iframe', { 'jotform-iframe--embedded': variant === 'embedded' }]}
|
class:list={['jotform-iframe', { 'jotform-iframe--embedded': variant === 'embedded', 'jotform-iframe--drawer': variant === 'drawer' }]}
|
||||||
role="group"
|
role="group"
|
||||||
aria-labelledby="jotform-iframe-heading"
|
aria-labelledby={hideHeading ? undefined : headingId}
|
||||||
|
aria-label={hideHeading ? config.title : undefined}
|
||||||
data-jotform-form-a
|
data-jotform-form-a
|
||||||
data-form-mode={config.mode}
|
data-form-mode={config.mode}
|
||||||
data-form-environment={config.environmentRole}
|
data-form-environment={config.environmentRole}
|
||||||
data-logical-form="general"
|
data-logical-form={config.context === 'consultation' ? 'service' : 'general'}
|
||||||
data-form-context={config.context}
|
data-form-context={config.context}
|
||||||
data-jotform-source="approved-form-a"
|
data-jotform-source="approved-form-a"
|
||||||
|
data-jotform-deferred={deferUntilActivated ? 'true' : undefined}
|
||||||
>
|
>
|
||||||
<h3 id="jotform-iframe-heading" class="jotform-iframe__heading">{heading}</h3>
|
{!hideHeading && <h3 id={headingId} class="jotform-iframe__heading">{heading}</h3>}
|
||||||
{description && <p class="jotform-iframe__description">{description}</p>}
|
{description && <p class="jotform-iframe__description">{description}</p>}
|
||||||
<iframe
|
<iframe
|
||||||
class="jotform-iframe__frame"
|
class="jotform-iframe__frame"
|
||||||
src={config.url}
|
src={deferUntilActivated ? undefined : config.url}
|
||||||
|
data-jotform-iframe-url={deferUntilActivated ? config.url : undefined}
|
||||||
title={config.title}
|
title={config.title}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
@@ -65,6 +73,7 @@ if (!config.enabled || !config.url || !config.title || !config.fallbackText || !
|
|||||||
fallback.hidden = true;
|
fallback.hidden = true;
|
||||||
|
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
|
let activated = false;
|
||||||
const hideFallback = () => {
|
const hideFallback = () => {
|
||||||
fallback.hidden = true;
|
fallback.hidden = true;
|
||||||
container.dataset.jotformFallback = 'hidden';
|
container.dataset.jotformFallback = 'hidden';
|
||||||
@@ -74,20 +83,37 @@ if (!config.enabled || !config.url || !config.title || !config.fallbackText || !
|
|||||||
fallback.hidden = false;
|
fallback.hidden = false;
|
||||||
container.dataset.jotformFallback = 'visible';
|
container.dataset.jotformFallback = 'visible';
|
||||||
};
|
};
|
||||||
const timer = window.setTimeout(showFallback, fallbackTimeoutMs);
|
let timer;
|
||||||
const onLoad = () => {
|
const onLoad = () => {
|
||||||
window.clearTimeout(timer);
|
if (timer) window.clearTimeout(timer);
|
||||||
// A late frame load supersedes the transport timeout. It is still not
|
// A late frame load supersedes the transport timeout. It is still not
|
||||||
// treated as a success, conversion, or cross-origin health signal.
|
// treated as a success, conversion, or cross-origin health signal.
|
||||||
hideFallback();
|
hideFallback();
|
||||||
};
|
};
|
||||||
const dispose = () => {
|
const dispose = () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
window.clearTimeout(timer);
|
if (timer) window.clearTimeout(timer);
|
||||||
frame.removeEventListener('load', onLoad);
|
frame.removeEventListener('load', onLoad);
|
||||||
|
container.removeEventListener('aia:jotform-activate', activate);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const activate = () => {
|
||||||
|
if (disposed || activated) return;
|
||||||
|
const deferred = container.dataset.jotformDeferred === 'true';
|
||||||
|
const approvedUrl = frame.dataset.jotformIframeUrl;
|
||||||
|
if (deferred && !approvedUrl) return;
|
||||||
|
activated = true;
|
||||||
frame.addEventListener('load', onLoad, { once: true });
|
frame.addEventListener('load', onLoad, { once: true });
|
||||||
|
timer = window.setTimeout(showFallback, fallbackTimeoutMs);
|
||||||
|
if (deferred) frame.src = approvedUrl;
|
||||||
|
container.dataset.jotformIframeActivated = 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (container.dataset.jotformDeferred === 'true') {
|
||||||
|
container.addEventListener('aia:jotform-activate', activate, { once: true });
|
||||||
|
} else {
|
||||||
|
activate();
|
||||||
|
}
|
||||||
window.addEventListener('pagehide', dispose, { once: true });
|
window.addEventListener('pagehide', dispose, { once: true });
|
||||||
document.addEventListener('astro:before-swap', dispose, { once: true });
|
document.addEventListener('astro:before-swap', dispose, { once: true });
|
||||||
};
|
};
|
||||||
@@ -184,6 +210,22 @@ if (!config.enabled || !config.url || !config.title || !config.fallbackText || !
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jotform-iframe--drawer {
|
||||||
|
--jotform-frame-height: 72rem;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
overflow: visible;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jotform-iframe--drawer .jotform-iframe__heading {
|
||||||
|
border: 0;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.jotform-iframe__frame {
|
.jotform-iframe__frame {
|
||||||
height: 68rem;
|
height: 68rem;
|
||||||
@@ -194,5 +236,10 @@ if (!config.enabled || !config.url || !config.title || !config.fallbackText || !
|
|||||||
height: 50rem;
|
height: 50rem;
|
||||||
min-height: 50rem;
|
min-height: 50rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jotform-iframe--drawer .jotform-iframe__frame {
|
||||||
|
height: 82rem;
|
||||||
|
min-height: 82rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,89 +1,194 @@
|
|||||||
---
|
---
|
||||||
import ContactObfuscation from './ContactObfuscation.astro';
|
import ContactObfuscation from './ContactObfuscation.astro';
|
||||||
import { formModeForRuntime, resolveFormMode, resolveMockFormResult } from '../utils/form-integration';
|
import JotformIframe from './JotformIframe.astro';
|
||||||
|
import {
|
||||||
|
formModeForRuntime,
|
||||||
|
resolveFormAIframe,
|
||||||
|
resolveFormMode,
|
||||||
|
resolveMockFormResult,
|
||||||
|
} from '../utils/form-integration';
|
||||||
|
|
||||||
type ServiceKey = 'aba-therapy' | 'autism-evaluations' | 'learner-social-club';
|
type ServiceKey = 'aba-therapy' | 'autism-evaluations' | 'learner-social-club';
|
||||||
|
type SupportedLocale = 'en' | 'es';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
service: ServiceKey;
|
service: ServiceKey;
|
||||||
|
lang: SupportedLocale;
|
||||||
|
route: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceCopy: Record<ServiceKey, {
|
const serviceNames: Record<SupportedLocale, Record<ServiceKey, string>> = {
|
||||||
heading: string[];
|
en: {
|
||||||
serviceName: string;
|
'aba-therapy': 'ABA Therapy',
|
||||||
statusLead: string;
|
'autism-evaluations': 'Autism Evaluations',
|
||||||
status: string;
|
'learner-social-club': 'Learner Social Club',
|
||||||
prompt: string;
|
|
||||||
}> = {
|
|
||||||
'aba-therapy': {
|
|
||||||
heading: ['No WAITLIST!'],
|
|
||||||
serviceName: 'ABA Therapy',
|
|
||||||
statusLead: 'is',
|
|
||||||
status: 'currently accepting new patients',
|
|
||||||
prompt: "To help guarantee your child's admission to our program, please provide their information."
|
|
||||||
},
|
},
|
||||||
'autism-evaluations': {
|
es: {
|
||||||
heading: ['Enrollment', 'Now Open'],
|
'aba-therapy': 'Terapia ABA',
|
||||||
serviceName: 'Autism Evaluations',
|
'autism-evaluations': 'Evaluaciones de autismo',
|
||||||
statusLead: 'are',
|
'learner-social-club': 'Learner Social Club',
|
||||||
status: 'now open for enrollment',
|
|
||||||
prompt: "To help guarantee your child's admission into one of our upcoming programs, please provide their information."
|
|
||||||
},
|
},
|
||||||
'learner-social-club': {
|
|
||||||
heading: ['Enrollment', 'Now Open'],
|
|
||||||
serviceName: 'Learner Social Club',
|
|
||||||
statusLead: 'is',
|
|
||||||
status: 'now open for enrollment',
|
|
||||||
prompt: "To help guarantee your child's spot in one of our upcoming group enrichment programs, please provide their information."
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { service } = Astro.props;
|
const drawerCopy: Record<SupportedLocale, {
|
||||||
const copy = serviceCopy[service];
|
open: (service: string) => string;
|
||||||
const drawerId = `service-inquiry-${service}`;
|
heading: (service: string) => string;
|
||||||
|
introduction: string;
|
||||||
|
privacy: string;
|
||||||
|
close: string;
|
||||||
|
noScript: string;
|
||||||
|
hostedLink: string;
|
||||||
|
legacyStatus: string;
|
||||||
|
legacyPrompt: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
childFirstName: string;
|
||||||
|
childLastName: string;
|
||||||
|
childBirthDate: string;
|
||||||
|
location: string;
|
||||||
|
service: string;
|
||||||
|
payment: string;
|
||||||
|
referral: string;
|
||||||
|
comments: string;
|
||||||
|
select: string;
|
||||||
|
submit: string;
|
||||||
|
disabled: string;
|
||||||
|
mock: string;
|
||||||
|
}> = {
|
||||||
|
en: {
|
||||||
|
open: (service) => `Talk with AIA about ${service}`,
|
||||||
|
heading: (service) => `Talk with AIA about ${service}`,
|
||||||
|
introduction: 'Use this form to request a conversation. An AIA client advocate can explain the next steps.',
|
||||||
|
privacy: 'Please use this form only for general contact information. AIA can discuss any needed details during follow-up.',
|
||||||
|
close: 'Close consultation form',
|
||||||
|
noScript: 'To request a conversation,',
|
||||||
|
hostedLink: 'open the consultation form in a new tab.',
|
||||||
|
legacyStatus: 'An AIA client advocate can discuss this service and possible next steps.',
|
||||||
|
legacyPrompt: 'Online submission is not yet connected. Please call AIA to begin a conversation.',
|
||||||
|
firstName: 'First name',
|
||||||
|
lastName: 'Last name',
|
||||||
|
phone: 'Phone number',
|
||||||
|
email: 'Email',
|
||||||
|
childFirstName: 'Child first name',
|
||||||
|
childLastName: 'Child last name',
|
||||||
|
childBirthDate: 'Child birth date',
|
||||||
|
location: 'Location interest',
|
||||||
|
service: 'Service interest',
|
||||||
|
payment: 'Insurance and payment options',
|
||||||
|
referral: 'How did you hear about us?',
|
||||||
|
comments: 'Additional information or comments',
|
||||||
|
select: 'Please select',
|
||||||
|
submit: 'Submit',
|
||||||
|
disabled: 'Online submission is not yet connected. Please call',
|
||||||
|
mock: 'Demo only: this local fixture never sends information.',
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
open: (service) => `Hable con AIA sobre ${service}`,
|
||||||
|
heading: (service) => `Hable con AIA sobre ${service}`,
|
||||||
|
introduction: 'Use este formulario para solicitar una conversación. Un defensor de clientes de AIA puede explicarle los próximos pasos.',
|
||||||
|
privacy: 'Use este formulario solo para información general de contacto. AIA puede hablar sobre los detalles necesarios durante el seguimiento.',
|
||||||
|
close: 'Cerrar el formulario de consulta',
|
||||||
|
noScript: 'Para solicitar una conversación,',
|
||||||
|
hostedLink: 'abra el formulario de consulta en una pestaña nueva.',
|
||||||
|
legacyStatus: 'Un defensor de clientes de AIA puede hablar sobre este servicio y los próximos pasos posibles.',
|
||||||
|
legacyPrompt: 'El envío en línea aún no está habilitado. Llame a AIA para iniciar una conversación.',
|
||||||
|
firstName: 'Nombre',
|
||||||
|
lastName: 'Apellido',
|
||||||
|
phone: 'Número de teléfono',
|
||||||
|
email: 'Correo electrónico',
|
||||||
|
childFirstName: 'Nombre del niño o la niña',
|
||||||
|
childLastName: 'Apellido del niño o la niña',
|
||||||
|
childBirthDate: 'Fecha de nacimiento del niño o la niña',
|
||||||
|
location: 'Ubicación de interés',
|
||||||
|
service: 'Servicio de interés',
|
||||||
|
payment: 'Opciones de seguro y pago',
|
||||||
|
referral: '¿Cómo se enteró de nosotros?',
|
||||||
|
comments: 'Información adicional o comentarios',
|
||||||
|
select: 'Seleccione una opción',
|
||||||
|
submit: 'Enviar',
|
||||||
|
disabled: 'El envío en línea aún no está habilitado. Llame al',
|
||||||
|
mock: 'Solo demostración: esta prueba local nunca envía información.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { service, lang, route } = Astro.props;
|
||||||
|
const copy = drawerCopy[lang];
|
||||||
|
const serviceName = serviceNames[lang][service];
|
||||||
|
const drawerId = `service-inquiry-${lang}-${service}`;
|
||||||
const titleId = `${drawerId}-title`;
|
const titleId = `${drawerId}-title`;
|
||||||
const noteId = `${drawerId}-note`;
|
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, import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED);
|
||||||
const mode = formModeForRuntime(config);
|
const mode = formModeForRuntime(config);
|
||||||
const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_RESULT);
|
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';
|
||||||
---
|
---
|
||||||
|
|
||||||
|
{consultationTestEnabled && formAIframe.url && <>
|
||||||
|
<button
|
||||||
|
class="service-inquiry-drawer__open"
|
||||||
|
type="button"
|
||||||
|
data-service-inquiry-open
|
||||||
|
aria-controls={drawerId}
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
>
|
||||||
|
{copy.open(serviceName)}
|
||||||
|
</button>
|
||||||
|
<noscript>
|
||||||
|
<p class="service-inquiry-drawer__noscript">
|
||||||
|
{copy.noScript} <a href={formAIframe.url} target="_blank" rel="noopener noreferrer">{copy.hostedLink}</a>
|
||||||
|
</p>
|
||||||
|
</noscript>
|
||||||
|
</>}
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
|
id={drawerId}
|
||||||
class="service-inquiry-drawer"
|
class="service-inquiry-drawer"
|
||||||
data-service-inquiry-drawer
|
data-service-inquiry-drawer
|
||||||
data-dismiss-key={`aia-service-inquiry-dismissed:${service}`}
|
data-explicit-activation={consultationTestEnabled ? 'true' : undefined}
|
||||||
|
data-dismiss-key={consultationTestEnabled ? undefined : `aia-service-inquiry-dismissed:${lang}:${service}`}
|
||||||
aria-labelledby={titleId}
|
aria-labelledby={titleId}
|
||||||
>
|
>
|
||||||
<button class="service-inquiry-drawer__close" type="button" data-service-inquiry-close aria-label="Close inquiry form">
|
<button class="service-inquiry-drawer__close" type="button" data-service-inquiry-close aria-label={copy.close}>
|
||||||
<svg aria-hidden="true" viewBox="0 0 32 32" width="18" height="18">
|
<svg aria-hidden="true" viewBox="0 0 32 32" width="18" height="18">
|
||||||
<path d="M4 4 28 28M28 4 4 28" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="3" />
|
<path d="M4 4 28 28M28 4 4 28" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="3" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<header class="service-inquiry-drawer__intro">
|
<header class="service-inquiry-drawer__intro">
|
||||||
<h2 id={titleId} data-service-inquiry-title tabindex="-1">
|
<h2 id={titleId} data-service-inquiry-title tabindex="-1">{consultationTestEnabled ? copy.heading(serviceName) : serviceName}</h2>
|
||||||
{copy.heading.map((line, index) => <Fragment>{index > 0 && <br />}{line}</Fragment>)}
|
<p>{consultationTestEnabled ? copy.introduction : copy.legacyStatus}</p>
|
||||||
</h2>
|
{consultationTestEnabled && <p class="service-inquiry-drawer__privacy">{copy.privacy}</p>}
|
||||||
<p>
|
|
||||||
Arizona Institute for Autism's<br />
|
|
||||||
<strong>{copy.serviceName}</strong><br />
|
|
||||||
{copy.statusLead} <em>{copy.status}</em>.
|
|
||||||
</p>
|
|
||||||
<p>{copy.prompt}</p>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="service-inquiry-form" role="group" data-aia-form data-form-mode={mode} data-form-mode-reason={config.reason} data-logical-form="service" data-mock-result={mockResult} data-mock-success="Demo only: the local fixture completed without sending anything." data-mock-failed="Demo only: the local fixture simulated a failed submission." data-mock-unavailable="Demo only: the local fixture simulated an unavailable service.">
|
{consultationTestEnabled
|
||||||
|
? <div class="service-inquiry-drawer__iframe">
|
||||||
|
<JotformIframe config={formAIframe} heading={copy.heading(serviceName)} variant="drawer" deferUntilActivated hideHeading />
|
||||||
|
</div>
|
||||||
|
: <div class="service-inquiry-form" role="group" data-aia-form data-form-mode={mode} data-form-mode-reason={config.reason} data-logical-form="service" data-mock-result={mockResult} data-mock-success="Demo only: the local fixture completed without sending anything." data-mock-failed="Demo only: the local fixture simulated a failed submission." data-mock-unavailable="Demo only: the local fixture simulated an unavailable service.">
|
||||||
<div class="service-inquiry-form__grid">
|
<div class="service-inquiry-form__grid">
|
||||||
<label>
|
<label>
|
||||||
<span>First Name<span class="required-mark" aria-hidden="true">*</span></span>
|
<span>{copy.firstName}<span class="required-mark" aria-hidden="true">*</span></span>
|
||||||
<input name="first_name" autocomplete="given-name" required disabled={mode === 'off'} />
|
<input name="first_name" autocomplete="given-name" required disabled={mode === 'off'} />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Last Name<span class="required-mark" aria-hidden="true">*</span></span>
|
<span>{copy.lastName}<span class="required-mark" aria-hidden="true">*</span></span>
|
||||||
<input name="last_name" autocomplete="family-name" required disabled={mode === 'off'} />
|
<input name="last_name" autocomplete="family-name" required disabled={mode === 'off'} />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Phone Number<span class="required-mark" aria-hidden="true">*</span></span>
|
<span>{copy.phone}<span class="required-mark" aria-hidden="true">*</span></span>
|
||||||
<span class="service-inquiry-form__phone">
|
<span class="service-inquiry-form__phone">
|
||||||
<span class="service-inquiry-form__country">
|
<span class="service-inquiry-form__country">
|
||||||
<span aria-hidden="true">🇺🇸</span>
|
<span aria-hidden="true">🇺🇸</span>
|
||||||
@@ -97,34 +202,34 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Email<span class="required-mark" aria-hidden="true">*</span></span>
|
<span>{copy.email}<span class="required-mark" aria-hidden="true">*</span></span>
|
||||||
<input type="email" name="email" autocomplete="email" required disabled={mode === 'off'} />
|
<input type="email" name="email" autocomplete="email" required disabled={mode === 'off'} />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Child First Name</span>
|
<span>{copy.childFirstName}</span>
|
||||||
<input name="child_first_name" autocomplete="off" disabled={mode === 'off'} />
|
<input name="child_first_name" autocomplete="off" disabled={mode === 'off'} />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Child Last Name</span>
|
<span>{copy.childLastName}</span>
|
||||||
<input name="child_last_name" autocomplete="off" disabled={mode === 'off'} />
|
<input name="child_last_name" autocomplete="off" disabled={mode === 'off'} />
|
||||||
</label>
|
</label>
|
||||||
<label class="service-inquiry-form__wide">
|
<label class="service-inquiry-form__wide">
|
||||||
<span>Child Birth Date</span>
|
<span>{copy.childBirthDate}</span>
|
||||||
<input name="child_birth_date" inputmode="numeric" autocomplete="bday" placeholder="MM - DD - YYYY" disabled={mode === 'off'} />
|
<input name="child_birth_date" inputmode="numeric" autocomplete="bday" placeholder="MM - DD - YYYY" disabled={mode === 'off'} />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Location Interest</span>
|
<span>{copy.location}</span>
|
||||||
<select name="location_interest" disabled={mode === 'off'}>
|
<select name="location_interest" disabled={mode === 'off'}>
|
||||||
<option value="" selected>Please Select</option>
|
<option value="" selected>{copy.select}</option>
|
||||||
<option value="aia_scottsdale">AIA Scottsdale</option>
|
<option value="aia_scottsdale">AIA Scottsdale</option>
|
||||||
<option value="aia_glendale">AIA Glendale</option>
|
<option value="aia_glendale">AIA Glendale</option>
|
||||||
<option value="other_none">Other/None</option>
|
<option value="other_none">Other/None</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Service Interest</span>
|
<span>{copy.service}</span>
|
||||||
<select name="service_interest" disabled={mode === 'off'}>
|
<select name="service_interest" disabled={mode === 'off'}>
|
||||||
<option value="" selected>Please Select</option>
|
<option value="" selected>{copy.select}</option>
|
||||||
<option value="behavioral">Behavioral - ABA Therapy Service</option>
|
<option value="behavioral">Behavioral - ABA Therapy Service</option>
|
||||||
<option value="psychological">Psychological - Autism Evaluation Service</option>
|
<option value="psychological">Psychological - Autism Evaluation Service</option>
|
||||||
<option value="sociological">Sociological - Learners Social Club Service</option>
|
<option value="sociological">Sociological - Learners Social Club Service</option>
|
||||||
@@ -132,9 +237,9 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Insurance & Payment Options</span>
|
<span>{copy.payment}</span>
|
||||||
<select name="payment_options" disabled={mode === 'off'}>
|
<select name="payment_options" disabled={mode === 'off'}>
|
||||||
<option value="" selected>Please select</option>
|
<option value="" selected>{copy.select}</option>
|
||||||
<option>United Healthcare AHCCCS</option>
|
<option>United Healthcare AHCCCS</option>
|
||||||
<option>BCBS AZ</option>
|
<option>BCBS AZ</option>
|
||||||
<option>United Healthcare</option>
|
<option>United Healthcare</option>
|
||||||
@@ -146,9 +251,9 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>How did you hear about us?</span>
|
<span>{copy.referral}</span>
|
||||||
<select name="referred_by" disabled={mode === 'off'}>
|
<select name="referred_by" disabled={mode === 'off'}>
|
||||||
<option value="" selected>Please select</option>
|
<option value="" selected>{copy.select}</option>
|
||||||
<option>Facebook</option>
|
<option>Facebook</option>
|
||||||
<option>Google</option>
|
<option>Google</option>
|
||||||
<option>Primary Doctor</option>
|
<option>Primary Doctor</option>
|
||||||
@@ -159,31 +264,36 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="service-inquiry-form__wide">
|
<label class="service-inquiry-form__wide">
|
||||||
<span>Additional Information/Comments</span>
|
<span>{copy.comments}</span>
|
||||||
<textarea name="comment" rows="2" disabled={mode === 'off'}></textarea>
|
<textarea name="comment" rows="2" disabled={mode === 'off'}></textarea>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="service-inquiry-form__footer">
|
<div class="service-inquiry-form__footer">
|
||||||
<p id={noteId} data-form-status>{mode === 'mock' ? 'Demo only: this local fixture never sends information.' : <>Online submission is not yet connected. Please call <ContactObfuscation phone="(480) 687-7099" hrefValue="+14806877099" obfuscatedText="9907-786 (084)" />.</>}</p>
|
<p id={noteId} data-form-status>{mode === 'mock' ? copy.mock : <>{copy.disabled} <ContactObfuscation phone="(480) 687-7099" hrefValue="+14806877099" obfuscatedText="9907-786 (084)" />.</>}</p>
|
||||||
<button type="button" data-mock-submit disabled={mode === 'off'} aria-describedby={noteId}>Submit</button>
|
<button type="button" data-mock-submit disabled={mode === 'off'} aria-describedby={noteId}>{copy.submit}</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>}
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { bindMockForms } from '../scripts/mock-form';
|
import { bindMockForms } from '../scripts/mock-form';
|
||||||
bindMockForms();
|
bindMockForms();
|
||||||
|
|
||||||
const initializeServiceInquiryDrawers = () => {
|
const initializeServiceInquiryDrawers = () => {
|
||||||
document.querySelectorAll('[data-service-inquiry-drawer]').forEach((element) => {
|
document.querySelectorAll('[data-service-inquiry-drawer]').forEach((element) => {
|
||||||
if (!(element instanceof HTMLDialogElement) || element.dataset.initialized === 'true') return;
|
if (!(element instanceof HTMLDialogElement) || element.dataset.initialized === 'true') return;
|
||||||
|
|
||||||
element.dataset.initialized = 'true';
|
element.dataset.initialized = 'true';
|
||||||
|
const explicitActivation = element.dataset.explicitActivation === 'true';
|
||||||
const closeButton = element.querySelector('[data-service-inquiry-close]');
|
const closeButton = element.querySelector('[data-service-inquiry-close]');
|
||||||
const title = element.querySelector('[data-service-inquiry-title]');
|
const title = element.querySelector('[data-service-inquiry-title]');
|
||||||
|
const frameAdapter = element.querySelector('[data-jotform-form-a]');
|
||||||
|
const triggers = Array.from(document.querySelectorAll(`[data-service-inquiry-open][aria-controls="${element.id}"]`));
|
||||||
const dismissKey = element.dataset.dismissKey;
|
const dismissKey = element.dataset.dismissKey;
|
||||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
let closeTimer: number | undefined;
|
let closeTimer: number | undefined;
|
||||||
|
let trigger: HTMLElement | undefined;
|
||||||
|
|
||||||
const wasDismissed = () => {
|
const wasDismissed = () => {
|
||||||
try {
|
try {
|
||||||
@@ -208,7 +318,7 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
|
|
||||||
const closeDrawer = () => {
|
const closeDrawer = () => {
|
||||||
if (!element.open) return;
|
if (!element.open) return;
|
||||||
rememberDismissal();
|
if (!explicitActivation) rememberDismissal();
|
||||||
element.classList.remove('is-open');
|
element.classList.remove('is-open');
|
||||||
window.clearTimeout(closeTimer);
|
window.clearTimeout(closeTimer);
|
||||||
|
|
||||||
@@ -220,17 +330,23 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
closeTimer = window.setTimeout(finishClose, 500);
|
closeTimer = window.setTimeout(finishClose, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDrawer = () => {
|
const openDrawer = (openedBy?: EventTarget | null) => {
|
||||||
if (element.open || wasDismissed()) return;
|
if (element.open || (!explicitActivation && wasDismissed())) return;
|
||||||
|
trigger = openedBy instanceof HTMLElement ? openedBy : undefined;
|
||||||
element.showModal();
|
element.showModal();
|
||||||
document.documentElement.classList.add('service-inquiry-open');
|
document.documentElement.classList.add('service-inquiry-open');
|
||||||
|
if (explicitActivation && frameAdapter instanceof HTMLElement) {
|
||||||
|
frameAdapter.dispatchEvent(new Event('aia:jotform-activate'));
|
||||||
|
}
|
||||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||||
element.classList.add('is-open');
|
element.classList.add('is-open');
|
||||||
if (title instanceof HTMLElement) title.focus({ preventScroll: true });
|
if (closeButton instanceof HTMLElement) closeButton.focus({ preventScroll: true });
|
||||||
|
else if (title instanceof HTMLElement) title.focus({ preventScroll: true });
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
closeButton?.addEventListener('click', closeDrawer);
|
closeButton?.addEventListener('click', closeDrawer);
|
||||||
|
triggers.forEach((openButton) => openButton.addEventListener('click', () => openDrawer(openButton)));
|
||||||
element.addEventListener('cancel', (event) => {
|
element.addEventListener('cancel', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
closeDrawer();
|
closeDrawer();
|
||||||
@@ -241,9 +357,13 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
element.addEventListener('close', () => {
|
element.addEventListener('close', () => {
|
||||||
element.classList.remove('is-open');
|
element.classList.remove('is-open');
|
||||||
document.documentElement.classList.remove('service-inquiry-open');
|
document.documentElement.classList.remove('service-inquiry-open');
|
||||||
|
if (trigger instanceof HTMLElement) {
|
||||||
|
trigger.focus({ preventScroll: true });
|
||||||
|
trigger = undefined;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.setTimeout(openDrawer, 10_000);
|
if (!explicitActivation) window.setTimeout(() => openDrawer(undefined), 10_000);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -255,6 +375,41 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.service-inquiry-drawer__open {
|
||||||
|
background: var(--color-accent, #ffbd59);
|
||||||
|
border: 2px solid var(--color-accent, #ffbd59);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 1.5rem auto 0;
|
||||||
|
padding: .75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-inquiry-drawer__open:hover,
|
||||||
|
.service-inquiry-drawer__open:focus-visible {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--color-accent-strong, #ff914d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-inquiry-drawer__open:focus-visible {
|
||||||
|
outline: 3px solid var(--color-focus, #115bbd);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-inquiry-drawer__noscript {
|
||||||
|
margin: 1.5rem auto 0;
|
||||||
|
max-width: 42rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-inquiry-drawer__noscript a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: .15em;
|
||||||
|
}
|
||||||
|
|
||||||
.service-inquiry-drawer {
|
.service-inquiry-drawer {
|
||||||
background: white;
|
background: white;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -271,7 +426,7 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
transform: translateX(calc(100% + 20px));
|
transform: translateX(calc(100% + 20px));
|
||||||
transition: opacity 300ms ease, transform 500ms ease;
|
transition: opacity 300ms ease, transform 500ms ease;
|
||||||
width: 608px;
|
width: min(760px, calc(100vw - 20px));
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-inquiry-drawer.is-open {
|
.service-inquiry-drawer.is-open {
|
||||||
@@ -301,11 +456,11 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-inquiry-drawer__close:focus-visible {
|
.service-inquiry-drawer__close:focus-visible {
|
||||||
outline: 2px solid var(--color-primary);
|
outline: 3px solid var(--color-focus, #115bbd);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,18 +472,24 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
.service-inquiry-drawer__intro h2 {
|
.service-inquiry-drawer__intro h2 {
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
font-size: 1.625rem;
|
font-size: 1.625rem;
|
||||||
line-height: 1.05;
|
line-height: 1.1;
|
||||||
margin: 10px 0 16px;
|
margin: 10px 0 16px;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-inquiry-drawer__intro p {
|
.service-inquiry-drawer__intro p {
|
||||||
line-height: 1.2;
|
line-height: 1.45;
|
||||||
margin: 0 0 16px;
|
margin: 0 auto 16px;
|
||||||
|
max-width: 42rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-inquiry-drawer__intro p:last-child {
|
.service-inquiry-drawer__privacy {
|
||||||
margin-bottom: 0;
|
color: var(--color-muted);
|
||||||
|
font-size: .9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-inquiry-drawer__iframe {
|
||||||
|
padding: 2rem 3.75rem 3.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-inquiry-form {
|
.service-inquiry-form {
|
||||||
@@ -477,8 +638,21 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
.service-inquiry-drawer__intro {
|
.service-inquiry-drawer {
|
||||||
padding-top: 20px;
|
inset: 0;
|
||||||
|
max-height: 100dvh;
|
||||||
|
max-width: 100vw;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-inquiry-drawer__intro,
|
||||||
|
.service-inquiry-drawer__iframe,
|
||||||
|
.service-inquiry-form {
|
||||||
|
padding-inline: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-inquiry-drawer__iframe {
|
||||||
|
padding-block: 1.5rem 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-inquiry-form__grid {
|
.service-inquiry-form__grid {
|
||||||
@@ -499,13 +673,6 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
.service-inquiry-drawer__intro,
|
|
||||||
.service-inquiry-form {
|
|
||||||
padding-inline: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.service-inquiry-drawer,
|
.service-inquiry-drawer,
|
||||||
.service-inquiry-drawer::backdrop {
|
.service-inquiry-drawer::backdrop {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const formAIframe = resolveFormAIframe({
|
|||||||
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
||||||
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
||||||
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_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,
|
productionApproved: import.meta.env.PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED,
|
||||||
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
||||||
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const formAIframe = resolveFormAIframe({
|
|||||||
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
||||||
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
||||||
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_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,
|
productionApproved: import.meta.env.PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED,
|
||||||
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
||||||
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const formAIframe = resolveFormAIframe({
|
|||||||
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
||||||
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
||||||
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_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,
|
productionApproved: import.meta.env.PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED,
|
||||||
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
||||||
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const pageTitles: Record<string, Record<string, string>> = {
|
|||||||
};
|
};
|
||||||
const bannerTitle = serviceTitles[entry.data.lang]?.[entry.data.slug] || pageTitles[entry.data.lang]?.[entry.data.slug] || entry.data.title.split('|')[0].trim();
|
const bannerTitle = serviceTitles[entry.data.lang]?.[entry.data.slug] || pageTitles[entry.data.lang]?.[entry.data.slug] || entry.data.title.split('|')[0].trim();
|
||||||
const isService = Boolean(serviceTitles[entry.data.lang]?.[entry.data.slug]);
|
const isService = Boolean(serviceTitles[entry.data.lang]?.[entry.data.slug]);
|
||||||
|
const route = entry.data.lang === 'en' ? `/${entry.data.slug}` : `/${entry.data.lang}/${entry.data.slug}`;
|
||||||
const heroImage = pageHeroImages[entry.data.slug];
|
const heroImage = pageHeroImages[entry.data.slug];
|
||||||
if (entry.data.slug === 'team' && entry.data.lang === 'en' && !entry.data.team) {
|
if (entry.data.slug === 'team' && entry.data.lang === 'en' && !entry.data.team) {
|
||||||
throw new Error('The English Team page requires structured team frontmatter.');
|
throw new Error('The English Team page requires structured team frontmatter.');
|
||||||
@@ -78,6 +79,6 @@ if (entry.data.slug === 'team' && entry.data.lang === 'en' && !entry.data.team)
|
|||||||
: entry.data.slug === 'contact'
|
: entry.data.slug === 'contact'
|
||||||
? <ContactPage title={bannerTitle} lang={entry.data.lang}><slot /></ContactPage>
|
? <ContactPage title={bannerTitle} lang={entry.data.lang}><slot /></ContactPage>
|
||||||
: <article class:list={['prose', 'source-page', 'container', { 'service-page': isService }]}><slot /></article>}
|
: <article class:list={['prose', 'source-page', 'container', { 'service-page': isService }]}><slot /></article>}
|
||||||
{isService && entry.data.lang === 'en' && <ServiceInquiryDrawer service={entry.data.slug} />}
|
{isService && (entry.data.lang === 'en' || entry.data.lang === 'es') && <ServiceInquiryDrawer service={entry.data.slug} lang={entry.data.lang} route={route} />}
|
||||||
{showForm && <FormShell lang={entry.data.lang} title={bannerTitle} logicalForm={entry.data.slug === 'referrals' ? 'referral' : 'service'} showBackground={entry.data.slug === 'client-consultation'} />}
|
{showForm && <FormShell lang={entry.data.lang} route={route} title={bannerTitle} logicalForm={entry.data.slug === 'referrals' ? 'referral' : 'service'} showBackground={entry.data.slug === 'client-consultation'} />}
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
@@ -27,6 +27,7 @@ export type FormAIframeReason =
|
|||||||
| 'enabled'
|
| 'enabled'
|
||||||
| 'mode-not-authorized'
|
| 'mode-not-authorized'
|
||||||
| 'isolated-test-not-approved'
|
| 'isolated-test-not-approved'
|
||||||
|
| 'consultation-test-not-approved'
|
||||||
| 'live-approval-not-disabled'
|
| 'live-approval-not-disabled'
|
||||||
| 'production-approval-not-granted'
|
| 'production-approval-not-granted'
|
||||||
| 'invalid-environment-role'
|
| 'invalid-environment-role'
|
||||||
@@ -52,6 +53,7 @@ export interface ResolveFormAIframeOptions {
|
|||||||
rawMode: string | undefined;
|
rawMode: string | undefined;
|
||||||
liveApproved: string | undefined;
|
liveApproved: string | undefined;
|
||||||
isolatedTestApproved: string | undefined;
|
isolatedTestApproved: string | undefined;
|
||||||
|
consultationTestApproved: string | undefined;
|
||||||
productionApproved: string | undefined;
|
productionApproved: string | undefined;
|
||||||
environmentRole: string | undefined;
|
environmentRole: string | undefined;
|
||||||
allowIndexing: string | undefined;
|
allowIndexing: string | undefined;
|
||||||
@@ -69,7 +71,7 @@ export const formAIframeId = '262946078314059' as const;
|
|||||||
const isolatedTestEnvironmentRole = 'isolated-test';
|
const isolatedTestEnvironmentRole = 'isolated-test';
|
||||||
const productionEnvironmentRole = 'production';
|
const productionEnvironmentRole = 'production';
|
||||||
|
|
||||||
export type FormAIframeContext = 'contact' | 'landing';
|
export type FormAIframeContext = 'contact' | 'landing' | 'consultation';
|
||||||
export type FormAIframeRoute =
|
export type FormAIframeRoute =
|
||||||
| '/contact'
|
| '/contact'
|
||||||
| '/es/contact'
|
| '/es/contact'
|
||||||
@@ -77,9 +79,18 @@ export type FormAIframeRoute =
|
|||||||
| '/lp/aba-insurance'
|
| '/lp/aba-insurance'
|
||||||
| '/lp/aba-ahcccs'
|
| '/lp/aba-ahcccs'
|
||||||
| '/lp/aba-private-pay'
|
| '/lp/aba-private-pay'
|
||||||
| '/lp/early-intervention';
|
| '/lp/early-intervention'
|
||||||
|
| '/client-consultation'
|
||||||
|
| '/es/client-consultation'
|
||||||
|
| '/schedule-consultation'
|
||||||
|
| '/aba-therapy'
|
||||||
|
| '/autism-evaluations'
|
||||||
|
| '/learner-social-club'
|
||||||
|
| '/es/aba-therapy'
|
||||||
|
| '/es/autism-evaluations'
|
||||||
|
| '/es/learner-social-club';
|
||||||
|
|
||||||
interface FormAIframePlacement {
|
export interface FormAIframePlacement {
|
||||||
route: FormAIframeRoute;
|
route: FormAIframeRoute;
|
||||||
locale: 'en' | 'es';
|
locale: 'en' | 'es';
|
||||||
context: FormAIframeContext;
|
context: FormAIframeContext;
|
||||||
@@ -102,19 +113,38 @@ export const formAIframePlacements: ReadonlyArray<FormAIframePlacement> = [
|
|||||||
{ route: '/lp/early-intervention', 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 an explicit visitor action.
|
||||||
|
*/
|
||||||
|
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' },
|
||||||
|
{ route: '/aba-therapy', locale: 'en', context: 'consultation' },
|
||||||
|
{ route: '/autism-evaluations', locale: 'en', context: 'consultation' },
|
||||||
|
{ route: '/learner-social-club', locale: 'en', context: 'consultation' },
|
||||||
|
{ route: '/es/aba-therapy', locale: 'es', context: 'consultation' },
|
||||||
|
{ route: '/es/autism-evaluations', locale: 'es', context: 'consultation' },
|
||||||
|
{ route: '/es/learner-social-club', locale: 'es', context: 'consultation' },
|
||||||
|
];
|
||||||
|
|
||||||
const isTrue = (value: string | undefined) => value?.trim().toLowerCase() === 'true';
|
const isTrue = (value: string | undefined) => value?.trim().toLowerCase() === 'true';
|
||||||
|
|
||||||
function formAIframeCopy(locale: 'en' | 'es') {
|
function formAIframeCopy(locale: 'en' | 'es', context: FormAIframeContext) {
|
||||||
|
const consultation = context === 'consultation';
|
||||||
return locale === 'es'
|
return locale === 'es'
|
||||||
? {
|
? {
|
||||||
title: 'Formulario de consultas generales de AIA',
|
title: consultation ? 'Formulario de consulta de AIA' : 'Formulario de consultas generales de AIA',
|
||||||
fallbackText: 'Si el formulario integrado no se carga,',
|
fallbackText: consultation ? 'Si el formulario de consulta integrado no se carga,' : 'Si el formulario integrado no se carga,',
|
||||||
fallbackLinkText: 'abra el formulario en una pestaña nueva.',
|
fallbackLinkText: consultation ? 'abra el formulario de consulta en una pestaña nueva.' : 'abra el formulario en una pestaña nueva.',
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
title: 'AIA general enquiries form',
|
title: consultation ? 'AIA consultation form' : 'AIA general enquiries form',
|
||||||
fallbackText: 'If the embedded form does not load,',
|
fallbackText: consultation ? 'If the embedded consultation form does not load,' : 'If the embedded form does not load,',
|
||||||
fallbackLinkText: 'open the form in a new tab.',
|
fallbackLinkText: consultation ? 'open the consultation form in a new tab.' : 'open the form in a new tab.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,9 +189,16 @@ export function resolveFormAIframe(options: ResolveFormAIframeOptions): FormAIfr
|
|||||||
return disabled('mode-not-authorized');
|
return disabled('mode-not-authorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
const placement = formAIframePlacements.find((candidate) => candidate.route === options.route);
|
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 (!placement) return disabled('route-not-authorized');
|
if (!placement) return disabled('route-not-authorized');
|
||||||
if (options.locale !== placement.locale) return disabled('locale-not-authorized');
|
if (options.locale !== placement.locale) return disabled('locale-not-authorized');
|
||||||
|
if (placement.context === 'consultation' && !isTrue(options.consultationTestApproved)) {
|
||||||
|
return disabled('consultation-test-not-approved');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -172,7 +209,7 @@ export function resolveFormAIframe(options: ResolveFormAIframeOptions): FormAIfr
|
|||||||
environmentRole: approvedEnvironment,
|
environmentRole: approvedEnvironment,
|
||||||
context: placement.context,
|
context: placement.context,
|
||||||
url: buildFormAIframeUrl(placement.locale, placement.context),
|
url: buildFormAIframeUrl(placement.locale, placement.context),
|
||||||
...formAIframeCopy(placement.locale),
|
...formAIframeCopy(placement.locale, placement.context),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +247,9 @@ export const formRouteMapping: ReadonlyArray<{
|
|||||||
{ route: '/aba-therapy', logicalForm: 'service', kind: 'native-instance' },
|
{ route: '/aba-therapy', logicalForm: 'service', kind: 'native-instance' },
|
||||||
{ route: '/autism-evaluations', logicalForm: 'service', kind: 'native-instance' },
|
{ route: '/autism-evaluations', logicalForm: 'service', kind: 'native-instance' },
|
||||||
{ route: '/learner-social-club', logicalForm: 'service', kind: 'native-instance' },
|
{ route: '/learner-social-club', logicalForm: 'service', kind: 'native-instance' },
|
||||||
|
{ route: '/es/aba-therapy', logicalForm: 'service', kind: 'native-instance' },
|
||||||
|
{ route: '/es/autism-evaluations', logicalForm: 'service', kind: 'native-instance' },
|
||||||
|
{ route: '/es/learner-social-club', logicalForm: 'service', kind: 'native-instance' },
|
||||||
{ route: '/lp/aba-therapy', logicalForm: 'general', kind: 'landing-journey' },
|
{ route: '/lp/aba-therapy', logicalForm: 'general', kind: 'landing-journey' },
|
||||||
{ route: '/lp/aba-insurance', logicalForm: 'general', kind: 'landing-journey' },
|
{ route: '/lp/aba-insurance', logicalForm: 'general', kind: 'landing-journey' },
|
||||||
{ route: '/lp/aba-ahcccs', logicalForm: 'general', kind: 'landing-journey' },
|
{ route: '/lp/aba-ahcccs', logicalForm: 'general', kind: 'landing-journey' },
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ test('Jotform handoff preserves AIA form tokens and accessible native controls',
|
|||||||
const css = read('integrations/jotform/aia-form-a.css');
|
const css = read('integrations/jotform/aia-form-a.css');
|
||||||
const guide = read('integrations/jotform/README.md');
|
const guide = read('integrations/jotform/README.md');
|
||||||
for (const value of ['#ffbd59', '#ff914d', '#254080', '#fcf9f5', '#edeef3']) assert.match(css, new RegExp(value));
|
for (const value of ['#ffbd59', '#ff914d', '#254080', '#fcf9f5', '#edeef3']) assert.match(css, new RegExp(value));
|
||||||
assert.match(css, /#form \.form-submit-button/);
|
assert.match(css, /\.form-all \.form-submit-button/);
|
||||||
|
assert.match(css, /\.form-all button\[type="submit"\]/);
|
||||||
assert.match(css, /:focus-visible/);
|
assert.match(css, /:focus-visible/);
|
||||||
assert.doesNotMatch(css, /https?:\/\/|@import|url\(/i);
|
assert.doesNotMatch(css, /https?:\/\/|@import|url\(/i);
|
||||||
assert.match(guide, /formContext/);
|
assert.match(guide, /formContext/);
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* Guarded consultation-only Form A browser regression.
|
||||||
|
* Run after an explicitly gated isolated-test build. Every Jotform request is
|
||||||
|
* fulfilled or aborted locally; no Jotform request is transmitted.
|
||||||
|
*/
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import http from 'node:http';
|
||||||
|
import net from 'node:net';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
|
import { setTimeout as delay } from 'node:timers/promises';
|
||||||
|
|
||||||
|
const root = fileURLToPath(new URL('../', import.meta.url));
|
||||||
|
const moduleName = process.env.PLAYWRIGHT_MODULE
|
||||||
|
? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href
|
||||||
|
: 'playwright';
|
||||||
|
const output = await fs.mkdtemp('/tmp/aia-form-a-consultation-');
|
||||||
|
const routes = [
|
||||||
|
{ route: '/client-consultation', locale: 'en', drawer: false, screenshot: 'client-consultation' },
|
||||||
|
{ route: '/es/client-consultation', locale: 'es', drawer: false, screenshot: 'es-client-consultation' },
|
||||||
|
{ route: '/schedule-consultation', locale: 'en', drawer: false, screenshot: 'schedule-consultation' },
|
||||||
|
{ route: '/aba-therapy', locale: 'en', drawer: true, screenshot: 'aba-therapy' },
|
||||||
|
{ route: '/autism-evaluations', locale: 'en', drawer: true, screenshot: 'autism-evaluations' },
|
||||||
|
{ route: '/learner-social-club', locale: 'en', drawer: true, screenshot: 'learner-social-club' },
|
||||||
|
{ route: '/es/aba-therapy', locale: 'es', drawer: true, screenshot: 'es-aba-therapy' },
|
||||||
|
{ route: '/es/autism-evaluations', locale: 'es', drawer: true, screenshot: 'es-autism-evaluations' },
|
||||||
|
{ 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 reserve = net.createServer();
|
||||||
|
await new Promise((resolve, reject) => { reserve.once('error', reject); reserve.listen(0, '127.0.0.1', resolve); });
|
||||||
|
const port = reserve.address().port;
|
||||||
|
await new Promise((resolve) => reserve.close(resolve));
|
||||||
|
const origin = 'http://127.0.0.1:' + port;
|
||||||
|
let browser;
|
||||||
|
let preview;
|
||||||
|
|
||||||
|
function expectedUrl(locale) {
|
||||||
|
return 'https://form.jotform.com/262946078314059?formContext=consultation&language=' + locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { chromium } = await import(moduleName);
|
||||||
|
browser = await chromium.launch(process.env.CHROMIUM_EXECUTABLE
|
||||||
|
? { headless: true, executablePath: process.env.CHROMIUM_EXECUTABLE }
|
||||||
|
: { headless: true });
|
||||||
|
preview = spawn(process.execPath, ['node_modules/astro/bin/astro.mjs', 'preview', '--host', '127.0.0.1', '--port', String(port)], {
|
||||||
|
cwd: root,
|
||||||
|
env: { ...process.env, ASTRO_TELEMETRY_DISABLED: '1' },
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
const reachable = () => new Promise((resolve) => {
|
||||||
|
const request = http.get(origin + '/', (response) => { response.resume(); resolve(response.statusCode === 200); });
|
||||||
|
request.setTimeout(500, () => request.destroy());
|
||||||
|
request.on('error', () => resolve(false));
|
||||||
|
});
|
||||||
|
for (let i = 0; i < 60 && !(await reachable()); i++) await delay(250);
|
||||||
|
assert.equal(await reachable(), true, 'Loopback preview did not become reachable');
|
||||||
|
|
||||||
|
for (const width of widths) {
|
||||||
|
const context = await browser.newContext({
|
||||||
|
viewport: { width, height: width < 500 ? 844 : 960 },
|
||||||
|
serviceWorkers: 'block',
|
||||||
|
});
|
||||||
|
await context.routeWebSocket('**/*', (socket) => socket.close());
|
||||||
|
await context.route('**/*', async (route) => {
|
||||||
|
const request = route.request();
|
||||||
|
const url = new URL(request.url());
|
||||||
|
if (url.origin === origin && ['GET', 'HEAD'].includes(request.method())) return route.continue();
|
||||||
|
const item = { origin: url.origin, path: url.pathname, search: url.search, method: request.method() };
|
||||||
|
evidence.blocked.push(item);
|
||||||
|
if (url.origin === 'https://form.jotform.com') {
|
||||||
|
evidence.jotformAttempts.push(item);
|
||||||
|
return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>Local consultation fixture</title><p>Local fixture</p>' });
|
||||||
|
}
|
||||||
|
return route.abort();
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const expected of routes) {
|
||||||
|
const page = await context.newPage();
|
||||||
|
const attemptsBeforeNavigation = evidence.jotformAttempts.length;
|
||||||
|
evidence.checks++;
|
||||||
|
const response = await page.goto(origin + expected.route, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||||
|
assert.equal(response?.status(), 200, expected.route);
|
||||||
|
assert.equal(await page.locator('main h1').count(), 1, expected.route);
|
||||||
|
assert.equal(await page.locator('main form').count(), 0, expected.route);
|
||||||
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), expected.route + ' has horizontal overflow at ' + width + 'px');
|
||||||
|
|
||||||
|
const adapter = page.locator('[data-jotform-form-a]');
|
||||||
|
const iframe = adapter.locator('iframe');
|
||||||
|
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 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);
|
||||||
|
assert.equal(await adapter.locator('[data-jotform-iframe-fallback]').getAttribute('hidden'), '', expected.route);
|
||||||
|
|
||||||
|
const attemptsBefore = evidence.jotformAttempts.length;
|
||||||
|
if (expected.drawer) {
|
||||||
|
const opener = page.locator('[data-service-inquiry-open]');
|
||||||
|
assert.equal(await opener.count(), 1, expected.route);
|
||||||
|
assert.equal(await iframe.getAttribute('src'), null, expected.route + ' must defer the iframe source');
|
||||||
|
assert.equal(await iframe.getAttribute('data-jotform-iframe-url'), url, expected.route);
|
||||||
|
assert.equal(evidence.jotformAttempts.length, attemptsBefore, expected.route + ' must not request Jotform while closed');
|
||||||
|
|
||||||
|
await opener.focus();
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
const dialog = page.locator('[data-service-inquiry-drawer]');
|
||||||
|
await assert.doesNotReject(dialog.waitFor({ state: 'visible', timeout: 3000 }));
|
||||||
|
assert.equal(await iframe.getAttribute('src'), url, expected.route);
|
||||||
|
await page.waitForTimeout(20);
|
||||||
|
assert.equal(evidence.jotformAttempts.length, attemptsBefore + 1, expected.route + ' should request only after opening');
|
||||||
|
assert.equal(await page.locator('.service-inquiry-form').count(), 0, expected.route + ' must not expose legacy fields');
|
||||||
|
assert.equal(await page.locator('[data-service-inquiry-close]').getAttribute('aria-label'), expected.locale === 'es' ? 'Cerrar el formulario de consulta' : 'Close consultation form');
|
||||||
|
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForFunction(() => !document.querySelector('[data-service-inquiry-drawer]')?.open);
|
||||||
|
assert.equal(await page.evaluate(() => document.activeElement?.getAttribute('data-service-inquiry-open')), '', expected.route + ' restores focus');
|
||||||
|
await opener.click();
|
||||||
|
await page.waitForTimeout(20);
|
||||||
|
assert.equal(await page.locator('[data-jotform-form-a] iframe').count(), 1, expected.route + ' must retain one iframe');
|
||||||
|
assert.equal(evidence.jotformAttempts.length, attemptsBefore + 1, expected.route + ' must not duplicate the request on reopen');
|
||||||
|
} else {
|
||||||
|
assert.equal(await iframe.getAttribute('src'), url, expected.route);
|
||||||
|
await page.waitForTimeout(20);
|
||||||
|
assert.ok(evidence.jotformAttempts.length >= attemptsBeforeNavigation + 1, expected.route + ' should request the configured iframe');
|
||||||
|
}
|
||||||
|
|
||||||
|
const box = await iframe.boundingBox();
|
||||||
|
const minimumHeight = expected.drawer ? width < 500 ? 1312 : 1152 : width < 500 ? 1088 : 960;
|
||||||
|
assert.ok(box && box.width > 0 && box.height >= minimumHeight, expected.route + ' has an insufficient consultation frame');
|
||||||
|
await page.screenshot({ path: path.join(output, expected.screenshot + '-' + width + '.png'), fullPage: true });
|
||||||
|
evidence.screenshots.push(expected.screenshot + '-' + width + '.png');
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
await context.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const noJavaScript = await browser.newContext({ viewport: { width: 390, height: 844 }, javaScriptEnabled: false, serviceWorkers: 'block' });
|
||||||
|
await noJavaScript.route('**/*', async (route) => {
|
||||||
|
const request = route.request();
|
||||||
|
const url = new URL(request.url());
|
||||||
|
if (url.origin === origin && ['GET', 'HEAD'].includes(request.method())) return route.continue();
|
||||||
|
const item = { origin: url.origin, path: url.pathname, search: url.search, method: request.method(), noJavaScript: true };
|
||||||
|
evidence.blocked.push(item);
|
||||||
|
if (url.origin === 'https://form.jotform.com') evidence.jotformAttempts.push(item);
|
||||||
|
return route.abort();
|
||||||
|
});
|
||||||
|
for (const expected of routes) {
|
||||||
|
const page = await noJavaScript.newPage();
|
||||||
|
evidence.checks++;
|
||||||
|
await page.goto(origin + expected.route, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||||
|
if (expected.drawer) {
|
||||||
|
const fallback = page.locator('.service-inquiry-drawer__noscript');
|
||||||
|
assert.equal(await fallback.count(), 1, expected.route + ' no-JS fallback');
|
||||||
|
assert.equal(await fallback.isVisible(), true, expected.route + ' no-JS fallback visibility');
|
||||||
|
assert.equal(await fallback.locator('a').getAttribute('href'), expectedUrl(expected.locale), expected.route);
|
||||||
|
} else {
|
||||||
|
const fallback = page.locator('[data-jotform-form-a] .jotform-iframe__fallback--noscript');
|
||||||
|
assert.equal(await fallback.count(), 1, expected.route + ' no-JS fallback');
|
||||||
|
assert.equal(await fallback.isVisible(), true, expected.route + ' no-JS fallback visibility');
|
||||||
|
}
|
||||||
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), expected.route + ' no-JS horizontal overflow');
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
await noJavaScript.close();
|
||||||
|
|
||||||
|
const fallbackContext = await browser.newContext({ viewport: { width: 390, height: 844 }, serviceWorkers: 'block' });
|
||||||
|
await fallbackContext.route('**/*', async (route) => {
|
||||||
|
const request = route.request();
|
||||||
|
const url = new URL(request.url());
|
||||||
|
if (url.origin === origin && ['GET', 'HEAD'].includes(request.method())) return route.continue();
|
||||||
|
const item = { origin: url.origin, path: url.pathname, search: url.search, method: request.method(), fallback: true };
|
||||||
|
evidence.blocked.push(item);
|
||||||
|
if (url.origin === 'https://form.jotform.com') {
|
||||||
|
evidence.jotformAttempts.push(item);
|
||||||
|
// Hold this guarded request rather than aborting it. Some browsers fire
|
||||||
|
// iframe load for an aborted/error document, which intentionally clears
|
||||||
|
// the fallback because load is only a transport signal.
|
||||||
|
return new Promise(() => {});
|
||||||
|
}
|
||||||
|
return route.abort();
|
||||||
|
});
|
||||||
|
const fallbackPage = await fallbackContext.newPage();
|
||||||
|
await fallbackPage.addInitScript(() => {
|
||||||
|
const original = window.setTimeout.bind(window);
|
||||||
|
window.setTimeout = (callback, timeout, ...args) => original(callback, timeout === 15000 ? 80 : timeout, ...args);
|
||||||
|
});
|
||||||
|
await fallbackPage.goto(origin + '/aba-therapy', { waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||||
|
await fallbackPage.locator('[data-service-inquiry-open]').click();
|
||||||
|
const fallback = fallbackPage.locator('[data-jotform-iframe-fallback]');
|
||||||
|
await fallbackPage.waitForFunction(() => document.querySelector('[data-jotform-iframe-fallback]')?.hidden === false);
|
||||||
|
assert.equal(await fallback.isVisible(), true, 'blocked drawer request exposes the hosted fallback after timeout');
|
||||||
|
evidence.fallbackChecks.push('drawer-timeout-fallback-visible');
|
||||||
|
await fallbackContext.close();
|
||||||
|
|
||||||
|
assert.ok(evidence.jotformAttempts.length >= routes.length * widths.length, 'Expected intercepted Jotform GET attempts');
|
||||||
|
for (const attempt of evidence.jotformAttempts) {
|
||||||
|
assert.equal(attempt.method, 'GET');
|
||||||
|
assert.equal(attempt.path, '/262946078314059');
|
||||||
|
assert.match(attempt.search, /^\?formContext=consultation&language=(?:en|es)$/);
|
||||||
|
}
|
||||||
|
assert.equal(evidence.blocked.filter((item) => !['GET', 'HEAD'].includes(item.method)).length, 0, 'A non-read request was attempted');
|
||||||
|
evidence.status = 'passed';
|
||||||
|
} catch (error) {
|
||||||
|
evidence.error = error instanceof Error ? error.message : String(error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await fs.writeFile(path.join(output, 'evidence.json'), JSON.stringify(evidence, null, 2));
|
||||||
|
if (preview && preview.exitCode === null) {
|
||||||
|
preview.kill('SIGTERM');
|
||||||
|
await Promise.race([new Promise((resolve) => preview.once('exit', resolve)), delay(3000)]);
|
||||||
|
if (preview.exitCode === null) preview.kill('SIGKILL');
|
||||||
|
}
|
||||||
|
await browser?.close();
|
||||||
|
console.log(JSON.stringify({ ...evidence, artifactDirectory: output }, null, 2));
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import {
|
import {
|
||||||
buildFormAIframeUrl,
|
buildFormAIframeUrl,
|
||||||
|
formAConsultationTestPlacements,
|
||||||
formAIframeId,
|
formAIframeId,
|
||||||
formAIframeOrigin,
|
formAIframeOrigin,
|
||||||
formAIframePlacements,
|
formAIframePlacements,
|
||||||
@@ -15,6 +16,7 @@ const isolated = (overrides = {}) => ({
|
|||||||
rawMode: 'isolated-test',
|
rawMode: 'isolated-test',
|
||||||
liveApproved: 'false',
|
liveApproved: 'false',
|
||||||
isolatedTestApproved: 'true',
|
isolatedTestApproved: 'true',
|
||||||
|
consultationTestApproved: 'false',
|
||||||
productionApproved: 'false',
|
productionApproved: 'false',
|
||||||
environmentRole: 'isolated-test',
|
environmentRole: 'isolated-test',
|
||||||
allowIndexing: 'false',
|
allowIndexing: 'false',
|
||||||
@@ -23,10 +25,18 @@ const isolated = (overrides = {}) => ({
|
|||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const consultation = (overrides = {}) => isolated({
|
||||||
|
consultationTestApproved: 'true',
|
||||||
|
route: '/client-consultation',
|
||||||
|
locale: 'en',
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
const production = (overrides = {}) => ({
|
const production = (overrides = {}) => ({
|
||||||
rawMode: 'live',
|
rawMode: 'live',
|
||||||
liveApproved: 'true',
|
liveApproved: 'true',
|
||||||
isolatedTestApproved: 'false',
|
isolatedTestApproved: 'false',
|
||||||
|
consultationTestApproved: 'true',
|
||||||
productionApproved: 'true',
|
productionApproved: 'true',
|
||||||
environmentRole: 'production',
|
environmentRole: 'production',
|
||||||
allowIndexing: 'true',
|
allowIndexing: 'true',
|
||||||
@@ -50,6 +60,10 @@ test('Form A iframe uses only fixed, approved route parameters', () => {
|
|||||||
buildFormAIframeUrl('en', 'landing'),
|
buildFormAIframeUrl('en', 'landing'),
|
||||||
'https://form.jotform.com/262946078314059?formContext=landing&language=en',
|
'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 english = resolveFormAIframe(isolated());
|
||||||
const spanish = resolveFormAIframe(isolated({ route: '/es/contact', locale: 'es' }));
|
const spanish = resolveFormAIframe(isolated({ route: '/es/contact', locale: 'es' }));
|
||||||
assert.equal(english.fallbackText, 'If the embedded form does not load,');
|
assert.equal(english.fallbackText, 'If the embedded form does not load,');
|
||||||
@@ -65,6 +79,17 @@ test('Form A iframe uses only fixed, approved route parameters', () => {
|
|||||||
'/lp/aba-private-pay',
|
'/lp/aba-private-pay',
|
||||||
'/lp/early-intervention',
|
'/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', () => {
|
test('Form A iframe requires every isolated-test or production approval gate', () => {
|
||||||
@@ -103,9 +128,44 @@ test('Form A iframe requires every isolated-test or production approval gate', (
|
|||||||
assert.equal(formModeForRuntime(resolveFormMode('live', 'true')), 'off');
|
assert.equal(formModeForRuntime(resolveFormMode('live', 'true')), 'off');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Only the seven allowlisted placements can render the adapter, with no helper, tracking, or parent submission code', () => {
|
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.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');
|
||||||
|
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', () => {
|
||||||
const contact = fs.readFileSync('src/components/pages/ContactPage.astro', 'utf8');
|
const contact = fs.readFileSync('src/components/pages/ContactPage.astro', 'utf8');
|
||||||
const adapter = fs.readFileSync('src/components/JotformIframe.astro', 'utf8');
|
const adapter = fs.readFileSync('src/components/JotformIframe.astro', 'utf8');
|
||||||
|
const shell = fs.readFileSync('src/components/FormShell.astro', 'utf8');
|
||||||
|
const drawer = fs.readFileSync('src/components/ServiceInquiryDrawer.astro', 'utf8');
|
||||||
|
const layout = fs.readFileSync('src/layouts/PageLayout.astro', 'utf8');
|
||||||
const campaign = fs.readFileSync('src/components/campaign/ConsultationForm.astro', 'utf8');
|
const campaign = fs.readFileSync('src/components/campaign/ConsultationForm.astro', 'utf8');
|
||||||
const earlyIntervention = fs.readFileSync('src/components/campaign/EarlyInterventionPage.astro', 'utf8');
|
const earlyIntervention = fs.readFileSync('src/components/campaign/EarlyInterventionPage.astro', 'utf8');
|
||||||
|
|
||||||
@@ -113,6 +173,7 @@ test('Only the seven allowlisted placements can render the adapter, with no help
|
|||||||
assert.match(contact, /route = lang === 'es' \? '\/es\/contact' : '\/contact'/);
|
assert.match(contact, /route = lang === 'es' \? '\/es\/contact' : '\/contact'/);
|
||||||
assert.match(contact, /JotformIframe/);
|
assert.match(contact, /JotformIframe/);
|
||||||
assert.match(contact, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
|
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, /resolveFormAIframe/);
|
||||||
assert.match(campaign, /JotformIframe/);
|
assert.match(campaign, /JotformIframe/);
|
||||||
assert.match(campaign, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
|
assert.match(campaign, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
|
||||||
@@ -125,16 +186,32 @@ test('Only the seven allowlisted placements can render the adapter, with no help
|
|||||||
assert.match(earlyIntervention, /route: '\/lp\/early-intervention'/);
|
assert.match(earlyIntervention, /route: '\/lp\/early-intervention'/);
|
||||||
assert.match(earlyIntervention, /JotformIframe/);
|
assert.match(earlyIntervention, /JotformIframe/);
|
||||||
assert.match(earlyIntervention, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
|
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-service-inquiry-open/);
|
||||||
|
assert.match(drawer, /data-explicit-activation/);
|
||||||
|
assert.match(drawer, /deferUntilActivated/);
|
||||||
|
assert.match(drawer, /aia:jotform-activate/);
|
||||||
|
assert.match(drawer, /Cerrar el formulario de consulta/);
|
||||||
|
assert.doesNotMatch(drawer, /No WAITLIST!|guarantee your child|now open for enrollment/i);
|
||||||
|
assert.match(layout, /entry\.data\.lang === 'en' \|\| entry\.data\.lang === 'es'/);
|
||||||
assert.match(adapter, /referrerpolicy="no-referrer"/);
|
assert.match(adapter, /referrerpolicy="no-referrer"/);
|
||||||
assert.match(adapter, /sandbox="allow-forms allow-scripts allow-same-origin"/);
|
assert.match(adapter, /sandbox="allow-forms allow-scripts allow-same-origin"/);
|
||||||
assert.match(adapter, /data-form-mode=\{config\.mode\}/);
|
assert.match(adapter, /data-form-mode=\{config\.mode\}/);
|
||||||
assert.match(adapter, /data-form-environment=\{config\.environmentRole\}/);
|
assert.match(adapter, /data-form-environment=\{config\.environmentRole\}/);
|
||||||
assert.match(adapter, /data-jotform-iframe-frame/);
|
assert.match(adapter, /data-jotform-iframe-frame/);
|
||||||
|
assert.match(adapter, /data-jotform-iframe-url/);
|
||||||
|
assert.match(adapter, /data-jotform-deferred/);
|
||||||
assert.match(adapter, /data-jotform-iframe-fallback hidden aria-live="polite" role="status"/);
|
assert.match(adapter, /data-jotform-iframe-fallback hidden aria-live="polite" role="status"/);
|
||||||
assert.match(adapter, /<noscript>/);
|
assert.match(adapter, /<noscript>/);
|
||||||
assert.match(adapter, /fallbackTimeoutMs = 15_000/);
|
assert.match(adapter, /fallbackTimeoutMs = 15_000/);
|
||||||
assert.match(adapter, /frame\.addEventListener\('load', onLoad, \{ once: true \}\)/);
|
assert.match(adapter, /frame\.addEventListener\('load', onLoad, \{ once: true \}\)/);
|
||||||
assert.match(adapter, /document\.addEventListener\('astro:before-swap', dispose, \{ once: true \}\)/);
|
assert.match(adapter, /document\.addEventListener\('astro:before-swap', dispose, \{ once: true \}\)/);
|
||||||
|
assert.match(adapter, /container\.addEventListener\('aia:jotform-activate'/);
|
||||||
assert.doesNotMatch(adapter, /set:html=|fallbackMarkup|escapeHtml\(/);
|
assert.doesNotMatch(adapter, /set:html=|fallbackMarkup|escapeHtml\(/);
|
||||||
assert.match(adapter, /target="_blank" rel="noopener noreferrer"/);
|
assert.match(adapter, /target="_blank" rel="noopener noreferrer"/);
|
||||||
assert.match(adapter, /--jotform-frame-height: 60rem/);
|
assert.match(adapter, /--jotform-frame-height: 60rem/);
|
||||||
@@ -143,6 +220,8 @@ test('Only the seven allowlisted placements can render the adapter, with no help
|
|||||||
assert.match(adapter, /\.jotform-iframe--embedded \{\s*--jotform-frame-height: 42rem/);
|
assert.match(adapter, /\.jotform-iframe--embedded \{\s*--jotform-frame-height: 42rem/);
|
||||||
assert.match(adapter, /\.jotform-iframe__frame \{\s*height: 68rem/);
|
assert.match(adapter, /\.jotform-iframe__frame \{\s*height: 68rem/);
|
||||||
assert.match(adapter, /\.jotform-iframe--embedded \.jotform-iframe__frame \{\s*height: 50rem/);
|
assert.match(adapter, /\.jotform-iframe--embedded \.jotform-iframe__frame \{\s*height: 50rem/);
|
||||||
|
assert.match(adapter, /\.jotform-iframe--drawer \{\s*--jotform-frame-height: 72rem/);
|
||||||
|
assert.match(adapter, /\.jotform-iframe--drawer \.jotform-iframe__frame \{\s*height: 82rem/);
|
||||||
assert.doesNotMatch(adapter, /for-form-embed-handler|jotformEmbedHandler|onload=|postMessage|fetch\(|form\.submit|analytics|googletag/i);
|
assert.doesNotMatch(adapter, /for-form-embed-handler|jotformEmbedHandler|onload=|postMessage|fetch\(|form\.submit|analytics|googletag/i);
|
||||||
assert.doesNotMatch(campaign, /262946078314059/);
|
assert.doesNotMatch(campaign, /262946078314059/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,12 +46,34 @@ const formAProductionUrls = new Map([
|
|||||||
['/lp/early-intervention', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
['/lp/early-intervention', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const formAConsultationStaticUrls = new Map([
|
||||||
|
['/client-consultation', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||||
|
['/es/client-consultation', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||||
|
['/schedule-consultation', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const formAConsultationDrawerUrls = new Map([
|
||||||
|
['/aba-therapy', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||||
|
['/autism-evaluations', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||||
|
['/learner-social-club', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||||
|
['/es/aba-therapy', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||||
|
['/es/autism-evaluations', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||||
|
['/es/learner-social-club', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const formAIsolatedStaticUrls = new Map([...formAProductionUrls, ...formAConsultationStaticUrls]);
|
||||||
|
|
||||||
function iframeSources(html) {
|
function iframeSources(html) {
|
||||||
// Astro correctly escapes query separators in serialized HTML attributes.
|
// Astro correctly escapes query separators in serialized HTML attributes.
|
||||||
// Normalize that representation before comparing the fixed allowlisted URL.
|
// Normalize that representation before comparing the fixed allowlisted URL.
|
||||||
return [...html.matchAll(/<iframe\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi)].map((match) => match[1].replaceAll('&', '&'));
|
return [...html.matchAll(/<iframe\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi)].map((match) => match[1].replaceAll('&', '&'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deferredIframeSources(html) {
|
||||||
|
return [...html.matchAll(/<iframe\b[^>]*\bdata-jotform-iframe-url=["']([^"']+)["'][^>]*>/gi)]
|
||||||
|
.map((match) => match[1].replaceAll('&', '&'));
|
||||||
|
}
|
||||||
|
|
||||||
function formAAdapters(html) {
|
function formAAdapters(html) {
|
||||||
return [...html.matchAll(/<section\b[^>]*\bdata-jotform-form-a\b[^>]*>/gi)].map((match) => match[0]);
|
return [...html.matchAll(/<section\b[^>]*\bdata-jotform-form-a\b[^>]*>/gi)].map((match) => match[0]);
|
||||||
}
|
}
|
||||||
@@ -71,6 +93,8 @@ export async function verifyArtifact(root, mode = 'staging') {
|
|||||||
const formAEnabled = mode === 'isolated-test' || mode === 'production';
|
const formAEnabled = mode === 'isolated-test' || mode === 'production';
|
||||||
const production = mode === 'production';
|
const production = mode === 'production';
|
||||||
const stagingPolicy = mode === 'staging' || mode === 'isolated-test';
|
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();
|
||||||
const htmlFiles = (await fg('**/*.html', { cwd: root, absolute: true })).sort();
|
const htmlFiles = (await fg('**/*.html', { cwd: root, absolute: true })).sort();
|
||||||
const routes = [];
|
const routes = [];
|
||||||
const canonicalMap = new Map();
|
const canonicalMap = new Map();
|
||||||
@@ -80,6 +104,7 @@ export async function verifyArtifact(root, mode = 'staging') {
|
|||||||
let activeForms = 0;
|
let activeForms = 0;
|
||||||
let secretSignals = [];
|
let secretSignals = [];
|
||||||
let formAIframes = 0;
|
let formAIframes = 0;
|
||||||
|
let deferredFormAIframes = 0;
|
||||||
|
|
||||||
for (const file of htmlFiles) {
|
for (const file of htmlFiles) {
|
||||||
const html = await readText(file);
|
const html = await readText(file);
|
||||||
@@ -90,7 +115,8 @@ export async function verifyArtifact(root, mode = 'staging') {
|
|||||||
const actions = formActions(html);
|
const actions = formActions(html);
|
||||||
const nonSearchActions = actions.filter((action) => action && action !== '/search');
|
const nonSearchActions = actions.filter((action) => action && action !== '/search');
|
||||||
const trailingSlashUrls = rootRelativeUrls(html).filter(hasTerminalSlash);
|
const trailingSlashUrls = rootRelativeUrls(html).filter(hasTerminalSlash);
|
||||||
const expectedFormAUrl = formAEnabled ? formAProductionUrls.get(route) : undefined;
|
const expectedFormAUrl = formAEnabled ? staticFormAUrls.get(route) : undefined;
|
||||||
|
const expectedDeferredFormAUrl = formAEnabled ? deferredFormAUrls.get(route) : undefined;
|
||||||
const signals = unsafeSignals(html, formAEnabled);
|
const signals = unsafeSignals(html, formAEnabled);
|
||||||
const jotformFrames = iframeSources(html).filter((src) => {
|
const jotformFrames = iframeSources(html).filter((src) => {
|
||||||
try {
|
try {
|
||||||
@@ -99,6 +125,13 @@ export async function verifyArtifact(root, mode = 'staging') {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const deferredJotformFrames = deferredIframeSources(html).filter((src) => {
|
||||||
|
try {
|
||||||
|
return new URL(src).origin === 'https://form.jotform.com';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
const adapters = formAAdapters(html);
|
const adapters = formAAdapters(html);
|
||||||
if (!title) errors.push(`${route}: missing title`);
|
if (!title) errors.push(`${route}: missing title`);
|
||||||
if (!canonical) errors.push(`${route}: missing canonical`);
|
if (!canonical) errors.push(`${route}: missing canonical`);
|
||||||
@@ -122,8 +155,16 @@ export async function verifyArtifact(root, mode = 'staging') {
|
|||||||
errors.push(`${route}: Form A adapter lacks the approved ${expectedMode} ${expectedEnvironment} gate`);
|
errors.push(`${route}: Form A adapter lacks the approved ${expectedMode} ${expectedEnvironment} gate`);
|
||||||
}
|
}
|
||||||
formAIframes += jotformFrames.length;
|
formAIframes += jotformFrames.length;
|
||||||
} else if (jotformFrames.length || adapters.length || /262946078314059/i.test(html)) {
|
} else if (expectedDeferredFormAUrl) {
|
||||||
errors.push(`${route}: unauthorized Form A iframe or identifier in production artifact`);
|
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`);
|
||||||
|
}
|
||||||
|
deferredFormAIframes += deferredJotformFrames.length;
|
||||||
|
} else if (jotformFrames.length || deferredJotformFrames.length || adapters.length || /262946078314059/i.test(html)) {
|
||||||
|
errors.push(`${route}: unauthorized Form A iframe or identifier in ${mode} artifact`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nonSearchActions.length) activeForms += nonSearchActions.length;
|
if (nonSearchActions.length) activeForms += nonSearchActions.length;
|
||||||
@@ -160,10 +201,11 @@ export async function verifyArtifact(root, mode = 'staging') {
|
|||||||
digest.update('\n');
|
digest.update('\n');
|
||||||
}
|
}
|
||||||
if (formAEnabled) {
|
if (formAEnabled) {
|
||||||
const expectedFormAFiles = new Set([...formAProductionUrls.keys()].map((route) => route === '/' ? 'index.html' : `${route.slice(1)}/index.html`));
|
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));
|
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 production routes: ${unexpectedFormAFiles.join(', ')}`);
|
||||||
if (formAIframes !== formAProductionUrls.size) errors.push(`production artifact has ${formAIframes} approved Form A iframe(s), expected ${formAProductionUrls.size}`);
|
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) {
|
} else if (formAResourceFiles.length) {
|
||||||
errors.push(`isolated-test Form A identifier appears in artifact: ${formAResourceFiles.join(', ')}`);
|
errors.push(`isolated-test Form A identifier appears in artifact: ${formAResourceFiles.join(', ')}`);
|
||||||
}
|
}
|
||||||
@@ -175,6 +217,7 @@ export async function verifyArtifact(root, mode = 'staging') {
|
|||||||
reviewIndex,
|
reviewIndex,
|
||||||
activeNativeFormActions: activeForms,
|
activeNativeFormActions: activeForms,
|
||||||
formAIframes,
|
formAIframes,
|
||||||
|
deferredFormAIframes,
|
||||||
artifactFiles: files.length,
|
artifactFiles: files.length,
|
||||||
artifactSha256: digest.digest('hex'),
|
artifactSha256: digest.digest('hex'),
|
||||||
routes,
|
routes,
|
||||||
|
|||||||
@@ -16,11 +16,26 @@ async function fixture(mode = 'staging') {
|
|||||||
['lp/aba-private-pay/index.html', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
['lp/aba-private-pay/index.html', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||||
['lp/early-intervention/index.html', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
['lp/early-intervention/index.html', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||||
];
|
];
|
||||||
|
const consultationFrames = [
|
||||||
|
['client-consultation/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||||
|
['es/client-consultation/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||||
|
['schedule-consultation/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||||
|
];
|
||||||
|
const consultationDrawers = [
|
||||||
|
['aba-therapy/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||||
|
['autism-evaluations/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||||
|
['learner-social-club/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=en'],
|
||||||
|
['es/aba-therapy/index.html', 'https://form.jotform.com/262946078314059?formContext=consultation&language=es'],
|
||||||
|
['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'
|
const pages = mode === 'staging'
|
||||||
? [['index.html', 'noindex,nofollow', ''] , ['lp/index.html', 'noindex,nofollow', '']]
|
? [['index.html', 'noindex,nofollow', ''] , ['lp/index.html', 'noindex,nofollow', '']]
|
||||||
: mode === 'isolated-test'
|
: 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>`]),
|
...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', ''],
|
['lp/index.html', 'noindex,nofollow', ''],
|
||||||
]
|
]
|
||||||
: mode === 'production'
|
: mode === 'production'
|
||||||
@@ -49,10 +64,11 @@ test('indexing artifact requires sitemap and excludes review index', async () =>
|
|||||||
assert.equal(result.reviewIndex, false);
|
assert.equal(result.reviewIndex, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isolated-test artifact permits exactly the seven approved Form A iframe placements under staging safeguards', async () => {
|
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');
|
const result = await verifyArtifact(await fixture('isolated-test'), 'isolated-test');
|
||||||
assert.equal(result.passed, true);
|
assert.equal(result.passed, true);
|
||||||
assert.equal(result.formAIframes, 7);
|
assert.equal(result.formAIframes, 10);
|
||||||
|
assert.equal(result.deferredFormAIframes, 6);
|
||||||
assert.equal(result.sitemapUrls, 0);
|
assert.equal(result.sitemapUrls, 0);
|
||||||
assert.equal(result.reviewIndex, true);
|
assert.equal(result.reviewIndex, true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,12 +45,15 @@ test('Form A meeting preview requires explicit mock mode and remains staging-onl
|
|||||||
assert.equal(formAPreviewEnabled('TRUE', 'FALSE', 'mock'), true);
|
assert.equal(formAPreviewEnabled('TRUE', 'FALSE', 'mock'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('form mapping covers nine native instances and five landing journeys', () => {
|
test('form mapping covers twelve native instances and five landing journeys', () => {
|
||||||
assert.equal(formRouteMapping.filter((entry) => entry.kind === 'native-instance').length, 9);
|
assert.equal(formRouteMapping.filter((entry) => entry.kind === 'native-instance').length, 12);
|
||||||
assert.equal(formRouteMapping.filter((entry) => entry.kind === 'landing-journey').length, 5);
|
assert.equal(formRouteMapping.filter((entry) => entry.kind === 'landing-journey').length, 5);
|
||||||
assert.equal(logicalFormForRoute('/contact'), 'general');
|
assert.equal(logicalFormForRoute('/contact'), 'general');
|
||||||
assert.equal(logicalFormForRoute('/es/client-consultation'), 'service');
|
assert.equal(logicalFormForRoute('/es/client-consultation'), 'service');
|
||||||
assert.equal(logicalFormForRoute('/referrals'), 'referral');
|
assert.equal(logicalFormForRoute('/referrals'), 'referral');
|
||||||
|
for (const route of ['/es/aba-therapy', '/es/autism-evaluations', '/es/learner-social-club']) {
|
||||||
|
assert.equal(logicalFormForRoute(route), 'service');
|
||||||
|
}
|
||||||
for (const route of ['/lp/aba-therapy', '/lp/aba-insurance', '/lp/aba-ahcccs', '/lp/aba-private-pay']) {
|
for (const route of ['/lp/aba-therapy', '/lp/aba-insurance', '/lp/aba-ahcccs', '/lp/aba-private-pay']) {
|
||||||
assert.equal(logicalFormForRoute(route), 'general');
|
assert.equal(logicalFormForRoute(route), 'general');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user