feat(community): „von @name“ statt Anzeigename, Benutzername vor dem Hochladen und Veröffentlichen
Logo-Kacheln, Logo-Infozeile und die Detailseite einer Community-Karte zeigen den Benutzernamen des Kontos (Hilfsfunktion byline: Profil, sonst alter Anzeigename, sonst „Anonym“). Die Namen kommen je geladener Seite in einer Abfrage nach, ein Fehler dabei bleibt ohne Fehlertext, ein eigenes Umbenennen aktualisiert die eigenen Einträge sofort. Die Felder „Anzeigename“ in den Dialogen „Logo hochladen“ und „Veröffentlichen“ fallen weg, ebenso account/display-name.ts und der localStorage-Schlüssel; der Client schickt displayName: ''. Vor dem Hochladen und Veröffentlichen verlangt requireUsername() einen Namen („Abbrechen“: es passiert nichts); antwortet der Dienst mit 403 usernameRequired, öffnet sich der Namensdialog, danach ist ein neuer Versuch möglich. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3440eb46c3
commit
56cb67be4b
26 files changed
+1247
-407
No files matched your search
@@ -4,6 +4,7 @@ const mocks = vi.hoisted(() => ({
|
||||
prepareLogo: vi.fn(),
|
||||
uploadPreparedLogo: vi.fn(),
|
||||
withLogin: vi.fn(),
|
||||
usernameDialog: vi.fn(),
|
||||
}));
|
||||
|
||||
// api.ts und with-login.ts hängen am Appwrite-Client; `prepare` und `upload` kommen meist von außen, nur die Standardwerte
|
||||
@@ -19,8 +20,9 @@ vi.mock('./prepare-logo', async (importOriginal) => ({
|
||||
prepareLogo: mocks.prepareLogo,
|
||||
}));
|
||||
vi.mock('./upload-logo', () => ({ uploadPreparedLogo: mocks.uploadPreparedLogo }));
|
||||
// Der Namensdialog ist eine Attrappe (sein eigenes Verhalten steht in account/username-dialog.test.ts).
|
||||
vi.mock('../account/username-dialog', () => ({ usernameDialog: mocks.usernameDialog }));
|
||||
|
||||
import { DISPLAY_NAME_KEY } from '../account/display-name';
|
||||
import { LOGIN_NEEDED, LoginCancelled } from '../account/with-login';
|
||||
import { ApiError } from '../community/api';
|
||||
import { setLang, t } from '../i18n/i18n';
|
||||
@@ -90,9 +92,8 @@ function typeInto(name: string, value: string): void {
|
||||
input(name).dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
function fill(values: { name?: string; displayName?: string; consent?: boolean } = {}): void {
|
||||
function fill(values: { name?: string; consent?: boolean } = {}): void {
|
||||
input('name').value = values.name ?? 'Beispielbank';
|
||||
input('displayName').value = values.displayName ?? '';
|
||||
input('consent').checked = values.consent ?? true;
|
||||
}
|
||||
|
||||
@@ -109,10 +110,10 @@ async function setup(opts: LogoDialogOptions = {}, file: File = imageFile()): Pr
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
document.body.innerHTML = '';
|
||||
for (const fn of [prepare, upload, mocks.prepareLogo, mocks.uploadPreparedLogo, mocks.withLogin]) fn.mockReset();
|
||||
for (const fn of [prepare, upload, mocks.prepareLogo, mocks.uploadPreparedLogo, mocks.withLogin, mocks.usernameDialog]) fn.mockReset();
|
||||
mocks.withLogin.mockImplementation((fn: () => Promise<unknown>) => fn());
|
||||
mocks.usernameDialog.mockResolvedValue(null);
|
||||
prepare.mockResolvedValue(prepared());
|
||||
created.length = 0;
|
||||
revoked.length = 0;
|
||||
@@ -128,15 +129,13 @@ beforeEach(() => {
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe('validateLogoForm', () => {
|
||||
const ok = { hasFile: true, name: 'Beispielbank', displayName: '', consent: true };
|
||||
const ok = { hasFile: true, name: 'Beispielbank', consent: true };
|
||||
|
||||
it('gültige Werte: kein Problem', () => {
|
||||
expect(validateLogoForm(ok)).toBeNull();
|
||||
expect(validateLogoForm({ ...ok, displayName: 'Kenso' })).toBeNull();
|
||||
});
|
||||
|
||||
it('ohne Datei', () => {
|
||||
@@ -169,19 +168,6 @@ describe('validateLogoForm', () => {
|
||||
expect(validateLogoForm({ ...ok, name })).toEqual({ field: 'name', message: 'Der Name enthält ungültige Zeichen.' });
|
||||
});
|
||||
|
||||
it('Anzeigename: höchstens 30 Zeichen, keine Steuerzeichen, leer ist erlaubt', () => {
|
||||
expect(validateLogoForm({ ...ok, displayName: 'x'.repeat(31) })).toEqual({
|
||||
field: 'displayName',
|
||||
message: 'Der Anzeigename ist zu lang (höchstens 30 Zeichen).',
|
||||
});
|
||||
expect(validateLogoForm({ ...ok, displayName: 'x'.repeat(30) })).toBeNull();
|
||||
expect(validateLogoForm({ ...ok, displayName: ' ' })).toBeNull();
|
||||
expect(validateLogoForm({ ...ok, displayName: 'Kenso' })).toEqual({
|
||||
field: 'displayName',
|
||||
message: 'Der Anzeigename enthält ungültige Zeichen.',
|
||||
});
|
||||
});
|
||||
|
||||
it('ohne Häkchen', () => {
|
||||
expect(validateLogoForm({ ...ok, consent: false })).toEqual({
|
||||
field: 'consent',
|
||||
@@ -189,12 +175,11 @@ describe('validateLogoForm', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('meldet das erste Problem in Formularreihenfolge: Datei, Name, Anzeigename, Häkchen', () => {
|
||||
const bad = { hasFile: false, name: '', displayName: 'x'.repeat(31), consent: false };
|
||||
it('meldet das erste Problem in Formularreihenfolge: Datei, Name, Häkchen', () => {
|
||||
const bad = { hasFile: false, name: '', consent: false };
|
||||
expect(validateLogoForm(bad)?.field).toBe('file');
|
||||
expect(validateLogoForm({ ...bad, hasFile: true })?.field).toBe('name');
|
||||
expect(validateLogoForm({ ...bad, hasFile: true, name: 'Bank' })?.field).toBe('displayName');
|
||||
expect(validateLogoForm({ ...bad, hasFile: true, name: 'Bank', displayName: '' })?.field).toBe('consent');
|
||||
expect(validateLogoForm({ ...bad, hasFile: true, name: 'Bank' })?.field).toBe('consent');
|
||||
});
|
||||
|
||||
it('Englisch: die Meldungen kommen in der Sprache der Seite', () => {
|
||||
@@ -202,9 +187,6 @@ describe('validateLogoForm', () => {
|
||||
expect(validateLogoForm({ ...ok, hasFile: false })?.message).toBe('Please choose a file.');
|
||||
expect(validateLogoForm({ ...ok, name: '' })?.message).toBe('Please enter a name.');
|
||||
expect(validateLogoForm({ ...ok, name: 'x'.repeat(41) })?.message).toBe('The name is too long (at most 40 characters).');
|
||||
expect(validateLogoForm({ ...ok, displayName: 'x'.repeat(31) })?.message).toBe(
|
||||
'The display name is too long (at most 30 characters).',
|
||||
);
|
||||
expect(validateLogoForm({ ...ok, consent: false })?.message).toBe('Please confirm that you’re allowed to share the logo.');
|
||||
});
|
||||
});
|
||||
@@ -214,12 +196,11 @@ describe('Logo-Dialog: Aufbau', () => {
|
||||
void open();
|
||||
expect(dialogEl()?.open).toBe(true);
|
||||
expect(dialogEl()!.querySelector('h2')!.textContent).toBe('Logo hochladen');
|
||||
expect([...form().querySelectorAll('input')].map((i) => i.name)).toEqual(['file', 'mono', 'name', 'displayName', 'consent']);
|
||||
expect([...form().querySelectorAll('input')].map((i) => i.name)).toEqual(['file', 'mono', 'name', 'consent']);
|
||||
expect([...form().querySelectorAll('label')].map((l) => l.textContent)).toEqual([
|
||||
'Datei (Pflicht)',
|
||||
'Einfarbig – passt sich Hell/Dunkel an',
|
||||
'Name (Pflicht)',
|
||||
'Anzeigename (optional)',
|
||||
LOGO_CONSENT_TEXT,
|
||||
]);
|
||||
expect(buttonByText('Abbrechen')).toBeTruthy();
|
||||
@@ -236,8 +217,6 @@ describe('Logo-Dialog: Aufbau', () => {
|
||||
it('beim Öffnen: Name leer, Häkchen ungesetzt, „Einfarbig“ gesperrt, keine Vorschau, Fokus im Datei-Feld', () => {
|
||||
void open();
|
||||
expect(input('name').value).toBe('');
|
||||
expect(input('displayName').value).toBe('');
|
||||
expect(input('displayName').placeholder).toBe('Anonym');
|
||||
expect(input('consent').checked).toBe(false);
|
||||
expect(input('mono').checked).toBe(false);
|
||||
expect(input('mono').disabled).toBe(true);
|
||||
@@ -246,10 +225,10 @@ describe('Logo-Dialog: Aufbau', () => {
|
||||
expect(dialogEl()!.textContent).not.toMatch(/@/);
|
||||
});
|
||||
|
||||
it('der Anzeigename ist mit dem zuletzt benutzten Namen vorbelegt', () => {
|
||||
localStorage.setItem(DISPLAY_NAME_KEY, 'Kenso');
|
||||
it('es gibt kein Feld „Anzeigename“ mehr (die Logos zeigen den Benutzernamen, Spec § 5)', () => {
|
||||
void open();
|
||||
expect(input('displayName').value).toBe('Kenso');
|
||||
expect(dialogEl()!.querySelector('input[name="displayName"]')).toBeNull();
|
||||
expect(dialogEl()!.textContent).not.toMatch(/Anzeigename|Anonym/);
|
||||
});
|
||||
|
||||
it('das Pflicht-Häkchen hat genau den Wortlaut der Spec', () => {
|
||||
@@ -273,7 +252,7 @@ describe('Logo-Dialog: Aufbau', () => {
|
||||
expect(inputs.map((i) => i.id)).not.toContain(heading.id);
|
||||
});
|
||||
|
||||
it('Englisch: Überschrift, Beschriftungen, Platzhalter und Knöpfe', () => {
|
||||
it('Englisch: Überschrift, Beschriftungen und Knöpfe', () => {
|
||||
setLang('en');
|
||||
void open();
|
||||
expect(dialogEl()!.querySelector('h2')!.textContent).toBe('Upload logo');
|
||||
@@ -281,10 +260,8 @@ describe('Logo-Dialog: Aufbau', () => {
|
||||
'File (required)',
|
||||
'Single color – adapts to light/dark mode',
|
||||
'Name (required)',
|
||||
'Display name (optional)',
|
||||
'I own the rights to this logo or have permission from the owner, and it may be shared publicly.',
|
||||
]);
|
||||
expect(input('displayName').placeholder).toBe('Anonymous');
|
||||
expect(buttonByText('Cancel')).toBeTruthy();
|
||||
expect(buttonByText('Upload')).toBeTruthy();
|
||||
});
|
||||
@@ -332,7 +309,7 @@ describe('Logo-Dialog: Datei wählen', () => {
|
||||
])('der Name wird aus dem Dateinamen ohne Endung vorbelegt: %s', async (_label, fileName, expected) => {
|
||||
await setup({}, imageFile(fileName));
|
||||
expect(input('name').value).toBe(expected);
|
||||
expect(validateLogoForm({ hasFile: true, name: input('name').value, displayName: '', consent: true })).toBeNull();
|
||||
expect(validateLogoForm({ hasFile: true, name: input('name').value, consent: true })).toBeNull();
|
||||
});
|
||||
|
||||
it('eine zweite Datei ersetzt den vorbelegten Namen, aber keinen selbst getippten', async () => {
|
||||
@@ -562,10 +539,6 @@ describe('Logo-Dialog: Prüfung vor dem Senden', () => {
|
||||
await invalid({ name: 'a\tb' }, 'Der Name enthält ungültige Zeichen.', 'name');
|
||||
});
|
||||
|
||||
it('Anzeigename länger als 30 Zeichen', async () => {
|
||||
await invalid({ displayName: 'x'.repeat(31) }, 'Der Anzeigename ist zu lang (höchstens 30 Zeichen).', 'displayName');
|
||||
});
|
||||
|
||||
it('Pflicht-Häkchen fehlt', async () => {
|
||||
await invalid({ consent: false }, 'Bitte bestätige, dass du das Logo teilen darfst.', 'consent');
|
||||
});
|
||||
@@ -582,10 +555,10 @@ describe('Logo-Dialog: Prüfung vor dem Senden', () => {
|
||||
expect(errorText()).toBe('');
|
||||
});
|
||||
|
||||
it('Grenzen sind inklusive: 40 Zeichen Name und 30 Zeichen Anzeigename gehen durch', async () => {
|
||||
it('die Grenze ist inklusive: 40 Zeichen Name gehen durch', async () => {
|
||||
upload.mockResolvedValue(logo());
|
||||
const { outcome } = await setup();
|
||||
fill({ name: 'x'.repeat(40), displayName: 'y'.repeat(30) });
|
||||
fill({ name: 'x'.repeat(40) });
|
||||
send();
|
||||
await outcome;
|
||||
expect(upload).toHaveBeenCalledOnce();
|
||||
@@ -594,7 +567,7 @@ describe('Logo-Dialog: Prüfung vor dem Senden', () => {
|
||||
it('Umlaute, Emoji und Bindestriche bleiben erlaubt', async () => {
|
||||
upload.mockResolvedValue(logo());
|
||||
const { outcome } = await setup();
|
||||
fill({ name: 'Größe 😀 – Möwe-Bank', displayName: 'Zoë' });
|
||||
fill({ name: 'Größe 😀 – Möwe-Bank' });
|
||||
send();
|
||||
await outcome;
|
||||
expect(upload).toHaveBeenCalledOnce();
|
||||
@@ -617,7 +590,6 @@ describe('Logo-Dialog: Barrierefreiheit', () => {
|
||||
it('ohne Fehler verweist kein Feld auf die Fehlerzeile; die Hinweise bleiben', () => {
|
||||
void open();
|
||||
expect(describedBy('file')).toEqual(['logo-upload-file-hint']);
|
||||
expect(describedBy('displayName')).toEqual(['logo-upload-displayName-hint']);
|
||||
expect(describedBy('name')).toEqual([]);
|
||||
expect(describedBy('consent')).toEqual([]);
|
||||
});
|
||||
@@ -634,27 +606,23 @@ describe('Logo-Dialog: Barrierefreiheit', () => {
|
||||
send();
|
||||
expect(describedBy('name')).toEqual(['logo-upload-error']);
|
||||
|
||||
fill({ displayName: 'x'.repeat(31) });
|
||||
send();
|
||||
expect(describedBy('displayName')).toEqual(['logo-upload-displayName-hint', 'logo-upload-error']);
|
||||
expect(describedBy('name')).toEqual([]);
|
||||
|
||||
fill({ consent: false });
|
||||
send();
|
||||
expect(describedBy('consent')).toEqual(['logo-upload-error']);
|
||||
expect(describedBy('displayName')).toEqual(['logo-upload-displayName-hint']);
|
||||
expect(describedBy('name')).toEqual([]); // nur das Feld mit dem aktuellen Problem
|
||||
});
|
||||
|
||||
it('ist das Feld behoben, fällt die Verknüpfung beim nächsten Senden weg und der Hinweis bleibt', async () => {
|
||||
upload.mockResolvedValue(logo());
|
||||
await setup();
|
||||
fill({ displayName: 'x'.repeat(31) });
|
||||
fill({ consent: false });
|
||||
send();
|
||||
expect(describedBy('displayName')).toEqual(['logo-upload-displayName-hint', 'logo-upload-error']);
|
||||
fill({ displayName: 'Kenso' });
|
||||
expect(describedBy('consent')).toEqual(['logo-upload-error']);
|
||||
fill({ consent: true });
|
||||
send();
|
||||
expect(describedBy('displayName')).toEqual(['logo-upload-displayName-hint']);
|
||||
expect(input('displayName').hasAttribute('aria-invalid')).toBe(false);
|
||||
expect(describedBy('consent')).toEqual([]);
|
||||
expect(input('consent').hasAttribute('aria-invalid')).toBe(false);
|
||||
expect(describedBy('file')).toEqual(['logo-upload-file-hint']); // der Hinweis bleibt
|
||||
});
|
||||
|
||||
it('eine neue Dateiwahl löscht den Fehlertext und damit auch die Markierung der Felder, die darauf verwiesen', async () => {
|
||||
@@ -671,18 +639,17 @@ describe('Logo-Dialog: Barrierefreiheit', () => {
|
||||
});
|
||||
|
||||
describe('Logo-Dialog: Senden', () => {
|
||||
it('sendet getrimmte Eingaben genau einmal, merkt den Anzeigenamen und löst mit dem Logo auf', async () => {
|
||||
it('sendet den getrimmten Namen genau einmal (ohne Anzeigenamen) und löst mit dem Logo auf', async () => {
|
||||
const p = prepared();
|
||||
prepare.mockResolvedValue(p);
|
||||
const result = logo({ id: 'L9' });
|
||||
upload.mockResolvedValue(result);
|
||||
const { outcome } = await setup();
|
||||
fill({ name: ' Meine Bank ', displayName: ' Kenso ' });
|
||||
fill({ name: ' Meine Bank ' });
|
||||
send();
|
||||
await expect(outcome).resolves.toBe(result);
|
||||
expect(upload).toHaveBeenCalledOnce();
|
||||
expect(upload).toHaveBeenCalledWith(p, { name: 'Meine Bank', displayName: 'Kenso', mono: false });
|
||||
expect(localStorage.getItem(DISPLAY_NAME_KEY)).toBe('Kenso');
|
||||
expect(upload).toHaveBeenCalledWith(p, { name: 'Meine Bank', mono: false });
|
||||
expect(dialogEl()).toBeNull();
|
||||
});
|
||||
|
||||
@@ -693,7 +660,7 @@ describe('Logo-Dialog: Senden', () => {
|
||||
fill();
|
||||
send();
|
||||
await outcome;
|
||||
expect(upload).toHaveBeenCalledWith(expect.anything(), { name: 'Beispielbank', displayName: '', mono: true });
|
||||
expect(upload).toHaveBeenCalledWith(expect.anything(), { name: 'Beispielbank', mono: true });
|
||||
});
|
||||
|
||||
it('ohne transparente Bereiche geht mono nie als true mit', async () => {
|
||||
@@ -706,18 +673,7 @@ describe('Logo-Dialog: Senden', () => {
|
||||
fill();
|
||||
send();
|
||||
await outcome;
|
||||
expect(upload).toHaveBeenCalledWith(expect.anything(), { name: 'Beispielbank', displayName: '', mono: false });
|
||||
});
|
||||
|
||||
it('ein geleerter Anzeigename entfernt den gemerkten (Anonym)', async () => {
|
||||
localStorage.setItem(DISPLAY_NAME_KEY, 'Alt');
|
||||
upload.mockResolvedValue(logo());
|
||||
const { outcome } = await setup();
|
||||
expect(input('displayName').value).toBe('Alt');
|
||||
fill({ displayName: '' });
|
||||
send();
|
||||
await outcome;
|
||||
expect(localStorage.getItem(DISPLAY_NAME_KEY)).toBeNull();
|
||||
expect(upload).toHaveBeenCalledWith(expect.anything(), { name: 'Beispielbank', mono: false });
|
||||
});
|
||||
|
||||
it('ein Doppelklick auf „Hochladen“ sendet nicht zweimal', async () => {
|
||||
@@ -744,7 +700,7 @@ describe('Logo-Dialog: Senden', () => {
|
||||
send();
|
||||
expect(buttonByText('Hochladen')!.disabled).toBe(true);
|
||||
expect(buttonByText('Abbrechen')!.disabled).toBe(true);
|
||||
for (const name of ['file', 'mono', 'name', 'displayName', 'consent']) expect(input(name).disabled).toBe(true);
|
||||
for (const name of ['file', 'mono', 'name', 'consent']) expect(input(name).disabled).toBe(true);
|
||||
expect(form().getAttribute('aria-busy')).toBe('true');
|
||||
expect(statusText()).toBe('Wird hochgeladen …');
|
||||
|
||||
@@ -752,7 +708,7 @@ describe('Logo-Dialog: Senden', () => {
|
||||
await settle();
|
||||
expect(buttonByText('Hochladen')!.disabled).toBe(false);
|
||||
expect(buttonByText('Abbrechen')!.disabled).toBe(false);
|
||||
for (const name of ['file', 'mono', 'name', 'displayName', 'consent']) expect(input(name).disabled).toBe(false);
|
||||
for (const name of ['file', 'mono', 'name', 'consent']) expect(input(name).disabled).toBe(false);
|
||||
expect(form().getAttribute('aria-busy')).toBe('false');
|
||||
expect(statusText()).toBe('');
|
||||
});
|
||||
@@ -818,18 +774,16 @@ describe('Logo-Dialog: Senden', () => {
|
||||
),
|
||||
);
|
||||
const { outcome } = await setup();
|
||||
fill({ displayName: 'Kenso' });
|
||||
fill({ name: 'Meine Bank' });
|
||||
send();
|
||||
await settle();
|
||||
expect(errorText()).toBe(
|
||||
'Hochladen fehlgeschlagen – Du hast schon 30 Logos hochgeladen. Lösche eins, um ein neues hochzuladen.',
|
||||
);
|
||||
expect(dialogEl()?.open).toBe(true);
|
||||
// Die Eingaben bleiben, und der Anzeigename wird erst bei Erfolg gemerkt.
|
||||
expect(input('name').value).toBe('Beispielbank');
|
||||
expect(input('displayName').value).toBe('Kenso');
|
||||
// Die Eingaben bleiben.
|
||||
expect(input('name').value).toBe('Meine Bank');
|
||||
expect(input('consent').checked).toBe(true);
|
||||
expect(localStorage.getItem(DISPLAY_NAME_KEY)).toBeNull();
|
||||
|
||||
upload.mockResolvedValueOnce(logo());
|
||||
send();
|
||||
@@ -837,7 +791,6 @@ describe('Logo-Dialog: Senden', () => {
|
||||
expect(upload).toHaveBeenCalledTimes(2);
|
||||
expect(upload.mock.calls[1][0]).toBe(upload.mock.calls[0][0]); // dasselbe fertige Logo, nicht neu verarbeitet
|
||||
expect(prepare).toHaveBeenCalledOnce();
|
||||
expect(localStorage.getItem(DISPLAY_NAME_KEY)).toBe('Kenso');
|
||||
});
|
||||
|
||||
it('Fehlertext des Dienstes in der Sprache der Seite (nach Fehlerschlüssel)', async () => {
|
||||
@@ -883,6 +836,118 @@ describe('Logo-Dialog: Senden', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Logo-Dialog: Benutzername nötig (403 usernameRequired)', () => {
|
||||
const required = () => new ApiError(403, 'Bitte wähle zuerst einen Benutzernamen.', true, 'usernameRequired');
|
||||
const REASON = 'Hochladen fehlgeschlagen – Bitte wähle zuerst einen Benutzernamen.';
|
||||
|
||||
it('der Namensdialog „required“ öffnet sich (mit dem Signal der Seite); der Dialog bleibt offen und nennt den Grund', async () => {
|
||||
mocks.usernameDialog.mockReturnValue(new Promise(() => {})); // der Nutzer überlegt noch
|
||||
upload.mockRejectedValueOnce(required());
|
||||
const controller = new AbortController();
|
||||
await setup({ signal: controller.signal });
|
||||
fill();
|
||||
send();
|
||||
await settle();
|
||||
expect(mocks.usernameDialog).toHaveBeenCalledExactlyOnceWith({ mode: 'required', signal: controller.signal });
|
||||
expect(dialogEl()?.open).toBe(true);
|
||||
expect(errorText()).toBe(REASON);
|
||||
expect(buttonByText('Hochladen')!.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('nach dem gespeicherten Namen ist der Grund weg, die Eingaben und das Logo bleiben, der Fokus liegt auf „Hochladen“ und ein neuer Versuch geht durch', async () => {
|
||||
mocks.usernameDialog.mockResolvedValue('Kenso');
|
||||
upload.mockRejectedValueOnce(required());
|
||||
const { outcome } = await setup();
|
||||
fill({ name: 'Meine Bank' });
|
||||
send();
|
||||
await settle();
|
||||
expect(errorText()).toBe('');
|
||||
expect(input('name').value).toBe('Meine Bank');
|
||||
expect(input('consent').checked).toBe(true);
|
||||
expect(previews()).toHaveLength(2); // das verarbeitete Logo steht noch
|
||||
expect(dialogEl()?.open).toBe(true);
|
||||
expect(document.activeElement).toBe(buttonByText('Hochladen'));
|
||||
|
||||
upload.mockResolvedValueOnce(logo());
|
||||
send();
|
||||
await expect(outcome).resolves.toMatchObject({ id: 'L1' });
|
||||
expect(upload).toHaveBeenCalledTimes(2);
|
||||
expect(upload.mock.calls[1][0]).toBe(upload.mock.calls[0][0]);
|
||||
expect(prepare).toHaveBeenCalledOnce();
|
||||
expect(mocks.usernameDialog).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('„Abbrechen“ im Namensdialog: der Grund bleibt stehen, nichts ist hochgeladen, ein neuer Versuch fragt der Dienst wieder', async () => {
|
||||
mocks.usernameDialog.mockResolvedValue(null);
|
||||
upload.mockRejectedValue(required());
|
||||
await setup();
|
||||
fill();
|
||||
send();
|
||||
await settle();
|
||||
expect(errorText()).toBe(REASON);
|
||||
expect(dialogEl()?.open).toBe(true);
|
||||
send();
|
||||
await settle();
|
||||
expect(upload).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.usernameDialog).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('lässt sich der Namensdialog nicht öffnen, bleibt der Grund stehen und nichts wirft', async () => {
|
||||
mocks.usernameDialog.mockRejectedValue(new Error('showModal kaputt'));
|
||||
upload.mockRejectedValueOnce(required());
|
||||
await setup();
|
||||
fill();
|
||||
send();
|
||||
await settle();
|
||||
expect(errorText()).toBe(REASON);
|
||||
expect(dialogEl()?.open).toBe(true);
|
||||
expect(buttonByText('Hochladen')!.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('wird der Dialog währenddessen geschlossen (Seite verlassen), bleibt die Antwort des Namensdialogs folgenlos', async () => {
|
||||
let answer!: (name: string | null) => void;
|
||||
mocks.usernameDialog.mockReturnValue(new Promise<string | null>((resolve) => (answer = resolve)));
|
||||
upload.mockRejectedValueOnce(required());
|
||||
const controller = new AbortController();
|
||||
const { outcome } = await setup({ signal: controller.signal });
|
||||
fill();
|
||||
send();
|
||||
await settle();
|
||||
controller.abort();
|
||||
await expect(outcome).resolves.toBeNull();
|
||||
answer('Kenso');
|
||||
await settle();
|
||||
expect(dialogEl()).toBeNull();
|
||||
expect(upload).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('andere Fehler (auch andere 403) öffnen den Namensdialog nicht', async () => {
|
||||
upload.mockRejectedValueOnce(new ApiError(403, 'Dazu hast du keine Berechtigung.', true, 'forbidden'));
|
||||
await setup();
|
||||
fill();
|
||||
send();
|
||||
await settle();
|
||||
upload.mockRejectedValueOnce(new ApiError(503, 'Der Dienst ist gerade nicht erreichbar.', false, 'unavailable'));
|
||||
send();
|
||||
await settle();
|
||||
upload.mockRejectedValueOnce(new TypeError('Failed to fetch'));
|
||||
send();
|
||||
await settle();
|
||||
expect(mocks.usernameDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Englisch: der Grund erscheint auf Englisch', async () => {
|
||||
setLang('en');
|
||||
mocks.usernameDialog.mockResolvedValue(null);
|
||||
upload.mockRejectedValueOnce(required());
|
||||
await setup();
|
||||
fill();
|
||||
send();
|
||||
await settle();
|
||||
expect(errorText()).toBe('Upload failed – Please choose a username first.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Logo-Dialog: Standardwerte', () => {
|
||||
it('ohne prepare und upload: prepareLogo, und uploadPreparedLogo über withLogin (abgelaufene Sitzung → Anmeldung)', async () => {
|
||||
const p = prepared();
|
||||
@@ -894,11 +959,11 @@ describe('Logo-Dialog: Standardwerte', () => {
|
||||
choose(file);
|
||||
await settle();
|
||||
expect(mocks.prepareLogo).toHaveBeenCalledExactlyOnceWith(file);
|
||||
fill({ displayName: 'Kenso' });
|
||||
fill();
|
||||
send();
|
||||
await expect(outcome).resolves.toBe(result);
|
||||
expect(mocks.withLogin).toHaveBeenCalledOnce();
|
||||
expect(mocks.uploadPreparedLogo).toHaveBeenCalledExactlyOnceWith(p, { name: 'Beispielbank', displayName: 'Kenso', mono: false });
|
||||
expect(mocks.uploadPreparedLogo).toHaveBeenCalledExactlyOnceWith(p, { name: 'Beispielbank', mono: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -911,7 +976,6 @@ describe('Logo-Dialog: Schließen', () => {
|
||||
expect(upload).not.toHaveBeenCalled();
|
||||
expect(dialogEl()).toBeNull();
|
||||
expect(revoked).toEqual(['blob:logo-1']);
|
||||
expect(localStorage.getItem(DISPLAY_NAME_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('Esc (close-Ereignis) und Klick auf den Hintergrund schließen; ohne Senden ist Esc nicht verhindert', async () => {
|
||||
@@ -941,7 +1005,7 @@ describe('Logo-Dialog: Schließen', () => {
|
||||
let finish!: (l: CommunityLogo) => void;
|
||||
upload.mockReturnValue(new Promise((resolve) => (finish = resolve)));
|
||||
const { outcome } = await setup({ signal: controller.signal });
|
||||
fill({ displayName: 'Kenso' });
|
||||
fill();
|
||||
send();
|
||||
controller.abort();
|
||||
await expect(outcome).resolves.toBeNull();
|
||||
@@ -950,7 +1014,6 @@ describe('Logo-Dialog: Schließen', () => {
|
||||
finish(logo());
|
||||
await settle();
|
||||
expect(dialogEl()).toBeNull();
|
||||
expect(localStorage.getItem(DISPLAY_NAME_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('ein Fehler nach dem Schließen durch das Signal wird verschluckt', async () => {
|
||||
|
||||
Reference in new issue
Block a user