mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 01:03:34 +02:00
Add better search management and operators + contact search quick link
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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}`, {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import pytest
|
||||
|
||||
from app.radio import radio_manager
|
||||
from app.repository import MessageRepository
|
||||
from app.repository import ChannelRepository, ContactRepository, MessageRepository
|
||||
|
||||
CHAN_KEY = "ABC123DEF456ABC123DEF456ABC12345"
|
||||
DM_KEY = "aa" * 32
|
||||
@@ -136,6 +136,181 @@ class TestMessageSearch:
|
||||
assert len(results) == 1
|
||||
assert results[0].sender_name == "Alice"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_user_operator_matches_channel_sender_name(self, test_db):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello from alice",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
sender_name="Alice",
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello from bob",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
sender_name="Bob",
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q='user:"Alice"')
|
||||
assert [message.text for message in results] == ["hello from alice"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_user_operator_matches_dm_contact_name(self, test_db):
|
||||
await ContactRepository.upsert(
|
||||
{
|
||||
"public_key": DM_KEY,
|
||||
"name": "Alice Smith",
|
||||
"type": 1,
|
||||
}
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="PRIV",
|
||||
text="hello from dm",
|
||||
conversation_key=DM_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="PRIV",
|
||||
text="hello from other dm",
|
||||
conversation_key=("bb" * 32),
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q='user:"Alice Smith"')
|
||||
assert [message.text for message in results] == ["hello from dm"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_user_operator_matches_key_prefix(self, test_db):
|
||||
await MessageRepository.create(
|
||||
msg_type="PRIV",
|
||||
text="dm by key prefix",
|
||||
conversation_key=DM_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="chan by key prefix",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
sender_key=DM_KEY,
|
||||
sender_name="Alice",
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="PRIV",
|
||||
text="other dm",
|
||||
conversation_key=("bb" * 32),
|
||||
sender_timestamp=102,
|
||||
received_at=102,
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q=f"user:{DM_KEY[:12]}")
|
||||
assert [message.text for message in results] == ["chan by key prefix", "dm by key prefix"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_channel_operator_matches_channel_name(self, test_db):
|
||||
await ChannelRepository.upsert(key=CHAN_KEY, name="#flightless", is_hashtag=True)
|
||||
await ChannelRepository.upsert(key=OTHER_CHAN_KEY, name="#other", is_hashtag=True)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello flightless",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello elsewhere",
|
||||
conversation_key=OTHER_CHAN_KEY,
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q='channel:"#flightless"')
|
||||
assert [message.text for message in results] == ["hello flightless"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_channel_operator_matches_quoted_name_with_spaces(self, test_db):
|
||||
await ChannelRepository.upsert(key=CHAN_KEY, name="#Ops Room", is_hashtag=True)
|
||||
await ChannelRepository.upsert(key=OTHER_CHAN_KEY, name="#Other Room", is_hashtag=True)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello ops room",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello other room",
|
||||
conversation_key=OTHER_CHAN_KEY,
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q='channel:"#Ops Room"')
|
||||
assert [message.text for message in results] == ["hello ops room"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_channel_operator_matches_channel_key_prefix(self, test_db):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="chan by key",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="other channel",
|
||||
conversation_key=OTHER_CHAN_KEY,
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q=f"channel:{CHAN_KEY[:8]}")
|
||||
assert [message.text for message in results] == ["chan by key"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_scope_operators_and_free_text_are_combined(self, test_db):
|
||||
await ChannelRepository.upsert(key=CHAN_KEY, name="#flightless", is_hashtag=True)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello operator",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
sender_name="Alice",
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="goodbye operator",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
sender_name="Alice",
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello operator",
|
||||
conversation_key=OTHER_CHAN_KEY,
|
||||
sender_timestamp=102,
|
||||
received_at=102,
|
||||
sender_name="Bob",
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(
|
||||
q='user:Alice channel:"#flightless" hello operator'
|
||||
)
|
||||
assert [message.text for message in results] == ["hello operator"]
|
||||
|
||||
|
||||
class TestMessagesAround:
|
||||
"""Tests for get_around()."""
|
||||
|
||||
Reference in New Issue
Block a user