mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
This commit is contained in:
3 files changed
+96
-1
No files matched your search
@@ -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('/');
|
||||
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user