Add better search management and operators + contact search quick link

This commit is contained in:
Jack Kingsman
2026-03-11 16:56:09 -07:00
parent ce9bbd1059
commit ad7028e508
13 changed files with 587 additions and 48 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
import type { FullConfig } from '@playwright/test';
const BASE_URL = 'http://localhost:8000';
const BASE_URL = 'http://localhost:8001';
const MAX_RETRIES = 10;
const RETRY_DELAY_MS = 2000;
+1 -1
View File
@@ -3,7 +3,7 @@
* These bypass the UI to set up preconditions and verify backend state.
*/
const BASE_URL = 'http://localhost:8000/api';
const BASE_URL = 'http://localhost:8001/api';
async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
+3 -3
View File
@@ -22,7 +22,7 @@ export default defineConfig({
reporter: [['list'], ['html', { open: 'never' }]],
use: {
baseURL: 'http://localhost:8000',
baseURL: 'http://localhost:8001',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
@@ -45,10 +45,10 @@ export default defineConfig({
echo "[e2e] $(date +%T.%3N) frontend/dist exists — skipping build"
fi
echo "[e2e] $(date +%T.%3N) Launching uvicorn..."
uv run uvicorn app.main:app --host 127.0.0.1 --port 8000
uv run uvicorn app.main:app --host 127.0.0.1 --port 8001
'`,
cwd: projectRoot,
port: 8000,
port: 8001,
reuseExistingServer: false,
timeout: 180_000,
env: {
+30 -9
View File
@@ -1,21 +1,29 @@
import { test, expect } from '@playwright/test';
import { createChannel, deleteChannel, getChannels } from '../helpers/api';
import { createChannel, createContact, deleteChannel, deleteContact } from '../helpers/api';
test.describe('Sidebar search/filter', () => {
const suffix = Date.now().toString().slice(-6);
const nameA = `#alpha${suffix}`;
const nameB = `#bravo${suffix}`;
const contactName = `Search Contact ${suffix}`;
const contactKey = `feed${suffix.padStart(8, '0')}${'ab'.repeat(26)}`;
let keyA = '';
let keyB = '';
test.beforeAll(async () => {
const chA = await createChannel(nameA);
const chB = await createChannel(nameB);
await createContact(contactKey, contactName);
keyA = chA.key;
keyB = chB.key;
});
test.afterAll(async () => {
try {
await deleteContact(contactKey);
} catch {
// Best-effort cleanup
}
for (const key of [keyA, keyB]) {
try {
await deleteChannel(key);
@@ -25,27 +33,40 @@ test.describe('Sidebar search/filter', () => {
}
});
test('search filters conversations by name', async ({ page }) => {
test('search filters channel and contact conversations by name and key prefix', async ({
page,
}) => {
await page.goto('/');
await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible();
// Both channels should be visible
// Seeded conversations should be visible.
await expect(page.getByText(nameA, { exact: true })).toBeVisible();
await expect(page.getByText(nameB, { exact: true })).toBeVisible();
await expect(page.getByText(contactName, { exact: true })).toBeVisible();
// Type partial name to filter
const searchInput = page.getByLabel('Search conversations');
await searchInput.fill(`alpha${suffix}`);
// Only nameA should be visible
// Channel name query should filter to the matching channel only.
await searchInput.fill(`alpha${suffix}`);
await expect(page.getByText(nameA, { exact: true })).toBeVisible();
await expect(page.getByText(nameB, { exact: true })).not.toBeVisible();
await expect(page.getByText(contactName, { exact: true })).not.toBeVisible();
// Clear search
// Contact name query should filter to the matching contact.
await searchInput.fill(`contact ${suffix}`);
await expect(page.getByText(contactName, { exact: true })).toBeVisible();
await expect(page.getByText(nameA, { exact: true })).not.toBeVisible();
await expect(page.getByText(nameB, { exact: true })).not.toBeVisible();
// Contact key prefix query should also match that contact.
await searchInput.fill(contactKey.slice(0, 12));
await expect(page.getByText(contactName, { exact: true })).toBeVisible();
await expect(page.getByText(nameA, { exact: true })).not.toBeVisible();
// Clear search should restore the full conversation list.
await page.getByTitle('Clear search').click();
// Both should return
await expect(page.getByText(nameA, { exact: true })).toBeVisible();
await expect(page.getByText(nameB, { exact: true })).toBeVisible();
await expect(page.getByText(contactName, { exact: true })).toBeVisible();
});
});