Merge branch 'feature/t47-live-analysis-external-sync-progress' into develop (#47)
This commit is contained in:
@ -129,6 +129,24 @@ class SettingsPage {
|
||||
} catch (err) {
|
||||
this.liveAnalysisStatus = { status: 'error', error: err.message };
|
||||
}
|
||||
const projectionPresentation = LiveAnalysisPresentation.present(this.liveAnalysisStatus);
|
||||
if (!projectionPresentation.busy && API.sources?.getStatus) {
|
||||
try {
|
||||
const sourceStatuses = await API.sources.getStatus();
|
||||
const liveSourceSyncing = Array.isArray(sourceStatuses) && sourceStatuses.some(source =>
|
||||
source?.status === 'syncing' && ['live', 'all'].includes(source?.type)
|
||||
);
|
||||
if (liveSourceSyncing) {
|
||||
this.liveAnalysisStatus = {
|
||||
status: 'building',
|
||||
phase: 'refreshing_live_sources',
|
||||
externalSourceSync: true
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// Source status is supplementary; projection status remains authoritative.
|
||||
}
|
||||
}
|
||||
this.renderLiveAnalysisStatus(this.liveAnalysisStatus);
|
||||
const presentation = LiveAnalysisPresentation.present(this.liveAnalysisStatus);
|
||||
if (presentation.busy) this.startLiveAnalysisPolling();
|
||||
@ -154,7 +172,7 @@ class SettingsPage {
|
||||
const presentation = LiveAnalysisPresentation.present(status);
|
||||
buttons.forEach(button => { button.disabled = presentation.busy; });
|
||||
const rebuildButton = document.getElementById('live-rebuild-catalog-button');
|
||||
const fullRefreshBusy = presentation.busy &&
|
||||
const fullRefreshBusy = presentation.busy && !status.externalSourceSync &&
|
||||
(this.currentLiveAnalysisAction === 'rebuild' ||
|
||||
LiveAnalysisPresentation.isFullRefreshPhase(presentation.phase));
|
||||
if (rebuildButton) rebuildButton.textContent = fullRefreshBusy ? 'Refreshing…' : 'Run full refresh';
|
||||
|
||||
134
test/t47LiveAnalysisExternalSyncProgressRegression.test.js
Normal file
134
test/t47LiveAnalysisExternalSyncProgressRegression.test.js
Normal file
@ -0,0 +1,134 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { JSDOM } = require('jsdom');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
|
||||
function readPublicScript(relativePath, exposeGlobal = '') {
|
||||
const source = fs.readFileSync(path.join(root, 'public', relativePath), 'utf8');
|
||||
return exposeGlobal ? `${source}\nwindow.${exposeGlobal} = ${exposeGlobal};` : source;
|
||||
}
|
||||
|
||||
function createSettingsDom() {
|
||||
const dom = new JSDOM(`<!doctype html>
|
||||
<button type="button" id="live-check-new-button">Check new streams</button>
|
||||
<button type="button" id="live-rebuild-catalog-button">Run full refresh</button>
|
||||
<button type="button" id="live-analysis-button">Analyze channels</button>
|
||||
<div id="live-analysis-status" role="status"></div>
|
||||
<div id="live-analysis-progress" hidden>
|
||||
<div id="live-analysis-progress-track"><span id="live-analysis-progress-bar"></span></div>
|
||||
<div id="live-analysis-progress-text"></div>
|
||||
</div>
|
||||
<div id="live-analysis-fallback"></div>
|
||||
<button type="button" id="live-analysis-details-toggle" hidden></button>
|
||||
<pre id="live-analysis-error-details" hidden></pre>
|
||||
`, {
|
||||
url: 'http://nodecast.test/',
|
||||
runScripts: 'outside-only',
|
||||
pretendToBeVisual: true
|
||||
});
|
||||
const { window } = dom;
|
||||
window.console = console;
|
||||
window.eval(readPublicScript('js/services/liveAnalysisPresentation.js'));
|
||||
window.eval(readPublicScript('js/pages/Settings.js', 'SettingsPage'));
|
||||
return window;
|
||||
}
|
||||
|
||||
test('Channel Analysis keeps progress visible for a live source sync started outside the current UI action', async () => {
|
||||
const window = createSettingsDom();
|
||||
let sourceStatusRead = false;
|
||||
window.API = {
|
||||
live: {
|
||||
getProjectionStatus: async () => ({
|
||||
status: 'success',
|
||||
phase: 'complete',
|
||||
projectedCount: 42,
|
||||
processed: 42,
|
||||
total: 42
|
||||
})
|
||||
},
|
||||
sources: {
|
||||
getStatus: async () => {
|
||||
sourceStatusRead = true;
|
||||
return [{
|
||||
source_id: 1,
|
||||
type: 'all',
|
||||
status: 'syncing',
|
||||
last_sync: Date.now(),
|
||||
error: null
|
||||
}];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const settingsPage = Object.create(window.SettingsPage.prototype);
|
||||
settingsPage.app = {
|
||||
player: { settings: { languageDetectionMode: 'algo' } },
|
||||
channelList: { loadChannels() {} }
|
||||
};
|
||||
settingsPage.layaStatus = {};
|
||||
settingsPage.currentLiveAnalysisAction = '';
|
||||
|
||||
try {
|
||||
await settingsPage.loadLiveAnalysisStatus();
|
||||
|
||||
assert.equal(sourceStatusRead, true, 'Settings should consult /api/sources/status when projection status is not busy');
|
||||
assert.equal(window.document.getElementById('live-analysis-progress').hidden, false);
|
||||
assert.equal(window.document.getElementById('live-analysis-status').textContent, 'Refreshing IPTV sources…');
|
||||
assert.equal(window.document.getElementById('live-analysis-progress-text').textContent, 'In progress — this step may take several minutes.');
|
||||
assert.equal(window.document.getElementById('live-analysis-progress-track').classList.contains('indeterminate'), true);
|
||||
const rebuildButton = window.document.getElementById('live-rebuild-catalog-button');
|
||||
assert.equal(rebuildButton.disabled, true);
|
||||
assert.equal(rebuildButton.textContent, 'Run full refresh');
|
||||
} finally {
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('busy projection status remains authoritative over a simultaneous source sync', async () => {
|
||||
const window = createSettingsDom();
|
||||
window.API = {
|
||||
live: { getProjectionStatus: async () => ({ status: 'building', phase: 'laya_language', phaseProcessed: 1, phaseTotal: 2 }) },
|
||||
sources: { getStatus: async () => { throw new Error('should not be called'); } }
|
||||
};
|
||||
const settingsPage = Object.create(window.SettingsPage.prototype);
|
||||
settingsPage.app = { player: { settings: { languageDetectionMode: 'laya' } } };
|
||||
settingsPage.layaStatus = {};
|
||||
settingsPage.currentLiveAnalysisAction = '';
|
||||
|
||||
try {
|
||||
await settingsPage.loadLiveAnalysisStatus();
|
||||
assert.equal(window.document.getElementById('live-analysis-status').textContent, 'Detecting stream languages…');
|
||||
assert.equal(window.document.getElementById('live-rebuild-catalog-button').textContent, 'Refreshing…');
|
||||
} finally {
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('EPG-only source sync is ignored and source status failures preserve projection status', async () => {
|
||||
for (const getStatus of [
|
||||
async () => [{ type: 'epg', status: 'syncing' }],
|
||||
async () => { throw new Error('source status unavailable'); }
|
||||
]) {
|
||||
const window = createSettingsDom();
|
||||
window.API = {
|
||||
live: { getProjectionStatus: async () => ({ status: 'success', phase: 'complete', projectedCount: 42 }) },
|
||||
sources: { getStatus }
|
||||
};
|
||||
const settingsPage = Object.create(window.SettingsPage.prototype);
|
||||
settingsPage.app = { player: { settings: { languageDetectionMode: 'algo' } } };
|
||||
settingsPage.layaStatus = {};
|
||||
settingsPage.currentLiveAnalysisAction = '';
|
||||
|
||||
try {
|
||||
const status = await settingsPage.loadLiveAnalysisStatus();
|
||||
assert.equal(status.phase, 'complete');
|
||||
assert.equal(window.document.getElementById('live-analysis-progress').hidden, true);
|
||||
assert.equal(window.document.getElementById('live-rebuild-catalog-button').disabled, false);
|
||||
} finally {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user