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:
ClaudeandClaude Opus 5.5 committed 2026-10-05 01:12:59 +00:00
1 parent 202254c09e
commit ec4e80c62c
2 files changed
+111 -1

No files matched your search

+97
View File
@@ -460,6 +460,103 @@ describe('Mehr laden', () => {
expect(alertText()).toBe('');
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', () => {