diff --git a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.spec.ts b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.spec.ts index 3bbd447f4..8b4e0b34f 100644 --- a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.spec.ts +++ b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.spec.ts @@ -1140,6 +1140,24 @@ describe('StalkerPortalImportComponent form', () => { expect(toggle.getAttribute('aria-pressed')).toBe('true'); }); + it('masks the password again when the form is cleared', () => { + const { fixture, root, component } = render(); + const password = root.querySelector('#password') as HTMLInputElement; + const toggle = password + .closest('mat-form-field') + ?.querySelector('button') as HTMLButtonElement; + + toggle.click(); + fixture.detectChanges(); + expect(password.type).toBe('text'); + + component.clearForm(); + fixture.detectChanges(); + + expect(password.type).toBe('password'); + expect(toggle.getAttribute('aria-pressed')).toBe('false'); + }); + it('shows a refusal inline under the portal URL and scrolls it into view', async () => { const scrollIntoView = jest.fn(); Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { diff --git a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts index 116d3813e..e373891b4 100644 --- a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts +++ b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts @@ -149,6 +149,9 @@ export class StalkerPortalImportComponent { readonly feedback = signal(null); private readonly feedbackMessage = viewChild>('feedbackMessage'); + private readonly passwordToggle = viewChild( + PasswordVisibilityToggleDirective + ); /** Whether the device IDs are being generated from the MAC. */ readonly derivesDeviceIds = signal(false); @@ -347,6 +350,8 @@ export class StalkerPortalImportComponent { this.invalidatePendingDerivation(); this.derivesDeviceIds.set(false); this.feedback.set(null); + // A cleared form is a fresh entry: the next password starts masked. + this.passwordToggle()?.hide(); this.form.controls.deviceId1.enable(); this.form.controls.deviceId2.enable(); this.form.reset({ diff --git a/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.spec.ts b/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.spec.ts index 1bc8d257b..72f4e8099 100644 --- a/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.spec.ts +++ b/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.spec.ts @@ -260,6 +260,24 @@ describe('XtreamCodeImportComponent form', () => { expect(toggle.getAttribute('aria-pressed')).toBe('false'); }); + it('masks the password again when the form is cleared', () => { + const { fixture, root, component } = render(); + const password = root.querySelector('#password') as HTMLInputElement; + const toggle = password + .closest('mat-form-field') + ?.querySelector('button') as HTMLButtonElement; + + toggle.click(); + fixture.detectChanges(); + expect(password.type).toBe('text'); + + component.clearForm(); + fixture.detectChanges(); + + expect(password.type).toBe('password'); + expect(toggle.getAttribute('aria-pressed')).toBe('false'); + }); + it('shows a neutral server hint until the URL is invalid', () => { const { fixture, root, component } = render(); diff --git a/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts b/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts index 3df2ecf52..83870b92d 100644 --- a/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts +++ b/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts @@ -3,6 +3,7 @@ import { EventEmitter, Output, inject, + viewChild, ChangeDetectionStrategy, } from '@angular/core'; import { @@ -93,6 +94,9 @@ export class XtreamCodeImportComponent { readonly store = inject(Store); readonly connectionTest = createXtreamConnectionTestState(this.form); + private readonly passwordToggle = viewChild( + PasswordVisibilityToggleDirective + ); get isTestingConnection(): boolean { return this.connectionTest.testing(); @@ -103,6 +107,8 @@ export class XtreamCodeImportComponent { } clearForm(): void { + // A cleared form is a fresh entry: the next password starts masked. + this.passwordToggle()?.hide(); this.form.reset({ _id: createRandomId(), title: '', diff --git a/libs/ui/components/src/lib/password-visibility-toggle/password-visibility-toggle.directive.spec.ts b/libs/ui/components/src/lib/password-visibility-toggle/password-visibility-toggle.directive.spec.ts index f2770750f..618a77f49 100644 --- a/libs/ui/components/src/lib/password-visibility-toggle/password-visibility-toggle.directive.spec.ts +++ b/libs/ui/components/src/lib/password-visibility-toggle/password-visibility-toggle.directive.spec.ts @@ -56,6 +56,24 @@ describe('PasswordVisibilityToggleDirective', () => { expect(button.getAttribute('aria-pressed')).toBe('false'); }); + it('masks again on hide()', () => { + const fixture = TestBed.createComponent(HostComponent); + fixture.detectChanges(); + const root = fixture.nativeElement as HTMLElement; + const button = root.querySelector('button') as HTMLButtonElement; + button.click(); + fixture.detectChanges(); + + fixture.debugElement + .query((node) => node.name === 'button') + .injector.get(PasswordVisibilityToggleDirective) + .hide(); + fixture.detectChanges(); + + expect(root.querySelector('input')?.type).toBe('password'); + expect(button.getAttribute('aria-pressed')).toBe('false'); + }); + it('does not submit the surrounding form', () => { const form = document.createElement('form'); const submit = jest.fn((event: Event) => event.preventDefault()); diff --git a/libs/ui/components/src/lib/password-visibility-toggle/password-visibility-toggle.directive.ts b/libs/ui/components/src/lib/password-visibility-toggle/password-visibility-toggle.directive.ts index 612e0dcdd..d1468d053 100644 --- a/libs/ui/components/src/lib/password-visibility-toggle/password-visibility-toggle.directive.ts +++ b/libs/ui/components/src/lib/password-visibility-toggle/password-visibility-toggle.directive.ts @@ -29,4 +29,9 @@ export class PasswordVisibilityToggleDirective { toggle(): void { this.visible.update((visible) => !visible); } + + /** Masks the field again, e.g. when its form is cleared for a new entry. */ + hide(): void { + this.visible.set(false); + } }