diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..b404775add --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,85 @@ +# UI Components - Development Guide + +Development guidance for the GoA Design System UI Components monorepo. + +## Project Overview + +This is a **monorepo** that provides UI components for Alberta government applications across three frameworks: Web Components (Svelte), React, and Angular. The project uses **Nx** for monorepo management and follows a **wrapper pattern** where Svelte is the single source of truth. + +## Quick Reference + +| Library | Location | Framework | Naming | +|---------|----------|-----------|--------| +| Web Components | `libs/web-components/` | Svelte | `goa-*` tags | +| React Wrappers | `libs/react-components/` | React | `Goab*` components | +| Angular Wrappers | `libs/angular-components/` | Angular | `goab-*` selectors | +| Shared Types | `libs/common/` | TypeScript | Types + utilities | + +--- + +## Architecture: The Wrapper Pattern + +**This is the most important concept in the codebase.** + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Source of Truth │ +│ │ +│ libs/web-components/src/components/button/Button.svelte │ +│ - All logic, styling, and behavior │ +│ - Compiles to custom element │ +│ - Uses Svelte's customElement directive │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Framework Wrappers │ +│ │ +│ React: libs/react-components/src/lib/button/button.tsx │ +│ - Wraps element │ +│ - Transforms React props → web component attributes │ +│ - Handles React-specific event binding │ +│ │ +│ Angular: libs/angular-components/src/lib/components/button/ │ +│ - Wraps element │ +│ - Uses @Input/@Output decorators │ +│ - Integrates with Angular forms (ControlValueAccessor) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Key insight:** Svelte components are the source of truth. When fixing bugs or adding features, start with the Svelte component first. The wrappers are thin layers that expose the web component to each framework. + +--- + +## Core Development Principles + +1. **Svelte is the source of truth** - Fix bugs in Svelte first, then update wrappers +2. **Design tokens first** - Use design tokens from `@abgov/design-tokens` (CSS custom properties in `libs/web-components/src/assets/css/`). Avoid hardcoding colors, spacing, or typography +3. **WCAG 2.2 AA compliance** - Accessibility is mandatory, not optional +4. **All three frameworks** - Every component change needs corresponding React + Angular wrapper updates +5. **Tests required** - No component changes without corresponding test updates +6. **Backward compatibility** - Don't break existing implementations + +--- + +## Package Manager & Commands + +- **Package manager:** `npm` (not yarn, pnpm, or bun) +- **Build all libraries:** `npm run build` +- **Full PR validation:** `npm run test:pr` (builds + runs all tests) +- **Run playgrounds:** `npm run serve:prs:react`, `npm run serve:prs:angular`, `npm run serve:prs:web` + +--- + +## Detailed Documentation + +For step-by-step workflows, testing strategies, and troubleshooting: + +- **Component Workflows:** `agent_docs/component_workflows.md` - Updating existing components and adding new ones +- **PR Testing Guide:** `agent_docs/pr_testing_guide.md` - How to use the PR playground in `apps/prs/` +- **Testing:** `agent_docs/testing.md` - Test commands, frameworks, and strategies +- **Naming Conventions:** `agent_docs/naming_conventions.md` - File, component, and prop naming rules +- **Common Issues:** `agent_docs/common_issues.md` - Troubleshooting guide +- **Folder Structure:** `agent_docs/folder_structure.md` - Complete directory reference + +These documents provide detailed guidance for specific tasks. Read them when relevant to your current work. diff --git a/agent_docs/common_issues.md b/agent_docs/common_issues.md new file mode 100644 index 0000000000..f852ceda25 --- /dev/null +++ b/agent_docs/common_issues.md @@ -0,0 +1,576 @@ +# Common Issues & Troubleshooting + +Solutions to frequently encountered problems when developing UI components. + +--- + +## Component Issues + +### Boolean Prop Not Working + +**Symptom:** Boolean prop has no effect on web component + +**Cause:** Web components receive all attributes as strings, not booleans. + +**Solution:** + +Web components must check for the string `"true"`: + +```svelte + + + + + + + +``` + +React/Angular wrappers must convert boolean to string: + +```typescript +// React + + +// Angular + +``` + +--- + +### Event Not Firing + +**Symptom:** Custom event listener not being called + +**Possible Causes:** + +#### 1. Event Name Mismatch + +Web component events use underscore prefix (`_click`), but wrappers expose them without it (`onClick`). + +**Check:** +- Web component dispatches: `_click` +- React prop name: `onClick` +- Angular output name: `onClick` + +#### 2. Event Not Being Dispatched + +Ensure the web component uses the `dispatch` utility: + +```svelte + +``` + +#### 3. Event Listener Not Attached (React) + +React wrappers need to manually attach event listeners: + +```typescript +// ✅ Correct +export function GoabButton({ onClick, ...props }: GoabButtonProps) { + const ref = useRef(null); + + useEffect(() => { + if (!ref.current || !onClick) return; + + const handler = (e: Event) => onClick(e as CustomEvent); + ref.current.addEventListener("_click", handler); + + return () => ref.current?.removeEventListener("_click", handler); + }, [onClick]); + + return ; +} + +// ❌ Wrong - missing event listener +export function GoabButton({ onClick, ...props }: GoabButtonProps) { + return ; // onClick is lost! +} +``` + +--- + +### Styles Not Applying + +**Symptom:** Component doesn't look right; styles are missing or incorrect + +**Possible Causes:** + +#### 1. Hardcoded Values Instead of Design Tokens + +```svelte + + + + + +``` + +**Solution:** Always use CSS custom properties from `libs/web-components/src/assets/css/variables.css` + +#### 2. Missing CSS Imports + +Ensure your Svelte component imports the necessary CSS files: + +```svelte + + + + + +``` + +#### 3. Styles Not Scoped Correctly + +Svelte automatically scopes styles to the component. If you need global styles, use `:global()`: + +```svelte + +``` + +#### 4. Styles Overridden by Specificity + +Check browser DevTools to see if styles are being overridden. Increase specificity if needed: + +```css +/* Lower specificity */ +.button { } + +/* Higher specificity */ +.goa-button.goa-button--primary { } +``` + +--- + +### Tests Failing After Component Update + +**Symptom:** Tests fail after making changes to a component + +**Possible Causes:** + +#### 1. Libraries Not Rebuilt + +Wrappers depend on the built web components. Rebuild before running tests: + +```bash +npm run build +npm run test:unit +``` + +#### 2. Test Snapshots Outdated + +If using snapshot tests, update them: + +```bash +# Vitest +npm run test:unit -- -u + +# Jest (Angular) +npm run test:angular -- -u +``` + +#### 3. Tests Not Updated in All Three Frameworks + +When you change a component, you must update tests in **all three libraries**: + +- `libs/web-components/src/components/[name]/[Name].spec.ts` +- `libs/react-components/src/lib/[name]/[name].spec.tsx` +- `libs/angular-components/src/lib/components/[name]/[name].spec.ts` + +#### 4. Async State Not Awaited + +Ensure async updates are properly awaited: + +```typescript +// ❌ Wrong +fireEvent.click(button); +expect(screen.getByText('Clicked')).toBeInTheDocument(); + +// ✅ Correct +await fireEvent.click(button); +expect(await screen.findByText('Clicked')).toBeInTheDocument(); +``` + +--- + +### Type Errors in Shared Types + +**Symptom:** TypeScript errors about missing or incompatible types + +**Solution:** + +Ensure types are properly exported from `libs/common/src/lib/common.ts` and imported in all three libraries: + +```typescript +// libs/common/src/lib/common.ts +export type GoabButtonType = "primary" | "secondary" | "tertiary"; + +// libs/web-components/src/components/button/Button.svelte +import type { GoabButtonType } from '@abgov/common'; + +// libs/react-components/src/lib/button/button.tsx +import type { GoabButtonType } from '@abgov/common'; + +// libs/angular-components/src/lib/components/button/button.ts +import type { GoabButtonType } from '@abgov/common'; +``` + +After updating types, rebuild: + +```bash +npm run build +``` + +--- + +## Build Issues + +### Build Fails with "Module not found" + +**Symptom:** Build error: `Cannot find module '@abgov/web-components'` + +**Cause:** Dependencies not properly built or installed + +**Solution:** + +```bash +# Clean and rebuild +rm -rf node_modules +npm install +npm run build +``` + +### Build Fails with Vite Errors + +**Symptom:** Vite configuration or plugin errors + +**Solution:** + +Check `vite.config.js` files in each library. Common issues: +- Incorrect plugin configuration +- Missing dependencies +- Outdated Vite version + +```bash +# Update Vite and plugins +npm update vite @vitejs/plugin-react vite-plugin-dts +``` + +### Angular Build Fails + +**Symptom:** `ng-packagr` errors during Angular build + +**Solution:** + +Check `ng-package.json` configuration. Ensure: +- Entry file path is correct +- TypeScript configuration is valid +- All dependencies are installed + +```bash +# Rebuild Angular library specifically +npm run build:angular +``` + +--- + +## Development Server Issues + +### Playground Not Loading Components + +**Symptom:** Components don't appear in the playground + +**Cause:** Libraries not built or hot reload issues + +**Solution:** + +```bash +# Build all libraries +npm run build + +# Restart the playground server +npm run serve:prs:react +``` + +### Port Already in Use + +**Symptom:** `Error: listen EADDRINUSE: address already in use :::3000` + +**Solution:** + +Kill the process using the port: + +```bash +# Find process on port 3000 +lsof -ti:3000 | xargs kill -9 + +# Or use a different port +PORT=3001 npm run serve:prs:react +``` + +### Hot Reload Not Working + +**Symptom:** Changes not reflected in browser after saving + +**Solution:** + +1. Hard refresh browser: `Ctrl+Shift+R` (or `Cmd+Shift+R` on Mac) +2. Clear browser cache +3. Restart dev server +4. Rebuild libraries: `npm run build` + +--- + +## Testing Issues + +### Tests Timeout + +**Symptom:** Tests fail with timeout errors + +**Possible Causes:** + +#### 1. Async Operations Not Resolved + +```typescript +// ❌ Wrong - missing await +it('should update after click', () => { + fireEvent.click(button); + expect(screen.getByText('Updated')).toBeInTheDocument(); +}); + +// ✅ Correct +it('should update after click', async () => { + await fireEvent.click(button); + expect(await screen.findByText('Updated')).toBeInTheDocument(); +}); +``` + +#### 2. Infinite Loops or Unresolved Promises + +Check for infinite loops or promises that never resolve. Use `waitFor` with timeout: + +```typescript +await waitFor( + () => { + expect(screen.getByText('Loaded')).toBeInTheDocument(); + }, + { timeout: 5000 } +); +``` + +### Tests Passing Locally but Failing in CI + +**Possible Causes:** + +#### 1. Timing Issues + +CI environments may be slower. Increase timeouts: + +```typescript +it('should load data', async () => { + // ... +}, { timeout: 10000 }); // 10 second timeout +``` + +#### 2. Missing Dependencies + +Ensure `package.json` includes all dev dependencies needed for testing. + +#### 3. Environment Differences + +Check Node version, environment variables, and browser availability in CI. + +### Browser Tests Not Running + +**Symptom:** Playwright tests fail to run + +**Solution:** + +Install Playwright browsers: + +```bash +npx playwright install +``` + +--- + +## Git/PR Issues + +### PR Checks Failing + +**Symptom:** GitHub Actions checks fail on PR + +**Solution:** + +Run the same commands locally before pushing: + +```bash +npm run build +npm run test:pr +``` + +Fix any errors before pushing again. + +### Merge Conflicts in package-lock.json + +**Solution:** + +```bash +# Accept their version +git checkout --theirs package-lock.json + +# Regenerate lock file +npm install + +# Stage and continue +git add package-lock.json +git rebase --continue +``` + +--- + +## Accessibility Issues + +### Screen Reader Not Announcing Changes + +**Cause:** Missing ARIA live regions or labels + +**Solution:** + +Add appropriate ARIA attributes: + +```svelte + +
+ {statusMessage} +
+ + + + + + +
+ {errorMessage} +
+``` + +### Focus Not Visible + +**Cause:** Missing or removed focus outline + +**Solution:** + +Never remove focus outline without providing an alternative: + +```css +/* ❌ Wrong */ +button:focus { + outline: none; +} + +/* ✅ Correct */ +button:focus { + outline: 2px solid var(--goa-color-focus); + outline-offset: 2px; +} + +/* Or use :focus-visible for keyboard-only focus */ +button:focus-visible { + outline: 2px solid var(--goa-color-focus); + outline-offset: 2px; +} +``` + +--- + +## Performance Issues + +### Slow Component Rendering + +**Possible Causes:** + +#### 1. Inefficient Reactivity (Svelte) + +Avoid expensive computations in reactive statements: + +```svelte + + + + + +``` + +#### 2. Missing Memoization (React) + +Use `useMemo` and `useCallback` for expensive operations: + +```typescript +// ❌ Wrong - recreates function on every render +const handleClick = () => doSomething(prop); + +// ✅ Correct +const handleClick = useCallback(() => doSomething(prop), [prop]); +``` + +--- + +## When All Else Fails + +1. **Read error messages carefully** - they often tell you exactly what's wrong +2. **Check the git history** - see how similar components were implemented +3. **Search the codebase** - use grep to find similar patterns: `grep -r "pattern" libs/` +4. **Ask the team** - they may have encountered the issue before +5. **Create a minimal reproduction** - isolate the problem in a simple test case diff --git a/agent_docs/component_workflows.md b/agent_docs/component_workflows.md new file mode 100644 index 0000000000..aaa805f74b --- /dev/null +++ b/agent_docs/component_workflows.md @@ -0,0 +1,255 @@ +# Component Development Workflows + +Detailed step-by-step workflows for working with components in the UI Components monorepo. + +--- + +## Updating an Existing Component + +When modifying an existing component, you must update all three framework implementations. + +### Step 1: Modify the Svelte Component + +Location: `libs/web-components/src/components/[name]/` + +1. Update the `.svelte` file with your changes: + - Modify logic, props, or styling + - Ensure design tokens are used (never hardcode values) + - Maintain WCAG 2.2 AA accessibility compliance + +2. Update the `.spec.ts` test file: + - Add tests for new functionality + - Update existing tests if behavior changed + - Run tests: `npm run test:unit:watch` + +### Step 2: Update the React Wrapper + +Location: `libs/react-components/src/lib/[name]/` + +1. Update `[name].tsx`: + - Add new props to the TypeScript interface + - Handle prop transformation if needed (e.g., boolean → string) + - Add event handlers for new events + +2. Update `[name].spec.tsx`: + - Add tests for new props/events + - Run tests: `npm run test:unit:watch` + +### Step 3: Update the Angular Wrapper + +Location: `libs/angular-components/src/lib/components/[name]/` + +1. Update `[name].ts`: + - Add new `@Input()` decorators for props + - Add new `@Output()` decorators for events + - Update `ControlValueAccessor` implementation if needed + +2. Update `[name].spec.ts`: + - Add tests for new inputs/outputs + - Run tests: `npm run test:angular` + +### Step 4: Update Shared Types (if needed) + +Location: `libs/common/src/lib/common.ts` + +If you added new prop types or shared constants, update the common types file and ensure all three frameworks reference them. + +### Step 5: Build and Test + +```bash +# Build all libraries +npm run build + +# Run full PR validation +npm run test:pr + +# Test in playgrounds +npm run serve:prs:react +npm run serve:prs:angular +npm run serve:prs:web +``` + +### Step 6: Create PR Test Page + +See `agent_docs/pr_testing_guide.md` for detailed instructions on creating a test page in `apps/prs/`. + +--- + +## Adding a New Component + +New components require files in all three libraries plus shared types. + +### File Structure Required + +**Web Component (Svelte):** +``` +libs/web-components/src/components/[name]/ +├── ComponentName.svelte # Main implementation +├── ComponentName.spec.ts # Unit tests +├── ComponentName.html-data.json # VS Code IntelliSense (optional) +└── doc.md # Documentation (optional) +``` + +**React Wrapper:** +``` +libs/react-components/src/lib/[name]/ +├── component-name.tsx # Wrapper component +└── component-name.spec.tsx # Tests +``` + +**Angular Wrapper:** +``` +libs/angular-components/src/lib/components/[name]/ +├── component-name.ts # Wrapper component +└── component-name.spec.ts # Tests +``` + +### Step-by-Step Process + +1. **Create the Svelte component** in `libs/web-components/src/components/[name]/` + - Use `customElement` directive: `` + - Import and use design tokens from CSS custom properties + - Add comprehensive tests + - Follow WCAG 2.2 AA guidelines + +2. **Export from web-components** in `libs/web-components/src/index.ts`: + ```typescript + export { default as GoaComponentName } from './components/[name]/ComponentName.svelte'; + ``` + +3. **Create React wrapper** in `libs/react-components/src/lib/[name]/[name].tsx`: + ```typescript + import React from 'react'; + + export interface GoabComponentNameProps { + // Define props with camelCase naming + } + + export function GoabComponentName(props: GoabComponentNameProps) { + // Wrap element + // Transform props as needed + } + ``` + +4. **Export from react-components** in `libs/react-components/src/index.ts`: + ```typescript + export * from './lib/[name]/[name]'; + ``` + +5. **Create Angular wrapper** in `libs/angular-components/src/lib/components/[name]/[name].ts`: + ```typescript + import { Component, Input, Output, EventEmitter } from '@angular/core'; + + @Component({ + selector: 'goab-component-name', + template: '' + }) + export class GoabComponentNameComponent { + @Input() someProp?: string; + @Output() onClick = new EventEmitter(); + } + ``` + +6. **Export from angular-components** in `libs/angular-components/src/index.ts`: + ```typescript + export * from './lib/components/[name]/[name]'; + ``` + +7. **Add shared types** to `libs/common/src/lib/common.ts` if needed: + ```typescript + export type GoabComponentNameType = "primary" | "secondary"; + ``` + +8. **Build, test, and create PR test page** (see steps in "Updating an Existing Component") + +--- + +## Props Transformation Rules + +Web components receive all attributes as strings. Wrappers must handle type conversion. + +### Boolean Props + +```typescript +// React wrapper +disabled={disabled ? "true" : undefined} + +// Angular wrapper +[attr.disabled]="disabled ? 'true' : null" +``` + +### Enum/Union Props + +```typescript +// React wrapper - pass through as-is +type={type} + +// Angular wrapper - pass through as-is +[attr.type]="type" +``` + +### Object/Array Props + +Web components cannot receive objects/arrays as attributes. Either: +1. Use JSON stringification (not recommended) +2. Use child elements or slots +3. Set properties directly via ref (React) or ViewChild (Angular) + +--- + +## Event Handling + +Web components dispatch custom events with underscore prefix. Wrappers expose them with framework conventions. + +### Web Component (Svelte) + +```svelte + + + +``` + +### React Wrapper + +```typescript +export interface GoabButtonProps { + onClick?: (event: CustomEvent) => void; +} + +export function GoabButton({ onClick, ...props }: GoabButtonProps) { + const ref = useRef(null); + + useEffect(() => { + if (!ref.current) return; + + const handler = (e: Event) => onClick?.(e as CustomEvent); + ref.current.addEventListener("_click", handler); + + return () => ref.current?.removeEventListener("_click", handler); + }, [onClick]); + + return ; +} +``` + +### Angular Wrapper + +```typescript +@Component({ + selector: 'goab-button', + template: '' +}) +export class GoabButtonComponent { + @Output() onClick = new EventEmitter(); + + handleClick(event: Event) { + this.onClick.emit(event as CustomEvent); + } +} +``` diff --git a/agent_docs/folder_structure.md b/agent_docs/folder_structure.md new file mode 100644 index 0000000000..baee2466ae --- /dev/null +++ b/agent_docs/folder_structure.md @@ -0,0 +1,435 @@ +# Folder Structure Reference + +Complete directory structure and organization of the UI Components monorepo. + +--- + +## High-Level Overview + +``` +ui-components/ +├── libs/ # Component libraries +│ ├── web-components/ # Svelte → Web Components +│ ├── react-components/ # React wrappers +│ ├── angular-components/# Angular wrappers +│ └── common/ # Shared types +├── apps/ # Applications +│ ├── prs/ # PR testing playgrounds +│ └── playground/ # Development playgrounds +├── docs/ # Documentation +├── tools/ # Build tools and scripts +└── agent_docs/ # Detailed development guides +``` + +--- + +## Libraries Directory (`libs/`) + +### Web Components (`libs/web-components/`) + +The source of truth for all UI components. Svelte components compile to web components. + +``` +libs/web-components/ +├── src/ +│ ├── components/ # All UI components +│ │ ├── button/ +│ │ │ ├── Button.svelte # Main component +│ │ │ ├── Button.spec.ts # Unit tests +│ │ │ ├── Button.html-data.json # VS Code IntelliSense (optional) +│ │ │ └── doc.md # Documentation (optional) +│ │ ├── input/ +│ │ │ ├── Input.svelte +│ │ │ └── Input.spec.ts +│ │ ├── date-picker/ +│ │ │ ├── DatePicker.svelte +│ │ │ └── DatePicker.spec.ts +│ │ └── ... (other components) +│ │ +│ ├── assets/ # Shared assets +│ │ └── css/ +│ │ ├── variables.css # Design tokens (CSS custom properties) +│ │ ├── components.css # Component-specific global styles +│ │ ├── fonts.css # Typography +│ │ └── reset.css # CSS reset +│ │ +│ ├── common/ # Shared utilities +│ │ ├── utils.ts # Utility functions (e.g., dispatch) +│ │ └── types.ts # Internal types +│ │ +│ └── index.ts # Public exports +│ +├── package.json # Dependencies and scripts +├── vite.config.js # Vite configuration +├── tsconfig.json # TypeScript configuration +└── README.md # Library documentation +``` + +**Key Files:** +- **`Button.svelte`**: Component implementation with `` +- **`Button.spec.ts`**: Vitest tests using `@testing-library/svelte` +- **`index.ts`**: Exports all components: `export { default as GoaButton } from './components/button/Button.svelte'` +- **`variables.css`**: Design tokens from `@abgov/design-tokens` + +--- + +### React Components (`libs/react-components/`) + +React wrappers for web components. + +``` +libs/react-components/ +├── src/ +│ ├── lib/ # React components +│ │ ├── button/ +│ │ │ ├── button.tsx # GoabButton wrapper +│ │ │ ├── button.spec.tsx # Unit tests +│ │ │ └── button.browser.spec.tsx # Playwright tests (optional) +│ │ ├── input/ +│ │ │ ├── input.tsx +│ │ │ └── input.spec.tsx +│ │ ├── date-picker/ +│ │ │ ├── date-picker.tsx +│ │ │ └── date-picker.spec.tsx +│ │ └── ... (other wrappers) +│ │ +│ └── index.ts # Public exports +│ +├── package.json +├── vite.config.ts # Vite configuration +├── tsconfig.json # TypeScript configuration +└── README.md +``` + +**Key Files:** +- **`button.tsx`**: React wrapper that wraps ``, handles props/events +- **`button.spec.tsx`**: Vitest tests using `@testing-library/react` +- **`button.browser.spec.tsx`**: Playwright browser tests (when needed) +- **`index.ts`**: Exports: `export * from './lib/button/button'` + +--- + +### Angular Components (`libs/angular-components/`) + +Angular wrappers for web components. + +``` +libs/angular-components/ +├── src/ +│ ├── lib/ +│ │ └── components/ # Angular components +│ │ ├── button/ +│ │ │ ├── button.ts # GoabButtonComponent +│ │ │ └── button.spec.ts # Jest tests +│ │ ├── input/ +│ │ │ ├── input.ts +│ │ │ └── input.spec.ts +│ │ ├── date-picker/ +│ │ │ ├── date-picker.ts +│ │ │ └── date-picker.spec.ts +│ │ └── ... (other wrappers) +│ │ +│ └── index.ts # Public exports +│ +├── package.json +├── ng-package.json # Angular packaging configuration +├── tsconfig.json +└── README.md +``` + +**Key Files:** +- **`button.ts`**: Angular component with `@Component` decorator, selector `goab-button` +- **`button.spec.ts`**: Jest tests using `@testing-library/angular` +- **`index.ts`**: Exports: `export * from './lib/components/button/button'` + +--- + +### Common (`libs/common/`) + +Shared TypeScript types and utilities used across all three frameworks. + +``` +libs/common/ +├── src/ +│ ├── lib/ +│ │ └── common.ts # Shared types +│ └── index.ts # Public exports +│ +├── package.json +└── tsconfig.json +``` + +**`common.ts` Example:** +```typescript +// Prop types +export type GoabButtonType = "primary" | "secondary" | "tertiary"; +export type GoabIconType = "add" | "remove" | "edit" | "close"; +export type GoabSpacing = "0" | "1" | "2" | "3" | "m" | "l" | "xl"; + +// Constants +export const BUTTON_TYPES = ["primary", "secondary", "tertiary"] as const; +``` + +--- + +## Applications Directory (`apps/`) + +### PR Testing Playgrounds (`apps/prs/`) + +Shared testing playgrounds where PR test pages are committed. + +``` +apps/prs/ +├── react/ # React playground (primary) +│ ├── src/ +│ │ ├── routes/ +│ │ │ ├── _TEMPLATE.tsx # Template for new test pages +│ │ │ ├── bugs/ # Bug fix test pages +│ │ │ │ ├── bug2878.tsx # Example: Bug #2878 +│ │ │ │ ├── bug2922.tsx +│ │ │ │ └── ... +│ │ │ └── features/ # Feature test pages +│ │ │ ├── feat1234.tsx +│ │ │ └── ... +│ │ ├── main.tsx # Route configuration +│ │ ├── app.tsx # App shell with side menu +│ │ └── index.html +│ ├── package.json +│ └── vite.config.ts +│ +├── angular/ # Angular playground +│ ├── src/ +│ │ ├── app/ +│ │ │ ├── bugs/ +│ │ │ └── features/ +│ │ └── main.ts +│ └── package.json +│ +└── web/ # Web components playground + ├── src/ + │ ├── bugs/ + │ └── features/ + └── package.json +``` + +**Key Files:** +- **`_TEMPLATE.tsx`**: Template for creating new test pages +- **`main.tsx`**: Route definitions - add new routes here +- **`app.tsx`**: Side menu links - add navigation links here +- **`bugs/bug{N}.tsx`**: Bug fix test pages (committed with PR) +- **`features/feat{N}.tsx`**: Feature test pages (committed with PR) + +--- + +### Development Playgrounds (`playground/`) + +Personal playgrounds for local development (not committed to git). + +``` +playground/ +├── react/ +│ ├── src/ +│ └── package.json +├── angular/ +│ ├── src/ +│ └── package.json +└── web/ + ├── src/ + └── package.json +``` + +**Note:** These are typically `.gitignore`d for quick local experimentation. + +--- + +## Documentation Directory (`docs/`) + +``` +docs/ +├── components/ # Component documentation +│ ├── button.md +│ ├── input.md +│ └── ... +├── guides/ # Developer guides +│ ├── getting-started.md +│ ├── contributing.md +│ └── ... +└── api/ # API references +``` + +--- + +## Agent Documentation (`agent_docs/`) + +Detailed guides for AI-assisted development (see CLAUDE.md). + +``` +agent_docs/ +├── component_workflows.md # Updating/adding components +├── pr_testing_guide.md # PR playground usage +├── testing.md # Testing strategies +├── naming_conventions.md # Naming standards +├── common_issues.md # Troubleshooting +└── folder_structure.md # This file +``` + +--- + +## Build Tools (`tools/`) + +``` +tools/ +├── scripts/ # Build and automation scripts +│ ├── build.sh +│ └── test.sh +└── generators/ # Code generators (if any) +``` + +--- + +## Configuration Files (Root) + +``` +ui-components/ +├── package.json # Root package.json (monorepo workspace) +├── package-lock.json +├── nx.json # Nx configuration +├── tsconfig.base.json # Base TypeScript config +├── .gitignore +├── .prettierrc # Prettier configuration +├── .eslintrc.json # ESLint configuration +├── vitest.config.ts # Vitest configuration (if global) +├── playwright.config.ts # Playwright configuration +├── README.md # Project README +└── CLAUDE.md # AI assistant guide +``` + +--- + +## Component File Example + +### Complete Web Component Structure + +``` +libs/web-components/src/components/button/ +├── Button.svelte # Main implementation +├── Button.spec.ts # Unit tests +├── Button.html-data.json # VS Code IntelliSense (optional) +└── doc.md # Component documentation (optional) +``` + +**Button.svelte:** +```svelte + + + + + + + +``` + +--- + +## Testing File Locations Summary + +| Framework | Component | Test | Browser Test | +|-----------|-----------|------|--------------| +| Web Components | `libs/web-components/src/components/button/Button.svelte` | `Button.spec.ts` | N/A | +| React | `libs/react-components/src/lib/button/button.tsx` | `button.spec.tsx` | `button.browser.spec.tsx` | +| Angular | `libs/angular-components/src/lib/components/button/button.ts` | `button.spec.ts` | N/A | + +--- + +## Import Path Examples + +### From Libraries + +```typescript +// Web Components +import { GoaButton } from '@abgov/web-components'; + +// React +import { GoabButton } from '@abgov/react-components'; +import type { GoabButtonProps } from '@abgov/react-components'; + +// Angular +import { GoabButtonComponent } from '@abgov/angular-components'; + +// Common types +import type { GoabButtonType } from '@abgov/common'; +``` + +### Internal Imports (within a library) + +```typescript +// Web Components +import { dispatch } from '../../common/utils'; +import type { InternalType } from '../../common/types'; + +// React +import { GoabButton } from '../button/button'; + +// Angular +import { GoabButtonComponent } from '../button/button'; +``` + +--- + +## Build Output Locations + +After running `npm run build`, compiled files appear in: + +``` +libs/web-components/dist/ # Built web components +libs/react-components/dist/ # Built React library +libs/angular-components/dist/ # Built Angular library +libs/common/dist/ # Built common types +``` + +These are what get published to npm or consumed by applications. + +--- + +## Quick Navigation + +**Need to:** +- **Update a component?** → `libs/web-components/src/components/[name]/` +- **Add tests?** → Same folder as component, `*.spec.ts` or `*.spec.tsx` +- **Create PR test page?** → `apps/prs/react/src/routes/bugs/` or `features/` +- **Update shared types?** → `libs/common/src/lib/common.ts` +- **Check design tokens?** → `libs/web-components/src/assets/css/variables.css` +- **Find troubleshooting help?** → `agent_docs/common_issues.md` diff --git a/agent_docs/naming_conventions.md b/agent_docs/naming_conventions.md new file mode 100644 index 0000000000..6199e4238f --- /dev/null +++ b/agent_docs/naming_conventions.md @@ -0,0 +1,396 @@ +# Naming Conventions + +Comprehensive naming standards for files, components, props, and attributes across all three frameworks. + +--- + +## Component Naming + +### Web Components (Svelte) + +| Element | Convention | Example | +|---------|-----------|---------| +| Tag name | `goa-{name}` (kebab-case) | `` | +| File name | `{Name}.svelte` (PascalCase) | `Button.svelte` | +| Component class | `{Name}` (PascalCase) | `GoaButton` | +| Multi-word tags | `goa-{word}-{word}` | `` | +| Multi-word files | `{Word}{Word}.svelte` | `DatePicker.svelte` | + +**Example:** +``` +libs/web-components/src/components/button/Button.svelte +→ Compiles to: +``` + +### React + +| Element | Convention | Example | +|---------|-----------|---------| +| Component name | `Goab{Name}` (PascalCase) | `GoabButton` | +| File name | `{name}.tsx` (kebab-case) | `button.tsx` | +| Multi-word component | `Goab{Word}{Word}` | `GoabDatePicker` | +| Multi-word file | `{word}-{word}.tsx` | `date-picker.tsx` | + +**Example:** +``` +libs/react-components/src/lib/button/button.tsx +→ Exports: GoabButton +``` + +### Angular + +| Element | Convention | Example | +|---------|-----------|---------| +| Selector | `goab-{name}` (kebab-case) | `` | +| Component class | `Goab{Name}Component` (PascalCase) | `GoabButtonComponent` | +| File name | `{name}.ts` (kebab-case) | `button.ts` | +| Multi-word selector | `goab-{word}-{word}` | `` | +| Multi-word class | `Goab{Word}{Word}Component` | `GoabDatePickerComponent` | +| Multi-word file | `{word}-{word}.ts` | `date-picker.ts` | + +**Example:** +``` +libs/angular-components/src/lib/components/button/button.ts +→ Selector: goab-button +→ Class: GoabButtonComponent +``` + +--- + +## Props and Attributes + +### Naming Convention by Framework + +| Framework | Convention | Example | +|-----------|-----------|---------| +| Web Component | lowercase (no camelCase) | `leadingicon` | +| React | camelCase | `leadingIcon` | +| Angular | camelCase (for @Input) | `leadingIcon` | + +### Multi-Word Props + +| Framework | Convention | Example | +|-----------|-----------|---------| +| Web Component | kebab-case attributes | `action-args` | +| React | camelCase props | `actionArgs` | +| Angular | camelCase inputs | `actionArgs` | + +**Why lowercase for web components?** +HTML attributes are case-insensitive, so web components receive all attributes as lowercase strings. + +--- + +## Boolean Props/Attributes + +Booleans require special handling because web components receive all attributes as strings. + +### Web Component (Svelte) + +```svelte + + + +``` + +**Usage:** +```html + +``` + +### React + +React wrappers must convert boolean props to strings: + +```typescript +export interface GoabButtonProps { + disabled?: boolean; +} + +export function GoabButton({ disabled, ...props }: GoabButtonProps) { + return ; +} +``` + +**Usage:** +```jsx + + {/* Same as disabled={true} */} +``` + +### Angular + +Angular wrappers handle this via attribute binding: + +```typescript +@Component({ + selector: 'goab-button', + template: '' +}) +export class GoabButtonComponent { + @Input() disabled?: boolean; +} +``` + +**Usage:** +```html + + +``` + +--- + +## Event Naming + +Events use different conventions across frameworks. + +### Web Component (Svelte) + +Custom events use **underscore prefix**: + +```svelte + + + +``` + +**Dispatched event:** `_click` + +### React + +React wrappers expose events with **camelCase `on` prefix**: + +```typescript +export interface GoabButtonProps { + onClick?: (event: CustomEvent) => void; +} + +export function GoabButton({ onClick, ...props }: GoabButtonProps) { + const ref = useRef(null); + + useEffect(() => { + if (!ref.current || !onClick) return; + + const handler = (e: Event) => onClick(e as CustomEvent); + ref.current.addEventListener("_click", handler); + + return () => ref.current?.removeEventListener("_click", handler); + }, [onClick]); + + return ; +} +``` + +**Usage:** +```jsx + console.log('Clicked!', e.detail)} /> +``` + +### Angular + +Angular wrappers expose events as **@Output** properties: + +```typescript +@Component({ + selector: 'goab-button', + template: '' +}) +export class GoabButtonComponent { + @Output() onClick = new EventEmitter(); + + handleClick(event: Event) { + this.onClick.emit(event as CustomEvent); + } +} +``` + +**Usage:** +```html + +``` + +### Event Naming Summary + +| Framework | Convention | Example | +|-----------|-----------|---------| +| Web Component | `_eventname` | `_click`, `_change` | +| React | `onEventname` | `onClick`, `onChange` | +| Angular | `onEventname` (@Output) | `onClick`, `onChange` | + +--- + +## Type Naming + +Shared types in `libs/common/src/lib/common.ts` follow these conventions: + +### Component Prop Types + +```typescript +// Format: Goab{ComponentName}{PropName} +export type GoabButtonType = "primary" | "secondary" | "tertiary"; +export type GoabIconType = "add" | "remove" | "edit"; +``` + +### Enum-like Constants + +```typescript +// Format: {COMPONENT_NAME}_{PROP_NAME} +export const BUTTON_TYPES = ["primary", "secondary", "tertiary"] as const; +export type GoabButtonType = typeof BUTTON_TYPES[number]; +``` + +--- + +## File Naming + +### Component Files + +| Framework | Pattern | Example | +|-----------|---------|---------| +| Svelte component | `{Name}.svelte` | `Button.svelte`, `DatePicker.svelte` | +| Svelte test | `{Name}.spec.ts` | `Button.spec.ts` | +| React component | `{name}.tsx` | `button.tsx`, `date-picker.tsx` | +| React test | `{name}.spec.tsx` | `button.spec.tsx` | +| React browser test | `{name}.browser.spec.tsx` | `button.browser.spec.tsx` | +| Angular component | `{name}.ts` | `button.ts`, `date-picker.ts` | +| Angular test | `{name}.spec.ts` | `button.spec.ts` | + +### Documentation Files + +| File Type | Pattern | Example | +|-----------|---------|---------| +| Component docs | `doc.md` | `libs/web-components/src/components/button/doc.md` | +| VS Code IntelliSense | `{Name}.html-data.json` | `Button.html-data.json` | + +--- + +## Spacing Values + +The `mt`, `mr`, `mb`, `ml` props accept standardized spacing values: + +### Numeric Values +```typescript +type NumericSpacing = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10"; +``` + +### T-Shirt Sizes +```typescript +type TShirtSpacing = "none" | "3xs" | "2xs" | "xs" | "s" | "m" | "l" | "xl" | "2xl" | "3xl" | "4xl"; +``` + +### Combined Type +```typescript +export type GoabSpacing = NumericSpacing | TShirtSpacing; +``` + +**Usage:** +```jsx +// Numeric + + +// T-shirt + +``` + +--- + +## CSS Class Naming + +When custom classes are needed (rare, since design tokens handle most styling): + +### BEM-like Convention + +```css +/* Block */ +.goa-button { } + +/* Element */ +.goa-button__icon { } + +/* Modifier */ +.goa-button--primary { } +.goa-button--disabled { } + +/* State */ +.goa-button.is-loading { } +``` + +**Note:** Most styling should use CSS custom properties (design tokens) instead of custom classes. + +--- + +## Directory Naming + +| Directory Type | Convention | Example | +|----------------|-----------|---------| +| Component folder | kebab-case | `button/`, `date-picker/` | +| Library folder | kebab-case | `web-components/`, `react-components/` | +| App folder | kebab-case | `prs/`, `playground/` | + +--- + +## Import/Export Naming + +### Exports + +**Web Components:** +```typescript +// libs/web-components/src/index.ts +export { default as GoaButton } from './components/button/Button.svelte'; +export { default as GoaDatePicker } from './components/date-picker/DatePicker.svelte'; +``` + +**React:** +```typescript +// libs/react-components/src/index.ts +export * from './lib/button/button'; +export * from './lib/date-picker/date-picker'; + +// Or named exports +export { GoabButton } from './lib/button/button'; +export type { GoabButtonProps } from './lib/button/button'; +``` + +**Angular:** +```typescript +// libs/angular-components/src/index.ts +export * from './lib/components/button/button'; +export * from './lib/components/date-picker/date-picker'; +``` + +### Imports + +```typescript +// Web components +import { GoaButton } from '@abgov/web-components'; + +// React +import { GoabButton } from '@abgov/react-components'; +import type { GoabButtonProps } from '@abgov/react-components'; + +// Angular +import { GoabButtonComponent } from '@abgov/angular-components'; +``` + +--- + +## Quick Reference Cheat Sheet + +| What | Web Component | React | Angular | +|------|---------------|-------|---------| +| **Component** | `` | `GoabButton` | `goab-button` | +| **File** | `Button.svelte` | `button.tsx` | `button.ts` | +| **Props/Attrs** | `leadingicon` | `leadingIcon` | `leadingIcon` | +| **Multi-word prop** | `action-args` | `actionArgs` | `actionArgs` | +| **Boolean value** | `"true"` / `undefined` | `true` / `false` | `true` / `false` | +| **Event** | `_click` | `onClick` | `onClick` | +| **Test file** | `Button.spec.ts` | `button.spec.tsx` | `button.spec.ts` | diff --git a/agent_docs/pr_testing_guide.md b/agent_docs/pr_testing_guide.md new file mode 100644 index 0000000000..960d3c1786 --- /dev/null +++ b/agent_docs/pr_testing_guide.md @@ -0,0 +1,285 @@ +# PR Testing Playground Guide + +The `apps/prs/` folder contains shared playgrounds for testing bugs and features. + +--- + +## Why This Matters + +**Test pages are committed with your PR** - they're shared artifacts, not throwaway code. + +- Reviewers use your test page to verify the fix/feature works +- Team members can add additional test cases if they find gaps +- The side menu item makes your test discoverable to everyone +- Test pages become regression tests for future changes + +--- + +## Playground Structure + +``` +apps/prs/ +├── react/ # React playground (primary) +│ ├── src/ +│ │ ├── routes/ +│ │ │ ├── bugs/ # Bug fix test pages +│ │ │ ├── features/ # Feature test pages +│ │ │ └── _TEMPLATE.tsx +│ │ ├── main.tsx # Route configuration +│ │ └── app.tsx # Side menu links +│ └── package.json +├── angular/ # Angular playground +└── web/ # Web components playground +``` + +--- + +## Creating a Test Page + +### Step 1: Copy the Template + +```bash +# For bug fixes (use the GitHub issue number) +cp apps/prs/react/src/routes/_TEMPLATE.tsx apps/prs/react/src/routes/bugs/bug{N}.tsx + +# For new features +cp apps/prs/react/src/routes/_TEMPLATE.tsx apps/prs/react/src/routes/features/feat{N}.tsx +``` + +**Examples:** +- Bug #2878 → `apps/prs/react/src/routes/bugs/bug2878.tsx` +- Feature #1234 → `apps/prs/react/src/routes/features/feat1234.tsx` + +### Step 2: Update the File Content + +1. **Rename the component:** + ```typescript + // Change from: + export default function TemplateRoute() { + + // To: + export default function Bug2878Route() { + // or + export default function Feat1234Route() { + ``` + +2. **Update the issue metadata:** + ```typescript + const ISSUE_NUMBER = "2878"; // Your issue number + const ISSUE_TITLE = "DatePicker onChange not firing"; + const ISSUE_DESCRIPTION = ` + Paste the issue description from GitHub here. + This helps reviewers understand what they're testing. + `; + ``` + +3. **Add test cases:** + Create clear test cases that demonstrate the bug fix or new feature. Each test case should: + - Have a descriptive title + - Show the expected behavior + - Be easy to interact with and verify + + ```typescript + export default function Bug2878Route() { + return ( +
+

Bug #{ISSUE_NUMBER}: {ISSUE_TITLE}

+

{ISSUE_DESCRIPTION}

+ +
+

Test Case 1: Basic onChange behavior

+

Expected: onChange should fire when date is selected

+ console.log('Date changed:', e.detail)} + /> +
+ +
+

Test Case 2: Edge case with...

+

Expected: ...

+ {/* Your test component */} +
+
+ ); + } + ``` + +### Step 3: Wire Up the Route + +**File:** `apps/prs/react/src/main.tsx` + +1. **Add import** (keep alphabetical order): + ```typescript + import Bug2878Route from './routes/bugs/bug2878'; + ``` + +2. **Add route** (keep numerical order): + ```typescript + } /> + ``` + +### Step 4: Add Side Menu Link + +**File:** `apps/prs/react/src/app.tsx` + +Add a link in the appropriate section with format: `{issue number} {short description}` + +```typescript + +``` + +**Naming guidelines for links:** +- Keep it short (3-5 words after the issue number) +- Describe the component and issue: `2878 DatePicker onChange` +- NOT too generic: ❌ `2878 Bug fix` +- NOT too long: ❌ `2878 DatePicker onChange event not firing when user selects date` + +### Step 5: Test Your Page + +```bash +npm run serve:prs:react +``` + +Navigate to your test page via the side menu and verify: +- The route loads correctly +- Test cases are clearly labeled +- Components demonstrate the fix/feature +- No console errors + +--- + +## Complete Checklist + +Before submitting your PR, ensure: + +- [ ] Test page file created in correct folder (`bugs/` or `features/`) +- [ ] Component renamed from `TemplateRoute` to `Bug{N}Route` or `Feat{N}Route` +- [ ] Issue number, title, and description updated from template +- [ ] Test cases added with clear descriptions and expected behavior +- [ ] Import added to `main.tsx` (alphabetical order) +- [ ] Route added to `main.tsx` (numerical order) +- [ ] **Link added to `app.tsx` side menu** with format: `{issue number} {short description}` +- [ ] Link is in numerical order +- [ ] Page loads without errors +- [ ] Navigation from side menu works + +--- + +## Best Practices + +### Good Test Cases + +**✓ Clear and specific:** +```typescript +
+

Test Case: onChange fires on date selection

+

Expected: Console should log the selected date

+ console.log('Selected:', e.detail)} /> +
+``` + +**✓ Demonstrates the issue:** +```typescript +
+

Test Case: Before fix (onChange was silent)

+

This would NOT have worked before the fix

+ {/* Component showing the problem */} +
+``` + +**✓ Covers edge cases:** +```typescript +
+

Test Case: With disabled state

+

Expected: onChange should not fire when disabled

+ +
+``` + +### Poor Test Cases + +**✗ No description:** +```typescript + +``` + +**✗ Unclear expected behavior:** +```typescript +
+

Test

+ +
+``` + +**✗ Too many unrelated components:** +```typescript +// Don't test 10 different components in one test page +// unless they're all related to the same issue +``` + +--- + +## Example: Complete Test Page + +```typescript +import { useState } from 'react'; +import { GoabDatePicker } from '@abgov/react-components'; + +const ISSUE_NUMBER = "2878"; +const ISSUE_TITLE = "DatePicker onChange not firing"; +const ISSUE_DESCRIPTION = ` + The onChange event was not being dispatched when users selected a date + in the DatePicker component. This affected form validation and state updates. +`; + +export default function Bug2878Route() { + const [selectedDate, setSelectedDate] = useState(''); + + return ( +
+

Bug #{ISSUE_NUMBER}: {ISSUE_TITLE}

+

{ISSUE_DESCRIPTION}

+ +
+

Test Case 1: Basic onChange behavior

+

Expected: Selected date should appear below the picker

+ setSelectedDate(e.detail.value)} + /> +

Selected: {selectedDate || 'None'}

+
+ +
+

Test Case 2: onChange with initial value

+

Expected: onChange should fire when changing from initial date

+ console.log('Changed from initial:', e.detail)} + /> +
+ +
+

Test Case 3: Disabled state

+

Expected: onChange should NOT fire when disabled

+ console.log('Should not see this:', e.detail)} + /> +
+
+ ); +} +``` + +--- + +## Additional Resources + +For more details, see: `apps/prs/react/src/routes/README.md` diff --git a/agent_docs/testing.md b/agent_docs/testing.md new file mode 100644 index 0000000000..bf2ccddea3 --- /dev/null +++ b/agent_docs/testing.md @@ -0,0 +1,383 @@ +# Testing Guide + +Comprehensive testing reference for the UI Components monorepo. + +--- + +## Test Commands + +### Development (Watch Mode) + +Use these during development for fast feedback: + +```bash +# Unit tests only (all libraries) +npm run test:unit:watch + +# Browser tests (Playwright) +npm run test:browser:watch + +# Headless browser tests +npm run test:headless:watch +``` + +### Full Validation + +Run before creating a PR: + +```bash +# Build all libraries + run all tests (headless) +npm run test:pr + +# This is equivalent to: +npm run build && npm run test:unit && npm run test:headless +``` + +### Framework-Specific + +```bash +# Angular tests only (Jest) +npm run test:angular + +# React tests only (Vitest) +npm run test:react + +# Web components tests only (Vitest) +npm run test:web +``` + +--- + +## Test Framework by Library + +Each library uses different testing tools based on framework requirements: + +| Library | Test Framework | Test Library | File Pattern | +|---------|---------------|--------------|--------------| +| Web Components | Vitest | @testing-library/svelte | `*.spec.ts` | +| React | Vitest | @testing-library/react | `*.spec.tsx` | +| React (browser) | Vitest + Playwright | @testing-library/react | `*.browser.spec.tsx` | +| Angular | Jest | @testing-library/angular | `*.spec.ts` | + +### Why Different Frameworks? + +- **Vitest**: Fast, modern testing for Svelte and React +- **Jest**: Standard testing framework for Angular +- **Playwright**: Browser automation for tests requiring real DOM APIs + +--- + +## Test File Locations + +Tests live alongside the components they test: + +``` +libs/web-components/src/components/button/ +├── Button.svelte +└── Button.spec.ts # ← Test here + +libs/react-components/src/lib/button/ +├── button.tsx +├── button.spec.tsx # ← Test here +└── button.browser.spec.tsx # ← Browser test here (if needed) + +libs/angular-components/src/lib/components/button/ +├── button.ts +└── button.spec.ts # ← Test here +``` + +--- + +## Writing Unit Tests + +### Web Components (Vitest + @testing-library/svelte) + +```typescript +import { render, screen, fireEvent } from '@testing-library/svelte'; +import { describe, it, expect } from 'vitest'; +import GoaButton from './Button.svelte'; + +describe('GoaButton', () => { + it('should render with text', () => { + render(GoaButton, { props: { text: 'Click me' } }); + expect(screen.getByText('Click me')).toBeInTheDocument(); + }); + + it('should emit click event', async () => { + const { component } = render(GoaButton); + const handleClick = vi.fn(); + component.$on('_click', handleClick); + + await fireEvent.click(screen.getByRole('button')); + expect(handleClick).toHaveBeenCalled(); + }); + + it('should be disabled when disabled prop is true', () => { + render(GoaButton, { props: { disabled: 'true' } }); + expect(screen.getByRole('button')).toBeDisabled(); + }); +}); +``` + +### React (Vitest + @testing-library/react) + +```typescript +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { GoabButton } from './button'; + +describe('GoabButton', () => { + it('should render with text', () => { + render(Click me); + expect(screen.getByText('Click me')).toBeInTheDocument(); + }); + + it('should call onClick when clicked', () => { + const handleClick = vi.fn(); + render(Click me); + + fireEvent.click(screen.getByRole('button')); + expect(handleClick).toHaveBeenCalled(); + }); + + it('should be disabled when disabled prop is true', () => { + render(Click me); + expect(screen.getByRole('button')).toHaveAttribute('disabled', 'true'); + }); +}); +``` + +### Angular (Jest + @testing-library/angular) + +```typescript +import { render, screen, fireEvent } from '@testing-library/angular'; +import { GoabButtonComponent } from './button'; + +describe('GoabButtonComponent', () => { + it('should render with text', async () => { + await render(GoabButtonComponent, { + componentProperties: { text: 'Click me' } + }); + expect(screen.getByText('Click me')).toBeInTheDocument(); + }); + + it('should emit onClick when clicked', async () => { + const onClick = jest.fn(); + await render(GoabButtonComponent, { + componentProperties: { onClick } + }); + + fireEvent.click(screen.getByRole('button')); + expect(onClick).toHaveBeenCalled(); + }); + + it('should be disabled when disabled input is true', async () => { + await render(GoabButtonComponent, { + componentProperties: { disabled: true } + }); + expect(screen.getByRole('button')).toBeDisabled(); + }); +}); +``` + +--- + +## Browser Tests (Playwright) + +Use browser tests when you need real browser APIs (like focus management, scroll behavior, or complex DOM interactions). + +**File naming:** `*.browser.spec.tsx` + +**Example:** + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('GoabModal', () => { + test('should trap focus within modal', async ({ page }) => { + await page.goto('http://localhost:3000/test/modal'); + + // Open modal + await page.click('#open-modal-button'); + + // Tab through focusable elements + await page.keyboard.press('Tab'); + const firstFocus = await page.evaluate(() => document.activeElement?.tagName); + expect(firstFocus).toBe('BUTTON'); + + // Tab to last element and verify focus wraps + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + const wrappedFocus = await page.evaluate(() => document.activeElement?.tagName); + expect(wrappedFocus).toBe('BUTTON'); + }); +}); +``` + +--- + +## Testing Best Practices + +### 1. Test User Behavior, Not Implementation + +**Good:** +```typescript +it('should show error message when input is invalid', () => { + render(); + fireEvent.input(screen.getByRole('textbox'), { target: { value: 'invalid' } }); + expect(screen.getByText('Invalid email')).toBeInTheDocument(); +}); +``` + +**Bad:** +```typescript +it('should set state.error to true', () => { + const wrapper = mount(); + wrapper.setState({ error: true }); + expect(wrapper.state('error')).toBe(true); +}); +``` + +### 2. Use Accessible Queries + +Prefer queries that match how users interact with your app: + +```typescript +// Good - uses accessible role +screen.getByRole('button', { name: 'Submit' }) + +// Good - uses label text +screen.getByLabelText('Email address') + +// Avoid - fragile, not accessible-focused +screen.getByClassName('submit-button') +``` + +### 3. Test Accessibility + +```typescript +it('should have proper ARIA attributes', () => { + render(×); + const button = screen.getByRole('button', { name: 'Close dialog' }); + expect(button).toHaveAttribute('aria-label', 'Close dialog'); +}); +``` + +### 4. Test All Three Frameworks + +When you update a component, update tests in all three libraries: +- `libs/web-components/src/components/[name]/[Name].spec.ts` +- `libs/react-components/src/lib/[name]/[name].spec.tsx` +- `libs/angular-components/src/lib/components/[name]/[name].spec.ts` + +--- + +## Common Testing Patterns + +### Testing Custom Events + +**Web Component:** +```typescript +it('should dispatch custom event', async () => { + const { component } = render(GoaButton); + const handler = vi.fn(); + component.$on('_click', handler); + + await fireEvent.click(screen.getByRole('button')); + expect(handler).toHaveBeenCalled(); + expect(handler.mock.calls[0][0].detail).toEqual({ value: 'something' }); +}); +``` + +**React:** +```typescript +it('should call event handler with detail', () => { + const handleClick = vi.fn(); + render(); + + fireEvent.click(screen.getByRole('button')); + expect(handleClick).toHaveBeenCalledWith( + expect.objectContaining({ detail: { value: 'something' } }) + ); +}); +``` + +### Testing Conditional Rendering + +```typescript +it('should show error message when error prop is provided', () => { + render(); + expect(screen.getByText('Invalid value')).toBeInTheDocument(); +}); + +it('should not show error message when error prop is not provided', () => { + render(); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); +}); +``` + +### Testing Props/Attributes + +```typescript +it('should apply variant class based on prop', () => { + render(Click); + const button = screen.getByRole('button'); + expect(button).toHaveAttribute('variant', 'primary'); +}); +``` + +--- + +## Debugging Tests + +### Enable Debug Output + +```typescript +import { render, screen } from '@testing-library/react'; + +it('debug example', () => { + render(Click me); + screen.debug(); // Prints current DOM to console +}); +``` + +### Run Single Test File + +```bash +# Vitest (web components, React) +npm run test:unit:watch -- path/to/file.spec.ts + +# Jest (Angular) +npm run test:angular -- path/to/file.spec.ts +``` + +### Run Single Test + +```typescript +// Use .only to run just one test +it.only('should do something', () => { + // This test will run in isolation +}); + +// Use .skip to skip a test +it.skip('should do something', () => { + // This test will be skipped +}); +``` + +--- + +## CI/CD Testing + +In CI/CD pipelines, the following command runs: + +```bash +npm run test:pr +``` + +This ensures: +1. All libraries build successfully +2. All unit tests pass +3. All headless browser tests pass + +Make sure this passes locally before pushing your PR.