mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(parental-lock): replay the shake when the same field is refused again
A second refusal of the same field within 400ms set `shake` to the value it already had, so the class never came off and the running CSS animation only finished instead of replaying. Re-adding the class would not help either: styles are not recomputed in between. The running shake animation is now rewound through the Web Animations API. Under reduced motion there is no animation, so this does nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
24fcea999d
commit
2e49c6feaf
3 files changed
+69
-3
No files matched your search
@@ -161,6 +161,33 @@ test.describe('Electron parental-lock PIN dialog', () => {
|
||||
expect(await pressEnterAndCatchShake(confirm)).toContain(
|
||||
'pin-dialog-shake'
|
||||
);
|
||||
// Refused again while it still shakes, the field shakes from
|
||||
// the start: its class never comes off, so the animation is
|
||||
// rewound. Held at 300ms, the second refusal must reset it.
|
||||
const replayedAt = await dialog
|
||||
.locator('form')
|
||||
.evaluate(async (form: HTMLFormElement) => {
|
||||
const nextFrame = () =>
|
||||
new Promise((resolve) =>
|
||||
requestAnimationFrame(() =>
|
||||
requestAnimationFrame(resolve)
|
||||
)
|
||||
);
|
||||
const field = form
|
||||
.querySelector(
|
||||
'[data-test-id="parental-lock-pin-confirm"]'
|
||||
)
|
||||
?.closest('mat-form-field') as HTMLElement;
|
||||
form.requestSubmit();
|
||||
await nextFrame();
|
||||
const [shake] = field.getAnimations();
|
||||
shake.pause();
|
||||
shake.currentTime = 300;
|
||||
form.requestSubmit();
|
||||
await nextFrame();
|
||||
return shake.currentTime;
|
||||
});
|
||||
expect(replayedAt).toBe(0);
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(mismatch).toBeVisible();
|
||||
await expect(toggle).toHaveAttribute('aria-checked', 'false');
|
||||
|
||||
+23
@@ -243,6 +243,29 @@ describe('ParentalLockPinDialogComponent', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('replays the shake when the same field is refused again', async () => {
|
||||
const dialog = await openDialog({ mode: 'set' });
|
||||
const field = dialog
|
||||
.query('parental-lock-pin')
|
||||
?.closest('mat-form-field') as HTMLElement & {
|
||||
getAnimations?: () => Partial<CSSAnimation>[];
|
||||
};
|
||||
// jsdom has no Web Animations: stand in for the running shake.
|
||||
const shake: Partial<CSSAnimation> = {
|
||||
animationName: '_ngcontent-x_pin-dialog-shake',
|
||||
currentTime: 250,
|
||||
};
|
||||
field.getAnimations = () => [shake];
|
||||
|
||||
await dialog.type('parental-lock-pin', '24');
|
||||
await dialog.pressEnter();
|
||||
expect(shake.currentTime).toBe(250);
|
||||
|
||||
await dialog.pressEnter();
|
||||
expect(dialog.component.shake()).toBe('pin');
|
||||
expect(shake.currentTime).toBe(0);
|
||||
});
|
||||
|
||||
it('refuses a too-short PIN on Enter with an error on the PIN field', async () => {
|
||||
const dialog = await openDialog({ mode: 'set' });
|
||||
const pin = () =>
|
||||
|
||||
+19
-3
@@ -252,9 +252,12 @@ export class ParentalLockPinDialogComponent {
|
||||
private refuse(field: PinDialogField): void {
|
||||
this.refused.set(field);
|
||||
this.shakeField(field);
|
||||
const input =
|
||||
field === 'pin' ? this.pinInput() : this.confirmationInput();
|
||||
input?.nativeElement.focus();
|
||||
this.fieldInput(field)?.focus();
|
||||
}
|
||||
|
||||
private fieldInput(field: PinDialogField): HTMLInputElement | undefined {
|
||||
return (field === 'pin' ? this.pinInput() : this.confirmationInput())
|
||||
?.nativeElement;
|
||||
}
|
||||
|
||||
private clearRefusal(field: PinDialogField): void {
|
||||
@@ -269,6 +272,19 @@ export class ParentalLockPinDialogComponent {
|
||||
if (this.shakeTimer !== null) {
|
||||
window.clearTimeout(this.shakeTimer);
|
||||
}
|
||||
if (this.shake() === field) {
|
||||
// Same field again: its class stays on, so the running shake
|
||||
// would only finish. Rewind it (none under reduced motion).
|
||||
this.fieldInput(field)
|
||||
?.closest('mat-form-field')
|
||||
?.getAnimations?.()
|
||||
.filter((animation) =>
|
||||
(animation as CSSAnimation).animationName?.includes(
|
||||
'pin-dialog-shake'
|
||||
)
|
||||
)
|
||||
.forEach((animation) => (animation.currentTime = 0));
|
||||
}
|
||||
this.shake.set(field);
|
||||
this.shakeTimer = window.setTimeout(() => {
|
||||
this.shakeTimer = null;
|
||||
|
||||
Reference in new issue
Block a user