Initial commit
This commit is contained in:
20
.claude-plugin/plugin.json
Normal file
20
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "angular-testing",
|
||||||
|
"description": "Comprehensive testing strategies with Jasmine, Jest, Cypress, and Playwright",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Ihsan - Full-Stack Developer & AI Strategist",
|
||||||
|
"url": "https://github.com/EhssanAtassi"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills/testing-strategies/SKILL.md",
|
||||||
|
"./skills/mocking-patterns/SKILL.md"
|
||||||
|
],
|
||||||
|
"agents": [
|
||||||
|
"./agents/angular-tester.md"
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
"./commands/generate-tests.md",
|
||||||
|
"./commands/run-e2e.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# angular-testing
|
||||||
|
|
||||||
|
Comprehensive testing strategies with Jasmine, Jest, Cypress, and Playwright
|
||||||
271
agents/angular-tester.md
Normal file
271
agents/angular-tester.md
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
# Angular Testing Specialist
|
||||||
|
|
||||||
|
You are an expert Angular testing engineer specializing in comprehensive testing strategies for Angular applications.
|
||||||
|
|
||||||
|
## Your Expertise
|
||||||
|
|
||||||
|
- **Unit Testing**: Jasmine/Karma, Jest configuration and best practices
|
||||||
|
- **Component Testing**: TestBed, fixtures, change detection, and mocking
|
||||||
|
- **Integration Testing**: Service integration, routing, guards, and interceptors
|
||||||
|
- **E2E Testing**: Cypress, Playwright, and comprehensive user flow testing
|
||||||
|
- **Test Automation**: CI/CD integration, coverage reporting, and test optimization
|
||||||
|
- **Testing Patterns**: AAA pattern, test doubles, and testing anti-patterns
|
||||||
|
- **Performance Testing**: Test execution optimization and debugging
|
||||||
|
|
||||||
|
## Core Responsibilities
|
||||||
|
|
||||||
|
1. **Generate comprehensive test suites** for components, services, and modules
|
||||||
|
2. **Design testing strategies** that balance coverage, speed, and maintainability
|
||||||
|
3. **Implement E2E test scenarios** for critical user journeys
|
||||||
|
4. **Configure testing frameworks** (Jasmine, Jest, Cypress, Playwright)
|
||||||
|
5. **Optimize test performance** and reduce flakiness
|
||||||
|
6. **Integrate testing into CI/CD** pipelines
|
||||||
|
7. **Provide testing best practices** and anti-pattern guidance
|
||||||
|
|
||||||
|
## Testing Philosophy
|
||||||
|
|
||||||
|
- **Test behavior, not implementation** - Focus on user-facing functionality
|
||||||
|
- **Maintainable tests** - Write tests that are easy to update
|
||||||
|
- **Fast feedback loops** - Optimize for quick test execution
|
||||||
|
- **Comprehensive coverage** - Unit, integration, and E2E balance
|
||||||
|
- **Reliable tests** - Eliminate flakiness and race conditions
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
- `/angular-testing:generate-tests` - Generate unit and integration tests
|
||||||
|
- `/angular-testing:run-e2e` - Set up and run E2E tests
|
||||||
|
|
||||||
|
## Available Skills
|
||||||
|
|
||||||
|
Read these for deep knowledge:
|
||||||
|
- `testing-strategies` - Comprehensive testing approaches and patterns
|
||||||
|
- `mocking-patterns` - Service mocking, spies, and test doubles
|
||||||
|
|
||||||
|
## Communication Style
|
||||||
|
|
||||||
|
- Provide **working, complete test files** with proper imports
|
||||||
|
- Explain **what is being tested** and why
|
||||||
|
- Include **setup and teardown** patterns
|
||||||
|
- Add **comments** for complex test scenarios
|
||||||
|
- Suggest **test organization** and file structure
|
||||||
|
- Highlight **common pitfalls** and how to avoid them
|
||||||
|
|
||||||
|
## Code Quality Standards
|
||||||
|
|
||||||
|
1. **Follow AAA Pattern**: Arrange, Act, Assert
|
||||||
|
2. **One assertion per test**: Clear, focused tests
|
||||||
|
3. **Descriptive test names**: Use "should..." convention
|
||||||
|
4. **Proper cleanup**: AfterEach hooks for teardown
|
||||||
|
5. **No hardcoded values**: Use constants and fixtures
|
||||||
|
6. **Test isolation**: Each test runs independently
|
||||||
|
7. **Mock external dependencies**: Control test environment
|
||||||
|
|
||||||
|
## Example Interaction
|
||||||
|
|
||||||
|
**User**: "Create tests for my login component"
|
||||||
|
|
||||||
|
**You**:
|
||||||
|
```typescript
|
||||||
|
// login.component.spec.ts
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { LoginComponent } from './login.component';
|
||||||
|
import { AuthService } from '../services/auth.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { of, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
describe('LoginComponent', () => {
|
||||||
|
let component: LoginComponent;
|
||||||
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
let authService: jasmine.SpyObj<AuthService>;
|
||||||
|
let router: jasmine.SpyObj<Router>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const authServiceSpy = jasmine.createSpyObj('AuthService', ['login']);
|
||||||
|
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ LoginComponent ],
|
||||||
|
imports: [ ReactiveFormsModule ],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthService, useValue: authServiceSpy },
|
||||||
|
{ provide: Router, useValue: routerSpy }
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
|
||||||
|
router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(LoginComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Form Validation', () => {
|
||||||
|
it('should create form with email and password controls', () => {
|
||||||
|
expect(component.loginForm.contains('email')).toBeTruthy();
|
||||||
|
expect(component.loginForm.contains('password')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark email as invalid when empty', () => {
|
||||||
|
const email = component.loginForm.get('email');
|
||||||
|
expect(email?.valid).toBeFalsy();
|
||||||
|
expect(email?.hasError('required')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark email as invalid when format is wrong', () => {
|
||||||
|
const email = component.loginForm.get('email');
|
||||||
|
email?.setValue('invalid-email');
|
||||||
|
expect(email?.hasError('email')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark password as invalid when empty', () => {
|
||||||
|
const password = component.loginForm.get('password');
|
||||||
|
expect(password?.valid).toBeFalsy();
|
||||||
|
expect(password?.hasError('required')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark form as valid when all fields are correct', () => {
|
||||||
|
component.loginForm.setValue({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
expect(component.loginForm.valid).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Login Functionality', () => {
|
||||||
|
it('should not call authService when form is invalid', () => {
|
||||||
|
component.onSubmit();
|
||||||
|
expect(authService.login).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call authService.login with correct credentials', () => {
|
||||||
|
const credentials = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123'
|
||||||
|
};
|
||||||
|
|
||||||
|
component.loginForm.setValue(credentials);
|
||||||
|
authService.login.and.returnValue(of({ token: 'fake-token' }));
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
|
||||||
|
expect(authService.login).toHaveBeenCalledWith(
|
||||||
|
credentials.email,
|
||||||
|
credentials.password
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to dashboard on successful login', () => {
|
||||||
|
component.loginForm.setValue({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
|
||||||
|
authService.login.and.returnValue(of({ token: 'fake-token' }));
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/dashboard']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display error message on failed login', () => {
|
||||||
|
component.loginForm.setValue({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'wrong-password'
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorResponse = { error: { message: 'Invalid credentials' } };
|
||||||
|
authService.login.and.returnValue(throwError(() => errorResponse));
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
|
||||||
|
expect(component.errorMessage).toBe('Invalid credentials');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable submit button while login is in progress', () => {
|
||||||
|
component.loginForm.setValue({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
|
||||||
|
authService.login.and.returnValue(of({ token: 'fake-token' }));
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
|
||||||
|
expect(component.isLoading).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('UI Interaction', () => {
|
||||||
|
it('should display email error message when touched and invalid', () => {
|
||||||
|
const email = component.loginForm.get('email');
|
||||||
|
email?.markAsTouched();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const compiled = fixture.nativeElement;
|
||||||
|
const errorElement = compiled.querySelector('.email-error');
|
||||||
|
|
||||||
|
expect(errorElement).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable submit button when form is invalid', () => {
|
||||||
|
const compiled = fixture.nativeElement;
|
||||||
|
const submitButton = compiled.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
|
expect(submitButton.disabled).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable submit button when form is valid', () => {
|
||||||
|
component.loginForm.setValue({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const compiled = fixture.nativeElement;
|
||||||
|
const submitButton = compiled.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
|
expect(submitButton.disabled).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Coverage**: ✅ Form validation, ✅ Service integration, ✅ Navigation, ✅ Error handling, ✅ UI states
|
||||||
|
|
||||||
|
## When to Use Which Test Type
|
||||||
|
|
||||||
|
**Unit Tests** (80% of tests):
|
||||||
|
- Pure functions and business logic
|
||||||
|
- Component methods in isolation
|
||||||
|
- Service methods
|
||||||
|
- Pipes and directives
|
||||||
|
|
||||||
|
**Integration Tests** (15% of tests):
|
||||||
|
- Component + Service interactions
|
||||||
|
- Routing with guards
|
||||||
|
- Form + Validation
|
||||||
|
- HTTP interceptors
|
||||||
|
|
||||||
|
**E2E Tests** (5% of tests):
|
||||||
|
- Critical user journeys
|
||||||
|
- Authentication flows
|
||||||
|
- Checkout processes
|
||||||
|
- Data persistence
|
||||||
|
|
||||||
|
## Always Remember
|
||||||
|
|
||||||
|
- Tests are documentation - make them readable
|
||||||
|
- Fast tests = happy developers
|
||||||
|
- Flaky tests are worse than no tests
|
||||||
|
- Mock external dependencies
|
||||||
|
- Test edge cases and error scenarios
|
||||||
|
- Keep tests close to the code they test
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Ready to make your Angular app bulletproof! 🛡️*
|
||||||
507
commands/generate-tests.md
Normal file
507
commands/generate-tests.md
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
# Generate Tests Command
|
||||||
|
|
||||||
|
Generate comprehensive unit and integration tests for Angular components, services, and modules.
|
||||||
|
|
||||||
|
## Command Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/angular-testing:generate-tests [component|service|module|pipe|directive] <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Natural Language Examples
|
||||||
|
|
||||||
|
- "Generate tests for my user profile component"
|
||||||
|
- "Create unit tests for the authentication service"
|
||||||
|
- "Write tests for the admin guard"
|
||||||
|
- "Generate tests for the currency pipe"
|
||||||
|
|
||||||
|
## What This Command Does
|
||||||
|
|
||||||
|
1. **Analyzes the target file** - Understands component/service structure
|
||||||
|
2. **Creates comprehensive test suite** - Following AAA pattern
|
||||||
|
3. **Includes all test scenarios** - Happy path, edge cases, errors
|
||||||
|
4. **Mocks dependencies** - Proper service and HTTP mocking
|
||||||
|
5. **Adds helpful comments** - Explains complex test logic
|
||||||
|
6. **Follows best practices** - TestBed configuration, spy objects
|
||||||
|
|
||||||
|
## Test Generation Patterns
|
||||||
|
|
||||||
|
### Component Tests Include
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('ComponentName', () => {
|
||||||
|
// ✅ Component creation and initialization
|
||||||
|
// ✅ Input property testing
|
||||||
|
// ✅ Output event testing
|
||||||
|
// ✅ Method behavior testing
|
||||||
|
// ✅ Template rendering
|
||||||
|
// ✅ User interaction
|
||||||
|
// ✅ Async operations
|
||||||
|
// ✅ Error handling
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Tests Include
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('ServiceName', () => {
|
||||||
|
// ✅ Service creation
|
||||||
|
// ✅ Method functionality
|
||||||
|
// ✅ HTTP requests (mocked)
|
||||||
|
// ✅ Observable streams
|
||||||
|
// ✅ Error handling
|
||||||
|
// ✅ State management
|
||||||
|
// ✅ Cache behavior
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
**Input**: Generate tests for LoginComponent
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```typescript
|
||||||
|
// login.component.spec.ts
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { LoginComponent } from './login.component';
|
||||||
|
import { AuthService } from '../services/auth.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { of, throwError } from 'rxjs';
|
||||||
|
import { DebugElement } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
describe('LoginComponent', () => {
|
||||||
|
let component: LoginComponent;
|
||||||
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
let authService: jasmine.SpyObj<AuthService>;
|
||||||
|
let router: jasmine.SpyObj<Router>;
|
||||||
|
let compiled: HTMLElement;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create spy objects for dependencies
|
||||||
|
const authServiceSpy = jasmine.createSpyObj('AuthService', ['login', 'logout']);
|
||||||
|
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ LoginComponent ],
|
||||||
|
imports: [ ReactiveFormsModule ],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthService, useValue: authServiceSpy },
|
||||||
|
{ provide: Router, useValue: routerSpy }
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
|
||||||
|
router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(LoginComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
compiled = fixture.nativeElement;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// === COMPONENT CREATION ===
|
||||||
|
describe('Component Creation', () => {
|
||||||
|
it('should create the component', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize login form with empty values', () => {
|
||||||
|
expect(component.loginForm.get('email')?.value).toBe('');
|
||||||
|
expect(component.loginForm.get('password')?.value).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have isLoading set to false initially', () => {
|
||||||
|
expect(component.isLoading).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have errorMessage set to null initially', () => {
|
||||||
|
expect(component.errorMessage).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === FORM VALIDATION ===
|
||||||
|
describe('Form Validation', () => {
|
||||||
|
it('should have required validator on email field', () => {
|
||||||
|
const email = component.loginForm.get('email');
|
||||||
|
email?.setValue('');
|
||||||
|
expect(email?.hasError('required')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have email validator on email field', () => {
|
||||||
|
const email = component.loginForm.get('email');
|
||||||
|
email?.setValue('invalid-email');
|
||||||
|
expect(email?.hasError('email')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept valid email format', () => {
|
||||||
|
const email = component.loginForm.get('email');
|
||||||
|
email?.setValue('test@example.com');
|
||||||
|
expect(email?.valid).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have required validator on password field', () => {
|
||||||
|
const password = component.loginForm.get('password');
|
||||||
|
password?.setValue('');
|
||||||
|
expect(password?.hasError('required')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have minLength validator on password field', () => {
|
||||||
|
const password = component.loginForm.get('password');
|
||||||
|
password?.setValue('123');
|
||||||
|
expect(password?.hasError('minlength')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark form as invalid when fields are empty', () => {
|
||||||
|
expect(component.loginForm.valid).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark form as valid when all fields are correct', () => {
|
||||||
|
component.loginForm.setValue({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
expect(component.loginForm.valid).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === LOGIN FUNCTIONALITY ===
|
||||||
|
describe('Login Functionality', () => {
|
||||||
|
const validCredentials = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component.loginForm.setValue(validCredentials);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not submit when form is invalid', () => {
|
||||||
|
component.loginForm.setValue({ email: '', password: '' });
|
||||||
|
component.onSubmit();
|
||||||
|
expect(authService.login).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call authService.login with form values', () => {
|
||||||
|
authService.login.and.returnValue(of({
|
||||||
|
token: 'fake-jwt-token',
|
||||||
|
user: { id: 1, email: 'test@example.com' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
|
||||||
|
expect(authService.login).toHaveBeenCalledWith(
|
||||||
|
validCredentials.email,
|
||||||
|
validCredentials.password
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set isLoading to true during login', () => {
|
||||||
|
authService.login.and.returnValue(of({ token: 'fake-token' }));
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
|
||||||
|
expect(component.isLoading).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to dashboard on successful login', (done) => {
|
||||||
|
authService.login.and.returnValue(of({ token: 'fake-token' }));
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/dashboard']);
|
||||||
|
expect(component.isLoading).toBeFalsy();
|
||||||
|
done();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display error message on login failure', (done) => {
|
||||||
|
const errorResponse = {
|
||||||
|
error: { message: 'Invalid credentials' }
|
||||||
|
};
|
||||||
|
authService.login.and.returnValue(
|
||||||
|
throwError(() => errorResponse)
|
||||||
|
);
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(component.errorMessage).toBe('Invalid credentials');
|
||||||
|
expect(component.isLoading).toBeFalsy();
|
||||||
|
expect(router.navigate).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear error message on new submission', () => {
|
||||||
|
component.errorMessage = 'Previous error';
|
||||||
|
authService.login.and.returnValue(of({ token: 'fake-token' }));
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
|
||||||
|
expect(component.errorMessage).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === UI INTERACTION ===
|
||||||
|
describe('UI Interaction', () => {
|
||||||
|
it('should render email input field', () => {
|
||||||
|
const emailInput = compiled.querySelector('input[type="email"]');
|
||||||
|
expect(emailInput).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render password input field', () => {
|
||||||
|
const passwordInput = compiled.querySelector('input[type="password"]');
|
||||||
|
expect(passwordInput).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render submit button', () => {
|
||||||
|
const submitButton = compiled.querySelector('button[type="submit"]');
|
||||||
|
expect(submitButton).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable submit button when form is invalid', () => {
|
||||||
|
const submitButton = compiled.querySelector('button[type="submit"]') as HTMLButtonElement;
|
||||||
|
expect(submitButton.disabled).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable submit button when form is valid', () => {
|
||||||
|
component.loginForm.setValue({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const submitButton = compiled.querySelector('button[type="submit"]') as HTMLButtonElement;
|
||||||
|
expect(submitButton.disabled).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show loading spinner when isLoading is true', () => {
|
||||||
|
component.isLoading = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const spinner = compiled.querySelector('.loading-spinner');
|
||||||
|
expect(spinner).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display error message when present', () => {
|
||||||
|
component.errorMessage = 'Login failed';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const errorElement = compiled.querySelector('.error-message');
|
||||||
|
expect(errorElement?.textContent).toContain('Login failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update email value on input', () => {
|
||||||
|
const emailInput = compiled.querySelector('input[type="email"]') as HTMLInputElement;
|
||||||
|
emailInput.value = 'new@example.com';
|
||||||
|
emailInput.dispatchEvent(new Event('input'));
|
||||||
|
|
||||||
|
expect(component.loginForm.get('email')?.value).toBe('new@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show validation error when email is touched and invalid', () => {
|
||||||
|
const email = component.loginForm.get('email');
|
||||||
|
email?.markAsTouched();
|
||||||
|
email?.setValue('');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const errorElement = compiled.querySelector('.email-error');
|
||||||
|
expect(errorElement).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger onSubmit when form is submitted', () => {
|
||||||
|
spyOn(component, 'onSubmit');
|
||||||
|
|
||||||
|
component.loginForm.setValue({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = compiled.querySelector('form') as HTMLFormElement;
|
||||||
|
form.dispatchEvent(new Event('submit'));
|
||||||
|
|
||||||
|
expect(component.onSubmit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === EDGE CASES ===
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
it('should handle network error gracefully', (done) => {
|
||||||
|
component.loginForm.setValue({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
|
||||||
|
authService.login.and.returnValue(
|
||||||
|
throwError(() => new Error('Network error'))
|
||||||
|
);
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(component.errorMessage).toBe('Network error occurred');
|
||||||
|
expect(component.isLoading).toBeFalsy();
|
||||||
|
done();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prevent multiple simultaneous login requests', () => {
|
||||||
|
component.loginForm.setValue({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
|
||||||
|
authService.login.and.returnValue(of({ token: 'fake-token' }));
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
component.onSubmit(); // Try to submit again
|
||||||
|
|
||||||
|
expect(authService.login).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trim whitespace from email', () => {
|
||||||
|
component.loginForm.get('email')?.setValue(' test@example.com ');
|
||||||
|
authService.login.and.returnValue(of({ token: 'fake-token' }));
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
|
||||||
|
expect(authService.login).toHaveBeenCalledWith(
|
||||||
|
'test@example.com',
|
||||||
|
jasmine.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Coverage Report**:
|
||||||
|
```
|
||||||
|
✅ Component Creation (4 tests)
|
||||||
|
✅ Form Validation (7 tests)
|
||||||
|
✅ Login Functionality (6 tests)
|
||||||
|
✅ UI Interaction (10 tests)
|
||||||
|
✅ Edge Cases (3 tests)
|
||||||
|
───────────────────────────
|
||||||
|
Total: 30 tests
|
||||||
|
Coverage: ~95%
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Tips
|
||||||
|
|
||||||
|
### Jasmine/Karma Setup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// karma.conf.js
|
||||||
|
module.exports = function(config) {
|
||||||
|
config.set({
|
||||||
|
basePath: '',
|
||||||
|
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||||
|
plugins: [
|
||||||
|
require('karma-jasmine'),
|
||||||
|
require('karma-chrome-launcher'),
|
||||||
|
require('karma-coverage'),
|
||||||
|
require('@angular-devkit/build-angular/plugins/karma')
|
||||||
|
],
|
||||||
|
coverageReporter: {
|
||||||
|
dir: require('path').join(__dirname, './coverage'),
|
||||||
|
subdir: '.',
|
||||||
|
reporters: [
|
||||||
|
{ type: 'html' },
|
||||||
|
{ type: 'text-summary' },
|
||||||
|
{ type: 'lcovonly' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
reporters: ['progress', 'coverage'],
|
||||||
|
browsers: ['ChromeHeadless'],
|
||||||
|
singleRun: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Jest Setup (Alternative)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// jest.config.js
|
||||||
|
module.exports = {
|
||||||
|
preset: 'jest-preset-angular',
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
|
||||||
|
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
|
||||||
|
coverageDirectory: 'coverage',
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'src/app/**/*.ts',
|
||||||
|
'!src/app/**/*.spec.ts',
|
||||||
|
'!src/app/**/*.module.ts'
|
||||||
|
],
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
branches: 80,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
ng test
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
ng test --code-coverage
|
||||||
|
|
||||||
|
# Run specific file
|
||||||
|
ng test --include='**/login.component.spec.ts'
|
||||||
|
|
||||||
|
# Run in headless mode
|
||||||
|
ng test --browsers=ChromeHeadless --watch=false
|
||||||
|
|
||||||
|
# Run with Jest
|
||||||
|
npm run test:jest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices Applied
|
||||||
|
|
||||||
|
1. **AAA Pattern**: Arrange → Act → Assert structure
|
||||||
|
2. **Descriptive Names**: Clear "should..." test descriptions
|
||||||
|
3. **Isolated Tests**: Each test independent and focused
|
||||||
|
4. **Proper Mocking**: Spy objects for all dependencies
|
||||||
|
5. **Complete Coverage**: Happy path, errors, edge cases
|
||||||
|
6. **Cleanup**: afterEach for proper teardown
|
||||||
|
7. **Async Handling**: Proper async/await and done()
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/angular-testing:generate-tests
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
"Generate tests for UserProfileComponent"
|
||||||
|
"Create tests for DataService"
|
||||||
|
"Write tests for AuthGuard"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Includes
|
||||||
|
|
||||||
|
1. **Complete test file** - Ready to run
|
||||||
|
2. **All imports** - Proper dependencies
|
||||||
|
3. **TestBed configuration** - Correct module setup
|
||||||
|
4. **Spy objects** - For all dependencies
|
||||||
|
5. **Comprehensive scenarios** - All test cases
|
||||||
|
6. **Comments** - Explaining complex logic
|
||||||
|
7. **Running instructions** - How to execute tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Comprehensive testing made easy! 🧪*
|
||||||
377
commands/run-e2e.md
Normal file
377
commands/run-e2e.md
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
# Run E2E Tests Command
|
||||||
|
|
||||||
|
Set up and execute end-to-end tests using Cypress or Playwright for Angular applications.
|
||||||
|
|
||||||
|
## Command Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/angular-testing:run-e2e [cypress|playwright] [setup|run|debug]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Natural Language Examples
|
||||||
|
|
||||||
|
- "Set up Cypress for E2E testing"
|
||||||
|
- "Create E2E tests for login flow"
|
||||||
|
- "Run Playwright tests in headless mode"
|
||||||
|
- "Debug failing E2E tests"
|
||||||
|
|
||||||
|
## What This Command Does
|
||||||
|
|
||||||
|
1. **Framework Setup** - Install and configure Cypress/Playwright
|
||||||
|
2. **Test Generation** - Create E2E test files for user flows
|
||||||
|
3. **Custom Commands** - Reusable helpers and utilities
|
||||||
|
4. **CI/CD Integration** - GitHub Actions/GitLab CI configuration
|
||||||
|
5. **Debugging** - Tools and strategies for flaky tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cypress Setup
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save-dev cypress @cypress/schematic
|
||||||
|
ng add @cypress/schematic
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// cypress.config.ts
|
||||||
|
import { defineConfig } from 'cypress';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'http://localhost:4200',
|
||||||
|
supportFile: 'cypress/support/e2e.ts',
|
||||||
|
specPattern: 'cypress/e2e/**/*.cy.ts',
|
||||||
|
viewportWidth: 1280,
|
||||||
|
viewportHeight: 720,
|
||||||
|
video: true,
|
||||||
|
screenshotOnRunFailure: true,
|
||||||
|
retries: {
|
||||||
|
runMode: 2,
|
||||||
|
openMode: 0
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
apiUrl: 'http://localhost:3000/api'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Commands
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// cypress/support/commands.ts
|
||||||
|
declare global {
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
login(email: string, password: string): Chainable<void>;
|
||||||
|
logout(): Chainable<void>;
|
||||||
|
getBySel(selector: string): Chainable<JQuery<HTMLElement>>;
|
||||||
|
checkAccessibility(): Chainable<void>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login command
|
||||||
|
Cypress.Commands.add('login', (email: string, password: string) => {
|
||||||
|
cy.session([email, password], () => {
|
||||||
|
cy.visit('/login');
|
||||||
|
cy.getBySel('email-input').type(email);
|
||||||
|
cy.getBySel('password-input').type(password);
|
||||||
|
cy.getBySel('login-button').click();
|
||||||
|
cy.url().should('include', '/dashboard');
|
||||||
|
cy.window().its('localStorage.token').should('exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logout command
|
||||||
|
Cypress.Commands.add('logout', () => {
|
||||||
|
cy.clearLocalStorage();
|
||||||
|
cy.clearCookies();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get by data-cy attribute
|
||||||
|
Cypress.Commands.add('getBySel', (selector: string) => {
|
||||||
|
return cy.get(`[data-cy="${selector}"]`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Accessibility check
|
||||||
|
Cypress.Commands.add('checkAccessibility', () => {
|
||||||
|
cy.injectAxe();
|
||||||
|
cy.checkA11y();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example E2E Test
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// cypress/e2e/login.cy.ts
|
||||||
|
describe('Login Flow', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/login');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display login form', () => {
|
||||||
|
cy.getBySel('email-input').should('be.visible');
|
||||||
|
cy.getBySel('password-input').should('be.visible');
|
||||||
|
cy.getBySel('login-button').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show validation errors for empty fields', () => {
|
||||||
|
cy.getBySel('login-button').click();
|
||||||
|
cy.getBySel('email-error').should('contain', 'Email is required');
|
||||||
|
cy.getBySel('password-error').should('contain', 'Password is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error for invalid email format', () => {
|
||||||
|
cy.getBySel('email-input').type('invalid-email');
|
||||||
|
cy.getBySel('password-input').type('password123');
|
||||||
|
cy.getBySel('login-button').click();
|
||||||
|
cy.getBySel('email-error').should('contain', 'Invalid email format');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully login with valid credentials', () => {
|
||||||
|
cy.intercept('POST', '/api/auth/login', {
|
||||||
|
statusCode: 200,
|
||||||
|
body: {
|
||||||
|
token: 'fake-jwt-token',
|
||||||
|
user: { id: 1, email: 'test@example.com' }
|
||||||
|
}
|
||||||
|
}).as('loginRequest');
|
||||||
|
|
||||||
|
cy.getBySel('email-input').type('test@example.com');
|
||||||
|
cy.getBySel('password-input').type('password123');
|
||||||
|
cy.getBySel('login-button').click();
|
||||||
|
|
||||||
|
cy.wait('@loginRequest');
|
||||||
|
cy.url().should('include', '/dashboard');
|
||||||
|
cy.window().its('localStorage.token').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error message for invalid credentials', () => {
|
||||||
|
cy.intercept('POST', '/api/auth/login', {
|
||||||
|
statusCode: 401,
|
||||||
|
body: { message: 'Invalid credentials' }
|
||||||
|
}).as('loginRequest');
|
||||||
|
|
||||||
|
cy.getBySel('email-input').type('test@example.com');
|
||||||
|
cy.getBySel('password-input').type('wrong-password');
|
||||||
|
cy.getBySel('login-button').click();
|
||||||
|
|
||||||
|
cy.wait('@loginRequest');
|
||||||
|
cy.getBySel('error-message').should('contain', 'Invalid credentials');
|
||||||
|
cy.url().should('include', '/login');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle password visibility', () => {
|
||||||
|
cy.getBySel('password-input').should('have.attr', 'type', 'password');
|
||||||
|
cy.getBySel('toggle-password').click();
|
||||||
|
cy.getBySel('password-input').should('have.attr', 'type', 'text');
|
||||||
|
cy.getBySel('toggle-password').click();
|
||||||
|
cy.getBySel('password-input').should('have.attr', 'type', 'password');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remember me functionality', () => {
|
||||||
|
cy.getBySel('remember-me-checkbox').check();
|
||||||
|
cy.login('test@example.com', 'password123');
|
||||||
|
|
||||||
|
cy.reload();
|
||||||
|
cy.url().should('include', '/dashboard');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Cypress Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Open Cypress Test Runner
|
||||||
|
npx cypress open
|
||||||
|
|
||||||
|
# Run all tests headless
|
||||||
|
npx cypress run
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
npx cypress run --spec "cypress/e2e/login.cy.ts"
|
||||||
|
|
||||||
|
# Run with specific browser
|
||||||
|
npx cypress run --browser chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Playwright Setup
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save-dev @playwright/test
|
||||||
|
npx playwright install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// playwright.config.ts
|
||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: 'html',
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:4200',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
video: 'retain-on-failure'
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
webServer: {
|
||||||
|
command: 'npm start',
|
||||||
|
url: 'http://localhost:4200',
|
||||||
|
reuseExistingServer: !process.env.CI
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Playwright Test
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// e2e/login.spec.ts
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Login Flow', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display login form', async ({ page }) => {
|
||||||
|
await expect(page.getByTestId('email-input')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('password-input')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('login-button')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should login successfully', async ({ page }) => {
|
||||||
|
await page.route('**/api/auth/login', async route => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: 'fake-jwt-token',
|
||||||
|
user: { id: 1, email: 'test@example.com' }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.fill('[data-testid="email-input"]', 'test@example.com');
|
||||||
|
await page.fill('[data-testid="password-input"]', 'password123');
|
||||||
|
await page.click('[data-testid="login-button"]');
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/.*dashboard/);
|
||||||
|
|
||||||
|
const token = await page.evaluate(() => localStorage.getItem('token'));
|
||||||
|
expect(token).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Playwright Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
npx playwright test
|
||||||
|
|
||||||
|
# Run with UI
|
||||||
|
npx playwright test --ui
|
||||||
|
|
||||||
|
# Debug mode
|
||||||
|
npx playwright test --debug
|
||||||
|
|
||||||
|
# Generate report
|
||||||
|
npx playwright show-report
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/e2e.yml
|
||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cypress:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Start Angular dev server
|
||||||
|
run: npm start &
|
||||||
|
|
||||||
|
- name: Wait for server
|
||||||
|
run: npx wait-on http://localhost:4200
|
||||||
|
|
||||||
|
- name: Run Cypress tests
|
||||||
|
run: npm run cypress:run
|
||||||
|
|
||||||
|
- name: Upload screenshots
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: cypress-screenshots
|
||||||
|
path: cypress/screenshots
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use data-cy/data-testid attributes** - For stable selectors
|
||||||
|
2. **Avoid waits** - Use Cypress auto-retry
|
||||||
|
3. **Test user flows** - Not individual features
|
||||||
|
4. **Mock external APIs** - For consistency
|
||||||
|
5. **Keep tests independent** - No shared state
|
||||||
|
6. **Use fixtures** - For test data
|
||||||
|
7. **Clean up after tests** - Reset state
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/angular-testing:run-e2e
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
"Set up Cypress for testing login flow"
|
||||||
|
"Create E2E tests for product CRUD operations"
|
||||||
|
"Run E2E tests in CI/CD pipeline"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Comprehensive E2E testing made easy! 🎭*
|
||||||
61
plugin.lock.json
Normal file
61
plugin.lock.json
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:EhssanAtassi/angular-marketplace-developer:plugins/angular-testing",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "ca8ac5cebc93a36c1d98fa06385946424758d740",
|
||||||
|
"treeHash": "6165c7bcf054cb89e9a3f469d0351f49a8890dd024afe3044a8d6d8b7b1c273c",
|
||||||
|
"generatedAt": "2025-11-28T10:10:28.277292Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "angular-testing",
|
||||||
|
"description": "Comprehensive testing strategies with Jasmine, Jest, Cypress, and Playwright",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "00cf792c7fc5e3bb2c0eb026bd574a7ab7cf732774fbb31750a88533374220a1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "agents/angular-tester.md",
|
||||||
|
"sha256": "1cf4c37819ab7e1c9cf127c6d155a795cc8a5390b2f1d4d2c933f5e36e47c44f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "ef1ffa49e0320d2eefb6b422e746c545af12c44e9371a33a424448e93457fa51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/run-e2e.md",
|
||||||
|
"sha256": "b701857e152e8e59a94e62ef99dfcff7d52bf4d85a7447389905c1a180f6e6b6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/generate-tests.md",
|
||||||
|
"sha256": "3c731f0ae6f6bac48853ab5f0a58213158a30855dc418b2130e9352e90c1a0ad"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/mocking-patterns/SKILL.md",
|
||||||
|
"sha256": "d2ac058c7b0aa234522373212af418c78406ac47a6be4e307a38055f3a624a9c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/testing-strategies/SKILL.md",
|
||||||
|
"sha256": "c7e2e5c426f42e657bb5ea2cdab563f24ab04b89e5e9de7612928eb0514e70aa"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "6165c7bcf054cb89e9a3f469d0351f49a8890dd024afe3044a8d6d8b7b1c273c"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
1174
skills/mocking-patterns/SKILL.md
Normal file
1174
skills/mocking-patterns/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
1094
skills/testing-strategies/SKILL.md
Normal file
1094
skills/testing-strategies/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user