Skip to content

Commit 760c2bb

Browse files
Fix salary-range tests to match Angular source
- salary-range-form.page.ts: rename titleInput to nameInput (form uses formControlName="name" not "title"); fix selectors for all inputs - data.fixtures.ts: rename SalaryRangeData.title to name; remove currency field (form has no currency input); update createSalaryRangeData() - salary-range-crud.spec.ts: add name to fillForm() calls; rewrite RBAC test (Manager CAN create/edit, only HRAdmin can delete per appHasRole directives); fix delete to use ConfirmDialogComponent Material dialog with waitForResponse; fix search to use range name not currency value - salary-range-validation.spec.ts: use mat-error element selector (not .mat-error class); use focus+blur pattern (no markAllAsTouched on submit); fix zero-salary test (Validators.min(0) makes 0 valid); replace "reject zero" test with "accept zero as valid"; add name maxLength test; fix negative value error text match
1 parent 88ea8ba commit 760c2bb

4 files changed

Lines changed: 182 additions & 217 deletions

File tree

fixtures/data.fixtures.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,9 @@ export interface PositionData {
3333
}
3434

3535
export interface SalaryRangeData {
36-
title?: string;
36+
name?: string;
3737
minSalary?: number;
3838
maxSalary?: number;
39-
currency?: string;
4039
}
4140

4241
/**
@@ -174,10 +173,9 @@ export function createSalaryRangeData(overrides: SalaryRangeData = {}): Required
174173
const baseMax = overrides.maxSalary || baseMin + 30000;
175174

176175
return {
177-
title: overrides.title || `Test Salary Range ${rangeNumber}`,
176+
name: overrides.name || `Test Salary Range ${rangeNumber}`,
178177
minSalary: baseMin,
179178
maxSalary: baseMax,
180-
currency: overrides.currency || 'USD',
181179
};
182180
}
183181

page-objects/salary-range-form.page.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,37 @@ import { BaseFormPage } from './base-form.page';
44
/**
55
* Salary Range Form Page Object
66
*
7-
* Covers the create/edit salary range form (HRAdmin only).
8-
* Shared behaviour (submit, cancel, validation, success) comes from BaseFormPage.
7+
* Angular source facts:
8+
* - Form fields: name (required, maxLength(100)), minSalary (required, min(0)), maxSalary (required, min(0))
9+
* - Custom validator: minSalary must be less than maxSalary (shows when form.touched)
10+
* - No currency field in the form — currency is not part of the form
11+
* - Submit button is NOT disabled by validation (no [disabled]="salaryRangeForm.invalid")
12+
* - onSubmit() does NOT call markAllAsTouched() — errors only appear after focus+blur
913
*/
1014
export class SalaryRangeFormPage extends BaseFormPage {
11-
readonly titleInput: Locator;
15+
readonly nameInput: Locator;
1216
readonly minSalaryInput: Locator;
1317
readonly maxSalaryInput: Locator;
1418

1519
constructor(page: Page) {
1620
super(page, '/salary-ranges');
1721

18-
this.titleInput = page.locator('input[formControlName="title"], input[name*="title"]');
19-
this.minSalaryInput = page.locator(
20-
'input[formControlName="minSalary"], input[name*="minSalary"], input[name*="min"]'
21-
);
22-
this.maxSalaryInput = page.locator(
23-
'input[formControlName="maxSalary"], input[name*="maxSalary"], input[name*="max"]'
24-
);
22+
this.nameInput = page.locator('input[formControlName="name"]');
23+
this.minSalaryInput = page.locator('input[formControlName="minSalary"]');
24+
this.maxSalaryInput = page.locator('input[formControlName="maxSalary"]');
2525
}
2626

2727
protected async isFormStillFilled(): Promise<boolean> {
28-
const titleValue = await this.titleInput.inputValue().catch(() => '');
29-
if (titleValue.length > 0) return true;
30-
// Fall back to checking min salary (salary range may not have a title field)
28+
const nameValue = await this.nameInput.inputValue().catch(() => '');
29+
if (nameValue.length > 0) return true;
3130
const minValue = await this.minSalaryInput.inputValue().catch(() => '');
3231
return minValue.length > 0;
3332
}
3433

3534
// ── Field fill methods ──────────────────────────────────────────────────
3635

37-
async fillTitle(title: string) {
38-
const isVisible = await this.titleInput.isVisible({ timeout: 2000 }).catch(() => false);
39-
if (isVisible) await this.titleInput.fill(title);
36+
async fillName(name: string) {
37+
await this.nameInput.fill(name);
4038
}
4139

4240
async fillMinSalary(amount: number | string) {
@@ -49,8 +47,8 @@ export class SalaryRangeFormPage extends BaseFormPage {
4947

5048
// ── fillForm convenience method ─────────────────────────────────────────
5149

52-
async fillForm(data: { title?: string; minSalary?: number; maxSalary?: number }) {
53-
if (data.title) await this.fillTitle(data.title);
50+
async fillForm(data: { name?: string; minSalary?: number; maxSalary?: number }) {
51+
if (data.name) await this.fillName(data.name);
5452
if (data.minSalary !== undefined) await this.fillMinSalary(data.minSalary);
5553
if (data.maxSalary !== undefined) await this.fillMaxSalary(data.maxSalary);
5654
}

tests/salary-ranges/salary-range-crud.spec.ts

Lines changed: 77 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@ import { SalaryRangeFormPage } from '../../page-objects/salary-range-form.page';
77
/**
88
* Salary Range CRUD Tests
99
*
10-
* Tests for salary range management operations:
11-
* - List salary ranges
12-
* - Create salary range (HRAdmin)
13-
* - Edit salary range
14-
* - Delete salary range
10+
* Angular source facts:
11+
* - Create/Edit buttons: *appHasRole="['HRAdmin', 'Manager']" — both roles can create/edit
12+
* - Delete button: *appHasRole="['HRAdmin']" — only HRAdmin can delete
13+
* - Form fields: name (required), minSalary (required, min(0)), maxSalary (required, min(0))
14+
* - Delete uses ConfirmDialogComponent (Material dialog) — NOT window.confirm()
15+
* - Delete API endpoint: /SalaryRanges/{id} — use case-insensitive URL match
16+
* - Salary values displayed as currency: $50,000 (not raw number)
17+
* - No route guard on /salary-ranges list (both Manager and Employee can view)
18+
* - Route guard (hrAdminGuard or managerGuard) on create/edit — need to confirm
1519
*/
1620

1721
test.describe('Salary Range CRUD', () => {
1822
test.beforeEach(async ({ page }) => {
19-
// Login as HRAdmin (likely required for salary ranges)
2023
await loginAsRole(page, 'hradmin');
2124
const list = new SalaryRangeListPage(page);
2225
await list.goto();
@@ -40,12 +43,16 @@ test.describe('Salary Range CRUD', () => {
4043
await list.clickCreate();
4144

4245
const salaryData = createSalaryRangeData({
46+
name: `TestRange_${Date.now()}`,
4347
minSalary: 50000,
4448
maxSalary: 80000,
45-
currency: 'USD',
4649
});
4750

48-
await form.fillForm({ minSalary: salaryData.minSalary, maxSalary: salaryData.maxSalary });
51+
await form.fillForm({
52+
name: salaryData.name,
53+
minSalary: salaryData.minSalary,
54+
maxSalary: salaryData.maxSalary,
55+
});
4956
await form.submit();
5057

5158
const result = await form.verifySubmissionSuccess();
@@ -62,17 +69,16 @@ test.describe('Salary Range CRUD', () => {
6269
const firstRange = list.getRow(0);
6370

6471
if (await firstRange.isVisible({ timeout: 3000 })) {
65-
// Click edit button (or fall back to row click)
66-
const editButton = firstRange.locator('button, a').filter({ hasText: /edit|update/i }).first();
67-
if (await editButton.isVisible({ timeout: 2000 })) {
68-
await editButton.click();
69-
await page.waitForTimeout(1000);
70-
} else {
71-
await firstRange.click();
72-
await page.waitForTimeout(1000);
72+
const editButton = firstRange.locator('button').filter({ hasText: /edit/i }).first();
73+
if (!(await editButton.isVisible({ timeout: 2000 }))) {
74+
test.skip();
75+
return;
7376
}
7477

75-
if (await form.maxSalaryInput.isVisible({ timeout: 2000 }).catch(() => false)) {
78+
await editButton.click();
79+
await page.waitForTimeout(1000);
80+
81+
if (await form.maxSalaryInput.isVisible({ timeout: 3000 }).catch(() => false)) {
7682
await form.fillMaxSalary(100000);
7783
}
7884

@@ -89,63 +95,70 @@ test.describe('Salary Range CRUD', () => {
8995
const list = new SalaryRangeListPage(page);
9096
const form = new SalaryRangeFormPage(page);
9197

92-
// First create a test salary range to delete
93-
if (await list.hasCreatePermission()) {
94-
await list.clickCreate();
95-
96-
const salaryData = createSalaryRangeData({
97-
minSalary: 30000,
98-
maxSalary: 45000,
99-
});
100-
101-
await form.fillForm({ minSalary: salaryData.minSalary, maxSalary: salaryData.maxSalary });
102-
await form.submit();
103-
104-
await page.waitForTimeout(2000);
98+
if (!(await list.hasCreatePermission())) {
99+
test.skip();
100+
return;
101+
}
105102

106-
// Navigate back to list
107-
await list.goto();
103+
// Create a salary range to delete
104+
const uniqueName = `ToDelete_${Date.now()}`;
105+
await list.clickCreate();
106+
await form.fillForm({ name: uniqueName, minSalary: 30000, maxSalary: 45000 });
107+
await form.submit();
108+
await page.waitForTimeout(2000);
108109

109-
// Find the salary range row (look for the max value)
110-
const rangeRow = list.getRowByText('45000');
110+
// Navigate back to list
111+
await list.goto();
112+
await page.waitForLoadState('networkidle');
111113

112-
if (await rangeRow.isVisible({ timeout: 3000 }).catch(() => false)) {
113-
const deleteButton = rangeRow.locator('button').filter({ hasText: /delete|remove/i }).first();
114+
// Find the row by unique name
115+
const rangeRow = list.getRowByText('ToDelete');
116+
if (!(await rangeRow.isVisible({ timeout: 3000 }).catch(() => false))) {
117+
test.skip();
118+
return;
119+
}
114120

115-
if (await deleteButton.isVisible({ timeout: 2000 })) {
116-
await deleteButton.click();
117-
await page.waitForTimeout(1000);
121+
const deleteButton = rangeRow.locator('button').filter({ hasText: /delete/i }).first();
122+
if (!(await deleteButton.isVisible({ timeout: 2000 }))) {
123+
test.skip();
124+
return;
125+
}
118126

119-
// Confirm deletion
120-
const confirmButton = page.locator('button').filter({ hasText: /yes|confirm|delete/i });
121-
await confirmButton.last().click();
127+
await deleteButton.click();
122128

123-
await page.waitForTimeout(2000);
129+
// Salary range delete uses ConfirmDialogComponent (Angular Material dialog)
130+
const dialogConfirm = page.locator('mat-dialog-actions button').filter({ hasText: /Delete/i });
131+
await dialogConfirm.waitFor({ state: 'visible', timeout: 5000 });
124132

125-
const successIndicator = page.locator('mat-snack-bar, .toast, .notification').filter({ hasText: /success|deleted|removed/i });
126-
const hasSuccess = await successIndicator.isVisible({ timeout: 3000 }).catch(() => false);
133+
const [deleteResponse] = await Promise.all([
134+
page.waitForResponse(
135+
resp => resp.url().toLowerCase().includes('/salaryranges/') && resp.request().method() === 'DELETE',
136+
{ timeout: 10000 }
137+
),
138+
dialogConfirm.click(),
139+
]);
127140

128-
expect(hasSuccess).toBe(true);
129-
} else {
130-
test.skip();
131-
}
132-
} else {
133-
test.skip();
134-
}
135-
} else {
136-
test.skip();
137-
}
141+
expect(deleteResponse.status()).toBe(200);
138142
});
139143

140144
test('should search salary ranges', async ({ page }) => {
141145
const list = new SalaryRangeListPage(page);
142146

143147
if (await list.searchInput.isVisible({ timeout: 2000 })) {
144-
await list.search('50000');
148+
// Get the name from the first row (1st column is Range Name)
149+
const firstRow = list.getRow(0);
150+
const firstCellText = (await firstRow.locator('td, mat-cell').first().textContent() || '').trim();
151+
152+
if (!firstCellText) {
153+
test.skip();
154+
return;
155+
}
156+
157+
await list.search(firstCellText);
158+
await page.waitForTimeout(1000);
145159

146160
const rowCount = await list.getRowCount();
147-
// Either has results or shows empty state
148-
expect(rowCount).toBeGreaterThanOrEqual(0);
161+
expect(rowCount).toBeGreaterThanOrEqual(1);
149162
} else {
150163
test.skip();
151164
}
@@ -159,10 +172,7 @@ test.describe('Salary Range CRUD', () => {
159172
if (await firstRow.isVisible({ timeout: 3000 })) {
160173
const rowText = await firstRow.textContent();
161174

162-
// Should contain salary values (with or without currency symbol)
163-
expect(rowText).toMatch(/\d{2,}/); // At least 2 digits for salary
164-
165-
// May contain currency symbols or separators
175+
// Salary values are displayed as $50,000 currency format
166176
const hasCurrencyFormat = /[$£¥]|\d{1,3}(,\d{3})*/.test(rowText || '');
167177
expect(hasCurrencyFormat).toBe(true);
168178
} else {
@@ -177,36 +187,33 @@ test.describe('Salary Range CRUD', () => {
177187
const columnHeaders = page.locator('th, mat-header-cell').filter({ hasText: /min|max|salary/i });
178188

179189
if (await columnHeaders.first().isVisible({ timeout: 2000 })) {
180-
// Get initial order
181190
const firstRow = list.getRow(0);
182191
const initialFirstValue = await firstRow.textContent();
183192

184-
// Click to sort
185193
await columnHeaders.first().click();
186194
await page.waitForTimeout(1000);
187195

188-
// Get new order
189196
const newFirstValue = await list.getRow(0).textContent();
190197

191-
// Values might have changed (sorted) — basic check that values exist
192198
expect(initialFirstValue).toBeTruthy();
193199
expect(newFirstValue).toBeTruthy();
194200
} else {
195201
test.skip();
196202
}
197203
});
198204

199-
test('should not allow non-HRAdmin to create salary range', async ({ page }) => {
205+
test('should show Create and Edit buttons for Manager (not Delete)', async ({ page }) => {
206+
// Manager can create and edit salary ranges but NOT delete
200207
await logout(page);
201208
await loginAsRole(page, 'manager');
202209

203210
const list = new SalaryRangeListPage(page);
204211
await list.goto();
205212

206213
const canCreate = await list.hasCreatePermission();
207-
const accessDenied = await page.locator('text=/access.*denied|forbidden|unauthorized/i').isVisible({ timeout: 2000 }).catch(() => false);
208-
const noTable = !(await list.table.isVisible({ timeout: 2000 }).catch(() => true));
214+
const canDelete = await list.hasDeletePermission();
209215

210-
expect(!canCreate || accessDenied || noTable).toBe(true);
216+
expect(canCreate).toBe(true); // Manager CAN create (appHasRole=['HRAdmin','Manager'])
217+
expect(canDelete).toBe(false); // Manager CANNOT delete (appHasRole=['HRAdmin'])
211218
});
212219
});

0 commit comments

Comments
 (0)