mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 13:16:16 -08:00
fix(sources): keep deletes busy until settled and on screen; refresh E2E via the switcher
PR #1932 round 1 (CI + local Codex/Greptile on the bot fixes): - CI: category-management.e2e refreshed the active playlist through the old switcher trigger's inline refresh button, which the redesign replaced with the switcher row menu (⋯ → Refresh playlist). The helper now goes through that menu; it failed on all three OS shards. - A desktop Xtream delete's completed event cleared its progress while PlaylistsService still cleaned up (parental lock, stored state), so the already-deleted source was actionable again on Sources and back in the switcher until the store dropped it. A terminal event now only ends Cancel; the row stays busy until the delete settles and the removal service clears it. - A running delete (from the switcher, say) disappeared from Sources when the page's saved filters excluded it, hiding its only progress and Cancel. Running deletes stay listed whatever the filters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a48d0b913f
commit
dac79af54f
5 files changed
+78
-12
No files matched your search
@@ -545,8 +545,15 @@ async function expectCategoryCentered(row: Locator): Promise<void> {
|
||||
}
|
||||
|
||||
async function refreshFromWorkspaceHeader(page: Page): Promise<void> {
|
||||
// Refresh for the active source lives in its switcher row menu.
|
||||
await page.getByTestId('playlist-switcher-trigger').click();
|
||||
await page
|
||||
.getByRole('button', { name: 'Refresh playlist', exact: true })
|
||||
.locator(
|
||||
'app-playlist-switcher-panel [role="option"][aria-current="true"] .source-row__more'
|
||||
)
|
||||
.click();
|
||||
await page
|
||||
.getByRole('menuitem', { name: 'Refresh playlist', exact: true })
|
||||
.click();
|
||||
|
||||
const dialog = page.locator('mat-dialog-container');
|
||||
|
||||
@@ -214,6 +214,15 @@ describe('RecentPlaylistsComponent', () => {
|
||||
expect(removal.remove).toHaveBeenCalledWith(M3U, FILE);
|
||||
});
|
||||
|
||||
it('keeps a running delete on screen whatever the filters', () => {
|
||||
(
|
||||
removal.progress as { isDeleting: (id: string) => boolean }
|
||||
).isDeleting = (id) => id === 'xtream';
|
||||
viewState.setType('m3u');
|
||||
fixture.detectChanges();
|
||||
expect(ids()).toEqual(['m3u', 'xtream', 'file']);
|
||||
});
|
||||
|
||||
it('applies the type and status filters together', () => {
|
||||
viewState.setType('m3u');
|
||||
expect(ids()).toEqual(['m3u', 'file']);
|
||||
|
||||
@@ -122,7 +122,7 @@ export class RecentPlaylistsComponent {
|
||||
};
|
||||
const options = this.sortOptions();
|
||||
const filter = this.viewState.filter();
|
||||
const filtered = filterSources(
|
||||
const matching = filterSources(
|
||||
this.playlists(),
|
||||
this.statusService.supportsStatusFilter
|
||||
? filter
|
||||
@@ -130,6 +130,14 @@ export class RecentPlaylistsComponent {
|
||||
this.searchQueryInput(),
|
||||
bucketOf
|
||||
);
|
||||
// A running delete keeps its row, whatever the filters: its
|
||||
// progress and Cancel live there (one started from the switcher
|
||||
// has nowhere else to show).
|
||||
const filtered = this.playlists().filter(
|
||||
(playlist) =>
|
||||
matching.includes(playlist) ||
|
||||
this.removal.progress.isDeleting(playlist._id)
|
||||
);
|
||||
const sorted = this.sortService.sortPlaylists(
|
||||
filtered,
|
||||
options,
|
||||
|
||||
@@ -77,18 +77,28 @@ export class SourceDeleteProgressService {
|
||||
return this.cancelled.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Follows a running delete. A terminal event ends what Cancel can stop,
|
||||
* but the row stays busy: the delete still cleans up (parental lock,
|
||||
* stored state) before its source leaves the store, so the caller clears
|
||||
* it once the whole delete has settled.
|
||||
*/
|
||||
track(id: string, event: DbOperationEvent): void {
|
||||
if (['completed', 'cancelled', 'error'].includes(event.status)) {
|
||||
this.clear(id);
|
||||
return;
|
||||
}
|
||||
const terminal = ['completed', 'cancelled', 'error'].includes(
|
||||
event.status
|
||||
);
|
||||
this.operations.update((current) =>
|
||||
new Map(current).set(id, {
|
||||
operationId: event.operationId,
|
||||
phase: event.phase,
|
||||
current: event.current,
|
||||
total: event.total,
|
||||
})
|
||||
new Map(current).set(
|
||||
id,
|
||||
terminal
|
||||
? { ...current.get(id), operationId: undefined }
|
||||
: {
|
||||
operationId: event.operationId,
|
||||
phase: event.phase,
|
||||
current: event.current,
|
||||
total: event.total,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -336,6 +336,38 @@ describe('SourceRemovalService', () => {
|
||||
expect(toasts).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('keeps a deleted row busy until the whole delete has settled', async () => {
|
||||
let finish!: (deleted: boolean) => void;
|
||||
deletePlaylist.mockImplementation(
|
||||
(
|
||||
_playlist: PlaylistMeta,
|
||||
options: { onEvent: (event: object) => void }
|
||||
) => {
|
||||
options.onEvent({
|
||||
operation: 'delete-playlist',
|
||||
operationId: 'op-2',
|
||||
status: 'completed',
|
||||
});
|
||||
return new Promise<boolean>((resolve) => (finish = resolve));
|
||||
}
|
||||
);
|
||||
service.remove({ ...playlist('x'), serverUrl: 'http://x.test' });
|
||||
expireUndo();
|
||||
await flush();
|
||||
|
||||
// The database is done, its cleanup is not: no Cancel, still busy.
|
||||
expect(service.progress.isDeleting('x')).toBe(true);
|
||||
expect(service.progress.canCancel('x')).toBe(false);
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
|
||||
finish(true);
|
||||
await flush();
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
PlaylistActions.playlistRemovalCommitted({ playlistId: 'x' })
|
||||
);
|
||||
expect(service.progress.isDeleting('x')).toBe(false);
|
||||
});
|
||||
|
||||
it('brings the source back and says so when the delete fails', async () => {
|
||||
deletePlaylist.mockResolvedValue(false);
|
||||
service.remove(playlist('a'));
|
||||
|
||||
Reference in new issue
Block a user