chore: migrate repository to Codeberg, bump version to 4.2.0, update update logic

This commit is contained in:
xRangerDE
2026-03-01 20:23:21 +01:00
parent d1fcbfaadb
commit 2631924ef5
42 changed files with 70 additions and 7124 deletions
+466
View File
@@ -0,0 +1,466 @@
const { _electron: electron } = require('playwright');
const path = require('path');
const fs = require('fs');
const { spawnSync } = require('child_process');
const APPDATA_DIR = path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'Twitch_VOD_Manager');
const CONFIG_FILE = path.join(APPDATA_DIR, 'config.json');
const QUEUE_FILE = path.join(APPDATA_DIR, 'download_queue.json');
const TMP_DIR = path.join(process.cwd(), 'tmp_e2e_full');
const MEDIA_A = path.join(TMP_DIR, 'in_a.mp4');
const MEDIA_B = path.join(TMP_DIR, 'in_b.mp4');
function backupFile(filePath) {
if (!fs.existsSync(filePath)) return null;
return fs.readFileSync(filePath);
}
function restoreFile(filePath, backup) {
if (backup === null) {
if (fs.existsSync(filePath)) {
fs.rmSync(filePath, { force: true });
}
return;
}
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, backup);
}
function findFileRecursive(rootDir, fileName) {
if (!fs.existsSync(rootDir)) return null;
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(rootDir, entry.name);
if (entry.isFile() && entry.name.toLowerCase() === fileName.toLowerCase()) {
return fullPath;
}
if (entry.isDirectory()) {
const nested = findFileRecursive(fullPath, fileName);
if (nested) return nested;
}
}
return null;
}
function resolveFfmpegBinary() {
const direct = spawnSync('ffmpeg', ['-version'], { stdio: 'ignore', windowsHide: true });
if (direct.status === 0) return 'ffmpeg';
const bundledRoot = path.join(APPDATA_DIR, 'tools', 'ffmpeg');
const bundled = findFileRecursive(bundledRoot, process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg');
if (bundled) return bundled;
throw new Error('ffmpeg not found. Install ffmpeg or run app preflight auto-fix first.');
}
function runFfmpeg(ffmpegPath, args) {
const res = spawnSync(ffmpegPath, args, { windowsHide: true, stdio: 'pipe' });
if (res.status !== 0) {
const stderr = (res.stderr || Buffer.from('')).toString('utf-8').slice(0, 800);
throw new Error(`ffmpeg failed: ${stderr || `exit ${res.status}`}`);
}
}
function ensureTestMedia() {
fs.mkdirSync(TMP_DIR, { recursive: true });
const ffmpeg = resolveFfmpegBinary();
runFfmpeg(ffmpeg, [
'-y',
'-f', 'lavfi',
'-i', 'testsrc=size=640x360:rate=30',
'-t', '4',
'-pix_fmt', 'yuv420p',
MEDIA_A
]);
runFfmpeg(ffmpeg, [
'-y',
'-f', 'lavfi',
'-i', 'testsrc=size=640x360:rate=30',
'-t', '3',
'-pix_fmt', 'yuv420p',
MEDIA_B
]);
}
async function run() {
const configBackup = backupFile(CONFIG_FILE);
const queueBackup = backupFile(QUEUE_FILE);
let app;
try {
ensureTestMedia();
const electronPath = require('electron');
app = await electron.launch({
executablePath: electronPath,
args: ['.'],
cwd: process.cwd()
});
const win = await app.firstWindow();
const issues = [];
win.on('pageerror', (err) => {
issues.push(`pageerror: ${String(err)}`);
});
win.on('console', (msg) => {
if (msg.type() === 'error') {
issues.push(`console.error: ${msg.text()}`);
}
});
await win.waitForTimeout(2200);
const summary = await win.evaluate(async ({ mediaA, mediaB, tmpDir }) => {
const failures = [];
const checks = {};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const assert = (condition, message) => {
if (!condition) failures.push(message);
};
const waitFor = async (predicate, timeoutMs = 15000, intervalMs = 250) => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (predicate()) return true;
await sleep(intervalMs);
}
return false;
};
const clearQueue = async () => {
const q = await window.api.getQueue();
for (const item of q) {
await window.api.removeFromQueue(item.id);
}
};
const cleanupDownloads = async () => {
await window.api.cancelDownload();
await sleep(400);
};
const initialConfig = await window.api.getConfig();
try {
await cleanupDownloads();
await clearQueue();
const requiredGlobals = [
'showTab',
'addStreamer',
'refreshVODs',
'downloadClip',
'saveSettings',
'runPreflight',
'refreshDebugLog',
'toggleDebugAutoRefresh',
'retryFailedDownloads',
'toggleDownload'
];
const missingGlobals = requiredGlobals.filter((name) => typeof window[name] !== 'function');
checks.globals = { missingGlobals };
assert(missingGlobals.length === 0, `Missing globals: ${missingGlobals.join(', ')}`);
const tabs = ['vods', 'clips', 'cutter', 'merge', 'settings'];
const tabChecks = {};
for (const tab of tabs) {
window.showTab(tab);
tabChecks[tab] = document.querySelector('.tab-content.active')?.id === `${tab}Tab`;
}
checks.tabs = tabChecks;
assert(Object.values(tabChecks).every(Boolean), 'Tab switching failed for at least one tab');
window.showTab('settings');
const preflight = await window.api.runPreflight(false);
await window.runPreflight(false);
await window.refreshDebugLog();
checks.preflight = {
ok: preflight.ok,
checks: preflight.checks,
panelText: (document.getElementById('preflightResult')?.textContent || '').slice(0, 180),
healthBadge: (document.getElementById('healthBadge')?.textContent || '').trim()
};
assert(Boolean(checks.preflight.panelText), 'Preflight panel is empty');
assert(Boolean(checks.preflight.healthBadge), 'Health badge is empty');
const lang = document.getElementById('languageSelect');
lang.value = 'de';
lang.dispatchEvent(new Event('change', { bubbles: true }));
await sleep(160);
const deState = {
nav: (document.getElementById('navSettingsText')?.textContent || '').trim(),
retry: (document.getElementById('btnRetryFailed')?.textContent || '').trim(),
deText: (document.getElementById('languageDeText')?.textContent || '').trim(),
deIcon: !!document.querySelector('#langOptionDe .flag-icon.flag-de'),
deActive: !!document.getElementById('langOptionDe')?.classList.contains('active')
};
lang.value = 'en';
lang.dispatchEvent(new Event('change', { bubbles: true }));
await sleep(160);
const enState = {
nav: (document.getElementById('navSettingsText')?.textContent || '').trim(),
retry: (document.getElementById('btnRetryFailed')?.textContent || '').trim(),
enText: (document.getElementById('languageEnText')?.textContent || '').trim(),
enIcon: !!document.querySelector('#langOptionEn .flag-icon.flag-en'),
enActive: !!document.getElementById('langOptionEn')?.classList.contains('active')
};
checks.language = { deState, enState };
assert(deState.nav.includes('Einstellungen'), 'German language switch failed');
assert(enState.nav.includes('Settings'), 'English language switch failed');
assert(deState.deIcon, 'German flag icon missing');
assert(enState.enIcon, 'English flag icon missing');
assert(deState.deActive, 'German language button did not activate');
assert(enState.enActive, 'English language button did not activate');
await window.api.saveConfig({ client_id: '', client_secret: '', download_path: tmpDir });
window.showTab('vods');
await window.selectStreamer('xrohat');
await waitFor(() => document.querySelectorAll('.vod-card').length > 0, 18000, 300);
const vodCards = document.querySelectorAll('.vod-card').length;
checks.vods = {
cards: vodCards,
status: (document.getElementById('statusText')?.textContent || '').trim()
};
assert(vodCards > 0, 'No VOD cards loaded');
if (vodCards > 0) {
document.querySelector('.vod-card .vod-btn.primary')?.click();
await sleep(350);
}
const queueAfterUiAdd = Number(document.getElementById('queueCount')?.textContent || '0');
checks.queueBasic = { queueAfterUiAdd };
assert(queueAfterUiAdd >= 1, 'Queue did not increase after VOD add button');
await clearQueue();
await window.api.saveConfig({ prevent_duplicate_downloads: true });
await window.api.addToQueue({
url: 'https://www.twitch.tv/videos/2695851503',
title: '__E2E_FULL__dup',
date: '2026-02-01T00:00:00Z',
streamer: 'xrohat',
duration_str: '1h0m0s'
});
await window.api.addToQueue({
url: 'https://www.twitch.tv/videos/2695851503',
title: '__E2E_FULL__dup',
date: '2026-02-01T00:00:00Z',
streamer: 'xrohat',
duration_str: '1h0m0s'
});
let q = await window.api.getQueue();
const duplicateCount = q.filter((item) => item.title === '__E2E_FULL__dup').length;
checks.duplicatePrevention = { duplicateCount };
assert(duplicateCount === 1, 'Duplicate prevention did not block second queue add');
await clearQueue();
const runtimeMetrics = await window.api.getRuntimeMetrics();
checks.runtimeMetrics = {
hasQueue: !!runtimeMetrics?.queue,
hasCache: !!runtimeMetrics?.caches,
hasConfig: !!runtimeMetrics?.config,
mode: runtimeMetrics?.config?.performanceMode || 'unknown'
};
assert(Boolean(checks.runtimeMetrics.hasQueue && checks.runtimeMetrics.hasCache && checks.runtimeMetrics.hasConfig), 'Runtime metrics snapshot missing expected sections');
window.showTab('clips');
const clipUrl = document.getElementById('clipUrl');
clipUrl.value = '';
await window.downloadClip();
const clipEmptyStatus = (document.getElementById('clipStatus')?.textContent || '').trim();
assert(clipEmptyStatus.includes('Please enter a URL') || clipEmptyStatus.includes('Bitte URL eingeben'), 'Empty clip URL validation failed');
clipUrl.value = 'invalid-url';
await window.downloadClip();
const clipInvalidStatus = (document.getElementById('clipStatus')?.textContent || '').trim();
assert(clipInvalidStatus.includes('Invalid clip URL') || clipInvalidStatus.includes('Ungueltige Clip-URL'), 'Invalid clip URL localization failed');
window.openClipDialog('https://www.twitch.tv/videos/2695851503', '__E2E_FULL__clip', '2026-02-01T00:00:00Z', 'xrohat', '1h0m0s');
document.getElementById('clipStartTime').value = '00:00:10';
document.getElementById('clipEndTime').value = '00:00:22';
window.updateFromInput('start');
window.updateFromInput('end');
await window.confirmClipDialog();
q = await window.api.getQueue();
const clipItem = q.find((item) => item.title === '__E2E_FULL__clip');
checks.clipQueue = { queued: !!clipItem, duration: clipItem?.customClip?.durationSec || 0 };
assert(Boolean(clipItem && clipItem.customClip && clipItem.customClip.durationSec === 12), 'Clip dialog queue entry invalid');
await clearQueue();
await window.api.addToQueue({
url: 'https://www.twitch.tv/videos/2695851503',
title: '__E2E_FULL__pause',
date: '2026-02-01T00:00:00Z',
streamer: 'xrohat',
duration_str: '4h0m0s'
});
await window.api.startDownload();
await waitFor(async () => {
const list = await window.api.getQueue();
const it = list.find((x) => x.title === '__E2E_FULL__pause');
return it && (it.status === 'downloading' || it.status === 'error');
}, 25000, 400);
await window.api.pauseDownload();
await sleep(1400);
q = await window.api.getQueue();
const paused = q.find((item) => item.title === '__E2E_FULL__pause');
checks.pauseResume = {
pausedStatus: paused?.status || 'none',
buttonText: (document.getElementById('btnStart')?.textContent || '').trim()
};
assert(paused?.status === 'paused', 'Pause did not set item status to paused');
await window.api.startDownload();
await sleep(900);
const resumed = await window.api.isDownloading();
checks.pauseResume.resumed = resumed;
assert(resumed === true, 'Resume did not restart downloading');
await cleanupDownloads();
await clearQueue();
await window.api.addToQueue({
url: 'not-a-valid-url',
title: '__E2E_FULL__retry',
date: '2026-02-01T00:00:00Z',
streamer: 'xrohat',
duration_str: '1h0m0s'
});
await window.api.startDownload();
const reachedError = await waitFor(async () => {
const list = await window.api.getQueue();
const it = list.find((item) => item.title === '__E2E_FULL__retry');
return it && it.status === 'error';
}, 90000, 1000);
q = await window.api.getQueue();
const failed = q.find((item) => item.title === '__E2E_FULL__retry');
checks.retryFlow = {
failedStatus: failed?.status || 'none',
failedReason: failed?.last_error || ''
};
assert(reachedError && failed?.status === 'error', 'Retry item did not reach deterministic error state');
assert(Boolean(failed?.last_error), 'Retry test item missing error reason');
await window.api.retryFailedDownloads();
await sleep(500);
q = await window.api.getQueue();
const afterRetry = q.find((item) => item.title === '__E2E_FULL__retry');
checks.retryFlow.afterRetryStatus = afterRetry?.status || 'none';
const retryAcceptedStatuses = ['pending', 'downloading', 'error'];
assert(retryAcceptedStatuses.includes(afterRetry?.status || ''), 'Retry failed action did not update item state');
await cleanupDownloads();
await clearQueue();
await window.api.addToQueue({
url: 'https://www.twitch.tv/videos/does-not-exist',
title: '__E2E_FULL__orderA',
date: '2026-02-01T00:00:00Z',
streamer: 'xrohat',
duration_str: '1h0m0s'
});
await window.api.addToQueue({
url: 'https://www.twitch.tv/videos/does-not-exist',
title: '__E2E_FULL__orderB',
date: '2026-02-01T00:00:00Z',
streamer: 'xrohat',
duration_str: '1h0m0s'
});
q = await window.api.getQueue();
const ids = q.map((item) => item.id);
const reversed = [...ids].reverse();
await window.api.reorderQueue(reversed);
const reordered = await window.api.getQueue();
const reorderOk = JSON.stringify(reordered.map((item) => item.id)) === JSON.stringify(reversed);
checks.reorder = { reorderOk };
assert(reorderOk, 'Queue reorder API failed');
await clearQueue();
const info = await window.api.getVideoInfo(mediaA);
const frame = await window.api.extractFrame(mediaA, 1);
const cut = await window.api.cutVideo(mediaA, 0.5, 1.7);
const merge = await window.api.mergeVideos([mediaA, mediaB], `${tmpDir.replace(/\\/g, '/')}/merged_full.mp4`);
checks.media = {
infoOk: !!info && info.duration > 0,
frameOk: typeof frame === 'string' && frame.length > 100,
cutOk: cut.success,
mergeOk: merge.success
};
assert(checks.media.infoOk, 'getVideoInfo failed for test media');
assert(checks.media.frameOk, 'extractFrame failed for test media');
assert(checks.media.cutOk, 'cutVideo failed for test media');
assert(checks.media.mergeOk, 'mergeVideos failed for test media');
const updateResult = await window.api.checkUpdate();
checks.update = updateResult;
assert(typeof updateResult === 'object', 'checkUpdate did not return object');
} catch (e) {
failures.push(`Unexpected exception: ${String(e)}`);
} finally {
await cleanupDownloads();
await clearQueue();
await window.api.saveConfig(initialConfig);
config = await window.api.getConfig();
await window.connect();
}
return { checks, failures };
}, {
mediaA: MEDIA_A.replace(/\\/g, '/'),
mediaB: MEDIA_B.replace(/\\/g, '/'),
tmpDir: TMP_DIR.replace(/\\/g, '/')
});
await app.close();
app = null;
const output = {
...summary,
runtimeIssues: issues
};
console.log(JSON.stringify(output, null, 2));
const failed = output.failures.length > 0 || output.runtimeIssues.length > 0;
process.exit(failed ? 1 : 0);
} finally {
if (app) {
try {
await app.close();
} catch {
// ignore
}
}
restoreFile(CONFIG_FILE, configBackup);
restoreFile(QUEUE_FILE, queueBackup);
fs.rmSync(TMP_DIR, { recursive: true, force: true });
}
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
+146
View File
@@ -0,0 +1,146 @@
const { _electron: electron } = require('playwright');
async function run() {
const electronPath = require('electron');
const app = await electron.launch({
executablePath: electronPath,
args: ['.'],
cwd: process.cwd()
});
const win = await app.firstWindow();
const issues = [];
const failures = [];
win.on('pageerror', (err) => {
issues.push(`pageerror: ${String(err)}`);
});
win.on('console', (msg) => {
if (msg.type() === 'error') {
issues.push(`console.error: ${msg.text()}`);
}
});
const fail = (message) => failures.push(message);
let settingsPreview = '';
let variableRows = 0;
let clipPreviewBefore = '';
let clipPreviewAfter = '';
try {
await win.waitForTimeout(2500);
await win.evaluate(() => {
window.showTab('settings');
});
await win.waitForTimeout(200);
await win.click('#settingsTemplateGuideBtn');
await win.waitForTimeout(180);
const guideVisibleFromSettings = await win.evaluate(() => {
return document.getElementById('templateGuideModal')?.classList.contains('show') || false;
});
if (!guideVisibleFromSettings) {
fail('Template guide did not open from settings');
}
await win.fill('#templateGuideInput', '{title}_{part_padded}_{date_custom="yyyy-MM-dd"}.mp4');
await win.waitForTimeout(160);
settingsPreview = await win.locator('#templateGuideOutput').innerText();
if (!settingsPreview.includes('.mp4')) {
fail('Settings template preview missing .mp4 output');
}
if (settingsPreview.includes('{title}') || settingsPreview.includes('{part_padded}') || settingsPreview.includes('{date_custom=')) {
fail('Settings template preview did not replace placeholders');
}
variableRows = await win.locator('#templateGuideBody tr').count();
if (variableRows < 12) {
fail(`Template variable table too short (${variableRows})`);
}
await win.click('#templateGuideUseParts');
await win.waitForTimeout(150);
const partsContext = await win.locator('#templateGuideContext').innerText();
if (!/part|teil/i.test(partsContext)) {
fail('Template guide parts context text missing');
}
await win.click('#templateGuideCloseBtn');
await win.waitForTimeout(100);
await win.evaluate(async () => {
window.showTab('vods');
await window.selectStreamer('xrohat');
});
await win.waitForTimeout(3200);
const clipButtons = win.locator('.vod-card .vod-btn.secondary');
const clipCount = await clipButtons.count();
if (clipCount < 1) {
fail('No clip buttons found in VOD list');
} else {
await clipButtons.first().click();
await win.waitForTimeout(260);
await win.locator('input[name="filenameFormat"][value="template"]').check();
await win.waitForTimeout(140);
await win.click('#clipTemplateGuideBtn');
await win.waitForTimeout(140);
const clipContext = await win.locator('#templateGuideContext').innerText();
if (!/clip/i.test(clipContext)) {
fail('Template guide clip context text missing');
}
await win.fill('#templateGuideInput', '{trim_start}_{part}.mp4');
await win.waitForTimeout(120);
clipPreviewBefore = await win.locator('#templateGuideOutput').innerText();
await win.fill('#clipStartTime', '00:00:10');
await win.evaluate(() => {
window.updateFromInput('start');
});
await win.waitForTimeout(240);
clipPreviewAfter = await win.locator('#templateGuideOutput').innerText();
if (clipPreviewAfter === clipPreviewBefore) {
fail('Clip template guide preview did not react to clip start time changes');
}
await win.click('#templateGuideCloseBtn');
await win.evaluate(() => {
window.closeClipDialog();
});
}
} finally {
await app.close();
}
const summary = {
failures,
issues,
checks: {
settingsPreview,
variableRows,
clipPreviewBefore,
clipPreviewAfter
}
};
console.log(JSON.stringify(summary, null, 2));
const hasFailure = failures.length > 0 || issues.length > 0;
process.exit(hasFailure ? 1 : 0);
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
@@ -0,0 +1,84 @@
const path = require('path');
const {
normalizeUpdateVersion,
compareUpdateVersions,
isNewerUpdateVersion
} = require(path.join(process.cwd(), 'dist', 'update-version-utils.js'));
function run() {
const failures = [];
const assert = (condition, message) => {
if (!condition) failures.push(message);
};
const comparisons = [
{ left: '4.1.18', right: '4.1.10', expected: 1 },
{ left: '4.1.10', right: '4.1.18', expected: -1 },
{ left: 'v4.1.12', right: '4.1.12', expected: 0 },
{ left: '4.1.12', right: '4.1.12.1', expected: -1 },
{ left: '4.2.0', right: '4.1.999', expected: 1 },
{ left: '4.1.12-beta', right: '4.1.12', expected: 0 }
];
const compareResults = comparisons.map((testCase) => {
const actual = compareUpdateVersions(testCase.left, testCase.right);
const pass = actual === testCase.expected;
assert(pass, `compare failed: ${testCase.left} vs ${testCase.right} expected ${testCase.expected}, got ${actual}`);
return { ...testCase, actual, pass };
});
const skipVersionScenarios = [
{
name: 'old downloaded, newer available',
downloaded: '4.1.11',
latestKnown: '4.1.18',
expectedNeedsNewer: true
},
{
name: 'already latest downloaded',
downloaded: '4.1.18',
latestKnown: '4.1.18',
expectedNeedsNewer: false
},
{
name: 'downgrade should not trigger',
downloaded: '4.1.18',
latestKnown: '4.1.11',
expectedNeedsNewer: false
}
];
const scenarioResults = skipVersionScenarios.map((scenario) => {
const needsNewer = isNewerUpdateVersion(scenario.latestKnown, scenario.downloaded);
const pass = needsNewer === scenario.expectedNeedsNewer;
assert(pass, `${scenario.name} expected ${scenario.expectedNeedsNewer}, got ${needsNewer}`);
return { ...scenario, needsNewer, pass };
});
const normalizationChecks = {
fromVPrefix: normalizeUpdateVersion('v4.1.12') === '4.1.12',
trimmed: normalizeUpdateVersion(' 4.1.12 ') === '4.1.12'
};
assert(normalizationChecks.fromVPrefix, 'normalize did not remove v prefix');
assert(normalizationChecks.trimmed, 'normalize did not trim whitespace');
const summary = {
checks: {
compareResults,
scenarioResults,
normalizationChecks
},
failures
};
console.log(JSON.stringify(summary, null, 2));
if (failures.length) {
process.exitCode = 1;
}
}
run();
+149
View File
@@ -0,0 +1,149 @@
const { _electron: electron } = require('playwright');
async function run() {
const electronPath = require('electron');
const app = await electron.launch({
executablePath: electronPath,
args: ['.'],
cwd: process.cwd()
});
const win = await app.firstWindow();
const issues = [];
win.on('pageerror', (err) => {
issues.push(`pageerror: ${String(err)}`);
});
win.on('console', (msg) => {
if (msg.type() === 'error') {
issues.push(`console.error: ${msg.text()}`);
}
});
await win.waitForTimeout(2500);
const globals = await win.evaluate(async () => {
const names = [
'showTab',
'addStreamer',
'refreshVODs',
'downloadClip',
'selectCutterVideo',
'startCutting',
'addMergeFiles',
'startMerging',
'saveSettings',
'checkUpdate',
'downloadUpdate',
'updateFromInput',
'updateFromSlider',
'runPreflight',
'retryFailedDownloads',
'toggleDebugAutoRefresh'
];
const map = {};
for (const n of names) map[n] = typeof window[n];
return map;
});
await win.evaluate(() => {
window.showTab('clips');
window.showTab('cutter');
window.showTab('merge');
window.showTab('settings');
window.showTab('vods');
});
const input = win.locator('#newStreamer');
const randomName = `smoketest_${Date.now()}`;
await input.fill(randomName);
await win.evaluate(async () => {
await window.addStreamer();
});
const hasTempStreamer = await win.locator('#streamerList').innerText();
await win.evaluate(async (name) => {
await window.removeStreamer(name);
}, randomName);
await win.evaluate(async () => {
await window.selectStreamer('xrohat');
});
await win.waitForTimeout(3500);
const vodCount = await win.locator('.vod-card').count();
if (vodCount > 0) {
await win.locator('.vod-card .vod-btn.primary').first().click();
await win.waitForTimeout(500);
}
const queueCountAfterAdd = await win.locator('#queueCount').innerText();
const queueRemove = win.locator('#queueList .remove').first();
if (await queueRemove.count()) {
await queueRemove.click();
await win.waitForTimeout(300);
}
await win.evaluate(() => {
window.showTab('clips');
});
await win.fill('#clipUrl', '');
await win.evaluate(async () => {
await window.downloadClip();
});
const clipStatus = await win.locator('#clipStatus').innerText();
await win.evaluate(async () => {
await window.runPreflight(false);
await window.startCutting();
await window.startMerging();
});
const mergeButtonDisabled = await win.locator('#btnMerge').isDisabled();
const preflightText = await win.locator('#preflightResult').innerText();
const healthBadge = await win.locator('#healthBadge').innerText();
await app.close();
const failedGlobals = Object.entries(globals)
.filter(([, type]) => type !== 'function')
.map(([name, type]) => `${name}=${type}`);
const summary = {
failedGlobals,
hasTempStreamer: hasTempStreamer.includes(randomName),
vodCount,
queueCountAfterAdd,
clipStatus,
mergeButtonDisabled,
preflightText,
healthBadge,
issues
};
console.log(JSON.stringify(summary, null, 2));
const hasFailure =
failedGlobals.length > 0 ||
!summary.hasTempStreamer ||
summary.vodCount < 1 ||
!(summary.clipStatus.includes('Bitte URL eingeben') || summary.clipStatus.includes('Please enter a URL')) ||
!summary.mergeButtonDisabled ||
!summary.preflightText ||
!summary.healthBadge ||
summary.issues.length > 0;
process.exit(hasFailure ? 1 : 0);
}
run().catch((err) => {
console.error(err);
process.exit(1);
});