Adds e2e smoke tests that verify: - All major pages load without 500 errors - No JavaScript console errors - No LiveView socket crashes (Jason.EncodeError, etc.) - All activity feed filter types work without encoding errors Also documents binary UUID fragment query pattern in AGENTS.md to prevent future Jason.EncodeError issues from PostgreSQL aggregations returning binary UUIDs instead of text.
386 lines
12 KiB
TypeScript
386 lines
12 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
/**
|
|
* Smoke Tests - Critical Path Coverage
|
|
*
|
|
* These tests navigate to every major page and verify:
|
|
* 1. Page loads without 500 errors
|
|
* 2. No JavaScript console errors
|
|
* 3. No LiveView socket crashes (Jason.EncodeError, etc.)
|
|
* 4. Basic content renders
|
|
*
|
|
* Purpose: Catch server-side crashes (like binary UUID encoding bugs)
|
|
* before they reach production.
|
|
*/
|
|
|
|
test.describe('Smoke Tests - All Pages Load Without Errors', () => {
|
|
let consoleErrors: string[];
|
|
let pageErrors: Error[];
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
consoleErrors = [];
|
|
pageErrors = [];
|
|
|
|
// Capture console errors
|
|
page.on('console', msg => {
|
|
if (msg.type() === 'error') {
|
|
consoleErrors.push(msg.text());
|
|
}
|
|
});
|
|
|
|
// Capture page errors (uncaught exceptions)
|
|
page.on('pageerror', error => {
|
|
pageErrors.push(error);
|
|
});
|
|
});
|
|
|
|
test.afterEach(async ({ page }) => {
|
|
// Report any errors found
|
|
if (consoleErrors.length > 0) {
|
|
console.log('Console errors:', consoleErrors);
|
|
}
|
|
if (pageErrors.length > 0) {
|
|
console.log('Page errors:', pageErrors);
|
|
}
|
|
|
|
// Don't fail tests on errors - just report them
|
|
// Some pages might have expected errors (missing data, etc.)
|
|
});
|
|
|
|
test('dashboard loads without errors', async ({ page }) => {
|
|
const response = await page.goto('/dashboard');
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Verify basic content loaded
|
|
await expect(page.locator('body')).toBeVisible();
|
|
|
|
// Check for common error indicators
|
|
const errorText = await page.locator(':has-text("error"), :has-text("Error")').first().isVisible({ timeout: 1000 }).catch(() => false);
|
|
if (errorText) {
|
|
console.warn('Dashboard may have errors visible on page');
|
|
}
|
|
});
|
|
|
|
test('devices list loads without errors', async ({ page }) => {
|
|
const response = await page.goto('/devices');
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
});
|
|
|
|
test('sites list loads without errors', async ({ page }) => {
|
|
const response = await page.goto('/sites');
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
});
|
|
|
|
test('alerts list loads without errors', async ({ page }) => {
|
|
const response = await page.goto('/alerts');
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
});
|
|
|
|
test('agents list loads without errors', async ({ page }) => {
|
|
const response = await page.goto('/agents');
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
});
|
|
|
|
test('activity feed loads without errors', async ({ page }) => {
|
|
const response = await page.goto('/activity');
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
|
|
// This page specifically had the binary UUID bug
|
|
// Verify no LiveView socket crash
|
|
const socketError = await page.locator(':has-text("socket"), :has-text("crash")').first().isVisible({ timeout: 1000 }).catch(() => false);
|
|
expect(socketError).toBe(false);
|
|
});
|
|
|
|
test('trace page loads without errors', async ({ page }) => {
|
|
const response = await page.goto('/trace');
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
});
|
|
|
|
test('integrations page loads without errors', async ({ page }) => {
|
|
const response = await page.goto('/integrations');
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
});
|
|
|
|
test('organization settings loads without errors', async ({ page }) => {
|
|
const response = await page.goto('/org/settings');
|
|
|
|
// Skip if redirected to sudo verify
|
|
if ((await page.url()).includes('sudo-verify')) {
|
|
return;
|
|
}
|
|
|
|
expect(response?.status()).toBeLessThan(500);
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
});
|
|
|
|
test('user settings loads without errors', async ({ page }) => {
|
|
const response = await page.goto('/users/settings');
|
|
|
|
// Skip if redirected to sudo verify
|
|
if ((await page.url()).includes('sudo-verify')) {
|
|
return;
|
|
}
|
|
|
|
expect(response?.status()).toBeLessThan(500);
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
});
|
|
|
|
test('billing page loads without errors', async ({ page }) => {
|
|
const response = await page.goto('/org/billing');
|
|
|
|
// Skip if redirected to sudo verify
|
|
if ((await page.url()).includes('sudo-verify')) {
|
|
return;
|
|
}
|
|
|
|
expect(response?.status()).toBeLessThan(500);
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
});
|
|
|
|
test.describe('Device Detail Pages', () => {
|
|
test('device detail loads without errors', async ({ page }) => {
|
|
// First get a device ID
|
|
await page.goto('/devices');
|
|
|
|
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
|
|
|
|
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
const href = await firstDevice.getAttribute('href');
|
|
|
|
if (href) {
|
|
const response = await page.goto(href);
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
}
|
|
}
|
|
});
|
|
|
|
test('device config timeline loads without errors', async ({ page }) => {
|
|
await page.goto('/devices');
|
|
|
|
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
|
|
|
|
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
const href = await firstDevice.getAttribute('href');
|
|
|
|
if (href) {
|
|
const deviceId = href.split('/').pop();
|
|
const response = await page.goto(`/devices/${deviceId}/config-timeline`);
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
}
|
|
}
|
|
});
|
|
|
|
test('device interfaces loads without errors', async ({ page }) => {
|
|
await page.goto('/devices');
|
|
|
|
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
|
|
|
|
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
const href = await firstDevice.getAttribute('href');
|
|
|
|
if (href) {
|
|
const deviceId = href.split('/').pop();
|
|
const response = await page.goto(`/devices/${deviceId}/interfaces`);
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
}
|
|
}
|
|
});
|
|
|
|
test('device neighbors loads without errors', async ({ page }) => {
|
|
await page.goto('/devices');
|
|
|
|
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
|
|
|
|
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
const href = await firstDevice.getAttribute('href');
|
|
|
|
if (href) {
|
|
const deviceId = href.split('/').pop();
|
|
const response = await page.goto(`/devices/${deviceId}/neighbors`);
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('Site Detail Pages', () => {
|
|
test('site detail loads without errors', async ({ page }) => {
|
|
await page.goto('/sites');
|
|
|
|
const firstSite = page.locator('a[href*="/sites/"]:not([href*="/sites/new"])').first();
|
|
|
|
if (await firstSite.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
const href = await firstSite.getAttribute('href');
|
|
|
|
if (href) {
|
|
const response = await page.goto(href);
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
}
|
|
}
|
|
});
|
|
|
|
test('site map view loads without errors', async ({ page }) => {
|
|
await page.goto('/sites');
|
|
|
|
const firstSite = page.locator('a[href*="/sites/"]:not([href*="/sites/new"])').first();
|
|
|
|
if (await firstSite.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
const href = await firstSite.getAttribute('href');
|
|
|
|
if (href) {
|
|
const siteId = href.split('/').pop();
|
|
const response = await page.goto(`/sites/${siteId}/map`);
|
|
expect(response?.status()).toBeLessThan(500);
|
|
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('body')).toBeVisible();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('LiveView Socket Stability', () => {
|
|
test('activity feed maintains socket connection', async ({ page }) => {
|
|
await page.goto('/activity');
|
|
|
|
// Wait for initial load
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Verify no socket errors in console
|
|
const hasSocketError = consoleErrors.some(err =>
|
|
err.includes('socket') ||
|
|
err.includes('Jason') ||
|
|
err.includes('encode')
|
|
);
|
|
|
|
expect(hasSocketError).toBe(false);
|
|
});
|
|
|
|
test('dashboard maintains socket connection', async ({ page }) => {
|
|
await page.goto('/dashboard');
|
|
|
|
await page.waitForTimeout(2000);
|
|
|
|
const hasSocketError = consoleErrors.some(err =>
|
|
err.includes('socket') ||
|
|
err.includes('Jason') ||
|
|
err.includes('encode')
|
|
);
|
|
|
|
expect(hasSocketError).toBe(false);
|
|
});
|
|
|
|
test('devices list maintains socket connection', async ({ page }) => {
|
|
await page.goto('/devices');
|
|
|
|
await page.waitForTimeout(2000);
|
|
|
|
const hasSocketError = consoleErrors.some(err =>
|
|
err.includes('socket') ||
|
|
err.includes('Jason') ||
|
|
err.includes('encode')
|
|
);
|
|
|
|
expect(hasSocketError).toBe(false);
|
|
});
|
|
});
|
|
|
|
test.describe('Edge Cases - Binary Data Handling', () => {
|
|
test('activity feed with sync events renders correctly', async ({ page }) => {
|
|
await page.goto('/activity');
|
|
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Look for sync-type activities (the ones that had binary UUID issue)
|
|
const syncActivity = page.locator('[data-activity-type="sync"], :has-text("sync")').first();
|
|
|
|
if (await syncActivity.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
// If sync activities exist, verify they rendered
|
|
await expect(syncActivity).toBeVisible();
|
|
}
|
|
|
|
// Verify no encoding errors
|
|
const encodingError = await page.locator(':has-text("EncodeError"), :has-text("invalid byte")').first().isVisible({ timeout: 1000 }).catch(() => false);
|
|
expect(encodingError).toBe(false);
|
|
});
|
|
|
|
test('all activity feed filter types work', async ({ page }) => {
|
|
await page.goto('/activity');
|
|
|
|
await page.waitForTimeout(1000);
|
|
|
|
const filterTypes = [
|
|
'Config',
|
|
'Alerts',
|
|
'Resolved',
|
|
'Events',
|
|
'Syncs',
|
|
'Devices'
|
|
];
|
|
|
|
for (const filterType of filterTypes) {
|
|
const filterButton = page.locator(`button:has-text("${filterType}")`).first();
|
|
|
|
if (await filterButton.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
await filterButton.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// Verify page still works
|
|
await expect(page.locator('body')).toBeVisible();
|
|
|
|
// Verify no errors
|
|
const hasError = consoleErrors.some(err =>
|
|
err.includes('EncodeError') ||
|
|
err.includes('invalid byte')
|
|
);
|
|
expect(hasError).toBe(false);
|
|
|
|
// Toggle back off
|
|
await filterButton.click();
|
|
await page.waitForTimeout(300);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|