From 70f5e418554ebd1095acd727d6d0dfff732e3971 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 05:23:31 +0000 Subject: [PATCH] test(playlist): E2E coverage for the auto-detect handoff; sanitize labeled URLs - Adds two web E2E specs for the paste -> pick candidate -> prefilled form workflow, exercising the real @switch/viewChild timing the dialog unit tests cannot reach: an Xtream handoff (including the masked password on the card) and a Stalker handoff whose pasted message survives a method switch. The repo requires E2E coverage for import workflows. - A labeled server URL now gets the same sentence-punctuation cleanup the URL scanner applies, so 'Server: http://host!' no longer prefills a hostname DNS can never resolve. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4 --- apps/web-e2e/src/basic.e2e.ts | 77 +++++++++++++++++++ .../provider-import-detection.util.spec.ts | 14 ++++ .../src/lib/provider-import-scan.util.ts | 6 +- 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/apps/web-e2e/src/basic.e2e.ts b/apps/web-e2e/src/basic.e2e.ts index 8fa6d4d13..792fb13ee 100644 --- a/apps/web-e2e/src/basic.e2e.ts +++ b/apps/web-e2e/src/basic.e2e.ts @@ -31,6 +31,83 @@ test('@web @m3u basic playlist import flow', async ({ page }) => { await expect(page.getByText('4. HappyKids TV')).toBeVisible(); }); +test('@web @auto-detect pasted provider message prefills the Xtream form', async ({ + page, +}) => { + await page.goto('/'); + + await page.getByRole('button', { name: 'Add playlist' }).click(); + const dialog = page.locator('mat-dialog-container'); + await expect(dialog).toBeVisible(); + await dialog.getByRole('radio', { name: /Auto-detect/i }).click(); + + // Shape of a real reseller handout, with fictional credentials. + await dialog + .locator('[data-test-id="auto-detect-textarea"]') + .fill( + 'β—‰π™Ώπ™Ύπšπšƒπ™°π™»βž€ http://tv.example.com:8080\n' + + 'β”œβ—‰πš„πš‚π™΄πšβž€ e2euser\n' + + 'β”œβ—‰π™Ώπ™°πš‚πš‚βž€ e2epass' + ); + + const candidate = dialog.locator('[data-test-id="auto-detect-candidate"]').first(); + await expect(candidate).toBeVisible(); + await expect(candidate.getByText('e2euser')).toBeVisible(); + // The card must never print the password in clear. + await expect(candidate.getByText('e2epass')).toHaveCount(0); + + await candidate.locator('[data-test-id="auto-detect-use"]').click(); + + // The dialog switches to the real Xtream form (@switch + viewChild + // timing) and the prefill lands in its controls. + await expect( + dialog.getByRole('radio', { name: /Xtream credentials/i }) + ).toBeChecked(); + await expect(dialog.getByLabel('Server URL')).toHaveValue( + 'http://tv.example.com:8080' + ); + await expect(dialog.getByLabel('Username')).toHaveValue('e2euser'); + await expect(dialog.getByLabel('Password')).toHaveValue('e2epass'); + await expect(dialog.getByLabel('Playlist title')).toHaveValue( + 'tv.example.com' + ); + + // Detection only proposes: the regular Add action stays in charge. + await expect( + dialog.getByRole('button', { name: 'Add', exact: true }) + ).toBeEnabled(); +}); + +test('@web @auto-detect keeps the pasted message when switching methods', async ({ + page, +}) => { + await page.goto('/'); + + await page.getByRole('button', { name: 'Add playlist' }).click(); + const dialog = page.locator('mat-dialog-container'); + await dialog.getByRole('radio', { name: /Auto-detect/i }).click(); + + const message = 'Portal: http://stb.example.com/c/\nMAC: 00:1A:79:12:34:56'; + await dialog.locator('[data-test-id="auto-detect-textarea"]').fill(message); + + const candidate = dialog.locator('[data-test-id="auto-detect-candidate"]').first(); + await expect(candidate.getByText('00:1A:79:12:34:56')).toBeVisible(); + await candidate.locator('[data-test-id="auto-detect-use"]').click(); + + await expect( + dialog.getByRole('radio', { name: /Stalker portal/i }) + ).toBeChecked(); + await expect(dialog.getByLabel('Mac Address')).toHaveValue( + '00:1A:79:12:34:56' + ); + + // Returning to auto-detect must not cost the user their paste. + await dialog.getByRole('radio', { name: /Auto-detect/i }).click(); + await expect(dialog.locator('[data-test-id="auto-detect-textarea"]')).toHaveValue( + message + ); +}); + test('@web keyboard shortcuts help opens from question mark', async ({ page }) => { await page.goto('/'); diff --git a/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts b/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts index 4d3650f14..93fa486ee 100644 --- a/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts +++ b/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts @@ -315,6 +315,20 @@ describe('detectProviderImportCandidates', () => { expect(xtream[0].serverUrl).toBe('http://panel.example.io:8080'); }); + it('strips sentence punctuation from a labeled server URL', () => { + const candidates = detectProviderImportCandidates( + [ + 'Server: http://panel.example.com!', + 'User: alice', + 'Pass: s3cret', + ].join('\n') + ); + + const xtream = only(candidates, 'xtream'); + expect(xtream).toHaveLength(1); + expect(xtream[0].serverUrl).toBe('http://panel.example.com'); + }); + it('completes a port-less API URL with the separately labeled port', () => { const candidates = detectProviderImportCandidates( [ diff --git a/libs/shared/interfaces/src/lib/provider-import-scan.util.ts b/libs/shared/interfaces/src/lib/provider-import-scan.util.ts index 9e117b79a..fb35cf204 100644 --- a/libs/shared/interfaces/src/lib/provider-import-scan.util.ts +++ b/libs/shared/interfaces/src/lib/provider-import-scan.util.ts @@ -410,7 +410,11 @@ export function extractMacAddresses( * form field, never on the wire directly. */ export function labeledHostUrl(labeled: LabeledFields): string | undefined { - const host = labeled.host; + // Same sentence-punctuation cleanup the URL scanner applies: a labeled + // value takes precedence over the scanned one, so leaving `Server: + // http://host!` uncleaned here would hand the forms a hostname DNS can + // never resolve while the sanitized scanned URL sat right beside it. + const host = labeled.host?.replace(TRAILING_PUNCTUATION, ''); if (!host) { return undefined; }