From 508afc61f85dcb2a52d79f9a90ab9b73f45d5031 Mon Sep 17 00:00:00 2001 From: Kiarokh Moattar Date: Thu, 7 May 2026 18:27:49 +0200 Subject: [PATCH 1/3] fix(markdown): drop color and background-color from inline-style allowlist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inline color values from email content can collide with the surrounding theme — black text on a dark surface, or white text on a light one — making feed snippets unreadable. is a generic renderer used throughout the app and shouldn't carry email-specific fidelity concerns. Sender emphasis colors remain available when the user opens the original email through . Co-Authored-By: Claude Opus 4.7 (1M context) --- .../markdown/allowed-css-properties.ts | 2 - .../markdown/examples/markdown-html.tsx | 13 ++--- .../markdown/markdown-parser.spec.ts | 16 +++--- .../markdown/sanitize-style.spec.ts | 52 ++++++++++--------- 4 files changed, 39 insertions(+), 44 deletions(-) diff --git a/src/components/markdown/allowed-css-properties.ts b/src/components/markdown/allowed-css-properties.ts index 3309e5779d..7fa2cf5d14 100644 --- a/src/components/markdown/allowed-css-properties.ts +++ b/src/components/markdown/allowed-css-properties.ts @@ -1,6 +1,4 @@ export const allowedCssProperties = [ - 'background-color', - 'color', 'font-style', 'font-weight', 'text-decoration-color', diff --git a/src/components/markdown/examples/markdown-html.tsx b/src/components/markdown/examples/markdown-html.tsx index b3c45cb993..3fb1f605ec 100644 --- a/src/components/markdown/examples/markdown-html.tsx +++ b/src/components/markdown/examples/markdown-html.tsx @@ -13,22 +13,19 @@ const markdown = `
It's better to use HTML tags.
Can you use text colors?
-
Yes, since I'm red!
+
No. Inline color styles are stripped so the text always inherits the surrounding theme — this stays readable in both light and dark mode.
Can you use background colors?
-
Yes, since I'm on a green background!
+
No. Inline background-color styles are stripped for the same reason.
-
Can you use more than one style at the same time?
-
Yes, since I'm a light sky blue on a dark coral background!
+
Can you make text bold or italic with inline styles?
+
Yes, since font-weight and font-style are allowed.
Can you use background images?
No, you should not be able to, so if there's a skull and crossbones background here, something is wrong.
-
Can you use background with a color value?
-
Yes. If the value is recognized as a color value, the value will be moved to background-color
-
Can you sneakily use background to insert an image?
-
No. If the value is not recognized as a color value, the background property will be stripped.
+
No, the background shorthand is stripped entirely.
`; diff --git a/src/components/markdown/markdown-parser.spec.ts b/src/components/markdown/markdown-parser.spec.ts index 820391232e..b430b34303 100644 --- a/src/components/markdown/markdown-parser.spec.ts +++ b/src/components/markdown/markdown-parser.spec.ts @@ -629,27 +629,25 @@ describe('sanitizeHTML', () => { describe('style attribute sanitization', () => { it('should allow safe CSS properties', async () => { const result = await sanitizeHTML( - '

Text

' + '

Text

' ); expect(result).toEqualHtml( - '

Text

' + '

Text

' ); }); it('should strip dangerous CSS properties', async () => { const result = await sanitizeHTML( - '

Text

' + '

Text

' ); - expect(result).toEqualHtml('

Text

'); + expect(result).toEqualHtml('

Text

'); }); - it('should normalize background to background-color', async () => { + it('should strip color and background-color', async () => { const result = await sanitizeHTML( - '

Text

' - ); - expect(result).toEqualHtml( - '

Text

' + '

Text

' ); + expect(result).toEqualHtml('

Text

'); }); }); diff --git a/src/components/markdown/sanitize-style.spec.ts b/src/components/markdown/sanitize-style.spec.ts index 85974bfd3d..89f2b89998 100644 --- a/src/components/markdown/sanitize-style.spec.ts +++ b/src/components/markdown/sanitize-style.spec.ts @@ -10,11 +10,11 @@ describe('sanitizeStyle', () => { const node = { tagName: 'div', properties: { - style: 'color: red; position: absolute;', + style: 'font-weight: bold; position: absolute;', }, }; sanitizeStyle(node); - expect(node.properties.style).toContain('color: red'); + expect(node.properties.style).toContain('font-weight: bold'); expect(node.properties.style).not.toContain('position: absolute'); }); @@ -40,7 +40,7 @@ describe('sanitizeStyle', () => { it('does nothing to a text node (node without `tagName`)', () => { const node = { properties: { - style: 'color: red; position: absolute;', + style: 'font-weight: bold; position: absolute;', }, }; const originalProperties = { ...node.properties }; @@ -52,13 +52,13 @@ describe('sanitizeStyle', () => { const node = { tagName: 'div', properties: { - style: 'color: red; animation: slidein 3s;', + style: 'font-weight: bold; animation: slidein 3s;', id: 'test-node', }, }; - // 'color' is allowed, but 'animation' is not. Also checks that 'id' remains unchanged. + // 'font-weight' is allowed, but 'animation' is not. Also checks that 'id' remains unchanged. sanitizeStyle(node); - expect(node.properties.style).toContain('color: red'); + expect(node.properties.style).toContain('font-weight: bold'); expect(node.properties.style).not.toContain('animation: slidein 3s'); expect(node.properties.id).toBe('test-node'); }); @@ -67,27 +67,29 @@ describe('sanitizeStyle', () => { describe('sanitizeStyleValue', () => { describe('with valid CSS', () => { it('retains allowed CSS properties', () => { - const style = 'color: blue; text-decoration: underline;'; + const style = 'font-weight: bold; text-decoration: underline;'; const sanitized = sanitizeStyleValue(style); - // 'color' and 'text-decoration' are in the allowedCssProperties list - expect(sanitized).toContain('color: blue'); + // 'font-weight' and 'text-decoration' are in the allowedCssProperties list + expect(sanitized).toContain('font-weight: bold'); expect(sanitized).toContain('text-decoration: underline'); }); it('removes disallowed CSS properties', () => { - const style = 'color: blue; margin: 10px;'; + const style = 'font-weight: bold; margin: 10px;'; const sanitized = sanitizeStyleValue(style); // 'margin' is not in the allowedCssProperties list - expect(sanitized).toContain('color: blue'); + expect(sanitized).toContain('font-weight: bold'); expect(sanitized).not.toContain('margin: 10px'); }); - it('converts background to background-color if valid and allowed', () => { - const style = 'background: red;'; + it('strips color and background-color', () => { + // Inline colors from email content can collide with the host + // theme (e.g. white text on a light surface), so they are not + // preserved by the markdown sanitizer. + const style = + 'color: blue; background-color: red; font-weight: bold;'; const sanitized = sanitizeStyleValue(style); - // 'background-color' is allowed and should replace 'background' - expect(sanitized).toContain('background-color: red'); - expect(sanitized).not.toContain('background: red'); + expect(sanitized).toBe('font-weight: bold'); }); }); @@ -100,12 +102,12 @@ describe('sanitizeStyleValue', () => { vi.restoreAllMocks(); }); it('returns an empty string for invalid CSS syntax', () => { - const style = 'color blue'; // Missing colon + const style = 'font-weight bold'; // Missing colon const sanitized = sanitizeStyleValue(style); expect(sanitized).toBe(''); }); it('logs an error to the console for invalid CSS syntax', () => { - const style = 'color blue'; // Missing colon + const style = 'font-weight bold'; // Missing colon sanitizeStyleValue(style); expect(console.error).toHaveBeenCalledTimes(1); }); @@ -113,26 +115,26 @@ describe('sanitizeStyleValue', () => { describe('special cases', () => { it('handles CSS with semi-colon at the end correctly', () => { - const style = 'color: blue;'; + const style = 'font-weight: bold;'; const sanitized = sanitizeStyleValue(style); // Ensure proper handling of trailing semi-colons - expect(sanitized).toBe('color: blue'); + expect(sanitized).toBe('font-weight: bold'); }); it('handles CSS without semi-colon at the end correctly', () => { - const style = 'color: blue'; + const style = 'font-weight: bold'; const sanitized = sanitizeStyleValue(style); - expect(sanitized).toBe('color: blue'); + expect(sanitized).toBe('font-weight: bold'); }); }); describe('integration with allowedCssProperties list', () => { it('only retains properties specified in allowedCssProperties', () => { const style = - 'color: blue; animation: slidein 3s; font-weight: bold;'; + 'font-style: italic; animation: slidein 3s; font-weight: bold;'; const sanitized = sanitizeStyleValue(style); - // 'animation' is not allowed, 'color' and 'font-weight' are allowed - expect(sanitized).toContain('color: blue'); + // 'animation' is not allowed, 'font-style' and 'font-weight' are allowed + expect(sanitized).toContain('font-style: italic'); expect(sanitized).toContain('font-weight: bold'); expect(sanitized).not.toContain('animation: slidein 3s'); }); From dc80087b677a0b01068cb9d1f152b32403bfff88 Mon Sep 17 00:00:00 2001 From: Kiarokh Moattar Date: Thu, 7 May 2026 18:28:02 +0200 Subject: [PATCH 2/3] fix(email-viewer): help users read emails whose colors clash with the theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The email body now picks its background based on the app theme by default, using contrast tokens that auto-invert with light/dark mode. A small "Hard to read?" toggle, with a tooltip explaining the why, sits sticky at the top of the body and flips the background to the opposite tone for emails whose hardcoded text colors collide with the default surface. The email content is never modified — only the surface behind it changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/email-viewer/email-viewer.scss | 62 +++++++++++++++- src/components/email-viewer/email-viewer.tsx | 70 ++++++++++++++++++- src/translations/da.ts | 6 ++ src/translations/de.ts | 6 ++ src/translations/en.ts | 6 ++ src/translations/fi.ts | 6 ++ src/translations/fr.ts | 6 ++ src/translations/nl.ts | 6 ++ src/translations/no.ts | 6 ++ src/translations/sv.ts | 6 ++ 10 files changed, 175 insertions(+), 5 deletions(-) diff --git a/src/components/email-viewer/email-viewer.scss b/src/components/email-viewer/email-viewer.scss index fe01325301..c8b7978d3c 100644 --- a/src/components/email-viewer/email-viewer.scss +++ b/src/components/email-viewer/email-viewer.scss @@ -1,6 +1,8 @@ @use '../../style/mixins'; :host(limel-email-viewer) { + isolation: isolate; + display: block; width: 100%; height: 100%; @@ -72,6 +74,7 @@ &.date { position: absolute; + z-index: 1; right: 0.25rem; bottom: 0; transform: translateY(50%); @@ -156,7 +159,6 @@ section { flex-direction: column; border-top: 1px dashed rgba(var(--contrast-700)); min-height: 2rem; - overflow-y: auto; } limel-collapsible-section { @@ -198,7 +200,16 @@ limel-collapsible-section { .body { flex-grow: 1; max-width: 100%; - padding: 0.75rem; + padding: 1rem; + padding-bottom: 3rem; + overflow-y: auto; + + background-color: rgb(var(--contrast-300)); + color-scheme: light; + &.toggled-surface-background { + background-color: rgb(var(--contrast-1500)); + color-scheme: dark; + } &.plain-text { white-space: pre-wrap; @@ -211,3 +222,50 @@ limel-collapsible-section { max-width: 100% !important; } } + +.toggle-dark-light { + display: flex; + align-items: center; + justify-content: flex-end; + + position: sticky; + top: 0; + z-index: 1; + + padding: 0.25rem 0.5rem; + $height: 2.5rem; + height: $height; + margin-bottom: -$height; + + pointer-events: none; + + button { + pointer-events: all; + @include mixins.reset-button-user-agent-styles; + @include mixins.is-elevated-clickable(); + @include mixins.visualize-keyboard-focus(); + + display: flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + + padding: 0.125rem 0.25rem; + height: fit-content; + + font-size: 0.75rem; + border-radius: 0.375rem; + border: 1px solid rgba(var(--contrast-700)); + + opacity: 0.6; + + &:hover, + &:focus-visible { + opacity: 1; + } + + limel-icon { + width: 1.125rem; + } + } +} diff --git a/src/components/email-viewer/email-viewer.tsx b/src/components/email-viewer/email-viewer.tsx index a9e06c341b..8e6e31bcdc 100644 --- a/src/components/email-viewer/email-viewer.tsx +++ b/src/components/email-viewer/email-viewer.tsx @@ -14,6 +14,7 @@ import { Email, EmailAttachment, EmailHeaderType } from './email-viewer.types'; import { applyRemoteImagesPolicy, containsRemoteImages } from './remote-images'; import { splitEmailAddressList } from './split-email-address-list'; import { formatBytes } from '../../util/format-bytes'; +import { createRandomString } from '../../util/random-string'; /** * This is a private component, used to render `.eml` files inside @@ -70,6 +71,11 @@ export class EmailViewer { @State() private allowRemoteImagesState = false; + @State() + private surfaceBackgroundState = false; + + private toggleButtonId = createRandomString(); + /** * Emitted when the user requests remote images to be loaded. * @@ -79,7 +85,11 @@ export class EmailViewer { public allowRemoteImagesChange: EventEmitter; @Watch('email') - protected resetAllowRemoteImages(newEmail?: Email, oldEmail?: Email) { + protected handleEmailChange(newEmail?: Email, oldEmail?: Email) { + // Surface preference is per-email-view: a new message always opens + // with the default light background. + this.surfaceBackgroundState = false; + if (!newEmail) { this.allowRemoteImagesState = false; return; @@ -98,6 +108,7 @@ export class EmailViewer { {this.renderRemoteImageBanner()}
{this.renderAttachments()} + {this.renderSurfaceToggle()} {this.renderBody()}
@@ -146,7 +157,13 @@ export class EmailViewer { this.getAllowRemoteImages() ); - return
; + return ( +
+ ); } private renderBodyText() { @@ -156,12 +173,59 @@ export class EmailViewer { } return ( -
+            
                 {bodyText}
             
); } + private getBodyClassNames(): string { + return this.surfaceBackgroundState + ? 'body toggled-surface-background' + : 'body'; + } + + private renderSurfaceToggle() { + const hasBody = Boolean(this.email?.bodyHtml || this.email?.bodyText); + if (!hasBody) { + return; + } + + const label = this.getTranslation('file-viewer.email.surface.toggle'); + const tooltipLabel = this.getTranslation( + 'file-viewer.email.surface.toggle.tooltip.label' + ); + const tooltipHelper = this.getTranslation( + 'file-viewer.email.surface.toggle.tooltip.helper' + ); + + return ( +
+ + +
+ ); + } + + private toggleSurfaceBackground = () => { + this.surfaceBackgroundState = !this.surfaceBackgroundState; + }; + private renderFallbackUrl() { if (!this.fallbackUrl) { return; diff --git a/src/translations/da.ts b/src/translations/da.ts index 44985d86ce..e1e423e516 100644 --- a/src/translations/da.ts +++ b/src/translations/da.ts @@ -56,6 +56,12 @@ Det kan afsløre for afsenderen, at du har åbnet beskeden, hvornår du åbnede Du kan fortsætte med at blokere billeder (e-mailen kan se ufuldstændig ud) eller indlæse dem, hvis du stoler på afsenderen.`, 'file-viewer.email.remote-images.load': 'Indlæs billeder', + 'file-viewer.email.surface.toggle': 'Svært at læse?', + 'file-viewer.email.surface.toggle.tooltip.label': + 'Klik for højere kontrast', + 'file-viewer.email.surface.toggle.tooltip.helper': `Nogle e-mails har deres egne hårdkodede tekstfarver – nogle gange meget lyse, nogle gange meget mørke. +Afhængigt af om appen er i lys eller mørk tilstand kan teksten være svær at læse. +Klik her for at vende e-mailens baggrund til den modsatte tone og forbedre kontrasten.`, 'editor-menu.bold': 'Fed', 'editor-menu.italic': 'Kursiv', 'editor-menu.strikethrough': 'Gennemstreget', diff --git a/src/translations/de.ts b/src/translations/de.ts index 4a903cb21f..53d3f98912 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -56,6 +56,12 @@ Dadurch kann der Absender erfahren, dass Sie die Nachricht geöffnet haben, wann Sie können Bilder weiterhin blockieren (die E-Mail kann unvollständig aussehen) oder sie laden, wenn Sie dem Absender vertrauen.`, 'file-viewer.email.remote-images.load': 'Bilder laden', + 'file-viewer.email.surface.toggle': 'Schwer lesbar?', + 'file-viewer.email.surface.toggle.tooltip.label': + 'Für mehr Kontrast klicken', + 'file-viewer.email.surface.toggle.tooltip.helper': `Manche E-Mails enthalten fest einprogrammierte Textfarben – manchmal sehr hell, manchmal sehr dunkel. +Je nachdem, ob die App im hellen oder dunklen Modus ist, kann der Text schwer lesbar werden. +Klicken Sie hier, um den Hintergrund der E-Mail in den gegenteiligen Ton umzuschalten und den Kontrast zu verbessern.`, 'editor-menu.bold': 'Fett', 'editor-menu.italic': 'Kursiv', 'editor-menu.strikethrough': 'Durchgestrichen', diff --git a/src/translations/en.ts b/src/translations/en.ts index ffdee04af6..873792fb67 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -55,6 +55,12 @@ This may reveal to the sender that you opened the message, when you opened it, a You can keep images blocked (the email may look incomplete), or load them if you trust the sender.`, 'file-viewer.email.remote-images.load': 'Load images', + 'file-viewer.email.surface.toggle': 'Hard to read?', + 'file-viewer.email.surface.toggle.tooltip.label': + 'Click to get higher contrast', + 'file-viewer.email.surface.toggle.tooltip.helper': `Some emails arrive with hardcoded text colors — sometimes very light, sometimes very dark. +Depending on whether the app is in light or dark mode, that text can be hard to read. +Click here to flip the email's background to the opposite tone and improve contrast.`, 'editor-menu.bold': 'Bold', 'editor-menu.italic': 'Italic', 'editor-menu.strikethrough': 'Strikethrough', diff --git a/src/translations/fi.ts b/src/translations/fi.ts index 3c95a2d4fc..1b42ae903b 100644 --- a/src/translations/fi.ts +++ b/src/translations/fi.ts @@ -56,6 +56,12 @@ Tämä voi paljastaa lähettäjälle, että avasit viestin, milloin avasit sen, Voit pitää kuvat estettyinä (sähköposti voi näyttää puutteelliselta) tai ladata ne, jos luotat lähettäjään.`, 'file-viewer.email.remote-images.load': 'Lataa kuvat', + 'file-viewer.email.surface.toggle': 'Vaikea lukea?', + 'file-viewer.email.surface.toggle.tooltip.label': + 'Klikkaa parantaaksesi kontrastia', + 'file-viewer.email.surface.toggle.tooltip.helper': `Joissakin sähköposteissa on omat kovakoodatut tekstivärit – joskus erittäin vaaleat, joskus erittäin tummat. +Sen mukaan, onko sovellus vaaleassa vai tummassa tilassa, teksti voi olla vaikealukuista. +Klikkaa tästä vaihtaaksesi sähköpostin taustan vastakkaiseen sävyyn ja parantaaksesi kontrastia.`, 'editor-menu.bold': 'Lihavoitu', 'editor-menu.italic': 'Kursivoitu', 'editor-menu.strikethrough': 'Yliviivaus', diff --git a/src/translations/fr.ts b/src/translations/fr.ts index a32caa7c54..fcf721e87d 100644 --- a/src/translations/fr.ts +++ b/src/translations/fr.ts @@ -56,6 +56,12 @@ Cela peut révéler à l'expéditeur que vous avez ouvert le message, quand vous Vous pouvez laisser les images bloquées (l'e-mail peut sembler incomplet) ou les charger si vous faites confiance à l'expéditeur.`, 'file-viewer.email.remote-images.load': 'Charger les images', + 'file-viewer.email.surface.toggle': 'Difficile à lire ?', + 'file-viewer.email.surface.toggle.tooltip.label': + 'Cliquez pour augmenter le contraste', + 'file-viewer.email.surface.toggle.tooltip.helper': `Certains e-mails contiennent des couleurs de texte codées en dur – parfois très claires, parfois très sombres. +Selon que l'application est en mode clair ou sombre, ce texte peut devenir difficile à lire. +Cliquez ici pour inverser le fond de l'e-mail et améliorer le contraste.`, 'editor-menu.bold': 'Gras', 'editor-menu.italic': 'Italique', 'editor-menu.strikethrough': 'Barré', diff --git a/src/translations/nl.ts b/src/translations/nl.ts index e09e8a7546..7653e262f8 100644 --- a/src/translations/nl.ts +++ b/src/translations/nl.ts @@ -55,6 +55,12 @@ Dit kan aan de afzender onthullen dat je het bericht hebt geopend, wanneer je he Je kunt afbeeldingen geblokkeerd houden (de e-mail kan er onvolledig uitzien) of ze laden als je de afzender vertrouwt.`, 'file-viewer.email.remote-images.load': 'Afbeeldingen laden', + 'file-viewer.email.surface.toggle': 'Moeilijk te lezen?', + 'file-viewer.email.surface.toggle.tooltip.label': + 'Klik voor hoger contrast', + 'file-viewer.email.surface.toggle.tooltip.helper': `Sommige e-mails bevatten hun eigen vaste tekstkleuren – soms zeer licht, soms zeer donker. +Afhankelijk van of de app in lichte of donkere modus staat, kan die tekst moeilijk leesbaar zijn. +Klik hier om de achtergrond van de e-mail naar de tegenovergestelde tint om te schakelen en het contrast te verbeteren.`, 'editor-menu.bold': 'Vet', 'editor-menu.italic': 'Cursief', 'editor-menu.strikethrough': 'Doorhalen', diff --git a/src/translations/no.ts b/src/translations/no.ts index 6f328a4f30..4a6f839414 100644 --- a/src/translations/no.ts +++ b/src/translations/no.ts @@ -55,6 +55,12 @@ Dette kan avsløre for avsenderen at du åpnet meldingen, når du åpnet den, og Du kan fortsette å blokkere bilder (e-posten kan se ufullstendig ut), eller laste dem inn hvis du stoler på avsenderen.`, 'file-viewer.email.remote-images.load': 'Last inn bilder', + 'file-viewer.email.surface.toggle': 'Vanskelig å lese?', + 'file-viewer.email.surface.toggle.tooltip.label': + 'Klikk for høyere kontrast', + 'file-viewer.email.surface.toggle.tooltip.helper': `Noen e-poster har sine egne hardkodede tekstfarger – noen ganger svært lyse, noen ganger svært mørke. +Avhengig av om appen er i lyst eller mørkt modus, kan teksten bli vanskelig å lese. +Klikk her for å bytte e-postens bakgrunn til motsatt tone og forbedre kontrasten.`, 'editor-menu.bold': 'Fet', 'editor-menu.italic': 'Kursiv', 'editor-menu.strikethrough': 'Gjennomstreking', diff --git a/src/translations/sv.ts b/src/translations/sv.ts index 96fce8337e..faba02cb57 100644 --- a/src/translations/sv.ts +++ b/src/translations/sv.ts @@ -56,6 +56,12 @@ Det kan avslöja för avsändaren att du öppnade meddelandet, när du öppnade Du kan fortsätta blockera bilder (e-posten kan se ofullständig ut) eller ladda dem om du litar på avsändaren.`, 'file-viewer.email.remote-images.load': 'Ladda bilder', + 'file-viewer.email.surface.toggle': 'Svår att läsa?', + 'file-viewer.email.surface.toggle.tooltip.label': + 'Klicka för högre kontrast', + 'file-viewer.email.surface.toggle.tooltip.helper': `Vissa e-postmeddelanden har egna fasta textfärger – ibland mycket ljusa, ibland mycket mörka. +Beroende på om appen är i ljust eller mörkt läge kan texten bli svår att läsa. +Klicka här för att vända e-postens bakgrund till motsatt ton och förbättra kontrasten.`, 'editor-menu.bold': 'Fet', 'editor-menu.italic': 'Kursiv', 'editor-menu.strikethrough': 'Genomstruken', From 6d4c834c5c3a8184d9c228e0fb0415f34a617a2b Mon Sep 17 00:00:00 2001 From: Kiarokh Moattar Date: Thu, 7 May 2026 20:23:40 +0200 Subject: [PATCH 3/3] fix(file-viewer): widen email-spec fetch mock to ignore icon SVG fetches The email viewer now renders a inside its surface toggle. That triggers an icon-cache fetch for the SVG, which calls `response.text()`. The existing test mock unconditionally returned a response object that only implemented `arrayBuffer()`, so the icon fetch threw an unhandled "response.text is not a function" error and failed the test run. The mock now routes the email URL to its EML payload and returns a benign empty response (with both `text()` and `arrayBuffer()`) for any other fetch the email viewer's render tree may make. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../file-viewer/file-viewer.spec.tsx | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/components/file-viewer/file-viewer.spec.tsx b/src/components/file-viewer/file-viewer.spec.tsx index 2223061d3d..087c252d24 100644 --- a/src/components/file-viewer/file-viewer.spec.tsx +++ b/src/components/file-viewer/file-viewer.spec.tsx @@ -23,11 +23,29 @@ describe('limel-file-viewer', () => { '\r\n' + 'Hello from EML!\r\n'; - globalThis.fetch = vi.fn().mockResolvedValue({ - ok: true, - arrayBuffer: async () => - new TextEncoder().encode(eml).buffer, - } as any); + // Route the email URL to its EML payload, and return a + // benign empty response for any other fetches the email + // viewer's render tree may trigger (e.g. icon SVGs from + // , which calls `response.text()`). + globalThis.fetch = vi + .fn() + .mockImplementation(async (input: any) => { + const url = + typeof input === 'string' ? input : input.url; + if (url === testCase.url) { + return { + ok: true, + arrayBuffer: async () => + new TextEncoder().encode(eml).buffer, + } as any; + } + + return { + ok: true, + text: async () => '', + arrayBuffer: async () => new ArrayBuffer(0), + } as any; + }); } const { root, waitForChanges } = await render(