-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathannotated-employee-test.example.ts
More file actions
221 lines (169 loc) Β· 11.9 KB
/
annotated-employee-test.example.ts
File metadata and controls
221 lines (169 loc) Β· 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/**
* ANNOTATED EXAMPLE β Employee Management Tests
*
* Purpose: Show new developers how this test suite uses Playwright.
* Read this file top-to-bottom before reading any real test files.
*
* Full guide: docs/PLAYWRIGHT_GUIDE.md
*/
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// IMPORTS
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
import { test, expect } from '@playwright/test';
// β β
// test runner assertion library β both come from Playwright
import { loginAsRole, logout } from '../fixtures/auth.fixtures';
// β Custom helpers so tests don't repeat the 6-step OIDC login flow
import { createEmployeeData } from '../fixtures/data.fixtures';
// β Generates unique test data (random email, employee number)
// so parallel tests don't clash with each other
import { EmployeeFormPage } from '../page-objects/employee-form.page';
// β Page Object β wraps all form interactions so tests stay clean
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// TEST SUITE (describe block groups related tests)
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
test.describe('Employee Management β Annotated Example', () => {
// ββ beforeEach runs before EVERY test in this describe block ββ
test.beforeEach(async ({ page }) => {
// β
// Playwright injects a fresh browser page for each test.
// Tests are isolated β they cannot share state.
await loginAsRole(page, 'manager');
// β Opens browser, clicks "Login", fills credentials on IdentityServer,
// waits for redirect back to Angular dashboard.
// Role options: 'employee' | 'manager' | 'hradmin'
});
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// TEST 1 β Navigate and assert
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
test('should display employee list', async ({ page }) => {
// ββ NAVIGATE ββββββββββββββββββββββββββββββββββββββββββββββ
await page.goto('/employees');
// β Relative URL β baseURL from playwright.config.ts is prepended.
// Resolves to http://localhost:4200/employees
await page.waitForLoadState('networkidle');
// β Waits until there are no in-flight network
// requests for 500 ms. Safe for SPA page loads.
// ββ LOCATOR βββββββββββββββββββββββββββββββββββββββββββββββ
const heading = page.locator('h1, h2, h3').filter({ hasText: /employees/i });
// β β β
// page.locator() multi-tag CSS filter() narrows down by text.
// returns a Locator β nothing has happened in the browser yet.
// Actions are lazy until you call .click(), .fill(), etc.
// ββ ASSERTION βββββββββββββββββββββββββββββββββββββββββββββ
await expect(heading.first()).toBeVisible();
// β β
// expect() auto-retries assertion β fails if element not visible
// until timeout (30 s) within the retry window
// Count rows (does NOT auto-retry β it's a snapshot)
const rowCount = await page.locator('tbody tr, mat-row').count();
expect(rowCount).toBeGreaterThan(0);
// β Jest-style matchers work in Playwright
});
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// TEST 2 β Form filling with Page Object
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
test('should create a new employee', async ({ page }) => {
// ββ GENERATE UNIQUE TEST DATA βββββββββββββββββββββββββββββ
const employee = createEmployeeData({
firstName: 'Jane',
lastName: 'Smith',
salary: 80000,
// email and employeeNumber are auto-generated as unique values
// e.g. "jane.smith.4821@example.com" β avoids conflicts in parallel runs
});
// ββ NAVIGATE TO CREATE PAGE βββββββββββββββββββββββββββββββ
await page.goto('/employees');
await page.waitForLoadState('networkidle');
const createButton = page.locator('button').filter({ hasText: /create/i });
await expect(createButton.first()).toBeVisible();
await createButton.first().click();
// ββ PAGE OBJECT βββββββββββββββββββββββββββββββββββββββββββ
const employeeForm = new EmployeeFormPage(page);
// β Constructor takes `page` and sets up all the locators internally.
// See: page-objects/employee-form.page.ts
await employeeForm.waitForForm();
// β Waits for the <form> element to be in the DOM
// ββ FILL FORM βββββββββββββββββββββββββββββββββββββββββββββ
await employeeForm.fillForm({
firstName: employee.firstName,
lastName: employee.lastName,
email: employee.email,
dateOfBirth: '01/01/1990', // MM/DD/YYYY β required by Angular datepicker
phoneNumber: employee.phoneNumber,
salary: employee.salary,
department: 1, // .nth(1) β skips the blank placeholder option
position: 1, // .nth(1) β same reason
gender: 1, // .nth(1) β same reason
});
// β fillForm() uses page.getByLabel() for text fields (most reliable)
// and formControlName selectors for dropdowns.
// ββ SUBMIT ββββββββββββββββββββββββββββββββββββββββββββββββ
await employeeForm.submit();
// β Clicks the first button matching /save|submit|create|update/i
// ββ VERIFY ββββββββββββββββββββββββββββββββββββββββββββββββ
const result = await employeeForm.verifySubmissionSuccess();
// β Checks three outcomes in order:
// 1. Success snackbar visible?
// 2. Page redirected away from /create?
// 3. Form fields still populated? (handles known API 401 error)
expect(result.success).toBe(true);
});
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// TEST 3 β Role-based access control
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
test('should hide Create button for Employee role', async ({ page }) => {
// ββ SWITCH ROLES ββββββββββββββββββββββββββββββββββββββββββ
await logout(page);
// β ALWAYS call logout() before switching roles.
// Angular keeps the session in memory β skipping this causes
// the old session to persist and tests time out.
await loginAsRole(page, 'employee');
// β antoinette16 β read-only permissions
// ββ NAVIGATE ββββββββββββββββββββββββββββββββββββββββββββββ
await page.goto('/employees');
await page.waitForLoadState('networkidle');
// ββ CHECK ELEMENT IS ABSENT βββββββββββββββββββββββββββββββ
const createButton = page.locator('button').filter({ hasText: /create/i });
// .isVisible() returns false without throwing β use for conditional checks
const isVisible = await createButton.isVisible({ timeout: 2000 }).catch(() => false);
// β Short timeout β we expect it's NOT there
expect(isVisible).toBe(false);
// Alternatively, use not.toBeVisible() which auto-retries:
// await expect(createButton).not.toBeVisible();
});
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// TEST 4 β Validation errors
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
test('should show validation errors on empty submit', async ({ page }) => {
await page.goto('/employees/create');
await page.waitForLoadState('networkidle');
const employeeForm = new EmployeeFormPage(page);
// ββ SUBMIT WITHOUT FILLING ANYTHING βββββββββββββββββββββββ
await employeeForm.submit();
// β Angular Material validates on form submission, not on blur.
await page.waitForTimeout(1000);
// β Short wait for validation errors to render
// ββ CHECK FOR ERROR MESSAGES ββββββββββββββββββββββββββββββ
const errors = page.locator('.mat-error, mat-error');
// β Angular Material validation messages use <mat-error>
const errorCount = await errors.count();
expect(errorCount).toBeGreaterThan(0);
// Still on the create page (form was not submitted)
await expect(page).toHaveURL(/\/employees\/create/);
});
});
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// COMMON SELECTOR PATTERNS β quick reference
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
//
// page.getByLabel('Phone Number') β BEST for inputs
// page.getByRole('button', { name: 'Save' }) β BEST for buttons
// page.locator('mat-select[formControlName="x"]') β Material dropdowns
// page.locator('button').filter({ hasText: /save/i }) β filter by text
// page.locator('tr').nth(1) β skip header row
// page.locator('mat-option').nth(1) β skip blank option
// .first() === .nth(0) β same thing
// .last() β last match
//
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ