mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-06-27 21:41:02 +02:00
Add a smatttering of tests and fix return-to-public after channel deletion
This commit is contained in:
@@ -81,6 +81,47 @@ export function deleteChannel(key: string): Promise<void> {
|
||||
return fetchJson(`/channels/${key}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// --- Contacts ---
|
||||
|
||||
export interface Contact {
|
||||
public_key: string;
|
||||
name: string | null;
|
||||
type: number;
|
||||
flags: number;
|
||||
last_path: string | null;
|
||||
last_path_len: number;
|
||||
last_advert: number | null;
|
||||
lat: number | null;
|
||||
lon: number | null;
|
||||
last_seen: number | null;
|
||||
on_radio: boolean;
|
||||
last_contacted: number | null;
|
||||
last_read_at: number | null;
|
||||
}
|
||||
|
||||
export function getContacts(limit: number = 100, offset: number = 0): Promise<Contact[]> {
|
||||
return fetchJson(`/contacts?limit=${limit}&offset=${offset}`);
|
||||
}
|
||||
|
||||
export function createContact(
|
||||
publicKey: string,
|
||||
name?: string,
|
||||
tryHistorical: boolean = false
|
||||
): Promise<Contact> {
|
||||
return fetchJson('/contacts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
public_key: publicKey,
|
||||
...(name ? { name } : {}),
|
||||
try_historical: tryHistorical,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteContact(publicKey: string): Promise<{ status: string }> {
|
||||
return fetchJson(`/contacts/${publicKey}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// --- Messages ---
|
||||
|
||||
export interface MessagePath {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { createChannel, deleteChannel, getChannels } from '../helpers/api';
|
||||
|
||||
test.describe('Conversation deletion flow', () => {
|
||||
test.beforeAll(async () => {
|
||||
const channels = await getChannels();
|
||||
if (!channels.some((c) => c.name === 'Public')) {
|
||||
await createChannel('Public');
|
||||
}
|
||||
});
|
||||
|
||||
test('deleting active channel removes it from sidebar and clears composer', async ({ page }) => {
|
||||
const channelName = `#e2edel${Date.now().toString().slice(-6)}`;
|
||||
const channel = await createChannel(channelName);
|
||||
|
||||
await page.goto('/');
|
||||
await expect(page.getByText('Connected')).toBeVisible();
|
||||
|
||||
await page.getByText(channelName, { exact: true }).first().click();
|
||||
await expect(page.getByPlaceholder(new RegExp(`message\\s+${channelName}`, 'i'))).toBeVisible();
|
||||
|
||||
page.once('dialog', async (dialog) => {
|
||||
await dialog.accept();
|
||||
});
|
||||
await page.getByTitle('Delete').click();
|
||||
|
||||
await expect(page.getByText('Channel deleted')).toBeVisible();
|
||||
await expect(page.getByText(channelName, { exact: true })).not.toBeVisible();
|
||||
await expect(page.getByPlaceholder(new RegExp(`message\\s+${channelName}`, 'i'))).not.toBeVisible();
|
||||
|
||||
try {
|
||||
await deleteChannel(channel.key);
|
||||
} catch {
|
||||
// Best-effort cleanup
|
||||
}
|
||||
});
|
||||
|
||||
test('deleting active channel falls back to Public conversation', async ({ page }) => {
|
||||
const channelName = `#e2edel${Date.now().toString().slice(-6)}`;
|
||||
const channel = await createChannel(channelName);
|
||||
|
||||
await page.goto('/');
|
||||
await expect(page.getByText('Connected')).toBeVisible();
|
||||
|
||||
await page.getByText(channelName, { exact: true }).first().click();
|
||||
await expect(page.getByPlaceholder(new RegExp(`message\\s+${channelName}`, 'i'))).toBeVisible();
|
||||
|
||||
page.once('dialog', async (dialog) => {
|
||||
await dialog.accept();
|
||||
});
|
||||
await page.getByTitle('Delete').click();
|
||||
|
||||
await expect(page.getByPlaceholder(/message\s+public/i)).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
createChannel,
|
||||
deleteChannel,
|
||||
getSettings,
|
||||
updateSettings,
|
||||
type Favorite,
|
||||
} from '../helpers/api';
|
||||
|
||||
test.describe('Favorites persistence', () => {
|
||||
let originalFavorites: Favorite[] = [];
|
||||
let channelName = '';
|
||||
let channelKey = '';
|
||||
|
||||
test.beforeAll(async () => {
|
||||
const settings = await getSettings();
|
||||
originalFavorites = settings.favorites ?? [];
|
||||
|
||||
// Start deterministic: no favorites
|
||||
await updateSettings({ favorites: [] });
|
||||
|
||||
channelName = `#e2efav${Date.now().toString().slice(-6)}`;
|
||||
const channel = await createChannel(channelName);
|
||||
channelKey = channel.key;
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
try {
|
||||
await deleteChannel(channelKey);
|
||||
} catch {
|
||||
// Best-effort cleanup
|
||||
}
|
||||
try {
|
||||
await updateSettings({ favorites: originalFavorites });
|
||||
} catch {
|
||||
// Best-effort cleanup
|
||||
}
|
||||
});
|
||||
|
||||
test('add and remove favorite channel with persistence across reload', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByText('Connected')).toBeVisible();
|
||||
|
||||
await page.getByText(channelName, { exact: true }).first().click();
|
||||
|
||||
const addFavoriteButton = page.getByTitle('Add to favorites');
|
||||
await expect(addFavoriteButton).toBeVisible();
|
||||
await addFavoriteButton.click();
|
||||
|
||||
await expect(page.getByTitle('Remove from favorites')).toBeVisible();
|
||||
await expect(page.getByText('Favorites')).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const settings = await getSettings();
|
||||
return settings.favorites.some((f) => f.type === 'channel' && f.id === channelKey);
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByText('Connected')).toBeVisible();
|
||||
await page.getByText(channelName, { exact: true }).first().click();
|
||||
await expect(page.getByTitle('Remove from favorites')).toBeVisible();
|
||||
await expect(page.getByText('Favorites')).toBeVisible();
|
||||
|
||||
await page.getByTitle('Remove from favorites').click();
|
||||
await expect(page.getByTitle('Add to favorites')).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const settings = await getSettings();
|
||||
return settings.favorites.some((f) => f.type === 'channel' && f.id === channelKey);
|
||||
})
|
||||
.toBe(false);
|
||||
await expect(page.getByText('Favorites')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { createChannel, createContact, deleteChannel, deleteContact } from '../helpers/api';
|
||||
|
||||
function randomHex(bytes: number): string {
|
||||
return randomBytes(bytes).toString('hex');
|
||||
}
|
||||
|
||||
function makeKeyWithPrefix(prefix: string): string {
|
||||
return `${prefix}${randomHex(26)}`;
|
||||
}
|
||||
|
||||
function escapeRegex(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
test.describe('Hash routing and conversation identity', () => {
|
||||
let channelName = '';
|
||||
let channelKey = '';
|
||||
let contactAKey = '';
|
||||
let contactAName = '';
|
||||
let contactBKey = '';
|
||||
let contactBName = '';
|
||||
|
||||
test.beforeAll(async () => {
|
||||
channelName = `#e2ehash${Date.now().toString().slice(-6)}`;
|
||||
const createdChannel = await createChannel(channelName);
|
||||
channelKey = createdChannel.key;
|
||||
|
||||
const sharedPrefix = randomHex(6);
|
||||
contactAKey = makeKeyWithPrefix(sharedPrefix);
|
||||
contactBKey = makeKeyWithPrefix(sharedPrefix);
|
||||
contactAName = `E2E Hash A ${Date.now().toString().slice(-5)}`;
|
||||
contactBName = `E2E Hash B ${Date.now().toString().slice(-5)}`;
|
||||
|
||||
await createContact(contactAKey, contactAName);
|
||||
await createContact(contactBKey, contactBName);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
try {
|
||||
await deleteChannel(channelKey);
|
||||
} catch {
|
||||
// Best-effort cleanup
|
||||
}
|
||||
try {
|
||||
await deleteContact(contactAKey);
|
||||
} catch {
|
||||
// Best-effort cleanup
|
||||
}
|
||||
try {
|
||||
await deleteContact(contactBKey);
|
||||
} catch {
|
||||
// Best-effort cleanup
|
||||
}
|
||||
});
|
||||
|
||||
test('legacy channel-name hash resolves and rewrites to stable channel-key hash', async ({
|
||||
page,
|
||||
}) => {
|
||||
const legacyToken = channelName.slice(1); // no leading '#'
|
||||
await page.goto(`/#channel/${encodeURIComponent(legacyToken)}`);
|
||||
|
||||
await expect(page.getByText('Connected')).toBeVisible();
|
||||
await expect(
|
||||
page.getByPlaceholder(new RegExp(`message\\s+${escapeRegex(channelName)}`, 'i'))
|
||||
).toBeVisible();
|
||||
|
||||
await expect.poll(() => page.url()).toContain(`#channel/${encodeURIComponent(channelKey)}/`);
|
||||
});
|
||||
|
||||
test('full-key contact hash selects the exact contact even with shared prefixes', async ({ page }) => {
|
||||
await page.goto(`/#contact/${contactBKey}`);
|
||||
|
||||
await expect(page.getByText('Connected')).toBeVisible();
|
||||
await expect(
|
||||
page.getByPlaceholder(new RegExp(`message\\s+${escapeRegex(contactBName)}`, 'i'))
|
||||
).toBeVisible();
|
||||
await expect(page.getByText(contactBKey, { exact: true })).toBeVisible();
|
||||
|
||||
await expect.poll(() => page.url()).toContain(`#contact/${encodeURIComponent(contactBKey)}/`);
|
||||
});
|
||||
|
||||
test('legacy contact-name hash resolves and rewrites to stable full-key hash', async ({ page }) => {
|
||||
await page.goto(`/#contact/${encodeURIComponent(contactAName)}`);
|
||||
|
||||
await expect(page.getByText('Connected')).toBeVisible();
|
||||
await expect(
|
||||
page.getByPlaceholder(new RegExp(`message\\s+${escapeRegex(contactAName)}`, 'i'))
|
||||
).toBeVisible();
|
||||
await expect(page.getByText(contactAKey, { exact: true })).toBeVisible();
|
||||
|
||||
await expect.poll(() => page.url()).toContain(`#contact/${encodeURIComponent(contactAKey)}/`);
|
||||
});
|
||||
});
|
||||
@@ -158,8 +158,8 @@ test.describe('Incoming mesh messages', () => {
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Verify the modal has the basic structural elements every path modal should have
|
||||
await expect(modal.getByText('Sender')).toBeVisible();
|
||||
await expect(modal.getByText('Receiver (me)')).toBeVisible();
|
||||
await expect(modal.getByText('Sender:').first()).toBeVisible();
|
||||
await expect(modal.getByText('Receiver (me):').first()).toBeVisible();
|
||||
|
||||
// Title should be either "Message Path" (single) or "Message Paths (N)" (multiple)
|
||||
const titleEl = modal.locator('h2, [class*="DialogTitle"]').first();
|
||||
|
||||
Reference in New Issue
Block a user