fix(logos): „Mehr laden“ beginnt von vorn, wenn der Cursor nicht mehr existiert
Schlussreview F2. Hat jemand das Logo gelöscht oder ausgeblendet, hinter dem „Mehr laden“ ansetzt (der Cursor), antwortet Appwrite mit
400 `general_cursor_not_found`. „Nochmal versuchen“ wiederholte dann denselben Cursor und scheiterte immer wieder.
- Dieser Fehlertyp beim Nachladen löst jetzt `load('reset')` aus: die erste Seite der aktuellen Ansicht (gleiche Sortierung, gleicher Filter)
wird neu geladen, die Kachel des gewählten Logos bleibt stehen. Kein Fehlertext, kein „Nochmal versuchen“ für den toten Cursor.
- Jeder andere Fehler beim Nachladen und jeder Fehler beim Laden der ersten Seite bleibt wie bisher (Meldung und „Nochmal versuchen“);
die erste Seite hat keinen Cursor, ein Neuladen im Kreis ist ausgeschlossen.
- Tests mit einem Fehler {code: 400, type: 'general_cursor_not_found'}: Neuladen von Seite 1 mit Anfragefolge, Ansicht „Gemeldet“ bleibt,
gewählte Kachel bleibt, Neuladen scheitert selbst, anderer Fehlertyp, Fehlertyp beim ersten Laden.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
202254c09e
commit
ec4e80c62c
2 files changed
+111
-1
No files matched your search
@@ -460,6 +460,103 @@ describe('Mehr laden', () => {
|
|||||||
expect(alertText()).toBe('');
|
expect(alertText()).toBe('');
|
||||||
expect(retryButton().hidden).toBe(true);
|
expect(retryButton().hidden).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('der Cursor ist weg (das Logo dahinter wurde von jemandem gelöscht oder ausgeblendet)', () => {
|
||||||
|
/** Appwrite: 400 `general_cursor_not_found`. */
|
||||||
|
const cursorGone = () => Object.assign(new Error('The current cursor could not be found.'), { code: 400, type: 'general_cursor_not_found' });
|
||||||
|
|
||||||
|
it('„Mehr laden“ beginnt von vorn: die erste Seite der Ansicht wird neu geladen, kein Fehler, kein „Nochmal versuchen“ mit dem toten Cursor', async () => {
|
||||||
|
fx.listLogos.mockResolvedValue({ logos: [logo('a'), logo('b')], nextCursor: 'b' });
|
||||||
|
await mount();
|
||||||
|
fx.listLogos.mockRejectedValueOnce(cursorGone());
|
||||||
|
fx.listLogos.mockResolvedValueOnce({ logos: [logo('c'), logo('b', { likeCount: 3 })], nextCursor: null });
|
||||||
|
moreButton().click();
|
||||||
|
await flush();
|
||||||
|
// erste Anfrage: Seite 1, zweite: mit dem toten Cursor, dritte: wieder Seite 1 (ohne Cursor), gleiche Ansicht
|
||||||
|
expect(fx.listLogos.mock.calls.map(([opts]) => opts)).toEqual([
|
||||||
|
{ sort: 'popular', filter: 'all' },
|
||||||
|
{ sort: 'popular', filter: 'all', cursor: 'b' },
|
||||||
|
{ sort: 'popular', filter: 'all' },
|
||||||
|
]);
|
||||||
|
expect(names()).toEqual(['Kein Logo', 'Logo C', 'Logo B']);
|
||||||
|
expect(heart('Logo B').textContent).toBe('3'); // die frische Zeile gilt
|
||||||
|
expect(alertText()).toBe('');
|
||||||
|
expect(retryButton().hidden).toBe(true);
|
||||||
|
expect(moreButton().hidden).toBe(true); // die neue Seite war die letzte
|
||||||
|
});
|
||||||
|
|
||||||
|
it('die Ansicht bleibt (hier die Admin-Ansicht „Gemeldet“): neu geladen wird mit derselben Sortierung und demselben Filter', async () => {
|
||||||
|
admin = true;
|
||||||
|
await mount();
|
||||||
|
fx.listLogos.mockResolvedValue({ logos: [logo('a')], nextCursor: 'a' });
|
||||||
|
sortButton('Gemeldet').click();
|
||||||
|
await flush();
|
||||||
|
fx.listLogos.mockRejectedValueOnce(cursorGone());
|
||||||
|
fx.listLogos.mockResolvedValueOnce({ logos: [logo('z')], nextCursor: 'z' });
|
||||||
|
moreButton().click();
|
||||||
|
await flush();
|
||||||
|
expect(fx.listLogos).toHaveBeenLastCalledWith({ sort: 'new', filter: 'reported' });
|
||||||
|
expect(names()).toEqual(['Kein Logo', 'Logo Z']);
|
||||||
|
expect(pressedSort()).toEqual(['Gemeldet']);
|
||||||
|
expect(moreButton().hidden).toBe(false); // und es geht mit dem neuen Cursor weiter
|
||||||
|
fx.listLogos.mockResolvedValueOnce({ logos: [logo('y')], nextCursor: null });
|
||||||
|
moreButton().click();
|
||||||
|
await flush();
|
||||||
|
expect(fx.listLogos).toHaveBeenLastCalledWith({ sort: 'new', filter: 'reported', cursor: 'z' });
|
||||||
|
expect(names()).toEqual(['Kein Logo', 'Logo Z', 'Logo Y']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('die Kachel des gewählten Logos bleibt stehen (kein Flackern), die übrigen werden ersetzt', async () => {
|
||||||
|
fx.listLogos.mockResolvedValue({ logos: [logo('a'), logo('b')], nextCursor: 'b' });
|
||||||
|
await mount();
|
||||||
|
picker.select('a');
|
||||||
|
const selected = tile('Logo A');
|
||||||
|
fx.listLogos.mockRejectedValueOnce(cursorGone());
|
||||||
|
fx.listLogos.mockResolvedValueOnce({ logos: [logo('a'), logo('c')], nextCursor: null });
|
||||||
|
moreButton().click();
|
||||||
|
await flush();
|
||||||
|
expect(names()).toEqual(['Kein Logo', 'Logo A', 'Logo C']);
|
||||||
|
expect(tile('Logo A')).toBe(selected);
|
||||||
|
expect(pressedTiles()).toEqual(['Logo A']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scheitert das Neuladen selbst (Netz), steht der gewohnte Fehler mit „Nochmal versuchen“, das die erste Seite holt', async () => {
|
||||||
|
fx.listLogos.mockResolvedValue({ logos: [logo('a')], nextCursor: 'a' });
|
||||||
|
await mount();
|
||||||
|
fx.listLogos.mockRejectedValueOnce(cursorGone());
|
||||||
|
fx.listLogos.mockRejectedValueOnce(new Error('offline'));
|
||||||
|
moreButton().click();
|
||||||
|
await flush();
|
||||||
|
expect(alertText()).toBe('Die Logos konnten nicht geladen werden.');
|
||||||
|
expect(retryButton().hidden).toBe(false);
|
||||||
|
fx.listLogos.mockResolvedValue({ logos: [logo('c')], nextCursor: null });
|
||||||
|
retryButton().click();
|
||||||
|
await flush();
|
||||||
|
expect(fx.listLogos).toHaveBeenLastCalledWith({ sort: 'popular', filter: 'all' });
|
||||||
|
expect(names()).toEqual(['Kein Logo', 'Logo C']);
|
||||||
|
expect(alertText()).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nur dieser Fehlertyp löst das Neuladen aus: jeder andere Fehler beim Nachladen bleibt ein Fehler mit „Nochmal versuchen“ für denselben Cursor', async () => {
|
||||||
|
fx.listLogos.mockResolvedValue({ logos: [logo('a')], nextCursor: 'a' });
|
||||||
|
await mount();
|
||||||
|
fx.listLogos.mockRejectedValueOnce(Object.assign(new Error('Bad request'), { code: 400, type: 'general_query_invalid' }));
|
||||||
|
moreButton().click();
|
||||||
|
await flush();
|
||||||
|
expect(fx.listLogos).toHaveBeenCalledTimes(2);
|
||||||
|
expect(alertText()).toBe('Die Logos konnten nicht geladen werden.');
|
||||||
|
expect(names()).toEqual(['Kein Logo', 'Logo A']);
|
||||||
|
expect(retryButton().hidden).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('beim ersten Laden einer Ansicht gibt es keinen Cursor: derselbe Fehlertyp dort ist ein gewöhnlicher Ladefehler, kein Neuladen im Kreis', async () => {
|
||||||
|
fx.listLogos.mockRejectedValue(cursorGone());
|
||||||
|
await mount();
|
||||||
|
expect(fx.listLogos).toHaveBeenCalledTimes(1);
|
||||||
|
expect(alertText()).toBe('Die Logos konnten nicht geladen werden.');
|
||||||
|
expect(retryButton().hidden).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Auswahl', () => {
|
describe('Auswahl', () => {
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ export interface LogoPicker {
|
|||||||
|
|
||||||
const LOAD_ERROR = msg('Die Logos konnten nicht geladen werden.');
|
const LOAD_ERROR = msg('Die Logos konnten nicht geladen werden.');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Das Logo, hinter dem „Mehr laden“ ansetzt (der Cursor), gibt es nicht mehr, weil jemand es gelöscht oder ausgeblendet hat: Appwrite
|
||||||
|
* antwortet mit 400 `general_cursor_not_found`. Ein Wiederholen mit demselben Cursor scheitert dann immer wieder.
|
||||||
|
*/
|
||||||
|
function isCursorGone(err: unknown): boolean {
|
||||||
|
return typeof err === 'object' && err !== null && (err as { type?: unknown }).type === 'general_cursor_not_found';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Die Ansichten des Umschalters. „Beliebt“ und „Neu“ sind öffentlich, „Gemeldet“ und „Ausgeblendet“ nur für Admins (der Server prüft
|
* Die Ansichten des Umschalters. „Beliebt“ und „Neu“ sind öffentlich, „Gemeldet“ und „Ausgeblendet“ nur für Admins (der Server prüft
|
||||||
* die Rechte; hier erscheinen die Knöpfe nur für sie). Die Admin-Ansichten zeigen die neuesten zuerst.
|
* die Rechte; hier erscheinen die Knöpfe nur für sie). Die Admin-Ansichten zeigen die neuesten zuerst.
|
||||||
@@ -337,9 +345,14 @@ export function createLogoPicker(handlers: { onSelect(id: string | null): void }
|
|||||||
nextCursor = page.nextCursor;
|
nextCursor = page.nextCursor;
|
||||||
addLogos(page.logos);
|
addLogos(page.logos);
|
||||||
void loadHearts(page.logos.map((logo) => logo.id));
|
void loadHearts(page.logos.map((logo) => logo.id));
|
||||||
} catch {
|
} catch (err) {
|
||||||
if (disposed || token !== loadToken) return;
|
if (disposed || token !== loadToken) return;
|
||||||
loading = false;
|
loading = false;
|
||||||
|
if (mode === 'more' && isCursorGone(err)) {
|
||||||
|
// Die Liste hat sich unter dem Cursor verändert: statt eines Fehlers, den „Nochmal versuchen“ nicht beheben kann, von vorn laden.
|
||||||
|
void load('reset');
|
||||||
|
return;
|
||||||
|
}
|
||||||
failed = mode;
|
failed = mode;
|
||||||
statusEl.textContent = t(LOAD_ERROR);
|
statusEl.textContent = t(LOAD_ERROR);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user