import { test, expect } from '@playwright/test'; import { createFanoutConfig, deleteFanoutConfig, getFanoutConfigs, } from '../helpers/api'; import { createCaptureServer, fanoutHeader, openFanoutSettings } from '../helpers/fanout'; test.describe('Apprise integration settings', () => { let createdAppriseId: string | null = null; let receiver: ReturnType; let appriseUrl: string; test.beforeAll(async () => { receiver = createCaptureServer((port) => `json://127.0.0.1:${port}`); appriseUrl = await receiver.listen(); }); test.afterAll(async () => { receiver.close(); }); test.afterEach(async () => { if (createdAppriseId) { try { await deleteFanoutConfig(createdAppriseId); } catch { console.warn('Failed to delete test apprise config'); } createdAppriseId = null; } }); test('create apprise via UI, configure URLs, save as enabled', async ({ page }) => { await openFanoutSettings(page); await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible(); // Open add menu and pick Apprise await page.getByRole('button', { name: 'Add Integration' }).click(); await page.getByRole('menuitem', { name: 'Apprise' }).click(); // Should navigate to the detail/edit view with a numbered default name await expect(page.locator('#fanout-edit-name')).toHaveValue(/Apprise #\d+/); // Fill in notification URL const urlsTextarea = page.locator('#fanout-apprise-urls'); await urlsTextarea.fill(appriseUrl); // Verify preserve identity checkbox is checked by default const preserveIdentity = page.getByText('Preserve identity on Discord'); await expect(preserveIdentity).toBeVisible(); // Verify include routing path checkbox is checked by default const includePath = page.getByText('Include routing path in notifications'); await expect(includePath).toBeVisible(); // Rename it const nameInput = page.locator('#fanout-edit-name'); await nameInput.clear(); await nameInput.fill('E2E Apprise'); // Save as enabled await page.getByRole('button', { name: /Save as Enabled/i }).click(); await expect(page.getByText('Integration saved and enabled')).toBeVisible(); // Should be back on list view with our apprise config visible await expect(page.getByText('E2E Apprise')).toBeVisible(); await expect(page.getByText(appriseUrl)).toBeVisible(); // Clean up via API const configs = await getFanoutConfigs(); const apprise = configs.find((c) => c.name === 'E2E Apprise'); if (apprise) { createdAppriseId = apprise.id; } }); test('create apprise via API, verify options persist after edit', async ({ page }) => { const apprise = await createFanoutConfig({ type: 'apprise', name: 'API Apprise', config: { urls: `${appriseUrl}\nslack://token_a/token_b/token_c`, preserve_identity: false, include_path: false, }, enabled: true, }); createdAppriseId = apprise.id; await openFanoutSettings(page); await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible(); // Click Edit on our apprise config const row = fanoutHeader(page, 'API Apprise'); await expect(row).toBeVisible(); await row.getByRole('button', { name: 'Edit' }).click(); // Verify the URLs textarea has our content const urlsTextarea = page.locator('#fanout-apprise-urls'); await expect(urlsTextarea).toHaveValue(new RegExp(appriseUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); await expect(urlsTextarea).toHaveValue(/slack:\/\/token_a/); // Verify checkboxes reflect our config (both unchecked) const preserveCheckbox = page .getByText('Preserve identity on Discord') .locator('xpath=ancestor::label[1]') .locator('input[type="checkbox"]'); await expect(preserveCheckbox).not.toBeChecked(); const pathCheckbox = page .getByText('Include routing path in notifications') .locator('xpath=ancestor::label[1]') .locator('input[type="checkbox"]'); await expect(pathCheckbox).not.toBeChecked(); // Go back page.once('dialog', (dialog) => dialog.accept()); await page.getByText('← Back to list').click(); await expect(row).toBeVisible(); }); test('apprise shows scope selector', async ({ page }) => { const apprise = await createFanoutConfig({ type: 'apprise', name: 'Scope Apprise', config: { urls: appriseUrl }, }); createdAppriseId = apprise.id; await openFanoutSettings(page); await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible(); const row = fanoutHeader(page, 'Scope Apprise'); await expect(row).toBeVisible(); await row.getByRole('button', { name: 'Edit' }).click(); // Verify scope selector is present await expect(page.getByText('Message Scope')).toBeVisible(); await expect(page.getByText('All messages')).toBeVisible(); // Select "All except listed" mode await page.getByText('All except listed channels/contacts').click(); // Should show channel and contact lists with exclude label await expect(page.getByText('Channels (exclude)')).toBeVisible(); // Go back page.once('dialog', (dialog) => dialog.accept()); await page.getByText('← Back to list').click(); await expect(row).toBeVisible(); }); test('apprise disabled config shows disabled status and can be enabled via save button', async ({ page, }) => { const apprise = await createFanoutConfig({ type: 'apprise', name: 'Disabled Apprise', config: { urls: appriseUrl }, enabled: false, }); createdAppriseId = apprise.id; await openFanoutSettings(page); await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible(); // Should show "Disabled" status text const row = fanoutHeader(page, 'Disabled Apprise'); await expect(row).toContainText('Disabled'); // Edit it await expect(row).toBeVisible(); await row.getByRole('button', { name: 'Edit' }).click(); // Save as enabled await page.getByRole('button', { name: /Save as Enabled/i }).click(); await expect(page.getByText('Integration saved and enabled')).toBeVisible(); // Verify it's now enabled via API const configs = await getFanoutConfigs(); const updated = configs.find((c) => c.id === apprise.id); expect(updated?.enabled).toBe(true); }); test('delete apprise via UI', async ({ page }) => { const apprise = await createFanoutConfig({ type: 'apprise', name: 'Delete Me Apprise', config: { urls: appriseUrl }, }); createdAppriseId = apprise.id; await openFanoutSettings(page); await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible(); const row = fanoutHeader(page, 'Delete Me Apprise'); await expect(row).toBeVisible(); await row.getByRole('button', { name: 'Edit' }).click(); // Accept the confirmation dialog page.once('dialog', (dialog) => dialog.accept()); await page.getByRole('button', { name: 'Delete' }).click(); // Should be back on list, apprise gone await expect(row).not.toBeVisible(); createdAppriseId = null; }); });