diff --git a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.html b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.html
index 2bbfceb50..ea6305e60 100644
--- a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.html
+++ b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.html
@@ -59,6 +59,8 @@
}
@case ('auto') {
}
diff --git a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts
index bdea15172..4e4bd7fd3 100644
--- a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts
+++ b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts
@@ -101,6 +101,12 @@ export class AddPlaylistDialogComponent {
private readonly pendingPrefill =
signal(null);
+ /**
+ * Survives the auto-detect surface being destroyed by a method switch, so
+ * returning to it after inspecting a prefilled form keeps the paste.
+ */
+ readonly autoDetectText = signal('');
+
// Order matches the v0.22 mockup left-to-right: URL first (Most common),
// then File, Xtream credentials, Stalker portal, raw text paste. Each
// entry stands on its own — no nested subtypes. Labels are short and
diff --git a/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.spec.ts b/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.spec.ts
new file mode 100644
index 000000000..d41a7b0c9
--- /dev/null
+++ b/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.spec.ts
@@ -0,0 +1,100 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { provideNoopAnimations } from '@angular/platform-browser/animations';
+import { TranslateModule } from '@ngx-translate/core';
+import { ProviderImportCandidate } from '@iptvnator/shared/interfaces';
+import { AutoImportComponent } from './auto-import.component';
+
+describe('AutoImportComponent', () => {
+ let component: AutoImportComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [AutoImportComponent, TranslateModule.forRoot()],
+ providers: [provideNoopAnimations()],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AutoImportComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('computes candidates from the pasted text', () => {
+ fixture.detectChanges();
+
+ component.textControl.setValue(
+ 'https://lists.example.com/main.m3u'
+ );
+
+ expect(component.candidates()).toHaveLength(1);
+ expect(component.candidates()[0].kind).toBe('m3u-url');
+ });
+
+ it('restores the dialog-held text so a method switch does not lose the paste', () => {
+ fixture.componentRef.setInput(
+ 'initialText',
+ 'MAC: 00:1A:79:12:34:56'
+ );
+
+ fixture.detectChanges();
+
+ expect(component.textControl.value).toBe('MAC: 00:1A:79:12:34:56');
+ expect(component.candidates()[0]?.kind).toBe('stalker');
+ });
+
+ it('reports edits back to the dialog', () => {
+ fixture.detectChanges();
+ const emitted: string[] = [];
+ component.textChanged.subscribe((value) => emitted.push(value));
+
+ component.textControl.setValue('some text');
+
+ expect(emitted).toEqual(['some text']);
+ });
+
+ it('clears the textarea and the candidates', () => {
+ fixture.detectChanges();
+ component.textControl.setValue('https://lists.example.com/main.m3u');
+
+ component.clearForm();
+
+ expect(component.textControl.value).toBe('');
+ expect(component.candidates()).toHaveLength(0);
+ });
+
+ it('emits the picked candidate', () => {
+ fixture.detectChanges();
+ const emitted: ProviderImportCandidate[] = [];
+ component.candidateSelected.subscribe((candidate) =>
+ emitted.push(candidate)
+ );
+ const candidate: ProviderImportCandidate = {
+ kind: 'xtream',
+ confidence: 'high',
+ username: 'alice',
+ };
+
+ component.selectCandidate(candidate);
+
+ expect(emitted).toEqual([candidate]);
+ });
+
+ it('masks the password in the summary rows', () => {
+ fixture.detectChanges();
+
+ const rows = component.summaryRows({
+ kind: 'xtream',
+ confidence: 'high',
+ serverUrl: 'http://tv.example.com:8080',
+ username: 'alice',
+ password: 's3cret',
+ });
+
+ const passwordRow = rows.find(
+ (row) => row.labelKey === 'HOME.XTREAM_PLAYLIST.PASSWORD'
+ );
+ expect(passwordRow?.value).toBe('••••••');
+ expect(JSON.stringify(rows)).not.toContain('s3cret');
+ });
+});
diff --git a/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts b/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts
index 5c86bd4a7..66533af24 100644
--- a/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts
+++ b/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts
@@ -1,5 +1,5 @@
-import { Component, computed, output } from '@angular/core';
-import { toSignal } from '@angular/core/rxjs-interop';
+import { Component, computed, input, OnInit, output } from '@angular/core';
+import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
@@ -59,15 +59,37 @@ const CONFIDENCE_LABEL_KEYS: Record = {
TranslatePipe,
],
})
-export class AutoImportComponent {
+export class AutoImportComponent implements OnInit {
readonly candidateSelected = output();
+ /**
+ * The pasted text lives in the DIALOG, not here: switching to a prefilled
+ * form destroys this component (`@switch`), and coming back to compare or
+ * pick another candidate must not cost the user their paste. The dialog
+ * feeds the last text back in and listens for edits.
+ */
+ readonly initialText = input('');
+ readonly textChanged = output();
+
readonly textControl = new FormControl('', { nonNullable: true });
private readonly textValue = toSignal(this.textControl.valueChanges, {
initialValue: '',
});
+ constructor() {
+ this.textControl.valueChanges
+ .pipe(takeUntilDestroyed())
+ .subscribe((value) => this.textChanged.emit(value));
+ }
+
+ ngOnInit(): void {
+ const initial = this.initialText();
+ if (initial) {
+ this.textControl.setValue(initial);
+ }
+ }
+
readonly candidates = computed(() =>
detectProviderImportCandidates(this.textValue())
);