Skip to content

Commit 9435dcb

Browse files
authored
Merge pull request #1559 from microting/feat/use-one-minute-intervals-fu-b-readd-b1m-edits
feat(b1m): re-add dashboard-edit-{a,b} clones now that shifts 3-5 enabled (FU-B)
2 parents 07878ef + f66cc06 commit 9435dcb

2 files changed

Lines changed: 191 additions & 1 deletion

File tree

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { test, expect, Page } from '@playwright/test';
2+
import { LoginPage } from '../../../Page objects/Login.page';
3+
import { OFFGRID_TIMES } from '../../../../helpers/one-minute-times';
4+
5+
/**
6+
* b1m variant of `b/dashboard-edit-a.spec.ts`: exercises the planned-shift
7+
* edit + round-trip flow with off-grid (NOT 5-min-aligned) time literals.
8+
* Relies on the b1m `post-migration.sql` patch to flip
9+
* `AssignedSites.UseOneMinuteIntervals = 1` (so the Material timepicker
10+
* runs at `minutesGap=1` and the position-based picker can land on any
11+
* minute 0-59), and to flip ThirdShiftActive / FourthShiftActive /
12+
* FifthShiftActive = 1 (FU-A) so the workday-entity dialog renders
13+
* shift 3-5 inputs without an in-spec settings dance.
14+
*
15+
* FU-B history: this clone was dropped twice (PR #1545 → commit 35a22382)
16+
* because the legacy two-shift fill (shift1 + shift2 only) caused the
17+
* server-side Update path to compute over null shift3-5 fields and
18+
* silently fail (Save returned `success:false`, no `/plannings/index`
19+
* POST followed, `waitForResponse` hung). FU-A enables shifts 3-5 in
20+
* the seed; the remaining fix is to FILL all five shifts ascending so
21+
* the PUT body always carries every non-null shift cell — matching the
22+
* b1m multishift-shape pattern. PRs #1546 (currentUserAsync.Id) and
23+
* #1547 (model null-guard) shipped earlier server-side fixes; FU-A
24+
* (#1556) extended the seed; FU-B (this spec) drives the full five-
25+
* shift PUT body so all three fixes are exercised end-to-end.
26+
*
27+
* Scope: form-input round-trip on flag-on rows. Cumulative-flex math
28+
* stays on the legacy `b/` shard.
29+
*/
30+
31+
async function waitForSpinner(page: Page) {
32+
if (await page.locator('.overlay-spinner').count() > 0) {
33+
await page.locator('.overlay-spinner').waitFor({ state: 'hidden', timeout: 30000 });
34+
}
35+
}
36+
37+
async function pickTime(page: Page, timeStr: string) {
38+
// Position-based clock-face clicks (cloned from b/dashboard-edit-multishift's
39+
// pickTime helper). Works at minute granularity because the position math
40+
// is `minuteAngle = m * 6`, no rotateZ-selector dependency.
41+
const [hourStr, minuteStr] = timeStr.split(':');
42+
const h = parseInt(hourStr, 10);
43+
const m = parseInt(minuteStr, 10);
44+
45+
const cx = 145, cy = 145;
46+
47+
const hourFace = page.locator('.clock-face');
48+
await hourFace.first().waitFor({ state: 'visible', timeout: 5000 });
49+
const hourAngle = (h % 12) * 30;
50+
const hourR = (h === 0 || h > 12) ? 60 : 100;
51+
const hourRad = hourAngle * Math.PI / 180;
52+
await hourFace.first().click({
53+
position: {
54+
x: Math.round(cx + hourR * Math.sin(hourRad)),
55+
y: Math.round(cy - hourR * Math.cos(hourRad)) + (Math.abs(Math.cos(hourRad)) < 0.01 ? 1 : 0),
56+
},
57+
});
58+
59+
await page.waitForTimeout(500);
60+
const minuteFace = page.locator('.clock-face');
61+
await minuteFace.first().waitFor({ state: 'visible', timeout: 5000 });
62+
const minuteAngle = m * 6;
63+
const minuteR = 100;
64+
const minuteRad = minuteAngle * Math.PI / 180;
65+
await minuteFace.first().click({
66+
position: {
67+
x: Math.round(cx + minuteR * Math.sin(minuteRad)),
68+
y: Math.round(cy - minuteR * Math.cos(minuteRad)) + (Math.abs(Math.cos(minuteRad)) < 0.01 ? 1 : 0),
69+
},
70+
});
71+
72+
await page.waitForTimeout(500);
73+
await page.locator('.timepicker-button span').filter({ hasText: 'Ok' }).click();
74+
}
75+
76+
async function setPlannedShift(
77+
page: Page,
78+
shiftId: 1|2|3|4|5,
79+
start: string,
80+
end: string,
81+
breakStr: string,
82+
) {
83+
await page.locator(`[data-testid="plannedStartOfShift${shiftId}"]`).click();
84+
await pickTime(page, start);
85+
await expect(page.locator(`[data-testid="plannedStartOfShift${shiftId}"]`)).toHaveValue(start);
86+
87+
await page.locator(`[data-testid="plannedEndOfShift${shiftId}"]`).click();
88+
await pickTime(page, end);
89+
await expect(page.locator(`[data-testid="plannedEndOfShift${shiftId}"]`)).toHaveValue(end);
90+
91+
await page.locator(`[data-testid="plannedBreakOfShift${shiftId}"]`).click();
92+
await pickTime(page, breakStr);
93+
await expect(page.locator(`[data-testid="plannedBreakOfShift${shiftId}"]`)).toHaveValue(breakStr);
94+
}
95+
96+
// All five shifts ascending, off-grid. Shifts 3-5 share a single break value
97+
// with shifts 1-2 — the `break` field is intentionally a single canonical
98+
// constant so adding new shifts doesn't mean adding new break literals (the
99+
// workday-entity-dialog has one break input per shift, but the value is the
100+
// same for every shift in this round-trip; what we're testing is the
101+
// position-based picker round-trip, not break-per-shift uniqueness).
102+
const allFivePlannedShifts = [
103+
{ id: 1 as const, start: OFFGRID_TIMES.shift1Start, end: OFFGRID_TIMES.shift1End, break: OFFGRID_TIMES.break },
104+
{ id: 2 as const, start: OFFGRID_TIMES.shift2Start, end: OFFGRID_TIMES.shift2End, break: OFFGRID_TIMES.break },
105+
{ id: 3 as const, start: OFFGRID_TIMES.shift3Start, end: OFFGRID_TIMES.shift3End, break: OFFGRID_TIMES.break },
106+
{ id: 4 as const, start: OFFGRID_TIMES.shift4Start, end: OFFGRID_TIMES.shift4End, break: OFFGRID_TIMES.break },
107+
{ id: 5 as const, start: OFFGRID_TIMES.shift5Start, end: OFFGRID_TIMES.shift5End, break: OFFGRID_TIMES.break },
108+
];
109+
110+
test.describe('Dashboard edit values (b1m, flag-on, 1-minute granularity)', () => {
111+
test.beforeEach(async ({ page }) => {
112+
await page.goto('http://localhost:4200');
113+
await new LoginPage(page).login();
114+
});
115+
116+
test('persists off-grid planned shifts 1-5 through save + reopen', async ({ page }) => {
117+
await page.locator('mat-nested-tree-node').filter({ hasText: 'Timeregistrering' }).click();
118+
const indexPromise = page.waitForResponse(r =>
119+
r.url().includes('/api/time-planning-pn/plannings/index') && r.request().method() === 'POST');
120+
await page.locator('mat-tree-node').filter({ hasText: 'Dashboard' }).click();
121+
await indexPromise;
122+
await waitForSpinner(page);
123+
124+
const cellId = '#cell3_0';
125+
await page.locator(cellId).scrollIntoViewIfNeeded();
126+
await page.locator(cellId).click();
127+
await expect(page.locator('#planHours')).toBeVisible();
128+
129+
// Fill all five shifts ascending. Shifts 3-5 inputs render because the
130+
// b1m post-migration patch (FU-A) sets ThirdShiftActive /
131+
// FourthShiftActive / FifthShiftActive = 1 on every active assigned
132+
// site, so the dialog template's `*ngIf="thirdShiftActive"` (etc.)
133+
// evaluates true on first render — no in-spec settings dance needed.
134+
for (const s of allFivePlannedShifts) {
135+
await setPlannedShift(page, s.id, s.start, s.end, s.break);
136+
}
137+
138+
// Save. Wait for the button to lose `disabled` (the form's cross-shift
139+
// validators run async after each pick) before clicking — `force: true`
140+
// would let Playwright dispatch the event but the browser still drops
141+
// clicks on disabled <button>s, so the request never fires and the
142+
// waitForResponse below would just hang.
143+
await expect(page.locator('#saveButton')).toBeEnabled({ timeout: 10000 });
144+
const updatePromise = page.waitForResponse(r =>
145+
r.url().includes('/api/time-planning-pn/plannings/') && r.request().method() === 'PUT');
146+
const reindexPromise = page.waitForResponse(r =>
147+
r.url().includes('/api/time-planning-pn/plannings/index') && r.request().method() === 'POST');
148+
await page.locator('#saveButton').click();
149+
await updatePromise;
150+
await reindexPromise;
151+
await waitForSpinner(page);
152+
await page.waitForTimeout(500);
153+
154+
// Re-open and assert every off-grid value round-tripped.
155+
await page.locator(cellId).scrollIntoViewIfNeeded();
156+
await page.locator(cellId).click();
157+
await expect(page.locator('#planHours')).toBeVisible();
158+
159+
for (const s of allFivePlannedShifts) {
160+
await expect(
161+
page.locator(`[data-testid="plannedStartOfShift${s.id}"]`),
162+
`shift ${s.id} start should round-trip`,
163+
).toHaveValue(s.start);
164+
await expect(
165+
page.locator(`[data-testid="plannedEndOfShift${s.id}"]`),
166+
`shift ${s.id} end should round-trip`,
167+
).toHaveValue(s.end);
168+
await expect(
169+
page.locator(`[data-testid="plannedBreakOfShift${s.id}"]`),
170+
`shift ${s.id} break should round-trip`,
171+
).toHaveValue(s.break);
172+
}
173+
174+
await page.locator('#cancelButton').click();
175+
});
176+
});

eform-client/playwright/helpers/one-minute-times.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,25 @@
1717
// shift{n+1}.start MUST be >= shift{n}.stop, otherwise saveButton stays
1818
// disabled with `hierarchyError`. Tests pick up the saved row, so all
1919
// values below are non-overlapping and ascending.
20+
//
21+
// FU-B extension: shifts 3-5 added so the b1m dashboard-edit-{a,b} clones
22+
// can fill all five shifts ascending — matching the multishift-shape
23+
// pattern (shifts 3-5 are now rendered post-FU-A because the b1m
24+
// post-migration patch flips ThirdShiftActive / FourthShiftActive /
25+
// FifthShiftActive on every active assigned site). Filling all five
26+
// ensures the PUT body always carries non-null shift data, which was the
27+
// remaining hypothesis for the dropped clones'`waitForResponse` timeouts.
2028
export const OFFGRID_TIMES = {
2129
shift1Start: '08:01',
2230
shift1End: '11:13',
2331
shift2Start: '12:17',
24-
shift2End: '16:23',
32+
shift2End: '14:23',
33+
shift3Start: '14:35',
34+
shift3End: '15:42',
35+
shift4Start: '15:55',
36+
shift4End: '17:08',
37+
shift5Start: '17:21',
38+
shift5End: '19:33',
2539
break: '00:27',
2640
} as const;
2741

0 commit comments

Comments
 (0)