@etus/bhono-app 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +46 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +26 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/generator.d.ts +14 -0
- package/dist/generator.js +142 -0
- package/dist/generator.js.map +1 -0
- package/dist/generator.test.d.ts +1 -0
- package/dist/generator.test.js +127 -0
- package/dist/generator.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +25 -0
- package/dist/prompts.js +83 -0
- package/dist/prompts.js.map +1 -0
- package/dist/prompts.test.d.ts +1 -0
- package/dist/prompts.test.js +24 -0
- package/dist/prompts.test.js.map +1 -0
- package/dist/providers/cloudflare.d.ts +37 -0
- package/dist/providers/cloudflare.js +61 -0
- package/dist/providers/cloudflare.js.map +1 -0
- package/dist/providers/cloudflare.test.d.ts +1 -0
- package/dist/providers/cloudflare.test.js +29 -0
- package/dist/providers/cloudflare.test.js.map +1 -0
- package/dist/providers/github.d.ts +16 -0
- package/dist/providers/github.js +57 -0
- package/dist/providers/github.js.map +1 -0
- package/dist/providers/github.test.d.ts +1 -0
- package/dist/providers/github.test.js +16 -0
- package/dist/providers/github.test.js.map +1 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +88 -0
- package/dist/templates.js.map +1 -0
- package/dist/templates.test.d.ts +1 -0
- package/dist/templates.test.js +26 -0
- package/dist/templates.test.js.map +1 -0
- package/package.json +36 -0
- package/templates/base/.claude/agents/architect-review.md +160 -0
- package/templates/base/.claude/agents/backend-architect.md +308 -0
- package/templates/base/.claude/agents/code-reviewer.md +170 -0
- package/templates/base/.claude/agents/performance-engineer.md +166 -0
- package/templates/base/.claude/agents/test-automator.md +219 -0
- package/templates/base/.claude/commands/check-skill-rules.md +53 -0
- package/templates/base/.claude/commands/claude-md.md +250 -0
- package/templates/base/.claude/commands/code-prompt.md +212 -0
- package/templates/base/.claude/commands/explain-code.md +194 -0
- package/templates/base/.claude/commands/init-projec.md +89 -0
- package/templates/base/.claude/commands/linear/README.md +297 -0
- package/templates/base/.claude/commands/linear/create-issue.md +190 -0
- package/templates/base/.claude/commands/linear/implement-issue.md +248 -0
- package/templates/base/.claude/commands/linear/process-triage.md +399 -0
- package/templates/base/.claude/commands/linear/setup.md +180 -0
- package/templates/base/.claude/commands/prime.md +9 -0
- package/templates/base/.claude/commands/review-gap.md +10 -0
- package/templates/base/.claude/commands/setup-aa.md +311 -0
- package/templates/base/.claude/commands/ship.md +262 -0
- package/templates/base/.claude/commands/tools.md +3 -0
- package/templates/base/.claude/docs/claude-progress.txt +107 -0
- package/templates/base/.claude/hooks/package-lock.json +556 -0
- package/templates/base/.claude/hooks/package.json +16 -0
- package/templates/base/.claude/hooks/skill-activation-prompt.sh +7 -0
- package/templates/base/.claude/hooks/skill-activation-prompt.ts +142 -0
- package/templates/base/.claude/hooks/tsconfig.json +19 -0
- package/templates/base/.claude/scripts/check-updates.sh +85 -0
- package/templates/base/.claude/scripts/install_pkgs.sh +66 -0
- package/templates/base/.claude/scripts/setup-project.sh +177 -0
- package/templates/base/.claude/scripts/validate-skill-rules.sh +94 -0
- package/templates/base/.claude/settings.json +113 -0
- package/templates/base/.claude/settings.local.json +11 -0
- package/templates/base/.claude/skills/architecture-analyzer/SKILL.md +531 -0
- package/templates/base/.claude/skills/architecture-analyzer/assets/report-template.md +215 -0
- package/templates/base/.claude/skills/architecture-analyzer/references/c4-templates.md +234 -0
- package/templates/base/.claude/skills/architecture-analyzer/references/confidence-levels.md +203 -0
- package/templates/base/.claude/skills/architecture-analyzer/scripts/analyze_structure.py +266 -0
- package/templates/base/.claude/skills/architecture-analyzer/scripts/analyze_tech_debt.py +776 -0
- package/templates/base/.claude/skills/architecture-analyzer/scripts/extract_apis.py +338 -0
- package/templates/base/.claude/skills/architecture-analyzer/scripts/generate_c4.py +283 -0
- package/templates/base/.claude/skills/architecture-analyzer/scripts/generate_erd.py +935 -0
- package/templates/base/.claude/skills/architecture-analyzer/scripts/map_dependencies.py +555 -0
- package/templates/base/.claude/skills/dev-browser/SKILL.md +318 -0
- package/templates/base/.claude/skills/dev-browser/bun.lock +443 -0
- package/templates/base/.claude/skills/dev-browser/package-lock.json +2927 -0
- package/templates/base/.claude/skills/dev-browser/package.json +27 -0
- package/templates/base/.claude/skills/dev-browser/scripts/start-server.ts +117 -0
- package/templates/base/.claude/skills/dev-browser/server.sh +24 -0
- package/templates/base/.claude/skills/dev-browser/src/client.ts +403 -0
- package/templates/base/.claude/skills/dev-browser/src/index.ts +281 -0
- package/templates/base/.claude/skills/dev-browser/src/snapshot/__tests__/snapshot.test.ts +223 -0
- package/templates/base/.claude/skills/dev-browser/src/snapshot/browser-script.ts +877 -0
- package/templates/base/.claude/skills/dev-browser/src/snapshot/index.ts +14 -0
- package/templates/base/.claude/skills/dev-browser/src/snapshot/inject.ts +13 -0
- package/templates/base/.claude/skills/dev-browser/src/types.ts +27 -0
- package/templates/base/.claude/skills/dev-browser/tsconfig.json +36 -0
- package/templates/base/.claude/skills/dev-browser/vitest.config.ts +12 -0
- package/templates/base/.claude/skills/linear/SKILL.md +440 -0
- package/templates/base/.claude/skills/linear/examples.md +262 -0
- package/templates/base/.claude/skills/linear/lib/client.ts +51 -0
- package/templates/base/.claude/skills/linear/lib/config.ts +106 -0
- package/templates/base/.claude/skills/linear/lib/output.ts +34 -0
- package/templates/base/.claude/skills/linear/package-lock.json +698 -0
- package/templates/base/.claude/skills/linear/package.json +27 -0
- package/templates/base/.claude/skills/linear/reference.md +263 -0
- package/templates/base/.claude/skills/linear/scripts/comments/create.ts +47 -0
- package/templates/base/.claude/skills/linear/scripts/comments/list.ts +47 -0
- package/templates/base/.claude/skills/linear/scripts/issues/archive.ts +30 -0
- package/templates/base/.claude/skills/linear/scripts/issues/create.ts +279 -0
- package/templates/base/.claude/skills/linear/scripts/issues/get.ts +68 -0
- package/templates/base/.claude/skills/linear/scripts/issues/list.ts +67 -0
- package/templates/base/.claude/skills/linear/scripts/issues/update.ts +281 -0
- package/templates/base/.claude/skills/linear/scripts/labels/add-to-issue.ts +63 -0
- package/templates/base/.claude/skills/linear/scripts/labels/create.ts +45 -0
- package/templates/base/.claude/skills/linear/scripts/labels/list.ts +30 -0
- package/templates/base/.claude/skills/linear/scripts/list-teams.ts +52 -0
- package/templates/base/.claude/skills/linear/scripts/setup/setup-credentials.ts +96 -0
- package/templates/base/.claude/skills/linear/scripts/status/list.ts +31 -0
- package/templates/base/.claude/skills/linear/scripts/status/set-by-name.ts +78 -0
- package/templates/base/.claude/skills/linear/scripts/status/update.ts +44 -0
- package/templates/base/.claude/skills/linear/scripts/users/list.ts +59 -0
- package/templates/base/.claude/skills/linear/scripts/users/me.ts +20 -0
- package/templates/base/.claude/skills/linear/templates/README.md +203 -0
- package/templates/base/.claude/skills/linear/templates/api-reference.md +258 -0
- package/templates/base/.claude/skills/linear/templates/bug-report.md +99 -0
- package/templates/base/.claude/skills/linear/templates/feature-request.md +118 -0
- package/templates/base/.claude/skills/linear/templates/security-issue.md +162 -0
- package/templates/base/.claude/skills/linear/templates/sprint-task.md +175 -0
- package/templates/base/.claude/skills/linear/templates/tech-debt.md +137 -0
- package/templates/base/.claude/skills/linear/tsconfig.json +17 -0
- package/templates/base/.claude/skills/linear/workflows/issue-lifecycle.md +317 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/SKILL.md +113 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/assets/global-setup.template.js +97 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/assets/playwright.config.template.js +171 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/assets/test-template.spec.js +163 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/examples/README.md +26 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/examples/ads.email-deeplink.spec.ts +12 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/examples/mobile.realism.spec.ts +16 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/examples/smoke.home.spec.ts +6 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/architecture.md +578 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/best-practices.md +260 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/ci-reporting.md +86 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/debugging.md +629 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/mobile-realism.md +50 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/optimization.md +488 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/patterns.md +513 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/resources.md +44 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/visual-a11y.md +66 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/scripts/auth-setup.js +202 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/scripts/performance-analyzer.js +240 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/scripts/trace-url.js +132 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/ci/github-actions.playwright.yml +78 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/fixtures.ts +44 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/global-setup.template.js +97 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/global.setup.ts +35 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/helpers/ad-gpt-observer.ts +80 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/helpers/chromium-mobile-profile.ts +93 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/playwright.config.template.js +171 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/playwright.config.ts +126 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/test-template.spec.js +163 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/tests/email-deeplink.ads.spec.ts +44 -0
- package/templates/base/.claude/skills/skill-rules.json +184 -0
- package/templates/base/.claude/skills/wrangler/SKILL.md +209 -0
- package/templates/base/.claude/skills/wrangler/resources/api.md +494 -0
- package/templates/base/.claude/skills/wrangler/resources/bundling.md +83 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/cert.md +64 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/check.md +66 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/containers.md +157 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/d1.md +843 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/delete.md +27 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/deploy.md +139 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/deployments.md +56 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/dev.md +157 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/dispatch-namespace.md +69 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/docs.md +61 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/how-to-run.md +62 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/hyperdrive.md +425 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/init.md +31 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/kv-bulk.md +265 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/kv-key.md +353 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/kv-namespace.md +265 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/login.md +23 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/logout.md +19 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/mtls-certificate.md +69 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/pages.md +175 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/pipelines.md +76 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/queues.md +132 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/r2-bucket.md +342 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/r2-object.md +267 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/r2-sql.md +65 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/rollback.md +40 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/secret.md +308 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/secrets-store-secret.md +100 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/secrets-store-store.md +60 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/setup.md +67 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/tail.md +37 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/telemetry.md +64 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/triggers.md +39 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/types.md +73 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/vectorize.md +941 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/versions.md +95 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/whoami.md +49 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/workflows.md +117 -0
- package/templates/base/.claude/skills/wrangler/resources/commands.md +138 -0
- package/templates/base/.claude/skills/wrangler/resources/configuration.md +2176 -0
- package/templates/base/.claude/skills/wrangler/resources/custom-builds.md +55 -0
- package/templates/base/.claude/skills/wrangler/resources/deprecations.md +279 -0
- package/templates/base/.claude/skills/wrangler/resources/enviroments.md +416 -0
- package/templates/base/.claude/skills/wrangler/resources/extract_sections.py +119 -0
- package/templates/base/.claude/skills/wrangler/resources/process_content.py +94 -0
- package/templates/base/.claude/skills/wrangler/resources/system-enviroments-variables.md +120 -0
- package/templates/base/.dev.vars.example +15 -0
- package/templates/base/.env.example +29 -0
- package/templates/base/.github/workflows/test.yml +127 -0
- package/templates/base/.nycrc.json +16 -0
- package/templates/base/CLAUDE.md +218 -0
- package/templates/base/README.md +670 -0
- package/templates/base/auth-setup-error.png +0 -0
- package/templates/base/config/drizzle.config.ts +10 -0
- package/templates/base/config/eslint.config.js +364 -0
- package/templates/base/config/wrangler.json +76 -0
- package/templates/base/docs/app_spec.txt +879 -0
- package/templates/base/docs/app_spec_template.md +681 -0
- package/templates/base/docs/architecture/README.md +8 -0
- package/templates/base/docs/architecture/data-requirements.md +109 -0
- package/templates/base/docs/architecture/erd.md +91 -0
- package/templates/base/docs/features/feature_list.json +3128 -0
- package/templates/base/docs/hono-boilerplate-plan.md +1774 -0
- package/templates/base/docs/test-coverage-gap-analysis.md +242 -0
- package/templates/base/docs/testing.md +188 -0
- package/templates/base/index.html +16 -0
- package/templates/base/package.json +115 -0
- package/templates/base/playwright.config.ts +158 -0
- package/templates/base/pnpm-lock.yaml +8175 -0
- package/templates/base/scripts/capture-prod-session.ts +250 -0
- package/templates/base/scripts/generate-openapi.ts +23 -0
- package/templates/base/scripts/init.sh +121 -0
- package/templates/base/src/client/__tests__/button.test.tsx +30 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-dashboard-stats-cards-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-quick-action-cards-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-recent-activity-section-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-user-first-name-in-welcome-message-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-user-information-in-sidebar-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-render-dashboard-when-authenticated-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-show-navigation-sidebar-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-unauthenticated-should-redirect-to-login-when-not-authenticated-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/login.test.tsx/Login-Page-should-display-Google-OAuth-login-button-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/login.test.tsx/Login-Page-should-have-a-link-back-to-home-page-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/login.test.tsx/Login-Page-should-render-login-content-without-waiting-for-authentication-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/login.test.tsx/Login-Page-should-render-login-page-at--login-route-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/login.test.tsx/Login-Page-should-show-Terms-of-Service-and-Privacy-Policy-links-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/login.test.tsx/Login-Page-should-trigger-OAuth-flow-when-clicking-login-button-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-404-handling-should-display-404-text-on-not-found-page-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-404-handling-should-have-navigation-options-on-404-page-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-404-handling-should-render-404-page-for-unknown-routes-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-account-page-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-integrations-page-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-settings-page-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-team-page-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-home-page-navigation-should-display-navigation-links-on-home-page-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-home-page-navigation-should-have-correct-link-destinations-on-home-page-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-allow-access-to-home-page-without-authentication-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-allow-access-to-login-page-without-authentication-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-redirect-unauthenticated-users-from-dashboard-to-login-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-redirect-unauthenticated-users-from-settings-to-login-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-redirect-unauthenticated-users-from-team-to-login-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/authenticated-layout.test.tsx +252 -0
- package/templates/base/src/client/__tests__/routes/dashboard.test.tsx +136 -0
- package/templates/base/src/client/__tests__/routes/error-components.test.tsx +186 -0
- package/templates/base/src/client/__tests__/routes/login.test.tsx +112 -0
- package/templates/base/src/client/__tests__/routes/navigation.test.tsx +272 -0
- package/templates/base/src/client/__tests__/routes/root-layout.test.tsx +65 -0
- package/templates/base/src/client/__tests__/setup-browser.ts +15 -0
- package/templates/base/src/client/__tests__/setup.ts +32 -0
- package/templates/base/src/client/__tests__/test-utils.tsx +135 -0
- package/templates/base/src/client/api/.gitkeep +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-can-be-collapsed-by-default-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-expands-when-collapsed-and-expand-button-is-clicked-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-handles-logout-button-click-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-handles-navigation-clicks-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-hides-navigation-labels-when-collapsed-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-highlights-active-route-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-renders-sidebar-navigation-items-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-shows-keyboard-shortcut-hint-when-expanded-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-shows-user-info-when-authenticated-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-shows-user-initials-in-avatar-fallback-1.png +0 -0
- package/templates/base/src/client/components/__tests__/error-boundary.test.tsx +97 -0
- package/templates/base/src/client/components/__tests__/sidebar.test.tsx +281 -0
- package/templates/base/src/client/components/error-boundary.tsx +68 -0
- package/templates/base/src/client/components/icons.tsx +106 -0
- package/templates/base/src/client/components/layout/.gitkeep +0 -0
- package/templates/base/src/client/components/sidebar.tsx +267 -0
- package/templates/base/src/client/components/ui/.gitkeep +0 -0
- package/templates/base/src/client/components/ui/__tests__/avatar.test.tsx +308 -0
- package/templates/base/src/client/components/ui/__tests__/card.test.tsx +214 -0
- package/templates/base/src/client/components/ui/__tests__/dialog.test.tsx +297 -0
- package/templates/base/src/client/components/ui/__tests__/error-fallback.test.tsx +145 -0
- package/templates/base/src/client/components/ui/__tests__/input.test.tsx +98 -0
- package/templates/base/src/client/components/ui/__tests__/loading-skeleton.test.tsx +139 -0
- package/templates/base/src/client/components/ui/__tests__/skeleton.test.tsx +44 -0
- package/templates/base/src/client/components/ui/__tests__/sonner.test.tsx +28 -0
- package/templates/base/src/client/components/ui/__tests__/tabs.test.tsx +233 -0
- package/templates/base/src/client/components/ui/avatar.tsx +101 -0
- package/templates/base/src/client/components/ui/badge.tsx +46 -0
- package/templates/base/src/client/components/ui/button.tsx +72 -0
- package/templates/base/src/client/components/ui/card.tsx +86 -0
- package/templates/base/src/client/components/ui/dialog.tsx +140 -0
- package/templates/base/src/client/components/ui/error-fallback.tsx +179 -0
- package/templates/base/src/client/components/ui/form.tsx +172 -0
- package/templates/base/src/client/components/ui/input.tsx +24 -0
- package/templates/base/src/client/components/ui/label.tsx +22 -0
- package/templates/base/src/client/components/ui/loading-skeleton.tsx +154 -0
- package/templates/base/src/client/components/ui/separator.tsx +33 -0
- package/templates/base/src/client/components/ui/skeleton.tsx +16 -0
- package/templates/base/src/client/components/ui/sonner.tsx +29 -0
- package/templates/base/src/client/components/ui/tabs.tsx +121 -0
- package/templates/base/src/client/hooks/.gitkeep +0 -0
- package/templates/base/src/client/hooks/__tests__/use-auth.test.tsx +306 -0
- package/templates/base/src/client/hooks/__tests__/use-theme.test.tsx +172 -0
- package/templates/base/src/client/hooks/use-auth.ts +53 -0
- package/templates/base/src/client/hooks/use-theme.tsx +78 -0
- package/templates/base/src/client/index.css +881 -0
- package/templates/base/src/client/lib/query-client.ts +11 -0
- package/templates/base/src/client/lib/utils.ts +7 -0
- package/templates/base/src/client/main.tsx +26 -0
- package/templates/base/src/client/routeTree.gen.ts +258 -0
- package/templates/base/src/client/router.ts +15 -0
- package/templates/base/src/client/routes/$.tsx +77 -0
- package/templates/base/src/client/routes/.gitkeep +0 -0
- package/templates/base/src/client/routes/__root.tsx +34 -0
- package/templates/base/src/client/routes/__tests__/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-display-accept-invitation-button-1.png +0 -0
- package/templates/base/src/client/routes/__tests__/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-display-decline-button-linking-to-homepage-1.png +0 -0
- package/templates/base/src/client/routes/__tests__/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-display-invitation-details--email--workspace--role--1.png +0 -0
- package/templates/base/src/client/routes/__tests__/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-have-a-logo-link-to-homepage-1.png +0 -0
- package/templates/base/src/client/routes/__tests__/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-render-invitation-page-with-inviter-name-and-workspace-1.png +0 -0
- package/templates/base/src/client/routes/__tests__/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-show-terms-of-service-and-privacy-policy-links-1.png +0 -0
- package/templates/base/src/client/routes/__tests__/invite.test.tsx +138 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/account.test.tsx/Account-Page-should-render-Active-Sessions-section-with-session-cards-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/account.test.tsx/Account-Page-should-render-page-with-correct-title--Account--1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/account.test.tsx/Account-Page-should-show-API-Access-section-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/account.test.tsx/Account-Page-should-show-Connected-Accounts-section-with-Google-connected-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/account.test.tsx/Account-Page-should-show-Danger-Zone-section-with-delete-button-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/account.test.tsx/Account-Page-should-show-Security-section-with-Two-Factor-Authentication-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-API-documentation-section-should-display-API-documentation-link-section-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-Create-Webhook-Dialog-should-have-Add-Webhook-trigger-button-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-category-filters-should-display-all-category-buttons-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-display-all-integration-cards-with-names-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-display-category-badges-on-cards-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-show-Configure-button-for-connected-integrations-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-show-Connect-button-for-not-connected-integrations-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-show-Connected-badge-for-connected-integrations-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-Available-Integrations-section-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-Webhooks-section-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-connected-count-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-page-description-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-search-input-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-render-with-correct-title--Integrations--1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-search-functionality-should-filter-integrations-based-on-search-query-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-search-functionality-should-search-by-description-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-search-functionality-should-show-no-results-message-when-search-has-no-matches-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-existing-webhook-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-webhook-events-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-webhook-last-delivery-info-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-webhook-success-status-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-have-Add-Webhook-button-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-Account-tab-should-display-connected-accounts-section-with-Google-provider-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-Account-tab-should-display-sessions-and-danger-zone-sections-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-display-all-notification-toggle-options-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-display-email-notifications-section-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-display-toggle-switches-for-notification-options-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-have-toggles-checked-by-default-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display--Save-Changes--button-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-personal-information-form-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-profile-picture-section-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-user-email-in-disabled-email-input-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-user-initials-in-avatar-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-user-name-in-the-name-input-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-page-rendering-should-display-all-three-tabs-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-page-rendering-should-display-page-description-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-page-rendering-should-render-with-correct-title--Settings--1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-tab-navigation-should-show-Profile-tab-as-default-active-tab-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-tab-navigation-should-switch-to-Account-tab-when-clicked-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/settings.test.tsx/Settings-Page-tab-navigation-should-switch-to-Notifications-tab-when-clicked-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/team.test.tsx/Team-Page-should-display-Active-Members-section-with-member-count-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/team.test.tsx/Team-Page-should-display-Pending-Invitations-section-with-invitation-details-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/team.test.tsx/Team-Page-should-have-invite-member-button-that-can-be-clicked-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/team.test.tsx/Team-Page-should-render-page-with-correct-title-and-description-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/team.test.tsx/Team-Page-should-render-search-input-that-filters-team-members-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/team.test.tsx/Team-Page-should-show-current-user-with---you---indicator-and-role-badge-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/account.test.tsx +324 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/integrations.test.tsx +520 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/settings.test.tsx +414 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/team.test.tsx +374 -0
- package/templates/base/src/client/routes/_authenticated/account.tsx +416 -0
- package/templates/base/src/client/routes/_authenticated/dashboard.tsx +151 -0
- package/templates/base/src/client/routes/_authenticated/integrations.tsx +553 -0
- package/templates/base/src/client/routes/_authenticated/settings.tsx +310 -0
- package/templates/base/src/client/routes/_authenticated/team.tsx +395 -0
- package/templates/base/src/client/routes/_authenticated.tsx +69 -0
- package/templates/base/src/client/routes/index.tsx +155 -0
- package/templates/base/src/client/routes/invite.$token.tsx +191 -0
- package/templates/base/src/client/routes/login.tsx +92 -0
- package/templates/base/src/server/__tests__/fixtures.ts +461 -0
- package/templates/base/src/server/__tests__/mocks/__tests__/db.test.ts +239 -0
- package/templates/base/src/server/__tests__/mocks/__tests__/kv.test.ts +293 -0
- package/templates/base/src/server/__tests__/mocks/__tests__/r2.test.ts +363 -0
- package/templates/base/src/server/__tests__/mocks/db.ts +186 -0
- package/templates/base/src/server/__tests__/mocks/index.ts +33 -0
- package/templates/base/src/server/__tests__/mocks/kv.ts +286 -0
- package/templates/base/src/server/__tests__/mocks/r2.ts +397 -0
- package/templates/base/src/server/__tests__/setup.ts +281 -0
- package/templates/base/src/server/auth/__tests__/guards.test.ts +162 -0
- package/templates/base/src/server/auth/guards.ts +92 -0
- package/templates/base/src/server/auth/permissions.test.ts +45 -0
- package/templates/base/src/server/auth/permissions.ts +139 -0
- package/templates/base/src/server/auth/roles.test.ts +169 -0
- package/templates/base/src/server/auth/roles.ts +141 -0
- package/templates/base/src/server/db/client.ts +12 -0
- package/templates/base/src/server/db/schema/accounts.ts +20 -0
- package/templates/base/src/server/db/schema/audit-logs.ts +26 -0
- package/templates/base/src/server/db/schema/index.ts +7 -0
- package/templates/base/src/server/db/schema/invitations.ts +30 -0
- package/templates/base/src/server/db/schema/refresh-tokens.ts +22 -0
- package/templates/base/src/server/db/schema/user-accounts.ts +25 -0
- package/templates/base/src/server/db/schema/users.ts +33 -0
- package/templates/base/src/server/db/seed.ts +267 -0
- package/templates/base/src/server/env.test.ts +84 -0
- package/templates/base/src/server/env.ts +78 -0
- package/templates/base/src/server/index.ts +82 -0
- package/templates/base/src/server/lib/audit.ts +73 -0
- package/templates/base/src/server/lib/audited-db.test.ts +107 -0
- package/templates/base/src/server/lib/audited-db.ts +154 -0
- package/templates/base/src/server/lib/email.test.ts +116 -0
- package/templates/base/src/server/lib/email.ts +82 -0
- package/templates/base/src/server/lib/errors.test.ts +49 -0
- package/templates/base/src/server/lib/errors.ts +64 -0
- package/templates/base/src/server/lib/oauth.test.ts +238 -0
- package/templates/base/src/server/lib/oauth.ts +113 -0
- package/templates/base/src/server/lib/pagination.test.ts +52 -0
- package/templates/base/src/server/lib/pagination.ts +32 -0
- package/templates/base/src/server/lib/password.test.ts +151 -0
- package/templates/base/src/server/lib/password.ts +151 -0
- package/templates/base/src/server/lib/providers.test.ts +105 -0
- package/templates/base/src/server/lib/providers.ts +62 -0
- package/templates/base/src/server/lib/r2-storage.test.ts +202 -0
- package/templates/base/src/server/lib/r2-storage.ts +107 -0
- package/templates/base/src/server/lib/schema-helpers.ts +16 -0
- package/templates/base/src/server/lib/session.test.ts +758 -0
- package/templates/base/src/server/lib/session.ts +267 -0
- package/templates/base/src/server/lib/tokens.test.ts +208 -0
- package/templates/base/src/server/lib/tokens.ts +65 -0
- package/templates/base/src/server/lib/transaction.test.ts +45 -0
- package/templates/base/src/server/lib/transaction.ts +24 -0
- package/templates/base/src/server/middleware/account.test.ts +201 -0
- package/templates/base/src/server/middleware/account.ts +66 -0
- package/templates/base/src/server/middleware/auth.test.ts +345 -0
- package/templates/base/src/server/middleware/auth.ts +146 -0
- package/templates/base/src/server/middleware/cors.test.ts +88 -0
- package/templates/base/src/server/middleware/cors.ts +26 -0
- package/templates/base/src/server/middleware/error-handler.test.ts +69 -0
- package/templates/base/src/server/middleware/error-handler.ts +43 -0
- package/templates/base/src/server/middleware/index.ts +8 -0
- package/templates/base/src/server/middleware/rate-limit.test.ts +472 -0
- package/templates/base/src/server/middleware/rate-limit.ts +294 -0
- package/templates/base/src/server/middleware/request-context.test.ts +175 -0
- package/templates/base/src/server/middleware/request-context.ts +28 -0
- package/templates/base/src/server/middleware/request-logger.test.ts +92 -0
- package/templates/base/src/server/middleware/request-logger.ts +50 -0
- package/templates/base/src/server/routes/accounts/__tests__/handlers.test.ts +460 -0
- package/templates/base/src/server/routes/accounts/handlers.ts +179 -0
- package/templates/base/src/server/routes/accounts/index.ts +55 -0
- package/templates/base/src/server/routes/accounts/routes.ts +205 -0
- package/templates/base/src/server/routes/accounts/schemas.ts +31 -0
- package/templates/base/src/server/routes/api.ts +37 -0
- package/templates/base/src/server/routes/audits/__tests__/handlers.test.ts +349 -0
- package/templates/base/src/server/routes/audits/handlers.ts +37 -0
- package/templates/base/src/server/routes/audits/index.ts +14 -0
- package/templates/base/src/server/routes/audits/routes.ts +29 -0
- package/templates/base/src/server/routes/audits/schemas.ts +43 -0
- package/templates/base/src/server/routes/auth/__tests__/handlers.test.ts +381 -0
- package/templates/base/src/server/routes/auth/handlers.ts +222 -0
- package/templates/base/src/server/routes/auth/index.ts +37 -0
- package/templates/base/src/server/routes/auth/routes.ts +136 -0
- package/templates/base/src/server/routes/auth/schemas.ts +48 -0
- package/templates/base/src/server/routes/auth/test-login.ts +156 -0
- package/templates/base/src/server/routes/health/__tests__/handlers.test.ts +237 -0
- package/templates/base/src/server/routes/health/handlers.ts +83 -0
- package/templates/base/src/server/routes/health/index.ts +13 -0
- package/templates/base/src/server/routes/health/routes.ts +90 -0
- package/templates/base/src/server/routes/index.ts +53 -0
- package/templates/base/src/server/routes/invitations/__tests__/handlers.test.ts +473 -0
- package/templates/base/src/server/routes/invitations/handlers.ts +71 -0
- package/templates/base/src/server/routes/invitations/index.ts +25 -0
- package/templates/base/src/server/routes/invitations/routes.ts +69 -0
- package/templates/base/src/server/routes/invitations/schemas.ts +39 -0
- package/templates/base/src/server/routes/openapi.ts +14 -0
- package/templates/base/src/server/routes/schemas.ts +53 -0
- package/templates/base/src/server/routes/storage/__tests__/handlers.test.ts +408 -0
- package/templates/base/src/server/routes/storage/handlers.ts +100 -0
- package/templates/base/src/server/routes/storage/index.ts +42 -0
- package/templates/base/src/server/routes/storage/routes.ts +91 -0
- package/templates/base/src/server/routes/storage/schemas.ts +56 -0
- package/templates/base/src/server/routes/users/__tests__/handlers.test.ts +526 -0
- package/templates/base/src/server/routes/users/handlers.ts +228 -0
- package/templates/base/src/server/routes/users/index.ts +67 -0
- package/templates/base/src/server/routes/users/routes.ts +265 -0
- package/templates/base/src/server/routes/users/schemas.ts +67 -0
- package/templates/base/src/server/services/__tests__/accounts.test.ts +764 -0
- package/templates/base/src/server/services/__tests__/audits.test.ts +235 -0
- package/templates/base/src/server/services/__tests__/auth.test.ts +765 -0
- package/templates/base/src/server/services/__tests__/invitations.test.ts +704 -0
- package/templates/base/src/server/services/__tests__/users.test.ts +755 -0
- package/templates/base/src/server/services/accounts.ts +269 -0
- package/templates/base/src/server/services/audits.ts +82 -0
- package/templates/base/src/server/services/auth.ts +225 -0
- package/templates/base/src/server/services/index.ts +6 -0
- package/templates/base/src/server/services/invitations.ts +306 -0
- package/templates/base/src/server/services/users.ts +350 -0
- package/templates/base/src/server/types/auth.ts +36 -0
- package/templates/base/src/server/types/index.ts +117 -0
- package/templates/base/src/shared/schemas/.gitkeep +0 -0
- package/templates/base/src/shared/schemas/__tests__/schemas.test.ts +547 -0
- package/templates/base/src/shared/schemas/account.ts +15 -0
- package/templates/base/src/shared/schemas/index.ts +6 -0
- package/templates/base/src/shared/schemas/invitation.ts +9 -0
- package/templates/base/src/shared/schemas/profile.ts +10 -0
- package/templates/base/src/shared/schemas/user.ts +16 -0
- package/templates/base/src/shared/schemas/webhook.ts +12 -0
- package/templates/base/src/shared/types/.gitkeep +0 -0
- package/templates/base/src/shared/types/account.ts +12 -0
- package/templates/base/src/shared/types/api.ts +1399 -0
- package/templates/base/src/shared/types/auth.ts +24 -0
- package/templates/base/src/shared/types/index.ts +5 -0
- package/templates/base/src/shared/types/user.ts +31 -0
- package/templates/base/src/test/vitest-zod-matcher.ts +37 -0
- package/templates/base/src/test/vitest.d.ts +19 -0
- package/templates/base/tests/e2e/README.md +141 -0
- package/templates/base/tests/e2e/a11y/accessibility.spec.ts +925 -0
- package/templates/base/tests/e2e/a11y/keyboard-navigation.spec.ts +610 -0
- package/templates/base/tests/e2e/api/accounts.spec.ts +148 -0
- package/templates/base/tests/e2e/api/audit-logs.spec.ts +130 -0
- package/templates/base/tests/e2e/api/authenticated-api.spec.ts +311 -0
- package/templates/base/tests/e2e/api/storage.spec.ts +109 -0
- package/templates/base/tests/e2e/auth-flows.unauth.spec.ts +117 -0
- package/templates/base/tests/e2e/auth-logout.unauth.spec.ts +103 -0
- package/templates/base/tests/e2e/auth.setup.ts +115 -0
- package/templates/base/tests/e2e/auth.spec.ts +146 -0
- package/templates/base/tests/e2e/compatibility/cross-browser.spec.ts +152 -0
- package/templates/base/tests/e2e/compatibility/cross-browser.spec.ts-snapshots/login-chromium-chromium-darwin.png +0 -0
- package/templates/base/tests/e2e/crud/account.spec.ts +356 -0
- package/templates/base/tests/e2e/crud/integrations.spec.ts +419 -0
- package/templates/base/tests/e2e/crud/team.spec.ts +287 -0
- package/templates/base/tests/e2e/crud/users.spec.ts +239 -0
- package/templates/base/tests/e2e/errors/error-boundary.spec.ts +428 -0
- package/templates/base/tests/e2e/errors/error-handling.spec.ts +47 -0
- package/templates/base/tests/e2e/errors/error-handling.unauth.spec.ts +205 -0
- package/templates/base/tests/e2e/fixtures.ts +266 -0
- package/templates/base/tests/e2e/forms/validation.spec.ts +569 -0
- package/templates/base/tests/e2e/invitations/invite-flow.unauth.spec.ts +204 -0
- package/templates/base/tests/e2e/journeys/account-lifecycle.spec.ts +314 -0
- package/templates/base/tests/e2e/journeys/audit-investigation.spec.ts +299 -0
- package/templates/base/tests/e2e/journeys/auth-onboarding.spec.ts +232 -0
- package/templates/base/tests/e2e/journeys/critical-flows.spec.ts +281 -0
- package/templates/base/tests/e2e/journeys/error-recovery.spec.ts +354 -0
- package/templates/base/tests/e2e/journeys/file-management.spec.ts +307 -0
- package/templates/base/tests/e2e/journeys/integrations.spec.ts +372 -0
- package/templates/base/tests/e2e/journeys/multi-account.spec.ts +317 -0
- package/templates/base/tests/e2e/journeys/rbac-enforcement.spec.ts +389 -0
- package/templates/base/tests/e2e/journeys/settings-profile.spec.ts +400 -0
- package/templates/base/tests/e2e/journeys/team-collaboration.spec.ts +410 -0
- package/templates/base/tests/e2e/mobile/responsive.spec.ts +178 -0
- package/templates/base/tests/e2e/navigation/routing.spec.ts +371 -0
- package/templates/base/tests/e2e/navigation/sidebar.spec.ts +425 -0
- package/templates/base/tests/e2e/pages/ui-features.spec.ts +393 -0
- package/templates/base/tests/e2e/performance/baselines.spec.ts +162 -0
- package/templates/base/tests/e2e/performance/benchmarks.spec.ts +371 -0
- package/templates/base/tests/e2e/smoke.unauth.spec.ts +196 -0
- package/templates/base/tests/e2e/visual/components.spec.ts +650 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/confirmation-dialog-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/dark-mode-background-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/dark-mode-card-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/dark-mode-sidebar-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/dashboard-card-single-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/dialog-content-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/dialog-with-backdrop-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/empty-search-results-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/input-default-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/input-focus-ring-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/primary-button-focus-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/primary-button-hover-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/primary-button-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/sidebar-active-nav-item-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/sidebar-collapsed-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/sidebar-navigation-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts +192 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/account-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/create-webhook-dialog-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/dashboard-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/delete-account-dialog-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/integrations-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/invite-member-dialog-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/login-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/settings-account-tab-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/settings-profile-tab-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/sidebar-navigation-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/team-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts +230 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/404-page-chromium-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/404-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/account-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/login-page-chromium-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/login-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/settings-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/team-invite-dialog-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/team-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/webhook-create-dialog-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/theme-colors.spec.ts +293 -0
- package/templates/base/tests/e2e/visual/ui-components.spec.ts +502 -0
- package/templates/base/tests/integration/accounts/crud.test.ts +1402 -0
- package/templates/base/tests/integration/audits/list.test.ts +1133 -0
- package/templates/base/tests/integration/auth/auth-service.test.ts +415 -0
- package/templates/base/tests/integration/auth/invitation-token.test.ts +529 -0
- package/templates/base/tests/integration/auth/logout.test.ts +524 -0
- package/templates/base/tests/integration/auth/oauth.test.ts +768 -0
- package/templates/base/tests/integration/auth/refresh-token.test.ts +364 -0
- package/templates/base/tests/integration/auth/session-expiry.test.ts +569 -0
- package/templates/base/tests/integration/auth/session.test.ts +520 -0
- package/templates/base/tests/integration/auth/super-admin.test.ts +451 -0
- package/templates/base/tests/integration/authorization/analytics-role.test.ts +1026 -0
- package/templates/base/tests/integration/authorization/billing-role.test.ts +776 -0
- package/templates/base/tests/integration/authorization/guards-roles.test.ts +539 -0
- package/templates/base/tests/integration/authorization/multi-tenancy.test.ts +1112 -0
- package/templates/base/tests/integration/authorization/role-hierarchy.test.ts +931 -0
- package/templates/base/tests/integration/authorization/roles.test.ts +1347 -0
- package/templates/base/tests/integration/config/production-behavior.test.ts +536 -0
- package/templates/base/tests/integration/db/constraints.test.ts +695 -0
- package/templates/base/tests/integration/fixtures/accounts.ts +136 -0
- package/templates/base/tests/integration/fixtures/index.ts +232 -0
- package/templates/base/tests/integration/fixtures/invitations.ts +195 -0
- package/templates/base/tests/integration/fixtures/users.ts +144 -0
- package/templates/base/tests/integration/health/health.test.ts +351 -0
- package/templates/base/tests/integration/invitations/crud.test.ts +1457 -0
- package/templates/base/tests/integration/invitations/email.test.ts +506 -0
- package/templates/base/tests/integration/lib/email.test.ts +174 -0
- package/templates/base/tests/integration/lib/oauth.test.ts +368 -0
- package/templates/base/tests/integration/lib/password.test.ts +192 -0
- package/templates/base/tests/integration/lib/schema-helpers.test.ts +129 -0
- package/templates/base/tests/integration/lib/tokens.test.ts +304 -0
- package/templates/base/tests/integration/middleware/auth.test.ts +499 -0
- package/templates/base/tests/integration/middleware/cors.test.ts +334 -0
- package/templates/base/tests/integration/middleware/request-context.test.ts +156 -0
- package/templates/base/tests/integration/middleware/request-logger.test.ts +313 -0
- package/templates/base/tests/integration/performance/response-times.test.ts +509 -0
- package/templates/base/tests/integration/security/cookie-security.test.ts +567 -0
- package/templates/base/tests/integration/security/csrf-protection.test.ts +542 -0
- package/templates/base/tests/integration/security/jwt-validation.test.ts +209 -0
- package/templates/base/tests/integration/security/log-sanitization.test.ts +658 -0
- package/templates/base/tests/integration/security/rate-limiting.test.ts +1251 -0
- package/templates/base/tests/integration/security/sql-injection.test.ts +663 -0
- package/templates/base/tests/integration/security/token-hashing.test.ts +371 -0
- package/templates/base/tests/integration/security/xss-prevention.test.ts +541 -0
- package/templates/base/tests/integration/setup.ts +834 -0
- package/templates/base/tests/integration/smoke.test.ts +288 -0
- package/templates/base/tests/integration/storage/upload.test.ts +1162 -0
- package/templates/base/tests/integration/storage/validation.test.ts +746 -0
- package/templates/base/tests/integration/users/crud.test.ts +1297 -0
- package/templates/base/tests/integration/users/list.test.ts +698 -0
- package/templates/base/tests/integration/vitest.config.ts +80 -0
- package/templates/base/tsconfig.app.json +18 -0
- package/templates/base/tsconfig.json +39 -0
- package/templates/base/tsconfig.node.json +16 -0
- package/templates/base/tsconfig.server.json +26 -0
- package/templates/base/tsconfig.tsbuildinfo +1 -0
- package/templates/base/vite.config.ts +46 -0
- package/templates/base/vitest.config.browser.ts +47 -0
- package/templates/base/vitest.config.frontend.ts +47 -0
- package/templates/base/vitest.config.ts +82 -0
- package/templates/base/vitest.workspace.ts +22 -0
- package/templates/modules/audit-logs/.gitkeep +0 -0
- package/templates/modules/billing/.gitkeep +0 -0
- package/templates/modules/invitations/.gitkeep +0 -0
- package/templates/modules/storage/.gitkeep +0 -0
- package/templates/modules/webhooks/.gitkeep +0 -0
- package/templates/providers/auth-email/.gitkeep +0 -0
- package/templates/providers/auth-github/.gitkeep +0 -0
- package/templates/providers/auth-google/.gitkeep +0 -0
- package/templates/providers/email-resend/.gitkeep +0 -0
- package/templates/providers/email-sendgrid/.gitkeep +0 -0
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
import { test, expect, isAuthenticated } from '../fixtures'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Accessibility E2E Tests
|
|
5
|
+
*
|
|
6
|
+
* Tests for accessibility features including keyboard navigation,
|
|
7
|
+
* focus management, semantic structure, and ARIA attributes.
|
|
8
|
+
*
|
|
9
|
+
* Uses Playwright's built-in accessibility testing features:
|
|
10
|
+
* - toHaveAccessibleName() for accessible names
|
|
11
|
+
* - toHaveRole() for ARIA roles
|
|
12
|
+
* - toHaveAccessibleDescription() for descriptions
|
|
13
|
+
*
|
|
14
|
+
* @tags @a11y
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
test.describe('Accessibility Tests @a11y', () => {
|
|
18
|
+
test.describe('Public Pages', () => {
|
|
19
|
+
test('login page has proper heading structure', async ({ page }) => {
|
|
20
|
+
await page.goto('/login')
|
|
21
|
+
|
|
22
|
+
// Should have a heading (Welcome back) - CardTitle renders as h3
|
|
23
|
+
const heading = page.getByRole('heading', { name: /welcome back/i })
|
|
24
|
+
await expect(heading).toBeVisible()
|
|
25
|
+
|
|
26
|
+
// Verify heading has semantic meaning (is an actual heading element)
|
|
27
|
+
const tagName = await heading.evaluate((el) => el.tagName.toLowerCase())
|
|
28
|
+
expect(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']).toContain(tagName)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('login page form is accessible (OAuth button focusable)', async ({ page }) => {
|
|
32
|
+
await page.goto('/login')
|
|
33
|
+
|
|
34
|
+
// OAuth button should be focusable
|
|
35
|
+
const googleButton = page.getByRole('button', { name: /continue with google/i })
|
|
36
|
+
await expect(googleButton).toBeVisible()
|
|
37
|
+
await expect(googleButton).toBeEnabled()
|
|
38
|
+
|
|
39
|
+
// Focus the button
|
|
40
|
+
await googleButton.focus()
|
|
41
|
+
await expect(googleButton).toBeFocused()
|
|
42
|
+
|
|
43
|
+
// Button should have accessible name
|
|
44
|
+
await expect(googleButton).toHaveAccessibleName(/continue with google/i)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('login page supports keyboard navigation', async ({ page }) => {
|
|
48
|
+
await page.goto('/login')
|
|
49
|
+
|
|
50
|
+
// Verify the OAuth button is keyboard accessible
|
|
51
|
+
const googleButton = page.getByRole('button', { name: /continue with google/i })
|
|
52
|
+
await expect(googleButton).toBeVisible()
|
|
53
|
+
|
|
54
|
+
// Focus the button directly to verify it can receive focus
|
|
55
|
+
await googleButton.focus()
|
|
56
|
+
await expect(googleButton).toBeFocused()
|
|
57
|
+
|
|
58
|
+
// Tab to next element and verify focus moves
|
|
59
|
+
await page.keyboard.press('Tab')
|
|
60
|
+
const activeElement = page.locator(':focus')
|
|
61
|
+
await expect(activeElement).toBeVisible()
|
|
62
|
+
|
|
63
|
+
// Verify we can navigate back with Shift+Tab
|
|
64
|
+
await page.keyboard.press('Shift+Tab')
|
|
65
|
+
await expect(googleButton).toBeFocused()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('login page links have proper focus indicators', async ({ page }) => {
|
|
69
|
+
await page.goto('/login')
|
|
70
|
+
|
|
71
|
+
// Navigate to a link
|
|
72
|
+
const termsLink = page.getByRole('link', { name: /terms of service/i })
|
|
73
|
+
await termsLink.focus()
|
|
74
|
+
await expect(termsLink).toBeFocused()
|
|
75
|
+
|
|
76
|
+
// Link should have underline for visibility
|
|
77
|
+
await expect(termsLink).toHaveClass(/underline/)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test.describe('Authenticated Pages', () => {
|
|
82
|
+
test.beforeEach(async ({ page }) => {
|
|
83
|
+
const authenticated = await isAuthenticated(page)
|
|
84
|
+
test.skip(!authenticated, 'No authenticated session available')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('dashboard has proper semantic structure', async ({ page }) => {
|
|
88
|
+
await page.goto('/dashboard')
|
|
89
|
+
|
|
90
|
+
// Should have main heading
|
|
91
|
+
const heading = page.getByRole('heading', { name: /dashboard/i })
|
|
92
|
+
await expect(heading).toBeVisible()
|
|
93
|
+
|
|
94
|
+
// Page should have a main content area (div with proper structure)
|
|
95
|
+
const mainContent = page.locator('main, [role="main"], .space-y-6').first()
|
|
96
|
+
await expect(mainContent).toBeVisible()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('team page table/list is accessible', async ({ page }) => {
|
|
100
|
+
await page.goto('/team')
|
|
101
|
+
|
|
102
|
+
// Should have heading
|
|
103
|
+
await expect(page.getByRole('heading', { name: /team members/i })).toBeVisible()
|
|
104
|
+
|
|
105
|
+
// Team members should be in a structured list (divide-y creates visual separation)
|
|
106
|
+
const memberList = page.locator('[class*="divide-y"]').first()
|
|
107
|
+
await expect(memberList).toBeVisible()
|
|
108
|
+
|
|
109
|
+
// Each member row should be visible and contain content
|
|
110
|
+
const firstMemberRow = memberList.locator('> div').first()
|
|
111
|
+
await expect(firstMemberRow).toBeVisible()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('settings tabs are keyboard accessible', async ({ page }) => {
|
|
115
|
+
await page.goto('/settings')
|
|
116
|
+
|
|
117
|
+
// Verify tabs are present
|
|
118
|
+
const tabList = page.getByRole('tablist')
|
|
119
|
+
await expect(tabList).toBeVisible()
|
|
120
|
+
|
|
121
|
+
// Profile tab should be selected by default
|
|
122
|
+
const profileTab = page.getByRole('tab', { name: /profile/i })
|
|
123
|
+
await expect(profileTab).toHaveAttribute('data-state', 'active')
|
|
124
|
+
|
|
125
|
+
// Focus on tab list
|
|
126
|
+
await profileTab.focus()
|
|
127
|
+
await expect(profileTab).toBeFocused()
|
|
128
|
+
|
|
129
|
+
// Arrow right should move to next tab (Account)
|
|
130
|
+
await page.keyboard.press('ArrowRight')
|
|
131
|
+
const accountTab = page.getByRole('tab', { name: /account/i })
|
|
132
|
+
await expect(accountTab).toBeFocused()
|
|
133
|
+
|
|
134
|
+
// Arrow right again to Notifications
|
|
135
|
+
await page.keyboard.press('ArrowRight')
|
|
136
|
+
const notificationsTab = page.getByRole('tab', { name: /notifications/i })
|
|
137
|
+
await expect(notificationsTab).toBeFocused()
|
|
138
|
+
|
|
139
|
+
// Arrow left should go back
|
|
140
|
+
await page.keyboard.press('ArrowLeft')
|
|
141
|
+
await expect(accountTab).toBeFocused()
|
|
142
|
+
|
|
143
|
+
// Enter should activate the tab
|
|
144
|
+
await page.keyboard.press('Enter')
|
|
145
|
+
await expect(accountTab).toHaveAttribute('data-state', 'active')
|
|
146
|
+
|
|
147
|
+
// Tab content should be visible
|
|
148
|
+
await expect(page.getByText(/connected accounts/i)).toBeVisible()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('team invite dialog is accessible (focus trap, escape to close)', async ({ page }) => {
|
|
152
|
+
await page.goto('/team')
|
|
153
|
+
|
|
154
|
+
// Open invite dialog
|
|
155
|
+
const inviteButton = page.getByRole('button', { name: /invite member/i })
|
|
156
|
+
await inviteButton.click()
|
|
157
|
+
|
|
158
|
+
// Dialog should be visible
|
|
159
|
+
const dialog = page.getByRole('dialog')
|
|
160
|
+
await expect(dialog).toBeVisible()
|
|
161
|
+
|
|
162
|
+
// Dialog should have proper title
|
|
163
|
+
await expect(page.getByRole('heading', { name: /invite team member/i })).toBeVisible()
|
|
164
|
+
|
|
165
|
+
// First focusable element in dialog should receive focus (email input)
|
|
166
|
+
const emailInput = page.getByLabel(/email address/i)
|
|
167
|
+
await expect(emailInput).toBeFocused()
|
|
168
|
+
|
|
169
|
+
// Tab through dialog elements - focus should stay trapped
|
|
170
|
+
await page.keyboard.press('Tab') // Move to role buttons
|
|
171
|
+
|
|
172
|
+
// Tab to cancel button
|
|
173
|
+
let tabCount = 0
|
|
174
|
+
const maxTabs = 10
|
|
175
|
+
while (tabCount < maxTabs) {
|
|
176
|
+
await page.keyboard.press('Tab')
|
|
177
|
+
tabCount++
|
|
178
|
+
const cancelButton = page.getByRole('button', { name: /cancel/i })
|
|
179
|
+
if (await cancelButton.evaluate((el) => el === document.activeElement)) {
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Escape should close dialog
|
|
185
|
+
await page.keyboard.press('Escape')
|
|
186
|
+
await expect(dialog).not.toBeVisible()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('form inputs have labels', async ({ page }) => {
|
|
190
|
+
await page.goto('/settings')
|
|
191
|
+
|
|
192
|
+
// Full Name input should have label
|
|
193
|
+
const nameInput = page.getByLabel(/full name/i)
|
|
194
|
+
await expect(nameInput).toBeVisible()
|
|
195
|
+
await expect(nameInput).toHaveAttribute('id', 'name')
|
|
196
|
+
|
|
197
|
+
// Email input should have label
|
|
198
|
+
const emailInput = page.getByLabel(/email address/i)
|
|
199
|
+
await expect(emailInput).toBeVisible()
|
|
200
|
+
await expect(emailInput).toHaveAttribute('id', 'email')
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('buttons have accessible names', async ({ page }) => {
|
|
204
|
+
await page.goto('/settings')
|
|
205
|
+
|
|
206
|
+
// Save Changes button
|
|
207
|
+
const saveButton = page.getByRole('button', { name: /save changes/i })
|
|
208
|
+
await expect(saveButton).toBeVisible()
|
|
209
|
+
await expect(saveButton).toHaveAccessibleName(/save changes/i)
|
|
210
|
+
|
|
211
|
+
// Change Photo button
|
|
212
|
+
const photoButton = page.getByRole('button', { name: /change photo/i })
|
|
213
|
+
await expect(photoButton).toBeVisible()
|
|
214
|
+
await expect(photoButton).toHaveAccessibleName(/change photo/i)
|
|
215
|
+
|
|
216
|
+
// Go to Account tab for Delete button
|
|
217
|
+
await page.getByRole('tab', { name: /account/i }).click()
|
|
218
|
+
const deleteButton = page.getByRole('button', { name: /delete account/i })
|
|
219
|
+
await expect(deleteButton).toBeVisible()
|
|
220
|
+
await expect(deleteButton).toHaveAccessibleName(/delete account/i)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('navigation is keyboard accessible', async ({ page }) => {
|
|
224
|
+
await page.goto('/dashboard')
|
|
225
|
+
|
|
226
|
+
// Find navigation links
|
|
227
|
+
const navLinks = page.locator('nav').getByRole('link')
|
|
228
|
+
const linkCount = await navLinks.count()
|
|
229
|
+
|
|
230
|
+
if (linkCount > 0) {
|
|
231
|
+
// Focus first nav link
|
|
232
|
+
await navLinks.first().focus()
|
|
233
|
+
await expect(navLinks.first()).toBeFocused()
|
|
234
|
+
|
|
235
|
+
// Should be able to tab through nav links
|
|
236
|
+
for (let i = 1; i < Math.min(linkCount, 3); i++) {
|
|
237
|
+
await page.keyboard.press('Tab')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Enter should activate the link (navigate)
|
|
241
|
+
const currentUrl = page.url()
|
|
242
|
+
await navLinks.first().focus()
|
|
243
|
+
await page.keyboard.press('Enter')
|
|
244
|
+
// URL should change or page should respond
|
|
245
|
+
await page.waitForLoadState('domcontentloaded')
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('color contrast check (basic - text is visible)', async ({ page }) => {
|
|
250
|
+
await page.goto('/settings')
|
|
251
|
+
|
|
252
|
+
// Verify main heading text is visible and has proper styling
|
|
253
|
+
const heading = page.getByRole('heading', { name: /settings/i })
|
|
254
|
+
await expect(heading).toBeVisible()
|
|
255
|
+
|
|
256
|
+
// Verify description text is visible (muted but readable)
|
|
257
|
+
const description = page.getByText(/manage your account settings/i)
|
|
258
|
+
await expect(description).toBeVisible()
|
|
259
|
+
|
|
260
|
+
// Verify form labels are visible
|
|
261
|
+
const nameLabel = page.getByText('Full Name')
|
|
262
|
+
await expect(nameLabel).toBeVisible()
|
|
263
|
+
|
|
264
|
+
// Verify input text is visible
|
|
265
|
+
const nameInput = page.getByLabel(/full name/i)
|
|
266
|
+
await expect(nameInput).toBeVisible()
|
|
267
|
+
|
|
268
|
+
// Verify button text is visible and has sufficient contrast
|
|
269
|
+
const saveButton = page.getByRole('button', { name: /save changes/i })
|
|
270
|
+
await expect(saveButton).toBeVisible()
|
|
271
|
+
|
|
272
|
+
// Verify disabled input has visible styling
|
|
273
|
+
const emailInput = page.getByLabel(/email address/i)
|
|
274
|
+
await expect(emailInput).toBeVisible()
|
|
275
|
+
await expect(emailInput).toBeDisabled()
|
|
276
|
+
|
|
277
|
+
// Navigate to Account tab to check destructive button contrast
|
|
278
|
+
await page.getByRole('tab', { name: /account/i }).click()
|
|
279
|
+
const deleteButton = page.getByRole('button', { name: /delete account/i })
|
|
280
|
+
await expect(deleteButton).toBeVisible()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test('team page search input is accessible', async ({ page }) => {
|
|
284
|
+
await page.goto('/team')
|
|
285
|
+
|
|
286
|
+
// Search input should have placeholder as accessible description
|
|
287
|
+
const searchInput = page.getByPlaceholder(/search members/i)
|
|
288
|
+
await expect(searchInput).toBeVisible()
|
|
289
|
+
await expect(searchInput).toBeEnabled()
|
|
290
|
+
|
|
291
|
+
// Should be focusable
|
|
292
|
+
await searchInput.focus()
|
|
293
|
+
await expect(searchInput).toBeFocused()
|
|
294
|
+
|
|
295
|
+
// Should accept keyboard input
|
|
296
|
+
await page.keyboard.type('test')
|
|
297
|
+
await expect(searchInput).toHaveValue('test')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
test('settings notification toggles are accessible', async ({ page }) => {
|
|
301
|
+
await page.goto('/settings')
|
|
302
|
+
|
|
303
|
+
// Navigate to notifications tab
|
|
304
|
+
await page.getByRole('tab', { name: /notifications/i }).click()
|
|
305
|
+
|
|
306
|
+
// Should see switch elements
|
|
307
|
+
const switches = page.getByRole('switch')
|
|
308
|
+
const switchCount = await switches.count()
|
|
309
|
+
|
|
310
|
+
expect(switchCount).toBeGreaterThan(0)
|
|
311
|
+
|
|
312
|
+
// First switch should be focusable
|
|
313
|
+
const firstSwitch = switches.first()
|
|
314
|
+
await firstSwitch.focus()
|
|
315
|
+
await expect(firstSwitch).toBeFocused()
|
|
316
|
+
|
|
317
|
+
// Should have aria-checked attribute
|
|
318
|
+
await expect(firstSwitch).toHaveAttribute('aria-checked')
|
|
319
|
+
|
|
320
|
+
// Should be keyboard toggleable with Space/Enter
|
|
321
|
+
const initialState = await firstSwitch.getAttribute('aria-checked')
|
|
322
|
+
|
|
323
|
+
// Skip if disabled
|
|
324
|
+
const isDisabled = await firstSwitch.isDisabled()
|
|
325
|
+
if (!isDisabled) {
|
|
326
|
+
await page.keyboard.press('Space')
|
|
327
|
+
const newState = await firstSwitch.getAttribute('aria-checked')
|
|
328
|
+
expect(newState).not.toBe(initialState)
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
test('avatar images have alt text', async ({ page }) => {
|
|
333
|
+
await page.goto('/settings')
|
|
334
|
+
|
|
335
|
+
// Avatar should have alt text
|
|
336
|
+
const avatarImage = page.locator('[class*="Avatar"] img')
|
|
337
|
+
const imageCount = await avatarImage.count()
|
|
338
|
+
|
|
339
|
+
if (imageCount > 0) {
|
|
340
|
+
await expect(avatarImage.first()).toHaveAttribute('alt')
|
|
341
|
+
} else {
|
|
342
|
+
// Fallback avatar should still be accessible
|
|
343
|
+
const avatarFallback = page.locator('[class*="AvatarFallback"]')
|
|
344
|
+
await expect(avatarFallback).toBeVisible()
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
test('skip to main content functionality', async ({ page }) => {
|
|
349
|
+
await page.goto('/dashboard')
|
|
350
|
+
|
|
351
|
+
// Many accessible sites have a skip link that appears on focus
|
|
352
|
+
// Check if one exists or if the page structure allows quick navigation
|
|
353
|
+
await page.keyboard.press('Tab')
|
|
354
|
+
|
|
355
|
+
// The first focusable element should allow navigation
|
|
356
|
+
// In most cases, this would be a skip link or the main navigation
|
|
357
|
+
const focusedElement = page.locator(':focus')
|
|
358
|
+
await expect(focusedElement).toBeVisible()
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
test('dialogs trap focus correctly', async ({ page }) => {
|
|
362
|
+
await page.goto('/team')
|
|
363
|
+
|
|
364
|
+
// Open invite dialog
|
|
365
|
+
await page.getByRole('button', { name: /invite member/i }).click()
|
|
366
|
+
const dialog = page.getByRole('dialog')
|
|
367
|
+
await expect(dialog).toBeVisible()
|
|
368
|
+
|
|
369
|
+
// Tab through all elements in the dialog
|
|
370
|
+
const tabPresses = 15 // More than enough to cycle through dialog
|
|
371
|
+
for (let i = 0; i < tabPresses; i++) {
|
|
372
|
+
await page.keyboard.press('Tab')
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Focus should still be within the dialog
|
|
376
|
+
const focusedElement = page.locator(':focus')
|
|
377
|
+
const isInDialog = await focusedElement.evaluate((el) => {
|
|
378
|
+
const dialog = el.closest('[role="dialog"]')
|
|
379
|
+
return dialog !== null
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
expect(isInDialog).toBe(true)
|
|
383
|
+
|
|
384
|
+
// Close dialog
|
|
385
|
+
await page.keyboard.press('Escape')
|
|
386
|
+
await expect(dialog).not.toBeVisible()
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
test('error states are accessible', async ({ page }) => {
|
|
390
|
+
await page.goto('/team')
|
|
391
|
+
|
|
392
|
+
// Open invite dialog
|
|
393
|
+
await page.getByRole('button', { name: /invite member/i }).click()
|
|
394
|
+
|
|
395
|
+
// Send button should be disabled when email is empty
|
|
396
|
+
const sendButton = page.getByRole('button', { name: /send invitation/i })
|
|
397
|
+
await expect(sendButton).toBeDisabled()
|
|
398
|
+
|
|
399
|
+
// Enter invalid email
|
|
400
|
+
const emailInput = page.getByLabel(/email address/i)
|
|
401
|
+
await emailInput.fill('invalid-email')
|
|
402
|
+
|
|
403
|
+
// Button state should reflect validity
|
|
404
|
+
// Most implementations either show inline error or disable button
|
|
405
|
+
await expect(sendButton).toBeVisible()
|
|
406
|
+
|
|
407
|
+
// Close dialog
|
|
408
|
+
await page.keyboard.press('Escape')
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
test('account page has proper headings and landmarks', async ({ page }) => {
|
|
412
|
+
await page.goto('/account')
|
|
413
|
+
|
|
414
|
+
// Should have main heading (h1)
|
|
415
|
+
const mainHeading = page.getByRole('heading', { level: 1 })
|
|
416
|
+
await expect(mainHeading).toBeVisible()
|
|
417
|
+
|
|
418
|
+
// Should have multiple section headings (h2) for different account sections
|
|
419
|
+
// Account page typically has: Security, Sessions, API Access, Danger Zone
|
|
420
|
+
const sectionHeadings = page.getByRole('heading', { level: 2 })
|
|
421
|
+
const headingCount = await sectionHeadings.count()
|
|
422
|
+
expect(headingCount).toBeGreaterThanOrEqual(3)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
test('dialogs have proper ARIA attributes', async ({ page }) => {
|
|
426
|
+
await page.goto('/team')
|
|
427
|
+
|
|
428
|
+
// Wait for the page heading to ensure content is loaded
|
|
429
|
+
const pageHeading = page.getByRole('heading', { level: 1, name: /team members/i })
|
|
430
|
+
await expect(pageHeading).toBeVisible()
|
|
431
|
+
|
|
432
|
+
// Open invite dialog - click the second button (the visible styled one)
|
|
433
|
+
const inviteButton = page.getByText('Invite Member', { exact: true })
|
|
434
|
+
await expect(inviteButton).toBeVisible()
|
|
435
|
+
await inviteButton.click()
|
|
436
|
+
|
|
437
|
+
// Dialog should be visible
|
|
438
|
+
const dialog = page.getByRole('dialog')
|
|
439
|
+
await expect(dialog).toBeVisible()
|
|
440
|
+
|
|
441
|
+
// Dialog should have proper ARIA attributes for accessibility
|
|
442
|
+
const hasAriaLabelledBy = await dialog.getAttribute('aria-labelledby')
|
|
443
|
+
const hasAriaLabel = await dialog.getAttribute('aria-label')
|
|
444
|
+
const hasAriaModal = await dialog.getAttribute('aria-modal')
|
|
445
|
+
|
|
446
|
+
// At least one labeling attribute or aria-modal should be present
|
|
447
|
+
const hasProperAriaAttributes =
|
|
448
|
+
hasAriaLabelledBy !== null || hasAriaLabel !== null || hasAriaModal !== null
|
|
449
|
+
expect(hasProperAriaAttributes).toBe(true)
|
|
450
|
+
|
|
451
|
+
// Close dialog
|
|
452
|
+
await page.keyboard.press('Escape')
|
|
453
|
+
await expect(dialog).not.toBeVisible()
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
test('form inputs have associated labels via getByLabel', async ({ page }) => {
|
|
457
|
+
await page.goto('/settings')
|
|
458
|
+
|
|
459
|
+
// Wait for the settings page heading to ensure page is loaded
|
|
460
|
+
const heading = page.getByRole('heading', { level: 1, name: /settings/i })
|
|
461
|
+
await expect(heading).toBeVisible()
|
|
462
|
+
|
|
463
|
+
// Name input should be accessible via label
|
|
464
|
+
const nameInput = page.getByLabel(/full name/i)
|
|
465
|
+
await expect(nameInput).toBeVisible()
|
|
466
|
+
await expect(nameInput).toBeEnabled()
|
|
467
|
+
|
|
468
|
+
// Email input should be accessible via label (but may be disabled for OAuth users)
|
|
469
|
+
const emailInput = page.getByLabel(/email address/i)
|
|
470
|
+
await expect(emailInput).toBeVisible()
|
|
471
|
+
|
|
472
|
+
// Name input should be focusable
|
|
473
|
+
await nameInput.focus()
|
|
474
|
+
await expect(nameInput).toBeFocused()
|
|
475
|
+
|
|
476
|
+
// Note: Email input may be disabled for OAuth-only users, so we just verify
|
|
477
|
+
// it has a proper label association (which getByLabel already confirms)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
test('interactive elements are focusable', async ({ page }) => {
|
|
481
|
+
await page.goto('/integrations')
|
|
482
|
+
|
|
483
|
+
// Wait for the page heading to ensure content is loaded
|
|
484
|
+
const pageHeading = page.getByRole('heading', { level: 1, name: /integrations/i })
|
|
485
|
+
await expect(pageHeading).toBeVisible()
|
|
486
|
+
|
|
487
|
+
// Focus on the search input first to establish a starting point
|
|
488
|
+
const searchInput = page.getByPlaceholder(/search integrations/i)
|
|
489
|
+
await expect(searchInput).toBeVisible()
|
|
490
|
+
await searchInput.focus()
|
|
491
|
+
await expect(searchInput).toBeFocused()
|
|
492
|
+
|
|
493
|
+
// Press Tab to move to next interactive element
|
|
494
|
+
await page.keyboard.press('Tab')
|
|
495
|
+
|
|
496
|
+
// Verify something is focused after Tab
|
|
497
|
+
const focusedElement = page.locator(':focus')
|
|
498
|
+
await expect(focusedElement).toBeVisible()
|
|
499
|
+
|
|
500
|
+
// Continue tabbing to verify multiple interactive elements are focusable
|
|
501
|
+
await page.keyboard.press('Tab')
|
|
502
|
+
const secondFocusedElement = page.locator(':focus')
|
|
503
|
+
await expect(secondFocusedElement).toBeVisible()
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
test.describe('Live Regions and Announcements', () => {
|
|
508
|
+
test.beforeEach(async ({ page }) => {
|
|
509
|
+
const authenticated = await isAuthenticated(page)
|
|
510
|
+
test.skip(!authenticated, 'No authenticated session available')
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
test('toast notifications have role="status"', async ({ page }) => {
|
|
514
|
+
await page.goto('/settings')
|
|
515
|
+
|
|
516
|
+
// Trigger a toast by saving settings
|
|
517
|
+
const nameInput = page.getByLabel(/full name/i)
|
|
518
|
+
await nameInput.fill('Test User Updated')
|
|
519
|
+
|
|
520
|
+
const saveButton = page.getByRole('button', { name: /save changes/i })
|
|
521
|
+
await saveButton.click()
|
|
522
|
+
|
|
523
|
+
// Toast should appear with proper role
|
|
524
|
+
const toast = page.getByRole('status')
|
|
525
|
+
await expect(toast.first()).toBeVisible({ timeout: 5000 })
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
test('loading states are announced', async ({ page }) => {
|
|
529
|
+
await page.goto('/settings')
|
|
530
|
+
|
|
531
|
+
// Check that buttons have loading indicators when submitting
|
|
532
|
+
const saveButton = page.getByRole('button', { name: /save changes/i })
|
|
533
|
+
|
|
534
|
+
// Button should indicate loading state accessibly
|
|
535
|
+
// Either through aria-busy, aria-disabled, or visible spinner
|
|
536
|
+
await saveButton.click()
|
|
537
|
+
|
|
538
|
+
// The button should either be disabled or have aria-busy during loading
|
|
539
|
+
const isDisabled = await saveButton.isDisabled()
|
|
540
|
+
const ariaBusy = await saveButton.getAttribute('aria-busy')
|
|
541
|
+
const hasSpinner = await saveButton.locator('[class*="spinner"], [class*="animate-spin"]').count()
|
|
542
|
+
|
|
543
|
+
// At least one loading indicator should be present
|
|
544
|
+
expect(isDisabled || ariaBusy === 'true' || hasSpinner > 0).toBe(true)
|
|
545
|
+
})
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
test.describe('Touch Targets', () => {
|
|
549
|
+
test.beforeEach(async ({ page }) => {
|
|
550
|
+
const authenticated = await isAuthenticated(page)
|
|
551
|
+
test.skip(!authenticated, 'No authenticated session available')
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
test('buttons meet minimum touch target size (44x44)', async ({ page }) => {
|
|
555
|
+
await page.goto('/dashboard')
|
|
556
|
+
|
|
557
|
+
// Get all buttons
|
|
558
|
+
const buttons = page.getByRole('button')
|
|
559
|
+
const count = await buttons.count()
|
|
560
|
+
|
|
561
|
+
// Check a sample of buttons for adequate size
|
|
562
|
+
const samplesToCheck = Math.min(count, 10)
|
|
563
|
+
let smallButtonsFound = 0
|
|
564
|
+
|
|
565
|
+
for (let i = 0; i < samplesToCheck; i++) {
|
|
566
|
+
const button = buttons.nth(i)
|
|
567
|
+
if (await button.isVisible()) {
|
|
568
|
+
const box = await button.boundingBox()
|
|
569
|
+
if (box) {
|
|
570
|
+
// WCAG recommends 44x44 minimum touch target
|
|
571
|
+
// We'll log a warning if smaller, but not fail
|
|
572
|
+
if (box.width < 44 || box.height < 44) {
|
|
573
|
+
smallButtonsFound++
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Allow some small icon buttons, but most should be adequate size
|
|
580
|
+
expect(smallButtonsFound).toBeLessThan(samplesToCheck / 2)
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
test('links have adequate click area', async ({ page }) => {
|
|
584
|
+
await page.goto('/dashboard')
|
|
585
|
+
|
|
586
|
+
// Check navigation links
|
|
587
|
+
const navLinks = page.locator('nav').getByRole('link')
|
|
588
|
+
const count = await navLinks.count()
|
|
589
|
+
|
|
590
|
+
for (let i = 0; i < Math.min(count, 5); i++) {
|
|
591
|
+
const link = navLinks.nth(i)
|
|
592
|
+
if (await link.isVisible()) {
|
|
593
|
+
const box = await link.boundingBox()
|
|
594
|
+
if (box) {
|
|
595
|
+
// Links should have reasonable padding for touch
|
|
596
|
+
expect(box.height).toBeGreaterThanOrEqual(32)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
})
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
test.describe('Image Accessibility', () => {
|
|
604
|
+
test.beforeEach(async ({ page }) => {
|
|
605
|
+
const authenticated = await isAuthenticated(page)
|
|
606
|
+
test.skip(!authenticated, 'No authenticated session available')
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
test('all images have alt attributes', async ({ page }) => {
|
|
610
|
+
await page.goto('/dashboard')
|
|
611
|
+
|
|
612
|
+
// Find all images
|
|
613
|
+
const images = page.locator('img')
|
|
614
|
+
const count = await images.count()
|
|
615
|
+
|
|
616
|
+
for (let i = 0; i < count; i++) {
|
|
617
|
+
const img = images.nth(i)
|
|
618
|
+
if (await img.isVisible()) {
|
|
619
|
+
// Image should have alt attribute (can be empty for decorative)
|
|
620
|
+
await expect(img).toHaveAttribute('alt')
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
test('SVG icons have accessible roles or are hidden', async ({ page }) => {
|
|
626
|
+
await page.goto('/dashboard')
|
|
627
|
+
|
|
628
|
+
// Find SVG elements
|
|
629
|
+
const svgs = page.locator('svg')
|
|
630
|
+
const count = await svgs.count()
|
|
631
|
+
|
|
632
|
+
for (let i = 0; i < Math.min(count, 10); i++) {
|
|
633
|
+
const svg = svgs.nth(i)
|
|
634
|
+
if (await svg.isVisible()) {
|
|
635
|
+
// SVG should either have role="img" with accessible name
|
|
636
|
+
// or be aria-hidden="true" if decorative
|
|
637
|
+
const role = await svg.getAttribute('role')
|
|
638
|
+
const ariaHidden = await svg.getAttribute('aria-hidden')
|
|
639
|
+
const ariaLabel = await svg.getAttribute('aria-label')
|
|
640
|
+
const title = await svg.locator('title').count()
|
|
641
|
+
|
|
642
|
+
// Either hidden or has accessible name
|
|
643
|
+
const isAccessible = ariaHidden === 'true' ||
|
|
644
|
+
role === 'img' ||
|
|
645
|
+
ariaLabel !== null ||
|
|
646
|
+
title > 0
|
|
647
|
+
|
|
648
|
+
// Most icons in buttons are decorative (text provides meaning)
|
|
649
|
+
// so aria-hidden is acceptable
|
|
650
|
+
expect(isAccessible || ariaHidden === null).toBe(true)
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
})
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
test.describe('Reduced Motion', () => {
|
|
657
|
+
test.beforeEach(async ({ page }) => {
|
|
658
|
+
const authenticated = await isAuthenticated(page)
|
|
659
|
+
test.skip(!authenticated, 'No authenticated session available')
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
test('app respects prefers-reduced-motion', async ({ page }) => {
|
|
663
|
+
// Emulate reduced motion preference
|
|
664
|
+
await page.emulateMedia({ reducedMotion: 'reduce' })
|
|
665
|
+
await page.goto('/dashboard')
|
|
666
|
+
|
|
667
|
+
// Check that the app loaded successfully with reduced motion
|
|
668
|
+
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible()
|
|
669
|
+
|
|
670
|
+
// Animations should be disabled or reduced
|
|
671
|
+
// Check sidebar collapse - should not have transition
|
|
672
|
+
const sidebar = page.locator('aside')
|
|
673
|
+
await expect(sidebar).toBeVisible()
|
|
674
|
+
|
|
675
|
+
// The app should be fully functional with reduced motion
|
|
676
|
+
const navLinks = page.locator('nav').getByRole('link')
|
|
677
|
+
await expect(navLinks.first()).toBeVisible()
|
|
678
|
+
})
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
test.describe('Color Independence', () => {
|
|
682
|
+
test.beforeEach(async ({ page }) => {
|
|
683
|
+
const authenticated = await isAuthenticated(page)
|
|
684
|
+
test.skip(!authenticated, 'No authenticated session available')
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
test('status indicators have text/icon alternatives to color', async ({ page }) => {
|
|
688
|
+
await page.goto('/integrations')
|
|
689
|
+
|
|
690
|
+
// Check connected status - should have "Connected" badge text, not just green color
|
|
691
|
+
const connectedBadge = page.locator('[class*="Badge"]', { hasText: /connected/i })
|
|
692
|
+
const count = await connectedBadge.count()
|
|
693
|
+
|
|
694
|
+
if (count > 0) {
|
|
695
|
+
await expect(connectedBadge.first()).toBeVisible()
|
|
696
|
+
// Badge has text, so color is not the only indicator
|
|
697
|
+
const text = await connectedBadge.first().textContent()
|
|
698
|
+
expect(text?.toLowerCase()).toContain('connected')
|
|
699
|
+
}
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
test('form validation errors have text descriptions', async ({ page }) => {
|
|
703
|
+
await page.goto('/team')
|
|
704
|
+
|
|
705
|
+
// Open invite dialog
|
|
706
|
+
await page.getByRole('button', { name: /invite member/i }).click()
|
|
707
|
+
const dialog = page.getByRole('dialog')
|
|
708
|
+
await expect(dialog).toBeVisible()
|
|
709
|
+
|
|
710
|
+
// Email input should have label for screen readers
|
|
711
|
+
const emailInput = page.getByLabel(/email address/i)
|
|
712
|
+
await expect(emailInput).toBeVisible()
|
|
713
|
+
|
|
714
|
+
// If we had invalid input, error should be in text, not just color
|
|
715
|
+
// For now, verify label exists
|
|
716
|
+
const label = page.locator('label', { hasText: /email/i })
|
|
717
|
+
await expect(label).toBeVisible()
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
test('role badges convey meaning without color', async ({ page }) => {
|
|
721
|
+
await page.goto('/team')
|
|
722
|
+
|
|
723
|
+
// Role badges should have text (admin, member, owner)
|
|
724
|
+
const badges = page.locator('[class*="Badge"]')
|
|
725
|
+
const count = await badges.count()
|
|
726
|
+
|
|
727
|
+
for (let i = 0; i < Math.min(count, 5); i++) {
|
|
728
|
+
const badge = badges.nth(i)
|
|
729
|
+
if (await badge.isVisible()) {
|
|
730
|
+
const text = await badge.textContent()
|
|
731
|
+
// Badge should have readable text content
|
|
732
|
+
expect(text?.trim().length).toBeGreaterThan(0)
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
})
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
test.describe('Focus Visibility', () => {
|
|
739
|
+
test.beforeEach(async ({ page }) => {
|
|
740
|
+
const authenticated = await isAuthenticated(page)
|
|
741
|
+
test.skip(!authenticated, 'No authenticated session available')
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
test('focused elements have visible focus indicator', async ({ page }) => {
|
|
745
|
+
await page.goto('/dashboard')
|
|
746
|
+
|
|
747
|
+
// Tab to first interactive element
|
|
748
|
+
await page.keyboard.press('Tab')
|
|
749
|
+
const focused = page.locator(':focus')
|
|
750
|
+
|
|
751
|
+
if (await focused.count() > 0) {
|
|
752
|
+
// Get computed style to check for focus ring
|
|
753
|
+
const styles = await focused.evaluate((el) => {
|
|
754
|
+
const computed = window.getComputedStyle(el)
|
|
755
|
+
return {
|
|
756
|
+
outline: computed.outline,
|
|
757
|
+
outlineWidth: computed.outlineWidth,
|
|
758
|
+
boxShadow: computed.boxShadow,
|
|
759
|
+
borderColor: computed.borderColor,
|
|
760
|
+
}
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
// Element should have some visible focus indicator
|
|
764
|
+
// (outline, box-shadow, or border change)
|
|
765
|
+
const hasVisibleFocus =
|
|
766
|
+
styles.outlineWidth !== '0px' ||
|
|
767
|
+
styles.boxShadow !== 'none' ||
|
|
768
|
+
styles.borderColor !== 'transparent'
|
|
769
|
+
|
|
770
|
+
expect(hasVisibleFocus).toBe(true)
|
|
771
|
+
}
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
test('buttons show focus state distinctly from hover', async ({ page }) => {
|
|
775
|
+
await page.goto('/settings')
|
|
776
|
+
|
|
777
|
+
const saveButton = page.getByRole('button', { name: /save changes/i })
|
|
778
|
+
|
|
779
|
+
// Get normal state
|
|
780
|
+
const normalStyles = await saveButton.evaluate((el) => {
|
|
781
|
+
const computed = window.getComputedStyle(el)
|
|
782
|
+
return {
|
|
783
|
+
outline: computed.outline,
|
|
784
|
+
boxShadow: computed.boxShadow,
|
|
785
|
+
}
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
// Focus the button
|
|
789
|
+
await saveButton.focus()
|
|
790
|
+
|
|
791
|
+
// Get focused state
|
|
792
|
+
const focusedStyles = await saveButton.evaluate((el) => {
|
|
793
|
+
const computed = window.getComputedStyle(el)
|
|
794
|
+
return {
|
|
795
|
+
outline: computed.outline,
|
|
796
|
+
boxShadow: computed.boxShadow,
|
|
797
|
+
}
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
// Focus state should be different from normal state
|
|
801
|
+
const hasDistinctFocus =
|
|
802
|
+
focusedStyles.outline !== normalStyles.outline ||
|
|
803
|
+
focusedStyles.boxShadow !== normalStyles.boxShadow
|
|
804
|
+
|
|
805
|
+
expect(hasDistinctFocus).toBe(true)
|
|
806
|
+
})
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
test.describe('Semantic HTML', () => {
|
|
810
|
+
test.beforeEach(async ({ page }) => {
|
|
811
|
+
const authenticated = await isAuthenticated(page)
|
|
812
|
+
test.skip(!authenticated, 'No authenticated session available')
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
test('page uses semantic HTML elements', async ({ page }) => {
|
|
816
|
+
await page.goto('/dashboard')
|
|
817
|
+
|
|
818
|
+
// Check for semantic structure
|
|
819
|
+
const main = page.locator('main')
|
|
820
|
+
await expect(main).toBeVisible()
|
|
821
|
+
|
|
822
|
+
const nav = page.locator('nav')
|
|
823
|
+
await expect(nav.first()).toBeVisible()
|
|
824
|
+
|
|
825
|
+
const aside = page.locator('aside')
|
|
826
|
+
await expect(aside).toBeVisible()
|
|
827
|
+
|
|
828
|
+
// Headings should be present
|
|
829
|
+
const h1 = page.getByRole('heading', { level: 1 })
|
|
830
|
+
await expect(h1).toBeVisible()
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
test('lists use proper list elements', async ({ page }) => {
|
|
834
|
+
await page.goto('/team')
|
|
835
|
+
|
|
836
|
+
// Team members should be in a structured format
|
|
837
|
+
// Even if not using ul/li, should have role="list" or similar structure
|
|
838
|
+
const memberList = page.locator('[class*="divide-y"]')
|
|
839
|
+
await expect(memberList.first()).toBeVisible()
|
|
840
|
+
|
|
841
|
+
// Should contain multiple items
|
|
842
|
+
const items = memberList.first().locator('> div')
|
|
843
|
+
const count = await items.count()
|
|
844
|
+
expect(count).toBeGreaterThanOrEqual(1)
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
test('buttons are not links and links are not buttons', async ({ page }) => {
|
|
848
|
+
await page.goto('/dashboard')
|
|
849
|
+
|
|
850
|
+
// Buttons should be <button> elements
|
|
851
|
+
const buttons = page.getByRole('button')
|
|
852
|
+
const buttonCount = await buttons.count()
|
|
853
|
+
|
|
854
|
+
for (let i = 0; i < Math.min(buttonCount, 5); i++) {
|
|
855
|
+
const button = buttons.nth(i)
|
|
856
|
+
if (await button.isVisible()) {
|
|
857
|
+
const tagName = await button.evaluate((el) => el.tagName.toLowerCase())
|
|
858
|
+
// Should be button or have role="button"
|
|
859
|
+
const role = await button.getAttribute('role')
|
|
860
|
+
expect(tagName === 'button' || role === 'button').toBe(true)
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Links should be <a> elements
|
|
865
|
+
const links = page.getByRole('link')
|
|
866
|
+
const linkCount = await links.count()
|
|
867
|
+
|
|
868
|
+
for (let i = 0; i < Math.min(linkCount, 5); i++) {
|
|
869
|
+
const link = links.nth(i)
|
|
870
|
+
if (await link.isVisible()) {
|
|
871
|
+
const tagName = await link.evaluate((el) => el.tagName.toLowerCase())
|
|
872
|
+
expect(tagName).toBe('a')
|
|
873
|
+
|
|
874
|
+
// Links should have href
|
|
875
|
+
const href = await link.getAttribute('href')
|
|
876
|
+
expect(href).toBeTruthy()
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
})
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
test.describe('Text Accessibility', () => {
|
|
883
|
+
test.beforeEach(async ({ page }) => {
|
|
884
|
+
const authenticated = await isAuthenticated(page)
|
|
885
|
+
test.skip(!authenticated, 'No authenticated session available')
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
test('text can be resized up to 200% without loss', async ({ page }) => {
|
|
889
|
+
await page.goto('/dashboard')
|
|
890
|
+
|
|
891
|
+
// Zoom to 200%
|
|
892
|
+
await page.evaluate(() => {
|
|
893
|
+
document.body.style.zoom = '2'
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
// Content should still be visible and usable
|
|
897
|
+
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible()
|
|
898
|
+
|
|
899
|
+
// Navigation should still work
|
|
900
|
+
const navLinks = page.locator('nav').getByRole('link')
|
|
901
|
+
await expect(navLinks.first()).toBeVisible()
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
test('line height and spacing support readability', async ({ page }) => {
|
|
905
|
+
await page.goto('/dashboard')
|
|
906
|
+
|
|
907
|
+
// Check paragraph text has adequate line height
|
|
908
|
+
const paragraphs = page.locator('p')
|
|
909
|
+
const count = await paragraphs.count()
|
|
910
|
+
|
|
911
|
+
if (count > 0) {
|
|
912
|
+
const styles = await paragraphs.first().evaluate((el) => {
|
|
913
|
+
const computed = window.getComputedStyle(el)
|
|
914
|
+
const fontSize = parseFloat(computed.fontSize)
|
|
915
|
+
const lineHeight = parseFloat(computed.lineHeight)
|
|
916
|
+
return { fontSize, lineHeight, ratio: lineHeight / fontSize }
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
// Line height should be at least 1.5 times font size for readability
|
|
920
|
+
// (WCAG AAA recommends 1.5, AA allows 1.2)
|
|
921
|
+
expect(styles.ratio).toBeGreaterThanOrEqual(1.2)
|
|
922
|
+
}
|
|
923
|
+
})
|
|
924
|
+
})
|
|
925
|
+
})
|