Add server information, an always-visible mute bar and public list actions

- Public servers: connect arrow and add button per row, sortable column headers
- Mute and deafen stay available while disconnected; the choice carries to the next connect
- Server information dialog, opened from a button next to mute
- Stacked layout: mute bar on top, without the profile
- Settings that need the upload host are disabled until it is set; link previews show the mode in effect
- YouTube links are plain links by default
- Composer has a border and matches the sidebar bar height
- Regenerate README screenshots

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 18:40:51 +02:00
co-authored by Claude Opus 5.5
parent 2c7a94441d
commit dc9e6696c9
23 changed files with 259 additions and 51 deletions
+48 -4
View File
@@ -92,6 +92,18 @@ try {
await page.getByRole('button', { name: 'Done', exact: true }).click();
console.log('ok: welcome page without composer, manage identities');
// Mute and deafen are there without a connection
const me = page.locator('.me');
await me.getByText('Not connected').waitFor();
assert.ok(await me.getByRole('button', { name: 'Server information' }).isDisabled());
await me.getByRole('button', { name: 'Deafen', exact: true }).click();
await me.getByRole('button', { name: 'Unmute', exact: true }).waitFor();
await me.getByText('Deafened').waitFor();
await me.getByRole('button', { name: 'Unmute', exact: true }).click();
await me.getByRole('button', { name: 'Deafen', exact: true }).waitFor();
await me.getByRole('button', { name: 'Mute', exact: true }).waitFor();
console.log('ok: mute and deafen shown while disconnected');
// Settings: upload host + key
if (process.env.UPLOAD_HOST && process.env.UPLOAD_KEY) {
await page.getByTitle('Settings').click();
@@ -256,15 +268,23 @@ try {
await page.locator('.chat .composer').getByRole('button', { name: 'Text formatting' }).click();
console.log('ok: formatted message from the toolbar');
// YouTube links become a click-to-play card; nothing loads from YouTube before the click
// YouTube links are plain links by default
bob.sendText({ channels: [0] }, 'watch <a href="https://youtu.be/dQw4w9WgXcQ?t=1m30s">https://youtu.be/dQw4w9WgXcQ?t=1m30s</a>');
const card = page.locator('.msg', { hasText: 'watch' }).locator('.yt');
const ytMsg = page.locator('.msg', { hasText: 'watch' });
await ytMsg.getByRole('link', { name: 'https://youtu.be/dQw4w9WgXcQ?t=1m30s' }).waitFor();
assert.equal(await ytMsg.locator('.yt').count(), 0, 'no YouTube card by default');
// Turned on, they become a click-to-play card; nothing loads from YouTube before the click
await page.getByTitle('Settings').click();
await page.getByRole('tab', { name: 'Chat and files' }).click();
await page.getByLabel('YouTube links').selectOption('click');
await page.getByRole('button', { name: 'Done', exact: true }).click();
const card = ytMsg.locator('.yt');
await card.getByText('from 1:30, click to play').waitFor();
assert.equal(await card.locator('iframe').count(), 0);
assert.equal(await card.locator('.facade').evaluate(el => getComputedStyle(el).backgroundImage), 'none', 'no thumbnail request by default');
assert.equal(await card.locator('.facade').evaluate(el => getComputedStyle(el).backgroundImage), 'none', 'no thumbnail request in click mode');
await card.locator('.facade').click();
assert.equal(await card.locator('iframe').getAttribute('src'), 'https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?autoplay=1&rel=0&start=90');
console.log('ok: YouTube link embedded as click-to-play');
console.log('ok: YouTube link plain by default, click-to-play when turned on');
// Bob -> DM
const alice = [...bob.users.values()].find(u => u.name === 'alice')!;
@@ -359,6 +379,15 @@ try {
const row = pub.locator('tr', { hasText: 'E2E Murmur' });
await row.getByText(/^\d+ \/ \d+$/).waitFor({ timeout: 8000 });
await row.getByText(/ms$/).waitFor();
await row.getByRole('button', { name: 'Connect' }).waitFor();
// Column headers sort; a second click reverses
const firstRow = pub.locator('tbody tr').first();
await pub.getByRole('button', { name: 'Server', exact: true }).click();
await firstRow.getByText('E2E Murmur').waitFor();
await pub.getByRole('button', { name: 'Server', exact: true }).click();
await firstRow.getByText('Offline one').waitFor();
await pub.getByRole('button', { name: 'People', exact: true }).click();
await firstRow.getByText('E2E Murmur').waitFor();
await row.getByRole('button', { name: 'Add' }).click();
assert.equal(await page.getByLabel('Address').inputValue(), host);
assert.equal(await page.getByLabel('Port').inputValue(), port);
@@ -532,6 +561,16 @@ try {
await page.locator('.panel h3', { hasText: 'bob' }).waitFor();
console.log('ok: user context menu and profile');
// Server information from the button next to mute
await page.locator('.me').getByRole('button', { name: 'Server information' }).click();
const srvInfo = page.getByRole('dialog', { name: 'Server information' });
await srvInfo.getByText(`${host}:${port}`).waitFor();
await srvInfo.getByText(/^\d+ of \d+$/).waitFor();
await srvInfo.getByText(/^\d+ characters$/).first().waitFor();
await srvInfo.getByRole('button', { name: 'Close', exact: true }).last().click();
await srvInfo.waitFor({ state: 'detached' });
console.log('ok: server information dialog');
// Information: own connection shows client details and the full certificate
await page.locator('.sidebar .user.self').click({ button: 'right' });
await page.getByRole('menuitem', { name: 'Information' }).click();
@@ -729,6 +768,11 @@ try {
await page.getByTitle('Settings').click();
await page.getByRole('tab', { name: 'Chat and files' }).click();
await page.getByLabel('Upload host').fill('');
// Settings that need the upload host are disabled and previews show as off
assert.ok(await page.getByLabel('Files expire after').isDisabled());
assert.ok(await page.getByLabel('Send image previews for regular Mumble clients').isDisabled());
assert.equal(await page.getByLabel('Link previews').inputValue(), 'off');
assert.ok(await page.getByLabel('Link previews').locator('option[value=host]').isDisabled());
await page.getByRole('button', { name: 'Done', exact: true }).click();
const big = path.join(userData, 'big.png');
writeFileSync(big, testPng(1600, 1200));