diff --git a/CHANGELOG.md b/CHANGELOG.md index 48ffd88..81f6e10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2.3.1 — 2026-10-23 + +- Provide runnable Next.js App Router consumer example in `examples/next-app/` with Server Component layout, client boundaries, and route transitions (closes #39). +- Add comprehensive Content Security Policy (CSP) guidance in `docs/CSP.md` for Next.js, Vite, and strict environments. +- Enhance Vite examples with light and dark theme switching and client-side view transition simulation. +- Verify React 18/19 Server-Side Rendering (SSR) in `tests/consumerSsr.test.tsx` ensuring zero server browser-global access and no hydration divergence. +- Add browser integration regression for Next.js App Router client-side Link transitions preserving accessibility preferences across routes. + ## 2.3.0 — 2026-10-22 - Introduce accessible feature card and control primitives (`FeatureCard`, `ToggleControl`, `ChoiceControl`, `StepperControl`) (closes #36). diff --git a/README.md b/README.md index 81b7be7..ab3257b 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,7 @@ export function AccessibilitySettings() { ``` See [installation and Next.js](docs/INSTALLATION.md) for App Router and Pages -Router examples. +Router examples, runnable code in [`examples/next-app/`](examples/next-app/), and [strict CSP guidance](docs/CSP.md). ## API and limitations @@ -274,6 +274,7 @@ See [troubleshooting](docs/TROUBLESHOOTING.md) before filing an issue. - [API reference](docs/API.md) - [Feature behavior and bounds](docs/FEATURES.md) - [Installation and Next.js](docs/INSTALLATION.md) +- [Content Security Policy (CSP) guidance](docs/CSP.md) - [Custom features](docs/CUSTOM_FEATURES.md) - [Migration from v1 and development baselines](docs/MIGRATION.md) - [Architecture and lifecycle decisions](docs/ARCHITECTURE.md) diff --git a/docs/BUILD_AND_RELEASE_PLAN.md b/docs/BUILD_AND_RELEASE_PLAN.md index 1415324..2029c70 100644 --- a/docs/BUILD_AND_RELEASE_PLAN.md +++ b/docs/BUILD_AND_RELEASE_PLAN.md @@ -70,14 +70,14 @@ completed. Closing a child issue does not replace the milestone exit review. ### M1 — Safe core and accessible widget -- [ ] [A11Y-101 — Typed preferences registry and state reducer #32](https://github.com/prabothcharith/accessibility-react/issues/32) — Implement typed preference definitions, validation, defaults, stable feature IDs and reducer logic for effective settings and profile overrides. -- [ ] [A11Y-102 — Scoped effect manager and cleanup lifecycle #33](https://github.com/prabothcharith/accessibility-react/issues/33) — Implement provider-owned idempotent effect apply/dispose lifecycle with scoped styles, listener cleanup, widget exclusion and Strict Mode safety. -- [ ] [A11Y-103 — Versioned storage, reset, migration and no-storage fallback #34](https://github.com/prabothcharith/accessibility-react/issues/34) — Implement versioned `none`/session/local storage, migrations, validation, reset of owned keys, blocked-storage handling and temporary no-remember mode. -- [ ] [A11Y-104 — Accessible launcher and non-modal widget panel #35](https://github.com/prabothcharith/accessibility-react/issues/35) — Build the keyboard and screen-reader accessible non-modal launcher/panel with visible focus, Escape, focus restoration, responsive layout and stable scrolling. -- [ ] [A11Y-105 — Feature cards and state/value UI #36](https://github.com/prabothcharith/accessibility-react/issues/36) — Build reusable native feature controls for toggles, choices, sliders, search, values, statuses, errors, unsupported states and profile previews. -- [ ] [A11Y-106 — Position, oversized controls, statement link and help #37](https://github.com/prabothcharith/accessibility-react/issues/37) — Implement position and non-drag movement, oversized controls, safe-area actions, conditional statement link and help while reconciling existing positioning work. -- [ ] [A11Y-107 — Base locale infrastructure and English strings #38](https://github.com/prabothcharith/accessibility-react/issues/38) — Add typed English locale strings, configured/html-language fallback, BCP 47 and RTL handling, announcements and safe missing-key behavior. -- [ ] [A11Y-108 — Vite and Next.js App Router consumer integrations #39](https://github.com/prabothcharith/accessibility-react/issues/39) — Provide Vite and Next.js App Router consumers and verify hydration, portals, React peer versions, CSS, declarations, themes and CSP guidance. +- [x] [A11Y-101 — Typed preferences registry and state reducer #32](https://github.com/prabothcharith/accessibility-react/issues/32) — Implement typed preference definitions, validation, defaults, stable feature IDs and reducer logic for effective settings and profile overrides. +- [x] [A11Y-102 — Scoped effect manager and cleanup lifecycle #33](https://github.com/prabothcharith/accessibility-react/issues/33) — Implement provider-owned idempotent effect apply/dispose lifecycle with scoped styles, listener cleanup, widget exclusion and Strict Mode safety. +- [x] [A11Y-103 — Versioned storage, reset, migration and no-storage fallback #34](https://github.com/prabothcharith/accessibility-react/issues/34) — Implement versioned `none`/session/local storage, migrations, validation, reset of owned keys, blocked-storage handling and temporary no-remember mode. +- [x] [A11Y-104 — Accessible launcher and non-modal widget panel #35](https://github.com/prabothcharith/accessibility-react/issues/35) — Build the keyboard and screen-reader accessible non-modal launcher/panel with visible focus, Escape, focus restoration, responsive layout and stable scrolling. +- [x] [A11Y-105 — Feature cards and state/value UI #36](https://github.com/prabothcharith/accessibility-react/issues/36) — Build reusable native feature controls for toggles, choices, sliders, search, values, statuses, errors, unsupported states and profile previews. +- [x] [A11Y-106 — Position, oversized controls, statement link and help #37](https://github.com/prabothcharith/accessibility-react/issues/37) — Implement position and non-drag movement, oversized controls, safe-area actions, conditional statement link and help while reconciling existing positioning work. +- [x] [A11Y-107 — Base locale infrastructure and English strings #38](https://github.com/prabothcharith/accessibility-react/issues/38) — Add typed English locale strings, configured/html-language fallback, BCP 47 and RTL handling, announcements and safe missing-key behavior. +- [x] [A11Y-108 — Vite and Next.js App Router consumer integrations #39](https://github.com/prabothcharith/accessibility-react/issues/39) — Provide Vite and Next.js App Router consumers and verify hydration, portals, React peer versions, CSS, declarations, themes and CSP guidance. - [ ] [A11Y-109 — CI for typecheck, lint, unit, axe and browser smoke #40](https://github.com/prabothcharith/accessibility-react/issues/40) — Establish CI for typecheck, lint, unit/integration, relevant axe checks, packed browser smoke and supported browser jobs without publication credentials. ### M2 — Stable visual and typographic features diff --git a/docs/CSP.md b/docs/CSP.md new file mode 100644 index 0000000..76a24e1 --- /dev/null +++ b/docs/CSP.md @@ -0,0 +1,185 @@ +# Content Security Policy (CSP) Guidance + +This guide explains how `accessibility-plugin-react` operates under Content Security Policy (CSP), the specific directives involved, and copy-pasteable recipes for strict environments in Next.js, Vite, and static deployments. + +--- + +## 1. How the Library Works Under CSP + +`accessibility-plugin-react` is designed to be lightweight, modular, and dependency-free. However, because it dynamically adapts host pages according to user accessibility preferences, several CSP directives are relevant: + +### A. Scoped Stylesheet Injection (`style-src`) + +- **Default behavior:** Importing `accessibility-plugin-react` automatically injects its scoped stylesheet (`.arap-root { ... }`) into `document.head` via a runtime ` diff --git a/examples/basic-usage/main.tsx b/examples/basic-usage/main.tsx index b453cb8..05ad60e 100644 --- a/examples/basic-usage/main.tsx +++ b/examples/basic-usage/main.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import ReactDOM from "react-dom/client"; import { AccessibilityPlugin, @@ -22,6 +22,19 @@ import { } from "accessibility-plugin-react"; function App() { + const [dark, setDark] = useState(false); + const [activeTab, setActiveTab] = useState<"overview" | "article">( + "overview", + ); + + useEffect(() => { + if (dark) { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + }, [dark]); + return ( -

React Accessibility Plugin

-

- Click the blue button in the bottom-right corner to open the - accessibility panel. Try adjusting font size, contrast, and other - settings to see them applied in real-time. -

-

- This example demonstrates all 15 prebuilt feature controllers working - together in a single panel. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim - veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea - commodo consequat. -

+
+ + +
+ +
+ {activeTab === "overview" ? ( +
+

React Accessibility Plugin

+

+ Click the blue button in the bottom-right corner to open the + accessibility panel. Try adjusting font size, contrast, spacing, + and color filters to see them applied in real-time. +

+

+ This example demonstrates all 15 prebuilt feature controllers + working together with persistent settings, theme adaptation, and + dynamic view transitions. +

+

+ Learn more in the{" "} + custom feature example or view + the{" "} + + GitHub repository + + . +

+
+ ) : ( +
+

Article & Route Transition

+

+ This tab simulates a dynamic client-side route or page view + transition. +

+

+ Notice how any active preferences (such as enlarged text or + increased line height) are immediately applied to this new content + as it enters the DOM. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer + nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. + Nulla quis sem at nibh elementum imperdiet. +

+
+ )} +
); } diff --git a/examples/index.html b/examples/index.html index 85503a3..701e077 100644 --- a/examples/index.html +++ b/examples/index.html @@ -3,15 +3,104 @@ - Accessibility examples + Accessibility Plugin Examples & Consumer Integrations +
-

Accessibility Plugin examples

- +

Accessibility Plugin Examples

+

+ Explore runnable consumer examples demonstrating Vite integration, + Next.js App Router client boundaries, custom features, and strict CSP + support. +

+ +
+

+ All Built-in Features & Theme Demo + Vite +

+

+ Demonstrates all 15 prebuilt feature controllers, light and dark + themes, non-modal focus management, and dynamic tab/route transitions. +

+
+ +
+

+ Custom Feature Lifecycle + Vite +

+

+ Demonstrates registering site-specific custom features using + useAccessibility with provider-owned cleanup and + persistence. +

+
+ +
+

+ Next.js App Router Consumer + Next.js 16 +

+

+ A complete Next.js App Router application in + examples/next-app/ featuring Server Component layouts, + "use client" boundaries, route navigation + (Link), dark theme toggling, and strict Content Security + Policy headers. +

+

+ Run locally: npm install && npm run dev in + examples/next-app. +

+
diff --git a/examples/next-app/README.md b/examples/next-app/README.md new file mode 100644 index 0000000..a60889e --- /dev/null +++ b/examples/next-app/README.md @@ -0,0 +1,65 @@ +# Next.js App Router Consumer Example + +This example demonstrates how to integrate `accessibility-plugin-react` into a Next.js App Router application with Server Components, client boundaries, persistent layouts, dark themes, and strict Content Security Policy (CSP). + +--- + +## Key Integration Patterns + +### 1. Client Boundary + +Keep the widget inside a Client Component: + +```tsx +// app/components/AccessibilitySettings.tsx +"use client"; + +import { + AccessibilityPlugin, + AccessibilityButton, + AccessibilityList, + FontSizeController, + ResetController, +} from "accessibility-plugin-react"; + +export function AccessibilitySettings() { + return ( + + + + + + + + ); +} +``` + +### 2. Persistent Layout + +Mount the `` component once inside `app/layout.tsx`. Because `layout.tsx` persists across route transitions, preferences and effect lifecycles are preserved when navigating between pages. + +### 3. Strict CSP Guidance + +See `next.config.mjs` for a working strict CSP header configuration. When `style-src 'unsafe-inline'` is restricted, import the static stylesheet: + +```tsx +import "accessibility-plugin-react/style.css"; +``` + +For complete CSP guidance, consult [docs/CSP.md](../../docs/CSP.md). + +--- + +## Running this Example + +```sh +# First, ensure the parent library is built +npm run build --prefix ../.. + +# Install and start the Next.js dev server +npm install +npm run dev +``` + +Visit [http://localhost:3000](http://localhost:3000) to view the application. diff --git a/examples/next-app/app/about/page.tsx b/examples/next-app/app/about/page.tsx new file mode 100644 index 0000000..173feab --- /dev/null +++ b/examples/next-app/app/about/page.tsx @@ -0,0 +1,27 @@ +import Link from "next/link"; + +export default function AboutPage() { + return ( +
+

About This Integration

+

+ You are now on the /about route. +

+

+ Because the AccessibilityPlugin provider is mounted in the + root layout.tsx, it maintains persistent effect management + and state across route navigations. +

+

+ When you navigate to this route, any active accessibility preferences + (such as enlarged text, custom line height, or contrast palettes) are + automatically and safely applied to this newly rendered page content. +

+

+ + ← Return to Home + +

+
+ ); +} diff --git a/examples/next-app/app/components/AccessibilitySettings.tsx b/examples/next-app/app/components/AccessibilitySettings.tsx new file mode 100644 index 0000000..7298b84 --- /dev/null +++ b/examples/next-app/app/components/AccessibilitySettings.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { + AccessibilityPlugin, + AccessibilityButton, + AccessibilityList, + FontSizeController, + LineHeightController, + LetterSpacingController, + ContrastController, + SaturationController, + InvertColorsController, + GrayscaleController, + LinkHighlightController, + TextAlignController, + HideImagesController, + HideVideosController, + BigCursorController, + ReadingMaskController, + ReadingGuideController, + ResetController, +} from "accessibility-plugin-react"; + +export function AccessibilitySettings() { + return ( + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/examples/next-app/app/components/ThemeToggle.tsx b/examples/next-app/app/components/ThemeToggle.tsx new file mode 100644 index 0000000..a734598 --- /dev/null +++ b/examples/next-app/app/components/ThemeToggle.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useEffect, useState } from "react"; + +export function ThemeToggle() { + const [dark, setDark] = useState(false); + + useEffect(() => { + if (dark) { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + }, [dark]); + + return ( + + ); +} diff --git a/examples/next-app/app/host.css b/examples/next-app/app/host.css new file mode 100644 index 0000000..e1fa3af --- /dev/null +++ b/examples/next-app/app/host.css @@ -0,0 +1,100 @@ +:root { + --bg-color: #f9fafb; + --text-color: #111827; + --card-bg: #ffffff; + --card-border: #e5e7eb; + --primary: #2563eb; + --primary-hover: #1d4ed8; +} + +html.dark { + --bg-color: #0f172a; + --text-color: #f8fafc; + --card-bg: #1e293b; + --card-border: #334155; + --primary: #3b82f6; + --primary-hover: #60a5fa; +} + +body { + margin: 0; + padding: 0; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, + sans-serif; + background-color: var(--bg-color); + color: var(--text-color); + transition: + background-color 0.2s ease, + color 0.2s ease; + min-height: 100vh; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + background-color: var(--card-bg); + border-bottom: 1px solid var(--card-border); +} + +.nav { + display: flex; + gap: 16px; +} + +.nav-link { + color: var(--primary); + text-decoration: none; + font-weight: 500; + padding: 6px 12px; + border-radius: 6px; +} + +.nav-link:hover { + text-decoration: underline; +} + +.theme-toggle-btn { + background: transparent; + border: 1px solid var(--card-border); + color: var(--text-color); + padding: 8px 14px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; +} + +.theme-toggle-btn:hover { + border-color: var(--primary); +} + +.content { + max-width: 800px; + margin: 40px auto; + padding: 0 20px; +} + +.card { + background-color: var(--card-bg); + border: 1px solid var(--card-border); + border-radius: 12px; + padding: 32px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.btn-primary { + display: inline-block; + background-color: var(--primary); + color: #ffffff; + padding: 10px 18px; + border-radius: 6px; + text-decoration: none; + font-weight: 500; + margin-top: 16px; +} + +.btn-primary:hover { + background-color: var(--primary-hover); +} diff --git a/examples/next-app/app/layout.tsx b/examples/next-app/app/layout.tsx new file mode 100644 index 0000000..6a3786c --- /dev/null +++ b/examples/next-app/app/layout.tsx @@ -0,0 +1,36 @@ +import type { ReactNode } from "react"; +import Link from "next/link"; +import { AccessibilitySettings } from "./components/AccessibilitySettings"; +import { ThemeToggle } from "./components/ThemeToggle"; +import "./host.css"; + +export const metadata = { + title: "Next.js App Router — Accessibility Plugin Example", + description: + "Next.js App Router integration with client boundary, route transitions, and strict CSP guidance", +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + +
+ + +
+
+ {children} +
+ {/* Mount one provider per document in the persistent root layout */} + + + + ); +} diff --git a/examples/next-app/app/page.tsx b/examples/next-app/app/page.tsx new file mode 100644 index 0000000..56c2835 --- /dev/null +++ b/examples/next-app/app/page.tsx @@ -0,0 +1,46 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+

Next.js App Router Integration

+

+ This example demonstrates how to integrate{" "} + accessibility-plugin-react into a Next.js App Router + application with Server Components, client boundaries, and persistent + root layouts. +

+

Key Architectural Highlights

+
    +
  • + Client Boundary: The widget is isolated in a{" "} + "use client" component ( + AccessibilitySettings.tsx), keeping the root layout a + Server Component. +
  • +
  • + No Hydration Mismatch: WidgetPortal{" "} + defers DOM portal insertion to the client using{" "} + useSyncExternalStore, avoiding SSR divergence. +
  • +
  • + Route Resilience: Click{" "} + About Page to navigate. Notice how your + accessibility preferences (font size, contrast, spacing) persist + across Next.js client-side route transitions without reset or + flickering. +
  • +
  • + Dark Mode Support: Toggle the theme button in the + header. The Clean light widget maintains legible contrast against dark + host backgrounds without being inverted. +
  • +
+

+ + Visit About Page → + +

+
+ ); +} diff --git a/examples/next-app/next.config.mjs b/examples/next-app/next.config.mjs new file mode 100644 index 0000000..7abaf57 --- /dev/null +++ b/examples/next-app/next.config.mjs @@ -0,0 +1,35 @@ +/** @type {import('next').NextConfig} */ +const cspHeader = ` + default-src 'self'; + script-src 'self' 'unsafe-eval' 'unsafe-inline'; + style-src 'self' 'unsafe-inline'; + style-src-attr 'unsafe-inline'; + img-src 'self' data:; + font-src 'self'; + object-src 'none'; + base-uri 'self'; + form-action 'self'; + frame-ancestors 'none'; +` + .replace(/\s{2,}/g, " ") + .trim(); + +const nextConfig = { + reactStrictMode: true, + outputFileTracingRoot: import.meta.dirname, + async headers() { + return [ + { + source: "/(.*)", + headers: [ + { + key: "Content-Security-Policy", + value: cspHeader, + }, + ], + }, + ]; + }, +}; + +export default nextConfig; diff --git a/examples/next-app/package.json b/examples/next-app/package.json new file mode 100644 index 0000000..e2f493e --- /dev/null +++ b/examples/next-app/package.json @@ -0,0 +1,23 @@ +{ + "name": "example-next-app", + "version": "1.0.0", + "private": true, + "description": "Runnable Next.js App Router consumer example for accessibility-plugin-react", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "accessibility-plugin-react": "file:../..", + "next": "^16.2.10", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } +} diff --git a/examples/next-app/tsconfig.json b/examples/next-app/tsconfig.json new file mode 100644 index 0000000..2c145a2 --- /dev/null +++ b/examples/next-app/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/package-lock.json b/package-lock.json index 6bb9070..013e196 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "accessibility-plugin-react", - "version": "2.2.2", + "version": "2.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "accessibility-plugin-react", - "version": "2.2.2", + "version": "2.3.1", "license": "ISC", "devDependencies": { "@axe-core/playwright": "^4.13.0", diff --git a/package.json b/package.json index e38fd5c..eb5b5cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "accessibility-plugin-react", - "version": "2.3.0", + "version": "2.3.1", "description": "A composable React display-preference widget with a clean light design, persistent settings and movable accessible controls.", "main": "dist/index.js", "module": "dist/index.mjs", @@ -38,6 +38,7 @@ "test:browser": "playwright test", "test:consumers": "npm run build && npm run pack:check && node scripts/prepare-consumers.mjs", "example": "vite examples", + "example:next": "npm --prefix examples/next-app run dev", "format": "prettier --write src tests scripts examples docs README.md AGENTS.md CHANGELOG.md *.ts *.json *.mjs .github", "format:check": "prettier --check src tests scripts examples docs README.md AGENTS.md CHANGELOG.md *.ts *.json *.mjs .github" }, diff --git a/scripts/prepare-consumers.mjs b/scripts/prepare-consumers.mjs index c0c5dd6..c60d2e3 100644 --- a/scripts/prepare-consumers.mjs +++ b/scripts/prepare-consumers.mjs @@ -130,7 +130,12 @@ export const invalid = content}`, + `import Fixture from '../Fixture'; import Link from 'next/link'; export default function Page(){return <>
Go to Route Test
}`, + ); + await mkdir(`${directory}/app/route-test`, { recursive: true }); + await writeFile( + `${directory}/app/route-test/page.jsx`, + `import Fixture from '../../Fixture'; import Link from 'next/link'; export default function RoutePage(){return <>
Back to Home
}`, ); await writeFile( `${directory}/pages/_app.jsx`, diff --git a/tests/browser/widget.spec.ts b/tests/browser/widget.spec.ts index f5468d1..4163604 100644 --- a/tests/browser/widget.spec.ts +++ b/tests/browser/widget.spec.ts @@ -218,6 +218,47 @@ test("Next Pages Router and direct Server Component imports hydrate", async ({ expect(errors).toEqual([]); }); +test("Next.js App Router client-side route transitions preserve preferences without hydration errors", async ({ + page, +}, info) => { + test.skip(info.project.name !== "next-app"); + await page.addInitScript(() => localStorage.clear()); + const errors: string[] = []; + page.on("pageerror", (error) => errors.push(error.message)); + + await page.goto("/"); + await page.getByLabel("Accessibility", { exact: true }).click(); + await page.getByLabel("Increase font size").click(); + await page.getByLabel("Close accessibility settings").click(); + + // Wait for preference write to storage + await page.waitForFunction(() => + localStorage + .getItem("react-accessibility-plugin:state") + ?.includes("fontSize"), + ); + + // Navigate to route-test via client-side Next Link + await page.locator("#link-route-test").click(); + await expect(page.locator("#link-home")).toBeVisible(); + + // Settings must remain applied on the new route + await page.getByLabel("Accessibility", { exact: true }).click(); + await expect( + page + .getByRole("group", { name: "Font Size", exact: true }) + .locator("output"), + ).toHaveText("110%"); + await page.getByLabel("Close accessibility settings").click(); + + // Navigate back to home + await page.locator("#link-home").click(); + await expect(page.locator("#link-route-test")).toBeVisible(); + + expect(errors).toEqual([]); + await page.evaluate(() => localStorage.clear()); +}); + test("trigger drag remembers placement and preferences without accidental activation", async ({ page, browserName, diff --git a/tests/consumerSsr.test.tsx b/tests/consumerSsr.test.tsx new file mode 100644 index 0000000..839229e --- /dev/null +++ b/tests/consumerSsr.test.tsx @@ -0,0 +1,161 @@ +import { describe, it, expect } from "vitest"; +import React from "react"; +import { renderToString } from "react-dom/server"; +import { + AccessibilityPlugin, + AccessibilityButton, + AccessibilityList, + FontSizeController, + LineHeightController, + LetterSpacingController, + ContrastController, + SaturationController, + InvertColorsController, + GrayscaleController, + LinkHighlightController, + TextAlignController, + HideImagesController, + HideVideosController, + BigCursorController, + ReadingMaskController, + ReadingGuideController, + ResetController, + FeatureCard, + ToggleControl, + ChoiceControl, + StepperControl, + ProfilePreviewCard, + PROFILE_DEFINITIONS, + SearchCount, + EmptyState, +} from "../src"; + +describe("Server-Side Rendering (SSR) & Consumer Boundaries", () => { + it("renders AccessibilityPlugin and all 15 controllers to string without errors or browser-global access", () => { + // During SSR in Next.js Server Components, window/localStorage are either undefined or should never be accessed. + const html = renderToString( + + + + + + + + + + + + + + + + + + + +
+

Server Content

+

Initial server rendered text

+
+
, + ); + + // Host children must render + expect(html).toContain("Server Content"); + expect(html).toContain("Initial server rendered text"); + + // Client portal must return null on the server to prevent hydration mismatch + expect(html).not.toContain("arap-root"); + expect(html).not.toContain("arap-panel"); + }); + + it("renders control primitives and profile cards to string without errors", () => { + const html = renderToString( +
+ + {}} + /> + + + + {}} + /> + + + {}} + /> + + {}} + onRevert={() => {}} + /> + + + +
, + ); + + expect(html).toContain("Text Scaling"); + expect(html).toContain("120%"); + expect(html).toContain(PROFILE_DEFINITIONS["motor"].label); + expect(html).toContain("Showing 3 of 15 tools matching "font""); + expect(html).toContain("No tools found"); + }); + + it("renders correctly when rendered as child in Server Component layout", () => { + function ServerLayout({ children }: { children: React.ReactNode }) { + return ( + + +
Layout Header
+
{children}
+ + + + + + + + + ); + } + + const html = renderToString( + +

Page Content

+
, + ); + + expect(html).toContain("Layout Header"); + expect(html).toContain("Page Content"); + }); +});