towerops/e2e/tests/smoke-test.spec.ts
Graham McIntire 668d5350a7
test: add comprehensive smoke tests for all pages
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.
2026-03-06 17:36:54 -06:00

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);
}
}
});
});
});