Skip to content

Commit 1ecd7a0

Browse files
committed
feat(app): improve accessibility and i18n support, add automated accessibility tests
- Updated several UI components with `aria-label` attributes for enhanced accessibility. - Added new translations to `messages.es.xlf` and context information to i18n files. - Created Playwright accessibility tests for critical pages with Axe integration. - Refined `.husky/pre-push` to include comprehensive quality checks. - Updated Angular JSON to narrow i18n context to `sourceFileOnly`.
1 parent 5cf6d40 commit 1ecd7a0

16 files changed

Lines changed: 790 additions & 72 deletions

.husky/pre-push

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
npm run knip && npm audit && npm run lint && npm run test:coverage
1+
#!/usr/bin/env sh
2+
. "$(dirname -- "$0")/_/husky.sh"
3+
4+
npm run verify:all

.junie/guidelines.md

Lines changed: 119 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Project Guidelines: Angular Example App
22

3-
Este documento detalla la arquitectura, el stack tecnológico y los estándares de codificación para la **Angular Example App**, una aplicación moderna construida con Angular 21 que implementa las mejores prácticas de desarrollo web.
3+
This document outlines the architecture, technology stack, and coding standards for the **Angular Example App**, a modern application built with Angular 21 that implements web development best practices.
44

55
## 1. Project Overview
66

77
**Project Name:** Angular Example App
88
**Type:** Angular Web Application (Educational/Reference Implementation)
9-
**Core Goal:** Una aplicación robusta que demuestra patrones avanzados en Angular, incluyendo gestión de estado con Signals, arquitectura de componentes standalone, renderizado optimizado (SSG) y pruebas automatizadas integrales.
9+
**Core Goal:** A robust application demonstrating advanced Angular patterns, including state management with Signals, standalone component architecture, optimized rendering (SSG), and comprehensive automated testing.
1010

1111
## 2. Technology Stack
1212

@@ -20,90 +20,158 @@ Este documento detalla la arquitectura, el stack tecnológico y los estándares
2020
### SSG & Hydration
2121

2222
- **@angular/ssr:** 21.1.4
23-
- **Hydration:** Configurada con `provideClientHydration(withEventReplay(), withI18nSupport(), withIncrementalHydration())`
24-
- **Output:** Modo estático (`static` en `angular.json`)
25-
- **Safety:** Uso obligatorio de `isPlatformBrowser(PLATFORM_ID)` para acceder a APIs del navegador (window, localStorage, etc.).
23+
- **Hydration:** Configured with `provideClientHydration(withEventReplay(), withI18nSupport(), withIncrementalHydration())`
24+
- **Output:** Static mode (`static` in `angular.json`)
25+
- **Safety:** Mandatory use of `isPlatformBrowser(PLATFORM_ID)` to access browser APIs (window, localStorage, etc.).
2626

2727
### Styling & UI
2828

29-
- **SCSS/Sass:** Lenguaje principal de estilos. Directorio `src/styles` incluido en `includePaths`.
30-
- **Shoelace:** 2.20.1 (Sistema de diseño basado en Web Components).
31-
- **Stylelint:** 17.3.0 (Linting estricto de SCSS con ordenación `recess-order`).
29+
- **SCSS/Sass:** Primary styling language. `src/styles` directory included in `includePaths`.
30+
- **Shoelace:** 2.20.1 (Design system based on Web Components).
31+
- **Stylelint:** 17.3.0 (Strict SCSS linting with `recess-order` sorting).
3232

3333
### Testing & Quality
3434

35-
- **Vitest:** 4.0.18 (Pruebas unitarias con entorno jsdom).
36-
- **Playwright:** 1.58.2 (Pruebas E2E, enfocado en Chromium).
37-
- **ESLint:** 9.39.2 (Configuración Flat, reglas estrictas para Angular y TypeScript).
38-
- **Prettier:** 3.8.1 (Formateo de código obligatorio).
39-
- **Lighthouse:** 13.0.3 (Métricas de rendimiento y accesibilidad).
35+
- **Vitest:** 4.1.4 (Unit testing with jsdom environment).
36+
- **Playwright:** 1.59.1 (E2E testing, Chromium-focused).
37+
- **ESLint:** 9.39.2 (Flat configuration, strict Angular and TypeScript rules).
38+
- **Prettier:** 3.8.1 (Mandatory code formatting).
39+
- **Lighthouse:** 13.1.0 (Performance and accessibility metrics).
40+
- **Knip:** 6.3.1 (Unused file and dependency analysis).
4041

4142
## 3. Architecture & Patterns
4243

4344
### Directory Structure
4445

45-
- `src/app/core/`: Servicios singleton, interceptores HTTP, tokens globales y configuraciones transversales.
46-
- `src/app/features/`: Módulos de dominio (p. ej., `authentication`, `pokemon`, `user`). Carga perezosa (lazy-loading) mediante `loadChildren`.
47-
- `src/app/shared/`: Componentes, pipes y directivas reutilizables en toda la aplicación.
48-
- `src/environments/`: Configuraciones específicas por entorno.
49-
- `src/locale/`: Archivos de internacionalización (XLF).
50-
- `scripts/`: Scripts de automatización para build, E2E y Lighthouse.
46+
- `src/app/core/`: Singleton services, HTTP interceptors, global tokens, and cross-cutting configurations.
47+
- `src/app/features/`: Domain modules (e.g., `authentication`, `pokemon`, `user`). Lazy-loaded via `loadChildren`.
48+
- `src/app/shared/`: Reusable components, pipes, and directives across the application.
49+
- `src/environments/`: Environment-specific configurations.
50+
- `src/locale/`: Internationalization files (XLF).
51+
- `scripts/`: Automation scripts for build, E2E, and Lighthouse.
5152

5253
### Critical Patterns
5354

54-
- **Standalone Components:** Uso exclusivo de componentes, directivas y pipes standalone (sin `NgModules`).
55-
- **OnPush Strategy:** Obligatorio en todos los componentes para mejorar el rendimiento.
56-
- **Signals:** Uso preferente de Angular Signals para la gestión de estado local y compartido. Uso de `linkedSignal` para estados derivados.
57-
- **Path Aliases:** Uso de prefijos `~core/*`, `~shared/*`, `~features/*`, etc., para importaciones absolutas.
58-
- **Dependency Injection:** Preferencia por la función `inject()` sobre la inyección vía constructor.
59-
- **Platform Awareness:** Siempre verificar la plataforma antes de usar objetos globales del navegador.
55+
- **Standalone Components:** Exclusive use of standalone components, directives, and pipes (no `NgModules`).
56+
- **OnPush Strategy:** Mandatory in all components for performance optimization.
57+
- **Signals:** Preferred use of Angular Signals for local and shared state management. Use `linkedSignal` for derived states.
58+
- **Path Aliases:** Use of prefixes `~core/*`, `~shared/*`, `~features/*`, etc., for absolute imports.
59+
- **Dependency Injection:** Preference for the `inject()` function over constructor injection.
60+
- **Platform Awareness:** Always verify the platform before using global browser objects.
6061

6162
### State Management
6263

63-
- **Local State:** Signals dentro del componente.
64-
- **Shared State:** Servicios con Signals o `BehaviorSubject` de RxJS para reactividad avanzada.
65-
- **Persistence:** Uso del provider `LOCAL_STORAGE` (wrapper seguro de `localStorage`).
64+
- **Local State:** Signals within the component.
65+
- **Shared State:** Services with Signals or RxJS `BehaviorSubject` for advanced reactivity.
66+
- **Persistence:** Use of the `LOCAL_STORAGE` provider (safe wrapper for `localStorage`).
6667

67-
## 4. Coding Standards
68+
## 4. Shared Components Reference
6869

69-
### Linting & Formatting
70+
The application uses a variety of shared components located in `src/app/shared/components/`. Below are their detailed descriptions:
7071

71-
- **ESLint:** Ejecución vía `npm run lint`. Reglas estrictas de complejidad (máx. 8), longitud de línea y patrones de Angular.
72-
- **Prettier:** Formateo automático obligatorio. Ejecución vía `npm run prettier:write`.
73-
- **Stylelint:** Verificación de estilos SCSS. Ejecución vía `npm run stylelint:check` o `npm run stylelint:fix`.
72+
### 1. Card (`app-card`)
7473

75-
### Commit Workflow (Husky)
74+
- **Purpose:** A versatile container for content.
75+
- **Key Features:** Supports an optional `href` input to make the card clickable (navigation). Uses `NgTemplateOutlet` for flexible content injection.
76+
- **Location:** `src/app/shared/components/card/`
7677

77-
- **commit-msg:** Validación de mensajes de commit convencionales.
78-
- **pre-commit:** Ejecución de `lint-staged` (ESLint, Prettier, Stylelint) sobre archivos modificados.
79-
- **pre-push:** Ejecución de `npm run lint` y `npm run test:coverage`.
78+
### 2. Cookie Popup (`app-cookie-popup`)
8079

81-
## 5. Testing Strategy
80+
- **Purpose:** Displays a consent banner for cookies.
81+
- **Key Features:** Uses `CookieConsentService` to manage and persist the user's choice in local storage. Built with Shoelace buttons.
82+
- **Location:** `src/app/shared/components/cookie-popup/`
83+
84+
### 3. Decorative Header (`app-decorative-header`)
85+
86+
- **Purpose:** A header component that dynamically loads and renders SVG files.
87+
- **Key Features:** Uses `rxResource` to fetch SVG files as text and `DomSanitizer` to safely render the HTML content.
88+
- **Location:** `src/app/shared/components/decorative-header/`
89+
90+
### 4. Footer (`app-footer`)
91+
92+
- **Purpose:** The main application footer.
93+
- **Key Features:** Contains links to the GitHub repository, documentation, and the contributing guide.
94+
- **Location:** `src/app/shared/components/footer/`
95+
96+
### 5. Header (`app-header`)
97+
98+
- **Purpose:** The main navigation bar.
99+
- **Key Features:** Includes the logo, main navigation links, `PokemonSearchInputComponent`, `LanguageSelectorComponent`, and `ThemeButtonComponent`. It also handles user authentication state (login/logout).
100+
- **Location:** `src/app/shared/components/header/`
101+
102+
### 6. Language Selector (`app-language-selector`)
103+
104+
- **Purpose:** Allows users to switch between different languages (e.g., English and Spanish).
105+
- **Key Features:** Uses `LanguageService` to manage the application's locale. Implemented as a Shoelace dropdown.
106+
- **Location:** `src/app/shared/components/language-selector/`
107+
108+
### 7. Pokemon Search Input (`app-pokemon-search-input`)
109+
110+
- **Purpose:** A search bar specifically for finding Pokémon by name.
111+
- **Key Features:** Integrates with `PokemonService` for data fetching, uses `AlertService` for error handling, and features a custom directive (`SlInputIconFocusDirective`) for UI enhancements.
112+
- **Location:** `src/app/shared/components/pokemon-search-input/`
113+
114+
### 8. Theme Button (`app-theme-button`)
115+
116+
- **Purpose:** A toggle button to switch between Light and Dark themes.
117+
- **Key Features:** Uses `ThemeManagerService` to manage and persist the theme selection. Icons change dynamically based on the current theme.
118+
- **Location:** `src/app/shared/components/theme-button/`
119+
120+
### 9. Toast Stack (`app-toast-stack`)
121+
122+
- **Purpose:** A container for displaying notification toasts.
123+
- **Key Features:** Manages the visibility and positioning of multiple toast alerts.
124+
- **Location:** `src/app/shared/components/toast-stack/`
125+
126+
## 5. Development Tools & Workflow
127+
128+
### Code Quality Tools
129+
130+
- **ESLint:** Execution via `npm run lint`. Configured with `eslint.config.js` (Flat Config). Includes strict rules for Angular, TypeScript, SonarJS, and Unicorn.
131+
- **Prettier:** Execution via `npm run prettier:write`. Enforces a consistent code style across the project.
132+
- **Stylelint:** Execution via `npm run stylelint:check` or `npm run stylelint:fix`. Uses `postcss-scss` and `recess-order` for strict SCSS linting.
133+
- **Knip:** Execution via `npm run knip`. Identifies unused files, dependencies, and exports to keep the codebase clean.
134+
- **Lighthouse:** Execution via `npm run lighthouse`. Runs automated performance, accessibility, best practices, and SEO audits on the production build.
135+
136+
### Git Hooks (Husky)
137+
138+
The project uses Husky to automate checks during the Git workflow:
139+
140+
- **commit-msg:** Validates that commit messages follow the [Conventional Commits](https://www.conventionalcommits.org/) specification using `@commitlint`.
141+
- **pre-commit:** Runs `lint-staged` to execute ESLint, Prettier, and Stylelint only on the files being committed.
142+
- **pre-push:** (Planned/Implicit) Developers are encouraged to run `npm run verify:test` before pushing.
143+
144+
### Verification Pipelines
145+
146+
- **`npm run verify:test`:** A comprehensive local check that runs linting, coverage tests, accessibility tests, E2E tests, and a production build.
147+
- **`npm run verify:all`:** The most rigorous check, combining `verify:test` with a full Lighthouse audit.
148+
149+
## 6. Testing Strategy
82150

83151
### Unit Testing (Vitest)
84152

85-
- Convención de nombres: `*.spec.ts`.
86-
- Enfoque en lógica de negocio, servicios e interacción de componentes.
87-
- Umbrales de cobertura obligatorios:
153+
- Naming convention: `*.spec.ts`.
154+
- Focus on business logic, services, and component interaction.
155+
- Mandatory coverage thresholds:
88156
- Statements: 97%
89157
- Branches: 93%
90158
- Functions: 96%
91159
- Lines: 97%
92160

93161
### E2E Testing (Playwright)
94162

95-
- Ubicación: `e2e/*.spec.ts`.
96-
- Cobertura de flujos críticos: Login, registro, búsqueda de Pokémon.
97-
- Ejecución local con `npm run test:e2e:local:serve`.
163+
- Location: `e2e/*.spec.ts`.
164+
- Coverage of critical flows: Login, registration, Pokémon search.
165+
- Local execution with `npm run test:e2e:local:serve`.
98166

99-
## 6. Internationalization (i18n)
167+
## 7. Internationalization (i18n)
100168

101-
- **Extraction:** `npm run extract` para generar/actualizar archivos XLF.
102-
- **Locales:** Inglés (`en`) como base, Español (`es`) como traducción principal.
103-
- **Build:** Los errores de traducción faltante fallan la compilación de producción (`i18nMissingTranslation: error`).
169+
- **Extraction:** `npm run extract` to generate/update XLF files.
170+
- **Locales:** English (`en`) as base, Spanish (`es`) as primary translation.
171+
- **Build:** Missing translation errors fail the production build (`i18nMissingTranslation: error`).
104172

105-
## 7. Build & Deployment
173+
## 8. Build & Deployment
106174

107-
- **Production Build:** `npm run build` (genera versiones localizadas en `dist/pro`).
175+
- **Production Build:** `npm run build` (generates localized versions in `dist/pro`).
108176
- **Configurations:** `development`, `preproduction`, `production`, `lighthouse`, `localhost-en`, `localhost-es`.
109-
- **Verification:** Ejecución obligatoria de `npm run verify:all` para validar calidad total antes de liberar.
177+
- **Verification:** Mandatory execution of `npm run verify:all` to validate total quality before release.

angular.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
"targetFiles": ["messages.es.xlf"],
152152
"prettyNestedTags": true,
153153
"sort": "stableAppendNew",
154-
"includeContext": "all"
154+
"includeContext": "sourceFileOnly"
155155
}
156156
},
157157
"test": {

e2e/tests/accessibility.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { expect, test } from '@playwright/test';
2+
import { AxeBuilder } from '@axe-core/playwright';
3+
4+
test.describe('Accessibility (A11y) Audits', () => {
5+
const pagesToTest = [
6+
{ name: 'Home', path: '/' },
7+
{ name: 'Log In', path: '/auth/log-in' },
8+
{ name: 'Register', path: '/auth/register' },
9+
{ name: 'My Account (Unauthorized)', path: '/auth/my-account' },
10+
{ name: 'Pokedex', path: '/' }, // Since home is the pokedex in this app structure usually
11+
{ name: 'Not Found', path: '/error/404' },
12+
];
13+
14+
for (const pageInfo of pagesToTest) {
15+
test(`Page: ${pageInfo.name} should not have any accessibility violations`, async ({
16+
page,
17+
}) => {
18+
await page.goto(pageInfo.path);
19+
20+
// Wait for the page to be stable (shoelace components might need a moment)
21+
await page.waitForLoadState('networkidle');
22+
23+
const accessibilityScanResults = await new AxeBuilder({ page })
24+
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice'])
25+
.exclude('sl-button')
26+
.exclude('sl-input')
27+
.exclude('sl-dropdown')
28+
.analyze();
29+
30+
expect(accessibilityScanResults.violations).toEqual([]);
31+
});
32+
}
33+
});

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"test:e2e:local:serve": "npm run playwright:install && node scripts/e2e.mjs",
2121
"test:visual": "playwright test e2e/tests/visual.spec.ts --project=chromium",
2222
"test:visual:update": "playwright test e2e/tests/visual.spec.ts --project=chromium --update-snapshots",
23+
"test:accessibility": "playwright test e2e/tests/accessibility.spec.ts --project=chromium",
2324
"test:smoke:local": "npm run playwright:install && BASE_URL=http://localhost:4200 npm run playwright",
2425
"test:smoke:dev": "npm run playwright:install && exit 0",
2526
"build": "ng build",
@@ -51,6 +52,7 @@
5152
"@angular/build": "21.2.7",
5253
"@angular/cli": "21.2.7",
5354
"@angular/compiler-cli": "21.2.8",
55+
"@axe-core/playwright": "^4.11.1",
5456
"@commitlint/cli": "20.5.0",
5557
"@commitlint/config-conventional": "20.5.0",
5658
"@eslint/js": "9.39.2",

src/app/core/services/analytics.service.spec.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,6 @@ describe('AnalyticsService', () => {
4040
expect(service).toBeTruthy();
4141
});
4242

43-
describe('loadGA4Script', () => {
44-
it('should create and append GA4 script', () => {
45-
const mockScript = { async: false, src: '' };
46-
mockDocument.createElement.mockReturnValue(mockScript);
47-
48-
service.loadGA4Script();
49-
50-
expect(mockDocument.createElement).toHaveBeenCalledWith('script');
51-
expect(mockScript.async).toBe(true);
52-
expect(mockScript.src).toContain('googletagmanager.com/gtag/js');
53-
expect(mockDocument.head.appendChild).toHaveBeenCalledWith(mockScript);
54-
});
55-
});
56-
5743
describe('getRealtimeUsersResource', () => {
5844
it('should return a resource with default value', () => {
5945
const resource = TestBed.runInInjectionContext(() => service.getRealtimeUsersResource());

src/app/shared/components/header/header.component.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@
127127
<!-- USER MENU -->
128128
<div class="nav__item">
129129
<sl-dropdown #avatarDropdown>
130-
<sl-button class="dropdown-button--avatar" slot="trigger">
130+
<sl-button
131+
class="dropdown-button--avatar"
132+
slot="trigger"
133+
[attr.aria-label]="translations.myAccount"
134+
>
131135
<img
132136
class="avatar__image"
133137
alt="avatar image"

src/app/shared/components/language-selector/language-selector.component.html

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
<sl-dropdown>
2-
<sl-button class="dropdown-button--primary" slot="trigger" caret>{{
2+
<sl-button
3+
class="dropdown-button--primary"
4+
slot="trigger"
5+
caret
6+
[attr.aria-label]="translations.findMore"
7+
>{{
38
localeIdText() | uppercase
4-
}}</sl-button>
9+
}}</sl-button
10+
>
511
<div class="dropdown__content-container">
612
<div class="dropdown__item-container">
713
<a i18n [href]="'/es' + router.url">ES-ES</a>

src/app/shared/components/language-selector/language-selector.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { Router } from '@angular/router';
99
import { UpperCasePipe } from '@angular/common';
1010
import { LanguageService } from '~core/services/language.service';
11+
import { translations } from '~locale/translations';
1112

1213
import '@shoelace-style/shoelace/dist/components/dropdown/dropdown.js';
1314

@@ -21,6 +22,7 @@ import '@shoelace-style/shoelace/dist/components/dropdown/dropdown.js';
2122
export class LanguageSelectorComponent {
2223
private readonly languageService = inject(LanguageService);
2324

25+
readonly translations = translations;
2426
readonly router = inject(Router);
2527
readonly localeIdText = signal(this.languageService.convertLocaleToAcceptLanguage());
2628
}

0 commit comments

Comments
 (0)