mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
fix(import): consistent add-source forms with masked passwords and URL errors
- Mask the Xtream password in add and edit (and the Stalker one in edit) behind a shared PasswordVisibilityToggleDirective: one translated "Show password" label, state in aria-pressed, type="button". - Give the Xtream server URL its own mat-error and a neutral hint instead of the EPG file error; give the M3U URL a mat-error. - Use "Playlist title" in every add form, "MAC address" casing, a single ellipsis in "Validating portal…" and one "Add playlist" submit label; translate the method radiogroup's aria-label. - Show Stalker refusals inline under the portal URL (role="status", like the Xtream connection test), translated in the template and cleared by edits; translate the snackbars for outcomes that close the dialog. - Translate new strings into all locales; reuse the identical Stalker URL error translations; fix MAC casing and ellipses; drop unused keys. - Unit specs per form, edit-dialog spec, new add-source-forms web E2E; update E2E locators; UI guidelines Forms section; Stalker contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
23a1860119
commit
0ea9ecdb27
56 files changed
+1208
-252
No files matched your search
@@ -16,6 +16,7 @@ export * from './lib/portal-detail-shell/portal-detail-shell.component';
|
||||
export * from './lib/view-in-portal-action/view-in-portal-action.component';
|
||||
export * from './lib/view-in-portal-action/view-in-portal-handoff.token';
|
||||
export * from './lib/parental-lock-pin-dialog/parental-lock-pin-dialog.component';
|
||||
export * from './lib/password-visibility-toggle/password-visibility-toggle.directive';
|
||||
export * from './lib/progress-capsule/progress-capsule.component';
|
||||
export * from './lib/resizable/resizable.directive';
|
||||
export * from './lib/season-container/season-container.component';
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { PasswordVisibilityToggleDirective } from './password-visibility-toggle.directive';
|
||||
|
||||
@Component({
|
||||
imports: [PasswordVisibilityToggleDirective],
|
||||
template: `
|
||||
<input [type]="toggle.inputType()" />
|
||||
<button
|
||||
appPasswordVisibilityToggle
|
||||
#toggle="appPasswordVisibilityToggle"
|
||||
aria-label="Show password"
|
||||
>
|
||||
{{ toggle.icon() }}
|
||||
</button>
|
||||
`,
|
||||
})
|
||||
class HostComponent {}
|
||||
|
||||
describe('PasswordVisibilityToggleDirective', () => {
|
||||
function render() {
|
||||
const fixture = TestBed.createComponent(HostComponent);
|
||||
fixture.detectChanges();
|
||||
const root = fixture.nativeElement as HTMLElement;
|
||||
return {
|
||||
fixture,
|
||||
input: root.querySelector('input') as HTMLInputElement,
|
||||
button: root.querySelector('button') as HTMLButtonElement,
|
||||
};
|
||||
}
|
||||
|
||||
it('starts masked and is a non-submitting toggle button', () => {
|
||||
const { input, button } = render();
|
||||
|
||||
expect(input.type).toBe('password');
|
||||
expect(button.type).toBe('button');
|
||||
expect(button.getAttribute('aria-pressed')).toBe('false');
|
||||
expect(button.textContent?.trim()).toBe('visibility');
|
||||
});
|
||||
|
||||
it('reveals and masks again on each click, keeping its label', () => {
|
||||
const { fixture, input, button } = render();
|
||||
|
||||
button.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(input.type).toBe('text');
|
||||
expect(button.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(button.textContent?.trim()).toBe('visibility_off');
|
||||
expect(button.getAttribute('aria-label')).toBe('Show password');
|
||||
|
||||
button.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(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());
|
||||
form.addEventListener('submit', submit);
|
||||
const { button } = render();
|
||||
form.appendChild(button);
|
||||
document.body.appendChild(form);
|
||||
|
||||
button.click();
|
||||
|
||||
expect(submit).not.toHaveBeenCalled();
|
||||
form.remove();
|
||||
});
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { computed, Directive, signal } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Turns a `mat-icon-button` suffix into the show/hide control of a password
|
||||
* field. The field starts masked; the input binds `[type]="toggle.inputType()"`
|
||||
* and the icon renders `toggle.icon()`.
|
||||
*
|
||||
* It is a toggle button in the ARIA sense: the template gives it one constant
|
||||
* label ("Show password") and `aria-pressed` carries the state, so a screen
|
||||
* reader announces "Show password, toggle button, pressed" instead of a label
|
||||
* that flips under the user's focus.
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'button[appPasswordVisibilityToggle]',
|
||||
exportAs: 'appPasswordVisibilityToggle',
|
||||
host: {
|
||||
type: 'button',
|
||||
'[attr.aria-pressed]': 'visible()',
|
||||
'(click)': 'toggle()',
|
||||
},
|
||||
})
|
||||
export class PasswordVisibilityToggleDirective {
|
||||
readonly visible = signal(false);
|
||||
readonly inputType = computed(() => (this.visible() ? 'text' : 'password'));
|
||||
readonly icon = computed(() =>
|
||||
this.visible() ? 'visibility_off' : 'visibility'
|
||||
);
|
||||
|
||||
toggle(): void {
|
||||
this.visible.update((visible) => !visible);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user