feat: improve queue card readability and detail controls
Windows CI / verify (push) Canceled after 0s

This commit is contained in:
Sucukdeluxe
2026-09-06 12:41:30 +02:00
parent c21520ee3d
commit d935e622d2
17 changed files with 285 additions and 61 deletions
+6
View File
@@ -7,6 +7,12 @@ import { ESLint } from 'eslint';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const eslint = new ESLint({ cwd: root, overrideConfigFile: path.join(root, 'eslint.config.mjs') });
test('ignores local development data and bundled third-party tools', async () => {
for (const file of ['.dev-program-data/tools/worker.js', '.dev-user-data/cache/script.js']) {
assert.equal(await eslint.isPathIgnored(path.join(root, file)), true);
}
});
async function messagesFor(source, filePath) {
const [result] = await eslint.lintText(source, { filePath: path.join(root, filePath) });
return result.messages;
+1
View File
@@ -34,6 +34,7 @@
"scripts/smoke-test-template-guide.js",
"scripts/smoke-test-update-version-logic.js",
"scripts/smoke-test-workspace-ui.js",
"scripts/smoke-test-queue-cards.js",
"scripts/smoke-test.js",
"src/index.html",
"src/main/domain/app-state-store.test.ts",
+2 -1
View File
@@ -553,7 +553,8 @@ const requiredScripts = {
'test:live:twitch': 'node scripts/smoke-test-live-integration.js twitch',
'test:live:updater-postpublish': 'node scripts/smoke-test-live-integration.js updater',
'test:e2e:cutter-matrix': 'npm run build && node scripts/smoke-test-cutter-media-matrix.js',
'test:e2e:focused': 'npm run test:e2e:isolation && npm run test:e2e:workspace-ui',
'test:e2e:focused': 'npm run test:e2e:isolation && npm run test:e2e:workspace-ui && npm run test:e2e:queue-cards',
'test:e2e:queue-cards': 'npm run build && node scripts/smoke-test-queue-cards.js',
'test:packaged-launch': 'node scripts/smoke-test-packaged-launch.js',
'test:installer': 'node scripts/smoke-test-installer.js',
'dist:ci': 'electron-builder --win nsis'
+2 -1
View File
@@ -8,7 +8,8 @@ const SMOKE_FILES = [
'scripts/smoke-test-template-guide.js',
'scripts/smoke-test-full.js',
'scripts/smoke-test-settings-autosave.js',
'scripts/smoke-test-workspace-ui.js'
'scripts/smoke-test-workspace-ui.js',
'scripts/smoke-test-queue-cards.js'
];
function inspectSources() {
+108
View File
@@ -0,0 +1,108 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { _electron: electron } = require('playwright');
const { createE2eEnvironment, getElectronLaunchOptions, verifyE2eIsolation, installOfflineFixtures, cleanupE2eEnvironment } = require('./e2e-test-environment');
async function main() {
const environment = createE2eEnvironment('queue-cards');
const artifacts = path.resolve(__dirname, '../tmp_queue-card-artifacts');
fs.mkdirSync(artifacts, { recursive: true });
let app;
try {
app = await electron.launch(getElectronLaunchOptions(environment));
const win = await app.firstWindow();
await verifyE2eIsolation(app, win, environment);
await installOfflineFixtures(app);
const errors = [];
win.on('pageerror', (error) => errors.push(String(error)));
await win.waitForFunction(() => typeof window.showTab === 'function' && typeof window.changeLanguage === 'function');
await win.evaluate(() => {
showTab('vods');
const panel = document.querySelector('[data-context-for="vods"]');
panel.dataset.vodsLayout = 'tabs';
panel.dataset.vodsWorkspace = 'queue';
queue = [
{ id: 'pending', status: 'pending', title: 'Ein langer Streamtitel mit gut lesbaren Download-Details und einer zweiten Zeile', progress: 0 },
{ id: 'running', status: 'downloading', title: 'Sommerstream am See gemeinsam unterwegs', progress: 42, progressStatus: 'Video wird heruntergeladen', speed: '12.5 MB/s', eta: '08:24' },
{ id: 'paused', status: 'paused', title: 'Community-Abend mit Freunden', progress: 23 },
{ id: 'error', status: 'error', title: 'Ein weiterer langer Streamtitel mit einer Fehlermeldung', progress: 10, last_error: 'Die Verbindung wurde unterbrochen. Bitte erneut versuchen.' },
{ id: 'completed', status: 'completed', title: 'Highlights vom Wochenende', progress: 100, outputFiles: ['C:\\fixture\\highlights.mp4'] },
{ id: 'live', status: 'downloading', title: 'Live aus dem Studio', progress: 0, isLive: true, recordingHealth: 'ok', progressStatus: 'Live-Aufnahme läuft' }
].map((item) => ({ url: `https://example.invalid/${item.id}`, date: '2026-09-06T10:00:00Z', streamer: 'Beispielkanal', duration_str: '2h 34m', ...item }));
renderQueue();
});
for (const theme of ['twitch', 'light']) {
for (const language of ['de', 'en']) {
await win.setViewportSize({ width: 1280, height: 900 });
await win.evaluate(({ theme, language }) => {
document.body.className = `theme-${theme}`;
changeLanguage(language);
}, { theme, language });
const layout = await win.locator('#queueList').evaluate((list) => [...list.querySelectorAll('.queue-item')].map((item) => {
const rect = (selector) => item.querySelector(selector).getBoundingClientRect();
const date = rect('.queue-date');
const bar = rect('.queue-progress-wrap');
const remove = rect('.remove');
return {
id: item.dataset.id,
dateBelow: date.top >= bar.bottom,
dateRight: Math.abs(date.right - bar.right) <= 1,
dateSize: parseFloat(getComputedStyle(item.querySelector('.queue-date')).fontSize),
removeSize: Math.min(remove.width, remove.height),
overflow: item.scrollWidth - item.clientWidth,
tinyText: [...item.querySelectorAll('*')].filter((element) => element.textContent.trim() && element.getBoundingClientRect().height > 0 && parseFloat(getComputedStyle(element).fontSize) < 10).length,
};
}));
for (const card of layout) {
assert(card.dateBelow && card.dateRight, `Date placement: ${JSON.stringify(card)}`);
assert(card.dateSize >= 12 && card.removeSize >= 32 && card.tinyText === 0, `Readability: ${JSON.stringify(card)}`);
assert(card.overflow <= 1, `Card overflow: ${JSON.stringify(card)}`);
}
await win.locator('.queue-section').screenshot({ path: path.join(artifacts, `queue-${theme}-${language}.png`) });
}
}
const card = win.locator('.queue-item[data-id="pending"]');
const toggle = card.locator('[data-queue-action="details"]');
for (const selector of ['.title', '.queue-date', '.queue-progress-wrap', '.queue-status-label']) {
await card.locator(selector).dblclick();
assert.equal(await toggle.getAttribute('aria-expanded'), 'true', `${selector} expands`);
await card.locator(selector).dblclick();
assert.equal(await toggle.getAttribute('aria-expanded'), 'false', `${selector} collapses`);
}
await card.dblclick({ position: { x: 4, y: 4 } });
assert.equal(await toggle.getAttribute('aria-expanded'), 'true', 'Card padding expands');
await toggle.click();
assert.equal(await toggle.getAttribute('aria-expanded'), 'false', 'Toggle collapses');
await toggle.dblclick();
assert.equal(await toggle.getAttribute('aria-expanded'), 'true', 'Double-clicking toggle changes state only once');
await toggle.focus();
await win.keyboard.press('Space');
assert.equal(await toggle.getAttribute('aria-expanded'), 'false', 'Space collapses');
await win.keyboard.press('Enter');
assert.equal(await toggle.getAttribute('aria-expanded'), 'true', 'Enter expands');
assert(await toggle.evaluate((button) => document.activeElement === button), 'Toggle retains keyboard focus');
await win.evaluate(() => {
queue.find((item) => item.id === 'running').progress = 61;
renderQueue();
});
assert.equal(await toggle.getAttribute('aria-expanded'), 'true', 'Expansion survives queue rerender');
assert.equal(await win.locator('[data-id="running"] .queue-progress-wrap').getAttribute('aria-valuenow'), '61');
await app.evaluate(({ ipcMain }) => {
ipcMain.removeHandler('remove-from-queue');
ipcMain.handle('remove-from-queue', async (_event, id) => {
if (id !== 'pending') throw new Error('Unexpected removal target');
return [];
});
});
await card.locator('.remove svg path').click();
await win.waitForFunction(() => document.querySelectorAll('#queueList .queue-item').length === 0);
assert.deepEqual(errors, []);
console.log(JSON.stringify({ failures: [], themes: 2, languages: 2, states: 6, interactions: 'passed', artifacts }));
} finally {
if (app) await app.close();
cleanupE2eEnvironment(environment);
}
}
main().catch((error) => { console.error(error); process.exitCode = 1; });
+4 -2
View File
@@ -1265,7 +1265,8 @@ async function run() {
progressInfoText: progressInfo?.textContent || '',
progressInfoBelowBar: Boolean(progressWrap && progressInfo && progressInfo.getBoundingClientRect().top >= progressWrap.getBoundingClientRect().bottom),
progressIsGreen,
statusRemoveCenterDelta: statusRect && removeRect ? Math.abs((statusRect.top + statusRect.bottom) / 2 - (removeRect.top + removeRect.bottom) / 2) : null,
statusBelowBar: Boolean(statusRect && progressWrap && statusRect.top >= progressWrap.getBoundingClientRect().bottom),
removeSize: removeRect ? Math.min(removeRect.width, removeRect.height) : 0,
reservedSelectionControls: document.querySelectorAll('#queueList .queue-selector, #queueList .queue-selector-placeholder').length,
mergeSelectActionVisible,
selectionOrderText: selectionOrder?.textContent?.trim() || '',
@@ -1290,7 +1291,8 @@ async function run() {
check(dynamicQueue.progressInfoText.includes('42.0%') && dynamicQueue.progressInfoText.includes('55.8 MB/s') && dynamicQueue.progressInfoText.includes('1/1'), `Queue progress details are incomplete: ${dynamicQueue.progressInfoText}`);
check(dynamicQueue.progressInfoBelowBar, 'Queue progress details are not positioned below the progress bar');
check(dynamicQueue.progressIsGreen, 'Running queue progress is not green');
check(dynamicQueue.statusRemoveCenterDelta !== null && dynamicQueue.statusRemoveCenterDelta <= 1, `Queue status and remove action are not vertically aligned: ${dynamicQueue.statusRemoveCenterDelta}`);
check(dynamicQueue.statusBelowBar, 'Queue status is not positioned below the progress bar');
check(dynamicQueue.removeSize >= 32, `Queue remove target is too small: ${dynamicQueue.removeSize}`);
check(dynamicQueue.reservedSelectionControls === 0, `Queue still reserves ${dynamicQueue.reservedSelectionControls} in-flow selection controls`);
check(dynamicQueue.mergeSelectActionVisible, 'Pending Queue item has no merge selection action in its context menu');
check(dynamicQueue.selectionOrderText === '1' && dynamicQueue.selectionOrderPosition === 'absolute', `Merge selection order is not a floating badge: ${dynamicQueue.selectionOrderText}/${dynamicQueue.selectionOrderPosition}`);