feat: improve queue card readability and detail controls
Windows CI / verify (push) Canceled after 0s
Windows CI / verify (push) Canceled after 0s
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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; });
|
||||
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user