Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,66 @@ test.describe('Dashboard — multi-shift (3-5) round-trip regression guard', ()

await page.locator('#cancelButton').click();
});

/**
* Regression guard for the "Use 1-minute intervals" first-user toggle in
* the Advanced settings section. The flag rides on AssignedSite end-to-end
* (entity → DTO → write mapping → angular model) and is persisted by
* TimeSettingService.UpdateAssignedSite. This test only verifies the UI
* plumbing — the toggle is dormant (no business-logic consumers yet).
*
* Gating: !data.resigned && (selectCurrentUserIsFirstUser$ | async).
* The CI seed logs in as admin@admin.com (LoginConstants.username), which
* is the first-user, so the toggle is visible in this test context.
*/
test('first-user can toggle Use 1-minute intervals; persists across save+reopen', async ({ page }) => {
await page.locator('mat-nested-tree-node').filter({ hasText: 'Timeregistrering' }).click();
const indexPromise = page.waitForResponse(r =>
r.url().includes('/api/time-planning-pn/plannings/index') && r.request().method() === 'POST');
await page.locator('mat-tree-node').filter({ hasText: 'Dashboard' }).click();
await indexPromise;
await waitForSpinner(page);

// Open the assigned-site dialog for the third worker — same convention
// as the other tests on this shard so they don't clobber each other.
await page.locator('#firstColumn3').click();
await expect(page.locator('mat-dialog-container')).toBeVisible({ timeout: 10000 });

// The toggle is gated behind first-user; the CI fixture logs in as
// first-user, so it must be visible.
await expect(page.locator('#useOneMinuteIntervals')).toBeVisible({ timeout: 10000 });

const cb = page.locator('#useOneMinuteIntervals input[type="checkbox"]');
await cb.waitFor({ state: 'attached', timeout: 10000 });

// Capture the starting state, flip it, save, re-open, assert it persisted.
const wasChecked = await cb.isChecked();
await page.locator('#useOneMinuteIntervals').click({ force: true });
if (wasChecked) {
await expect(cb).not.toBeChecked();
} else {
await expect(cb).toBeChecked();
}

const assignSitePromise = page.waitForResponse(
r => r.url().includes('/api/time-planning-pn/settings/assigned-site') && r.request().method() === 'PUT');
await page.locator('#saveButton').click({ force: true });
await assignSitePromise;
await waitForSpinner(page);
await expect(page.locator('mat-dialog-container')).toHaveCount(0, { timeout: 10000 });

// Re-open and assert the new value round-tripped.
await page.locator('#firstColumn3').click();
await expect(page.locator('mat-dialog-container')).toBeVisible({ timeout: 10000 });

const cbReopened = page.locator('#useOneMinuteIntervals input[type="checkbox"]');
await cbReopened.waitFor({ state: 'attached', timeout: 10000 });
if (wasChecked) {
await expect(cbReopened).not.toBeChecked();
} else {
await expect(cbReopened).toBeChecked();
}

await page.locator('#cancelButton').click();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,21 @@ <h3 class="section-header">{{ 'Employee status' | translate }}</h3>
</mat-error>
</mat-form-field>
</div>

<!-- ========= Advanced settings (first-user only) ========= -->
<h3 class="section-header" *ngIf="!data.resigned && (selectCurrentUserIsFirstUser$ | async)">{{ 'Advanced settings' | translate }}</h3>

<div class="d-flex flex-row" *ngIf="!data.resigned && (selectCurrentUserIsFirstUser$ | async)">
<mat-checkbox class="p-1"
[id]="'useOneMinuteIntervals'"
[name]="'useOneMinuteIntervals'"
formControlName="useOneMinuteIntervals">
<div>
<div>{{ 'Use 1-minute intervals' | translate }}</div>
<small class="checkbox-description">{{ 'Enable 1-minute granularity for time entry. Default increments are 5 or 15 minutes.' | translate }}</small>
</div>
</mat-checkbox>
</div>
</mat-tab>
<mat-tab label="{{'Plan hours' | translate }} "
*ngIf="data.useOnlyPlanHours && selectCurrentUserIsFirstUser$ | async">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -565,4 +565,62 @@ describe('AssignedSiteDialogComponent', () => {
}
});
});

describe('Use 1-minute intervals toggle', () => {
// The toggle's *ngIf gates on:
// !data.resigned && (selectCurrentUserIsFirstUser$ | async)
// We follow the same no-`detectChanges` pattern adopted in PR #1538:
// assert against component state and observables, not the rendered DOM.
// Rendering the dialog under NO_ERRORS_SCHEMA hits the
// NG01203 "No value accessor for form control name: 'resigned'" issue
// because Material form-control directives aren't registered in the
// tested module.

function setupWithData(
overrides: Partial<typeof mockAssignedSiteData>,
): AssignedSiteDialogComponent {
const data = { ...mockAssignedSiteData, ...overrides };
TestBed.resetTestingModule();
TestBed.configureTestingModule({
declarations: [AssignedSiteDialogComponent],
imports: [ReactiveFormsModule, TranslateModule.forRoot(), HttpClientTestingModule],
schemas: [NO_ERRORS_SCHEMA],
providers: [
FormBuilder,
{ provide: MAT_DIALOG_DATA, useValue: data },
{ provide: TimePlanningPnSettingsService, useValue: mockSettingsService },
{ provide: TimePlanningPnPayRuleSetsService, useValue: mockPayRuleSetsService },
{ provide: Store, useValue: mockStore },
],
}).compileComponents();
const newFixture = TestBed.createComponent(AssignedSiteDialogComponent);
const c = newFixture.componentInstance;
c.ngOnInit();
return c;
}

it('should expose the useOneMinuteIntervals form control', () => {
const c = setupWithData({});
expect(c.assignedSiteForm.get('useOneMinuteIntervals')).not.toBeNull();
});

it('should default useOneMinuteIntervals form value to data value (false by default)', () => {
const cFalse = setupWithData({});
expect(cFalse.assignedSiteForm.get('useOneMinuteIntervals')?.value).toBe(false);

const cTrue = setupWithData({ useOneMinuteIntervals: true } as any);
expect(cTrue.assignedSiteForm.get('useOneMinuteIntervals')?.value).toBe(true);
});

it('should expose first-user state via selectCurrentUserIsFirstUser$', () => {
// The new section is gated by !data.resigned && (selectCurrentUserIsFirstUser$ | async);
// assert the observable wiring instead of rendering the DOM (see top-of-block rationale).
const c = setupWithData({});
expect(c.selectCurrentUserIsFirstUser$).toBeDefined();
let isFirstUser = false;
c.selectCurrentUserIsFirstUser$.subscribe((v: boolean) => (isFirstUser = v));
// mockStore.select returns of(true) for every selector; verifies the gate flows through.
expect(isFirstUser).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export class AssignedSiteDialogComponent implements DoCheck, OnInit {
this.assignedSiteForm = this.fb.group({
useGoogleSheetAsDefault: new FormControl(this.data.useGoogleSheetAsDefault),
useOnlyPlanHours: new FormControl(this.data.useOnlyPlanHours),
useOneMinuteIntervals: new FormControl(this.data.useOneMinuteIntervals ?? false),
autoBreakCalculationActive: new FormControl(this.data.autoBreakCalculationActive),
allowPersonalTimeRegistration: new FormControl(this.data.allowPersonalTimeRegistration),
allowEditOfRegistrations: new FormControl(this.data.allowEditOfRegistrations),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ export const da = {
'Mobile registration': 'Mobilregistrering',
Shifts: 'Vagter',
'Payroll rules': 'Lønregler',
'Advanced settings': 'Avancerede indstillinger',
'Use 1-minute intervals': 'Brug 1-minutters intervaller',
'Enable 1-minute granularity for time entry. Default increments are 5 or 15 minutes.': 'Aktiver 1-minutters granularitet for tidsregistrering. Standardintervaller er 5 eller 15 minutter.',
'How working time is captured': 'Sådan registreres arbejdstiden',
'Manual entry': 'Manuel indtastning',
'Allow entry of forgotten days': 'Tillad indtastning af glemte dage',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ export const enUS = {
'Mobile registration': 'Mobile registration',
'Shifts': 'Shifts',
'Payroll rules': 'Payroll rules',
'Advanced settings': 'Advanced settings',
'Use 1-minute intervals': 'Use 1-minute intervals',
'Enable 1-minute granularity for time entry. Default increments are 5 or 15 minutes.': 'Enable 1-minute granularity for time entry. Default increments are 5 or 15 minutes.',
'Hide employee from daily operations. Requires a resignation date. All settings below are hidden.': 'Hide employee from daily operations. Requires a resignation date. All settings below are hidden.',
'Breaks are calculated automatically from the rule set. Manual break entry on shift tabs is disabled.': 'Breaks are calculated automatically from the rule set. Manual break entry on shift tabs is disabled.',
'Main switch. When active, select registration method and editing policy below.': 'Main switch. When active, select registration method and editing policy below.',
Expand Down
Loading