mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
The startup auto-refresh always opened the `HOME.PLAYLISTS.AUTO_REFRESH_UPDATE_SUCCESS` snackbar, even when the backend had dropped playlists it could not refresh. Isolating per-playlist failures (#1233) means the result set is lossy by design, so an unreachable source that now fails within `PLAYLIST_FETCH_TIMEOUT_MS` produces a false success toast. `autoUpdatePlaylists()` now returns `AutoUpdatePlaylistsResult` — the refreshed playlists plus one outcome per requested playlist (`updated` / `failed` / `skipped`), in request order — on top of the existing bounded-concurrency refresh. The renderer derives the message from those outcomes: - all updated -> `AUTO_REFRESH_UPDATE_SUCCESS` (unchanged) - some failed -> `AUTO_REFRESH_UPDATE_PARTIAL` (error styling, dismissable) - some failed and some skipped -> `AUTO_REFRESH_UPDATE_PARTIAL_WITH_SKIPPED` - none updated -> `AUTO_REFRESH_UPDATE_FAILED` (error styling, dismissable) - only sourceless playlists left over -> `AUTO_REFRESH_UPDATE_SKIPPED` Playlists with neither a URL nor a file path are reported as skipped rather than failed, since there is no source to refresh them from. The mixed failed+skipped message exists because the plain partial text names only updated/total/failed, which would leave the skipped playlists as an unexplained remainder. Titles of unresolved playlists are logged for diagnosability. Tests: five new `electron.service` cases (one per message branch), outcome assertions across the existing `playlist-auto-update` and `playlist.events` specs, and an Electron E2E that restarts the app against a killed playlist server — verified to fail against the old unconditional toast. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
114 lines
4.1 KiB
TypeScript
114 lines
4.1 KiB
TypeScript
import type { Locator, Page } from '@playwright/test';
|
|
import {
|
|
closeElectronApp,
|
|
createMutableTextServer,
|
|
expect,
|
|
importM3uPlaylistFromUrl,
|
|
launchElectronApp,
|
|
openSourceEditor,
|
|
openSources,
|
|
restartElectronApp,
|
|
saveSourceDialog,
|
|
sourceRowByTitle,
|
|
test,
|
|
updateSourceDialog,
|
|
waitForM3uCatalog,
|
|
} from './electron-test-fixtures';
|
|
|
|
const autoRefreshSourceName = 'auto-refresh-source.m3u';
|
|
|
|
function buildAutoRefreshM3u(channelName: string): string {
|
|
return `#EXTM3U
|
|
#EXTINF:-1 group-title="News",${channelName}
|
|
https://streams.example.test/${channelName.toLowerCase().replace(/\s+/g, '-')}.m3u8
|
|
`;
|
|
}
|
|
|
|
function snackBarLabel(page: Page, text: string): Locator {
|
|
return page
|
|
.locator('.mat-mdc-snack-bar-label')
|
|
.filter({ hasText: text })
|
|
.last();
|
|
}
|
|
|
|
async function openAutoRefreshPlaylistCatalog(page: Page): Promise<void> {
|
|
await openSources(page);
|
|
await sourceRowByTitle(page, autoRefreshSourceName).first().click();
|
|
await waitForM3uCatalog(page);
|
|
}
|
|
|
|
test.describe('Electron startup playlist auto-refresh', () => {
|
|
test('reports failed playlists instead of an unconditional success toast', async ({
|
|
dataDir,
|
|
}) => {
|
|
const urlServer = await createMutableTextServer(
|
|
buildAutoRefreshM3u('Original Auto Channel'),
|
|
{
|
|
contentType: 'application/vnd.apple.mpegurl',
|
|
resourcePath: `/${autoRefreshSourceName}`,
|
|
}
|
|
);
|
|
const app = await launchElectronApp(dataDir);
|
|
|
|
try {
|
|
await importM3uPlaylistFromUrl(
|
|
app.mainWindow,
|
|
urlServer.resourceUrl
|
|
);
|
|
await openSources(app.mainWindow);
|
|
const dialog = await openSourceEditor(
|
|
app.mainWindow,
|
|
autoRefreshSourceName
|
|
);
|
|
await updateSourceDialog(dialog, { autoRefresh: true });
|
|
await saveSourceDialog(app.mainWindow, dialog);
|
|
|
|
// Reachable source: the startup run really refreshes the playlist.
|
|
// Asserted through the persisted catalog rather than the 2s success
|
|
// toast, which is too short-lived to poll for reliably.
|
|
urlServer.setBody(buildAutoRefreshM3u('Refreshed Auto Channel'));
|
|
const successfulRestart = await restartElectronApp(app, dataDir);
|
|
app.electronApp = successfulRestart.electronApp;
|
|
app.mainWindow = successfulRestart.mainWindow;
|
|
|
|
await openAutoRefreshPlaylistCatalog(app.mainWindow);
|
|
await expect(
|
|
app.mainWindow.getByTestId('channel-item').filter({
|
|
hasText: 'Refreshed Auto Channel',
|
|
})
|
|
).toHaveCount(1);
|
|
|
|
// Unreachable source: the backend drops the playlist from its
|
|
// result, so the startup toast must say so instead of claiming
|
|
// everything was updated.
|
|
await urlServer.close();
|
|
|
|
const failedRestart = await restartElectronApp(app, dataDir);
|
|
app.electronApp = failedRestart.electronApp;
|
|
app.mainWindow = failedRestart.mainWindow;
|
|
|
|
await expect(
|
|
snackBarLabel(app.mainWindow, 'Playlist auto-refresh failed')
|
|
).toBeVisible({ timeout: 30000 });
|
|
await expect(
|
|
app.mainWindow.locator('.mat-mdc-snack-bar-label').filter({
|
|
hasText: 'The playlists were successfully updated',
|
|
})
|
|
).toHaveCount(0);
|
|
|
|
// A failed refresh must not wipe the previously imported channels.
|
|
await openAutoRefreshPlaylistCatalog(app.mainWindow);
|
|
await expect(
|
|
app.mainWindow.getByTestId('channel-item').filter({
|
|
hasText: 'Refreshed Auto Channel',
|
|
})
|
|
).toHaveCount(1);
|
|
} finally {
|
|
await closeElectronApp(app);
|
|
// The test closes the server mid-run to make the source
|
|
// unreachable; a second close rejects with ERR_SERVER_NOT_RUNNING.
|
|
await urlServer.close().catch(() => undefined);
|
|
}
|
|
});
|
|
});
|