fix(card): fehlendes Logo bricht nur ab, wenn die Vorlage es zeichnet; Download aus „Meine Karten“ meldet es
Entscheidung des Controllers zu Task 5: Ein fehlendes Community-Logo ist beim Rendern einer gespeicherten Karte nur dann ein Problem, wenn die Vorlage es zeichnet (layoutFor(template).logo). Bei „Nur Bild“ wird ein Logo nie gezeichnet, dort fehlt nichts. Der Editor-Hinweis (Auswahlzustand) und der Download im Editor bleiben unverändert. - community/publish.ts: neue gemeinsame Prüfung assertLogoAvailable(config, assets); sie wirft LOGO_MISSING_PUBLISH, wenn assets.logoMissing gilt und die Vorlage das Logo zeichnet (minimal, classic, signature). renderForGallery nutzt sie: bei einer zeichnenden Vorlage Abbruch vor dem Rendern (nichts hochgeladen), bei „Nur Bild“ wird normal gerendert und veröffentlicht. - pages/cards-page.ts: der Download einer gespeicherten Karte ruft dieselbe Prüfung. Statt stillschweigend ohne Logo herunterzuladen, zeigt die Kachel den Text LOGO_MISSING_PUBLISH über den vorhandenen Fehlermechanismus (run() und failureText() schreiben in .card-msg), übersetzt mit t(); es wird nichts exportiert oder gespeichert. Bei „Nur Bild“ läuft der Download normal. - Tests: publish.test.ts (classic/minimal/signature mit fehlendem Logo brechen ab, „Nur Bild“ veröffentlicht normal, Wahrheitstabelle für assertLogoAvailable) und cards-page.test.ts (Download mit fehlendem Logo: Meldung, kein Download; „Nur Bild“ und vorhandenes Logo: Download wie sonst; zweiter Versuch; Englisch). Vitest 45 Dateien, 1338 Tests grün, tsc und Build sauber. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0be5abd954
commit
8205f7c37f
4 files changed
+146
-13
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { DEFAULT_CONFIG } from '../card/config';
|
import { DEFAULT_CONFIG, type TemplateId } from '../card/config';
|
||||||
import type { SavedCard } from '../account/cards';
|
import type { SavedCard } from '../account/cards';
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
@@ -30,6 +30,7 @@ import { ApiError, publishCard } from './api';
|
|||||||
import {
|
import {
|
||||||
LOGO_MISSING_PUBLISH,
|
LOGO_MISSING_PUBLISH,
|
||||||
PNG_MAX_BYTES,
|
PNG_MAX_BYTES,
|
||||||
|
assertLogoAvailable,
|
||||||
normalizeTags,
|
normalizeTags,
|
||||||
publishSavedCard,
|
publishSavedCard,
|
||||||
renderForGallery,
|
renderForGallery,
|
||||||
@@ -263,7 +264,12 @@ describe('publishSavedCard', () => {
|
|||||||
describe('Logo der Karte beim Veröffentlichen', () => {
|
describe('Logo der Karte beim Veröffentlichen', () => {
|
||||||
const photo = { source: {}, w: 3000, h: 2000 };
|
const photo = { source: {}, w: 3000, h: 2000 };
|
||||||
const source = new Blob(['src'], { type: 'image/jpeg' });
|
const source = new Blob(['src'], { type: 'image/jpeg' });
|
||||||
const withLogo = (logo: string | null): SavedCard => ({ ...card, config: { ...DEFAULT_CONFIG, logo } });
|
const withLogo = (logo: string | null, template: TemplateId = DEFAULT_CONFIG.template): SavedCard => ({
|
||||||
|
...card,
|
||||||
|
config: { ...DEFAULT_CONFIG, template, logo },
|
||||||
|
});
|
||||||
|
/** Die Vorlagen, die das Community-Logo zeichnen (alle außer „Nur Bild“). */
|
||||||
|
const DRAWING = ['minimal', 'classic', 'signature'] as const;
|
||||||
const assetsFor = (patch: Record<string, unknown> = {}) => ({ image: photo, logo: null, brand: null, logoMissing: false, ...patch });
|
const assetsFor = (patch: Record<string, unknown> = {}) => ({ image: photo, logo: null, brand: null, logoMissing: false, ...patch });
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -304,10 +310,21 @@ describe('Logo der Karte beim Veröffentlichen', () => {
|
|||||||
expect(out.png.type).toBe('image/png');
|
expect(out.png.type).toBe('image/png');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fehlendes Logo (gelöscht oder ausgeblendet): Abbruch mit LOGO_MISSING_PUBLISH, es wird nichts gerendert', async () => {
|
it.each(DRAWING)(
|
||||||
|
'fehlendes Logo (gelöscht oder ausgeblendet), Vorlage %s zeichnet es: Abbruch mit LOGO_MISSING_PUBLISH, es wird nichts gerendert',
|
||||||
|
async (template) => {
|
||||||
mocks.loadCardAssets.mockResolvedValue(assetsFor({ logoMissing: true }));
|
mocks.loadCardAssets.mockResolvedValue(assetsFor({ logoMissing: true }));
|
||||||
await expect(renderForGallery(withLogo('gone'), source)).rejects.toThrow(LOGO_MISSING_PUBLISH);
|
await expect(renderForGallery(withLogo('gone', template), source)).rejects.toThrow(LOGO_MISSING_PUBLISH);
|
||||||
expect(mocks.exportCard).not.toHaveBeenCalled();
|
expect(mocks.exportCard).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('fehlendes Logo bei „Nur Bild“ (image): die Vorlage zeichnet es nie, es wird ganz normal gerendert', async () => {
|
||||||
|
mocks.loadCardAssets.mockResolvedValue(assetsFor({ logoMissing: true }));
|
||||||
|
const out = await renderForGallery(withLogo('gone', 'image'), source);
|
||||||
|
expect(mocks.exportCard).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mocks.exportCard.mock.calls[0][2]).toEqual({ width: 1536, type: 'image/png' });
|
||||||
|
expect([out.png.type, out.preview.type]).toEqual(['image/png', 'image/webp']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ein Ladefehler der Logos (Netz) ist kein fehlendes Logo: der Fehler kommt unverändert an', async () => {
|
it('ein Ladefehler der Logos (Netz) ist kein fehlendes Logo: der Fehler kommt unverändert an', async () => {
|
||||||
@@ -316,16 +333,24 @@ describe('Logo der Karte beim Veröffentlichen', () => {
|
|||||||
await expect(renderForGallery(withLogo('abc123'), source)).rejects.toBe(failure);
|
await expect(renderForGallery(withLogo('abc123'), source)).rejects.toBe(failure);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('publishSavedCard: mit fehlendem Logo wird nichts hochgeladen, nichts gelöscht und die API nicht gerufen', async () => {
|
it('publishSavedCard: classic mit fehlendem Logo – nichts wird hochgeladen, nichts gelöscht, die API nicht gerufen', async () => {
|
||||||
mocks.loadCardAssets.mockResolvedValue(assetsFor({ logoMissing: true }));
|
mocks.loadCardAssets.mockResolvedValue(assetsFor({ logoMissing: true }));
|
||||||
const fake = fakeDeps({ render: renderForGallery });
|
const fake = fakeDeps({ render: renderForGallery });
|
||||||
await expect(publishSavedCard(withLogo('gone'), form, fake.deps)).rejects.toThrow(LOGO_MISSING_PUBLISH);
|
await expect(publishSavedCard(withLogo('gone', 'classic'), form, fake.deps)).rejects.toThrow(LOGO_MISSING_PUBLISH);
|
||||||
expect(fake.names()).toEqual(['userId', 'loadImage']);
|
expect(fake.names()).toEqual(['userId', 'loadImage']);
|
||||||
expect(fake.of('createFile')).toHaveLength(0);
|
expect(fake.of('createFile')).toHaveLength(0);
|
||||||
expect(fake.of('deleteFile')).toHaveLength(0);
|
expect(fake.of('deleteFile')).toHaveLength(0);
|
||||||
expect(fake.of('publishCard')).toHaveLength(0);
|
expect(fake.of('publishCard')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('publishSavedCard: „Nur Bild“ (image) mit fehlendem Logo – wird veröffentlicht, als wäre nichts', async () => {
|
||||||
|
mocks.loadCardAssets.mockResolvedValue(assetsFor({ logoMissing: true }));
|
||||||
|
const fake = fakeDeps({ render: renderForGallery });
|
||||||
|
await expect(publishSavedCard(withLogo('gone', 'image'), form, fake.deps)).resolves.toEqual({ id: 'g1' });
|
||||||
|
expect(fake.names()).toEqual(['userId', 'loadImage', 'createFile', 'createFile', 'publishCard']);
|
||||||
|
expect(fake.of('deleteFile')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('publishSavedCard: mit vorhandenem Logo läuft alles wie sonst', async () => {
|
it('publishSavedCard: mit vorhandenem Logo läuft alles wie sonst', async () => {
|
||||||
const fake = fakeDeps({ render: renderForGallery });
|
const fake = fakeDeps({ render: renderForGallery });
|
||||||
await expect(publishSavedCard(withLogo('abc123'), form, fake.deps)).resolves.toEqual({ id: 'g1' });
|
await expect(publishSavedCard(withLogo('abc123'), form, fake.deps)).resolves.toEqual({ id: 'g1' });
|
||||||
@@ -333,6 +358,23 @@ describe('Logo der Karte beim Veröffentlichen', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('assertLogoAvailable', () => {
|
||||||
|
const config = (template: TemplateId) => ({ ...DEFAULT_CONFIG, template, logo: 'abc123' });
|
||||||
|
const assets = (logoMissing: boolean) => ({ image: null, logo: null, brand: null, logoMissing });
|
||||||
|
|
||||||
|
it.each(['minimal', 'classic', 'signature'] as const)('Logo fehlt und Vorlage %s zeichnet es: wirft LOGO_MISSING_PUBLISH', (template) => {
|
||||||
|
expect(() => assertLogoAvailable(config(template), assets(true))).toThrow(LOGO_MISSING_PUBLISH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Logo fehlt, aber „Nur Bild“ zeichnet nie ein Logo: wirft nicht', () => {
|
||||||
|
expect(() => assertLogoAvailable(config('image'), assets(true))).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['image', 'minimal', 'classic', 'signature'] as const)('Logo ist da (oder keins gewählt): Vorlage %s wirft nicht', (template) => {
|
||||||
|
expect(() => assertLogoAvailable(config(template), assets(false))).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('mit der echten API-Schicht (publishCard, apiPost) und gestubbtem fetch', () => {
|
describe('mit der echten API-Schicht (publishCard, apiPost) und gestubbtem fetch', () => {
|
||||||
const json = (status: number, body: unknown) =>
|
const json = (status: number, body: unknown) =>
|
||||||
new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
|
new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
import { ID, Permission, Role } from 'appwrite';
|
import { ID, Permission, Role } from 'appwrite';
|
||||||
import { IDS, storage } from '../account/appwrite';
|
import { IDS, storage } from '../account/appwrite';
|
||||||
import { cardsService, requireUserId, toUploadFile, type SavedCard } from '../account/cards';
|
import { cardsService, requireUserId, toUploadFile, type SavedCard } from '../account/cards';
|
||||||
import { CARD_W, PREVIEW_QUALITY, PREVIEW_WIDTH } from '../card/geometry';
|
import type { CardConfig } from '../card/config';
|
||||||
|
import { CARD_W, PREVIEW_QUALITY, PREVIEW_WIDTH, layoutFor } from '../card/geometry';
|
||||||
import { clampCrop } from '../card/crop';
|
import { clampCrop } from '../card/crop';
|
||||||
import { exportCard, loadCardAssets } from '../card/render';
|
import { exportCard, loadCardAssets, type LoadedAssets } from '../card/render';
|
||||||
import { decodeImage } from '../editor/upload';
|
import { decodeImage } from '../editor/upload';
|
||||||
import { msg } from '../i18n/i18n';
|
import { msg } from '../i18n/i18n';
|
||||||
import { isDefinitiveFailure, publishCard, type PublishInput } from './api';
|
import { isDefinitiveFailure, publishCard, type PublishInput } from './api';
|
||||||
@@ -15,7 +16,7 @@ import { isDefinitiveFailure, publishCard, type PublishInput } from './api';
|
|||||||
export const PNG_MAX_BYTES = 5_242_880;
|
export const PNG_MAX_BYTES = 5_242_880;
|
||||||
export const TOO_LARGE = 'Das Kartenbild ist zu groß für die Galerie (max. 5 MB).';
|
export const TOO_LARGE = 'Das Kartenbild ist zu groß für die Galerie (max. 5 MB).';
|
||||||
export const NOT_RENDERED = 'Das Kartenbild konnte nicht erzeugt werden.';
|
export const NOT_RENDERED = 'Das Kartenbild konnte nicht erzeugt werden.';
|
||||||
/** Die Karte wählt ein Logo, das gelöscht oder ausgeblendet wurde: veröffentlicht wird sie nicht ohne dieses Logo. */
|
/** Die Karte wählt ein Logo, das gelöscht oder ausgeblendet wurde: ohne dieses Logo wird sie weder veröffentlicht noch heruntergeladen. */
|
||||||
export const LOGO_MISSING_PUBLISH = msg(
|
export const LOGO_MISSING_PUBLISH = msg(
|
||||||
'Das Logo dieser Karte ist nicht mehr verfügbar. Öffne die Karte im Editor und wähle ein anderes.',
|
'Das Logo dieser Karte ist nicht mehr verfügbar. Öffne die Karte im Editor und wähle ein anderes.',
|
||||||
);
|
);
|
||||||
@@ -52,12 +53,22 @@ export function normalizeTags(input: string): string[] {
|
|||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft nach dem Laden der Bilder einer gespeicherten Karte, ob ihr beim Rendern etwas fehlt (Veröffentlichen und Herunterladen
|
||||||
|
* aus „Meine Karten“): Gibt es das gewählte Community-Logo nicht mehr (gelöscht, ausgeblendet) und zeichnet die Vorlage es, wirft
|
||||||
|
* das LOGO_MISSING_PUBLISH. So wird nie eine andere Karte veröffentlicht oder heruntergeladen, als der Nutzer sie gestaltet hat.
|
||||||
|
* Bei „Nur Bild“ wird ein Logo nie gezeichnet: dort fehlt nichts, und es wird ganz normal gerendert.
|
||||||
|
*/
|
||||||
|
export function assertLogoAvailable(config: CardConfig, assets: LoadedAssets): void {
|
||||||
|
if (assets.logoMissing && layoutFor(config.template).logo) throw new Error(LOGO_MISSING_PUBLISH);
|
||||||
|
}
|
||||||
|
|
||||||
export async function renderForGallery(card: SavedCard, imageBlob: Blob): Promise<{ png: Blob; preview: Blob }> {
|
export async function renderForGallery(card: SavedCard, imageBlob: Blob): Promise<{ png: Blob; preview: Blob }> {
|
||||||
const image = await decodeImage(imageBlob);
|
const image = await decodeImage(imageBlob);
|
||||||
const config = { ...card.config, crop: clampCrop(image, card.config.crop) };
|
const config = { ...card.config, crop: clampCrop(image, card.config.crop) };
|
||||||
const assets = await loadCardAssets(config, image);
|
const assets = await loadCardAssets(config, image);
|
||||||
// Ohne das gewählte Logo würde eine andere Karte veröffentlicht, als der Nutzer sie gestaltet hat: lieber abbrechen.
|
// Lieber abbrechen, als eine Karte ohne ihr Logo zu veröffentlichen.
|
||||||
if (assets.logoMissing) throw new Error(LOGO_MISSING_PUBLISH);
|
assertLogoAvailable(config, assets);
|
||||||
const png = await exportCard(config, assets, { width: CARD_W, type: 'image/png' });
|
const png = await exportCard(config, assets, { width: CARD_W, type: 'image/png' });
|
||||||
const preview = await exportCard(config, assets, { width: PREVIEW_WIDTH, type: 'image/webp', quality: PREVIEW_QUALITY });
|
const preview = await exportCard(config, assets, { width: PREVIEW_WIDTH, type: 'image/webp', quality: PREVIEW_QUALITY });
|
||||||
return { png, preview };
|
return { png, preview };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import type { User } from '../account/auth';
|
import type { User } from '../account/auth';
|
||||||
import type { SavedCard } from '../account/cards';
|
import type { SavedCard } from '../account/cards';
|
||||||
import { DEFAULT_CONFIG } from '../card/config';
|
import { DEFAULT_CONFIG, type TemplateId } from '../card/config';
|
||||||
import { createStore, initialEditorState } from '../editor/state';
|
import { createStore, initialEditorState } from '../editor/state';
|
||||||
import type { GalleryCard } from '../community/gallery';
|
import type { GalleryCard } from '../community/gallery';
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ vi.mock('../community/api', async (importOriginal) => ({
|
|||||||
vi.mock('../community/gallery', () => ({ myPublications: mocks.myPublications }));
|
vi.mock('../community/gallery', () => ({ myPublications: mocks.myPublications }));
|
||||||
|
|
||||||
import { ApiError } from '../community/api';
|
import { ApiError } from '../community/api';
|
||||||
|
import { LOGO_MISSING_PUBLISH } from '../community/publish';
|
||||||
import { mountCardsPage } from './cards-page';
|
import { mountCardsPage } from './cards-page';
|
||||||
|
|
||||||
const user: User = { $id: 'u1', email: 'a@b.de' };
|
const user: User = { $id: 'u1', email: 'a@b.de' };
|
||||||
@@ -301,6 +302,80 @@ describe('Meine Karten: Öffnen und Herunterladen', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Meine Karten: Herunterladen mit fehlendem Logo', () => {
|
||||||
|
const image = { source: {}, w: 3000, h: 2000 };
|
||||||
|
const png = new Blob(['png'], { type: 'image/png' });
|
||||||
|
/** Eine Karte, die ein Logo wählt, das es nicht mehr gibt (loadCardAssets meldet logoMissing). */
|
||||||
|
const onTemplate = (template: TemplateId) =>
|
||||||
|
card('a', 'Alpha', { config: { ...structuredClone(DEFAULT_CONFIG), template, logo: 'gone' } });
|
||||||
|
const msg = () => tileFor('a').querySelector('.card-msg')!;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.loadImage.mockResolvedValue(new Blob(['i'], { type: 'image/jpeg' }));
|
||||||
|
mocks.decodeImage.mockResolvedValue(image);
|
||||||
|
mocks.loadCardAssets.mockResolvedValue({ image, logo: null, brand: null, logoMissing: true });
|
||||||
|
mocks.exportCard.mockResolvedValue(png);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function download(template: TemplateId): Promise<void> {
|
||||||
|
mocks.list.mockResolvedValue([onTemplate(template)]);
|
||||||
|
mount();
|
||||||
|
await vi.waitFor(() => expect(tiles()).toHaveLength(1));
|
||||||
|
button(tileFor('a'), 'Herunterladen').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
it.each(['minimal', 'classic', 'signature'] as const)(
|
||||||
|
'Vorlage %s zeichnet das Logo: die Meldung steht an der Kachel, es wird nichts heruntergeladen',
|
||||||
|
async (template) => {
|
||||||
|
await download(template);
|
||||||
|
await vi.waitFor(() => expect(msg().textContent).toBe(LOGO_MISSING_PUBLISH));
|
||||||
|
expect(msg().textContent).toBe(
|
||||||
|
'Das Logo dieser Karte ist nicht mehr verfügbar. Öffne die Karte im Editor und wähle ein anderes.',
|
||||||
|
);
|
||||||
|
expect(msg().classList.contains('is-warning')).toBe(false);
|
||||||
|
expect(mocks.exportCard).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.saveBlobAs).not.toHaveBeenCalled();
|
||||||
|
// Der Knopf ist danach wieder bedienbar und trägt seine Beschriftung.
|
||||||
|
const again = button(tileFor('a'), 'Herunterladen');
|
||||||
|
expect(again.disabled).toBe(false);
|
||||||
|
expect(tileFor('a').getAttribute('aria-busy')).toBeNull();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('„Nur Bild“ (image) zeichnet nie ein Logo: der Download läuft ganz normal, ohne Meldung', async () => {
|
||||||
|
await download('image');
|
||||||
|
await vi.waitFor(() => expect(mocks.saveBlobAs).toHaveBeenCalledWith(png, 'aircard-alpha.png'));
|
||||||
|
expect(mocks.exportCard).toHaveBeenCalledTimes(1);
|
||||||
|
expect(msg().textContent).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nach der Meldung geht ein zweiter Versuch (Logo wieder da) durch, die Meldung ist dann weg', async () => {
|
||||||
|
await download('classic');
|
||||||
|
await vi.waitFor(() => expect(msg().textContent).toBe(LOGO_MISSING_PUBLISH));
|
||||||
|
mocks.loadCardAssets.mockResolvedValue({ image, logo: { source: {}, w: 200, h: 100 }, brand: null, logoMissing: false });
|
||||||
|
button(tileFor('a'), 'Herunterladen').click();
|
||||||
|
await vi.waitFor(() => expect(mocks.saveBlobAs).toHaveBeenCalledTimes(1));
|
||||||
|
expect(msg().textContent).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Englisch: dieselbe Meldung, übersetzt', async () => {
|
||||||
|
const { setLang } = await import('../i18n/i18n');
|
||||||
|
setLang('en', { persist: false });
|
||||||
|
await download('signature');
|
||||||
|
await vi.waitFor(() =>
|
||||||
|
expect(msg().textContent).toBe('This card’s logo is no longer available. Open the card in the editor and choose another one.'),
|
||||||
|
);
|
||||||
|
expect(mocks.saveBlobAs).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('eine Karte ohne gewähltes Logo wird normal heruntergeladen', async () => {
|
||||||
|
mocks.loadCardAssets.mockResolvedValue({ image, logo: null, brand: null, logoMissing: false });
|
||||||
|
await download('classic');
|
||||||
|
await vi.waitFor(() => expect(mocks.saveBlobAs).toHaveBeenCalledWith(png, 'aircard-alpha.png'));
|
||||||
|
expect(msg().textContent).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Meine Karten: Sitzung weg, Appwrite antwortet mit 404 statt 401', () => {
|
describe('Meine Karten: Sitzung weg, Appwrite antwortet mit 404 statt 401', () => {
|
||||||
const fileGone = () => Object.assign(new Error('File not found'), { code: 404, type: 'storage_file_not_found' });
|
const fileGone = () => Object.assign(new Error('File not found'), { code: 404, type: 'storage_file_not_found' });
|
||||||
const rowGone = () => Object.assign(new Error('Row not found'), { code: 404, type: 'row_not_found' });
|
const rowGone = () => Object.assign(new Error('Row not found'), { code: 404, type: 'row_not_found' });
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ import { CARD_W } from '../card/geometry';
|
|||||||
import { exportCard, loadCardAssets } from '../card/render';
|
import { exportCard, loadCardAssets } from '../card/render';
|
||||||
import { ApiError, isAlreadyPublished, isDefinitiveFailure, withdrawCard } from '../community/api';
|
import { ApiError, isAlreadyPublished, isDefinitiveFailure, withdrawCard } from '../community/api';
|
||||||
import { myPublications } from '../community/gallery';
|
import { myPublications } from '../community/gallery';
|
||||||
import { publishSavedCard, type PublishForm } from '../community/publish';
|
import { LOGO_MISSING_PUBLISH, assertLogoAvailable, publishSavedCard, type PublishForm } from '../community/publish';
|
||||||
import { publishDialog } from '../community/publish-dialog';
|
import { publishDialog } from '../community/publish-dialog';
|
||||||
import { confirmDialog } from '../editor/confirm-dialog';
|
import { confirmDialog } from '../editor/confirm-dialog';
|
||||||
import { el } from '../editor/dom';
|
import { el } from '../editor/dom';
|
||||||
import { downloadFileName, pngSizeWarning, saveBlobAs } from '../editor/download';
|
import { downloadFileName, pngSizeWarning, saveBlobAs } from '../editor/download';
|
||||||
import type { EditorStore } from '../editor/state';
|
import type { EditorStore } from '../editor/state';
|
||||||
import { PIXELS_MESSAGE, READ_MESSAGE, decodeImage } from '../editor/upload';
|
import { PIXELS_MESSAGE, READ_MESSAGE, decodeImage } from '../editor/upload';
|
||||||
|
import { t } from '../i18n/i18n';
|
||||||
import { formatDate } from './format';
|
import { formatDate } from './format';
|
||||||
import { navigate } from './router';
|
import { navigate } from './router';
|
||||||
|
|
||||||
@@ -429,6 +430,9 @@ export function mountCardsPage(root: HTMLElement, store: EditorStore): () => voi
|
|||||||
const image = await decodeImage(blob);
|
const image = await decodeImage(blob);
|
||||||
const config = { ...card.config, crop: clampCrop(image, card.config.crop) };
|
const config = { ...card.config, crop: clampCrop(image, card.config.crop) };
|
||||||
const assets = await loadCardAssets(config, image);
|
const assets = await loadCardAssets(config, image);
|
||||||
|
// Fehlt das gewählte Logo und würde die Vorlage es zeichnen, nicht stillschweigend ohne Logo herunterladen: die Meldung
|
||||||
|
// steht dann an der Kachel (siehe failureText). Bei „Nur Bild“ fehlt nichts.
|
||||||
|
assertLogoAvailable(config, assets);
|
||||||
const png = await exportCard(config, assets, { width: CARD_W, type: 'image/png' });
|
const png = await exportCard(config, assets, { width: CARD_W, type: 'image/png' });
|
||||||
saveBlobAs(png, downloadFileName(card.name, new Date()));
|
saveBlobAs(png, downloadFileName(card.name, new Date()));
|
||||||
const warning = pngSizeWarning(png.size);
|
const warning = pngSizeWarning(png.size);
|
||||||
@@ -533,6 +537,7 @@ export function mountCardsPage(root: HTMLElement, store: EditorStore): () => voi
|
|||||||
if (err instanceof LoginCancelled) return LOGIN_NEEDED;
|
if (err instanceof LoginCancelled) return LOGIN_NEEDED;
|
||||||
if (err instanceof ApiError) return err.message; // der Dienst antwortet auf Deutsch
|
if (err instanceof ApiError) return err.message; // der Dienst antwortet auf Deutsch
|
||||||
if (err instanceof Error && (err.message === READ_MESSAGE || err.message === PIXELS_MESSAGE)) return err.message;
|
if (err instanceof Error && (err.message === READ_MESSAGE || err.message === PIXELS_MESSAGE)) return err.message;
|
||||||
|
if (err instanceof Error && err.message === LOGO_MISSING_PUBLISH) return t(LOGO_MISSING_PUBLISH);
|
||||||
return errorMessage(err, 'load');
|
return errorMessage(err, 'load');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user