Files
automaker/apps/ui/tests/settings/event-hooks-settings.spec.ts

272 lines
10 KiB
TypeScript

/**
* Event Hooks Settings Page Tests
*
* Tests for the event hooks settings section, including:
* - Event hooks management
* - Ntfy endpoint configuration
* - Dialog state management (useEffect hook validation)
*
* This test also serves as a regression test for the bug where
* useEffect was not imported in the event-hooks-section.tsx file,
* causing a runtime error when opening the Ntfy endpoint dialog.
*/
import { test, expect, type Page } from '@playwright/test';
import { authenticateForTests, navigateToSettings, waitForSuccessToast } from '../utils';
// Timeout constants for maintainability
const TIMEOUTS = {
sectionVisible: 10000,
dialogVisible: 5000,
dialogHidden: 5000,
endpointVisible: 5000,
} as const;
// Selectors for reuse
const SELECTORS = {
eventHooksButton: 'button:has-text("Event Hooks")',
endpointsTab: 'button[role="tab"]:has-text("Endpoints")',
sectionText: 'text=Run custom commands or send notifications',
addEndpointButton: 'button:has-text("Add Endpoint")',
dialog: '[role="dialog"]',
dialogTitle: 'text=Add Ntfy Endpoint',
} as const;
/**
* Navigate to the Event Hooks Endpoints tab
* This helper reduces code duplication across tests
*/
async function navigateToEndpointsTab(page: Page): Promise<void> {
await navigateToSettings(page);
// Click on the Event Hooks section in the navigation
await page.locator(SELECTORS.eventHooksButton).first().click();
// Wait for the event hooks section to be visible
await expect(page.locator(SELECTORS.sectionText)).toBeVisible({
timeout: TIMEOUTS.sectionVisible,
});
// Switch to Endpoints tab (ntfy endpoints)
await page.locator(SELECTORS.endpointsTab).click();
}
test.describe('Event Hooks Settings', () => {
test.beforeEach(async ({ page }) => {
await authenticateForTests(page);
});
test('should load event hooks settings section without errors', async ({ page }) => {
await navigateToSettings(page);
// Click on the Event Hooks section in the navigation
await page.locator(SELECTORS.eventHooksButton).first().click();
// Wait for the event hooks section to be visible
await expect(page.locator(SELECTORS.sectionText)).toBeVisible({
timeout: TIMEOUTS.sectionVisible,
});
// Verify the tabs are present
await expect(page.locator('button[role="tab"]:has-text("Hooks")')).toBeVisible();
await expect(page.locator(SELECTORS.endpointsTab)).toBeVisible();
await expect(page.locator('button[role="tab"]:has-text("History")')).toBeVisible();
});
test('should open add ntfy endpoint dialog and verify useEffect resets form', async ({
page,
}) => {
// This test specifically validates that the useEffect hook in NtfyEndpointDialog
// works correctly - if useEffect was not imported, the form would not reset
await navigateToEndpointsTab(page);
// Click Add Endpoint button
await page.locator(SELECTORS.addEndpointButton).click();
// Dialog should be visible
const dialog = page.locator(SELECTORS.dialog);
await expect(dialog).toBeVisible({ timeout: TIMEOUTS.dialogVisible });
// Dialog title should indicate adding new endpoint
await expect(dialog.locator(SELECTORS.dialogTitle)).toBeVisible();
// Form should have default values (useEffect reset)
// This is the critical test - if useEffect was not imported or not working,
// these assertions would fail because the form state would not be reset
const nameInput = dialog.locator('input#endpoint-name');
const serverUrlInput = dialog.locator('input#endpoint-server');
const topicInput = dialog.locator('input#endpoint-topic');
// Name should be empty (reset by useEffect)
await expect(nameInput).toHaveValue('');
// Server URL should have default value (reset by useEffect)
await expect(serverUrlInput).toHaveValue('https://ntfy.sh');
// Topic should be empty (reset by useEffect)
await expect(topicInput).toHaveValue('');
// Close the dialog
await page.keyboard.press('Escape');
await expect(dialog).toBeHidden({ timeout: TIMEOUTS.dialogHidden });
});
test('should open and close endpoint dialog without JavaScript errors', async ({ page }) => {
// This test verifies the dialog opens without throwing a "useEffect is not defined" error
// Listen for console errors
const consoleErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text());
}
});
await navigateToEndpointsTab(page);
// Open and close the dialog multiple times to stress test the useEffect
for (let i = 0; i < 3; i++) {
await page.locator(SELECTORS.addEndpointButton).click();
const dialog = page.locator(SELECTORS.dialog);
await expect(dialog).toBeVisible({ timeout: TIMEOUTS.dialogVisible });
await page.keyboard.press('Escape');
await expect(dialog).toBeHidden({ timeout: TIMEOUTS.dialogHidden });
}
// Verify no React hook related errors occurred
// This catches "useEffect is not defined", "useState is not defined", etc.
const reactHookError = consoleErrors.find(
(error) =>
(error.includes('useEffect') ||
error.includes('useState') ||
error.includes('useCallback')) &&
error.includes('is not defined')
);
expect(reactHookError).toBeUndefined();
});
test('should have enabled toggle working in endpoint dialog', async ({ page }) => {
await navigateToEndpointsTab(page);
// Click Add Endpoint button
await page.locator(SELECTORS.addEndpointButton).click();
const dialog = page.locator(SELECTORS.dialog);
await expect(dialog).toBeVisible({ timeout: TIMEOUTS.dialogVisible });
// Verify the enabled switch exists and is checked by default (useEffect sets enabled=true)
const enabledSwitch = dialog.locator('#endpoint-enabled');
await expect(enabledSwitch).toBeChecked();
// Click the switch to toggle it off
await enabledSwitch.click();
await expect(enabledSwitch).not.toBeChecked();
// Click it again to toggle it back on
await enabledSwitch.click();
await expect(enabledSwitch).toBeChecked();
// Close the dialog
await page.keyboard.press('Escape');
});
test('should have Add Endpoint button disabled when form is invalid', async ({ page }) => {
await navigateToEndpointsTab(page);
// Click Add Endpoint button
await page.locator(SELECTORS.addEndpointButton).click();
const dialog = page.locator(SELECTORS.dialog);
await expect(dialog).toBeVisible({ timeout: TIMEOUTS.dialogVisible });
// The Add Endpoint button should be disabled because form is empty (name and topic required)
const addButton = dialog.locator('button:has-text("Add Endpoint")').last();
await expect(addButton).toBeDisabled();
// Fill in name but not topic
await dialog.locator('input#endpoint-name').fill('Test Name');
// Button should still be disabled (topic is required)
await expect(addButton).toBeDisabled();
// Fill in topic with invalid value (contains space)
await dialog.locator('input#endpoint-topic').fill('invalid topic');
// Button should still be disabled (topic has space which is invalid)
await expect(addButton).toBeDisabled();
// Fix the topic
await dialog.locator('input#endpoint-topic').fill('valid-topic');
// Now button should be enabled
await expect(addButton).toBeEnabled();
// Close the dialog
await page.keyboard.press('Escape');
});
test('should persist ntfy endpoint after adding and page reload', async ({ page }) => {
// This test verifies that ntfy endpoints are correctly saved to the server
// and restored when the page is reloaded - the core bug fix being tested
await navigateToEndpointsTab(page);
// Add a new endpoint
await page.locator(SELECTORS.addEndpointButton).click();
const dialog = page.locator(SELECTORS.dialog);
await expect(dialog).toBeVisible({ timeout: TIMEOUTS.dialogVisible });
// Fill in the endpoint form
const uniqueSuffix = Date.now();
await dialog.locator('input#endpoint-name').fill(`Test Endpoint ${uniqueSuffix}`);
await dialog.locator('input#endpoint-server').fill('https://ntfy.sh');
await dialog.locator('input#endpoint-topic').fill(`test-topic-${uniqueSuffix}`);
// Save the endpoint
const addButton = dialog.locator('button:has-text("Add Endpoint")').last();
await addButton.click();
// Wait for the success toast to confirm the save completed (including API call)
await waitForSuccessToast(page, 'Endpoint added', { timeout: 10000 });
// Dialog should close
await expect(dialog).toBeHidden({ timeout: TIMEOUTS.dialogHidden });
// Wait for the endpoint to appear in the list
await expect(page.locator(`text=Test Endpoint ${uniqueSuffix}`)).toBeVisible({
timeout: TIMEOUTS.endpointVisible,
});
// Reload the page
await page.reload();
// Re-authenticate after reload
await authenticateForTests(page);
// Navigate back to the endpoints tab
await navigateToEndpointsTab(page);
// Verify the endpoint persisted after reload
await expect(page.locator(`text=Test Endpoint ${uniqueSuffix}`)).toBeVisible({
timeout: TIMEOUTS.sectionVisible,
});
});
test('should display existing endpoints on initial load', async ({ page }) => {
// This test verifies that any existing endpoints are displayed when the page first loads
// Navigate to the page and check if we can see the endpoints section
await navigateToEndpointsTab(page);
// The endpoints tab should show either existing endpoints or the empty state
// The key is that it should NOT show "empty" if there are endpoints on the server
// Either we see "No ntfy endpoints configured" OR we see endpoint cards
const emptyState = page.locator('text=No ntfy endpoints configured');
const endpointCard = page.locator('[data-testid="endpoint-card"]').first();
// One of these should be visible (use Playwright's .or() to match either locator)
await expect(emptyState.or(endpointCard)).toBeVisible({
timeout: TIMEOUTS.endpointVisible,
});
});
});