Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/components/image/image.stories.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,68 @@ export const FrameworkReact = () => (
objectFit="cover"
></BdsImage>
);

export const DataURL = () => {
const el = document.getElementsByClassName('sb-story');
if (el.length !== 0) {
el[0].style.width = '100%';
}

// Create different data URLs for demonstration
const svgDataUrl = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMDA3YmZmIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIyNCIgZmlsbD0id2hpdGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiPlNWRyBEYXRhIFVSTDwvdGV4dD48L3N2Zz4=';

// 1x1 pixel PNG (red)
const pngDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==';

// SVG circle
const svgCircleDataUrl = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSIxNTAiIGN5PSIxMDAiIHI9IjgwIiBmaWxsPSIjZmYwMDAwIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxOCIgZmlsbD0id2hpdGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiPkNpcmNsZTwvdGV4dD48L3N2Zz4=';

return (
<bds-grid xxs="12" direction="column" gap="3" align-items="center">
<bds-grid xxs="12" direction="column" align-items="center" gap="1">
<bds-typo variant="fs-20" bold="bold" tag="h3">Data URL Support</bds-typo>
<bds-typo variant="fs-14">
The bds-image component now supports data URLs, allowing you to embed images directly without fetching them.
</bds-typo>
</bds-grid>
<bds-grid xxs="12" gap="3" flex-wrap="wrap" justify-content="center">
<bds-grid direction="column" align-items="center" gap="1">
<bds-paper>
<bds-image
src={svgDataUrl}
alt="SVG Data URL with blue background"
width="300px"
height="200px"
object-fit="cover"
></bds-image>
</bds-paper>
<bds-typo variant="fs-12">SVG Data URL</bds-typo>
</bds-grid>
<bds-grid direction="column" align-items="center" gap="1">
<bds-paper>
<bds-image
src={svgCircleDataUrl}
alt="SVG circle data URL"
width="300px"
height="200px"
object-fit="contain"
></bds-image>
</bds-paper>
<bds-typo variant="fs-12">SVG Circle</bds-typo>
</bds-grid>
<bds-grid direction="column" align-items="center" gap="1">
<bds-paper>
<bds-image
src={pngDataUrl}
alt="PNG Data URL (1x1 red pixel)"
width="300px"
height="200px"
object-fit="fill"
></bds-image>
</bds-paper>
<bds-typo variant="fs-12">PNG Data URL (1x1 pixel stretched)</bds-typo>
</bds-grid>
</bds-grid>
</bds-grid>
);
};
23 changes: 17 additions & 6 deletions src/components/image/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,26 @@ export class Image {
if (this.src) {
this.imageHasLoading = true;
try {
const response = await fetch(this.src);
if (response.ok) {
const blob = await response.blob();
const objectURL = URL.createObjectURL(blob);
this.currentSrc = objectURL;
// Check if src is a data URL
if (this.src.startsWith('data:')) {
// Data URLs don't need to be fetched - use directly
// Use Promise.resolve to keep it async and avoid state changes during render
await Promise.resolve();
this.currentSrc = this.src;
this.imageLoaded = true;
this.imageHasLoading = false;
} else {
this.loadError = true;
// Regular URLs need to be fetched
const response = await fetch(this.src);
if (response.ok) {
const blob = await response.blob();
const objectURL = URL.createObjectURL(blob);
this.currentSrc = objectURL;
this.imageLoaded = true;
this.imageHasLoading = false;
} else {
this.loadError = true;
}
}
} catch {
this.imageHasLoading = false;
Expand Down
104 changes: 70 additions & 34 deletions src/components/image/test/image.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,75 @@
import { newE2EPage } from '@stencil/core/testing';

describe('bds-image e2e tests', () => {
let page;

beforeEach(async () => {
page = await newE2EPage({
html: `
<bds-image
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjY2NjIi8+PC9zdmc+"
alt="Test image"
loading="lazy"
fade="true"
></bds-image>
`,
describe('Data URL Support', () => {
it('should render data URL image successfully', async () => {
const dataUrl = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMDA3YmZmIi8+PC9zdmc+';

const page = await newE2EPage({
html: `<bds-image src="${dataUrl}" alt="Data URL test"></bds-image>`,
});

// Wait for the component to load
await page.waitForChanges();

const image = await page.find('bds-image');
expect(image).toBeTruthy();

// Check that src attribute is set correctly
const src = await image.getAttribute('src');
expect(src).toBe(dataUrl);

// Check that the image was loaded (no error state)
const illustration = await page.find('bds-image >>> bds-illustration');
expect(illustration).toBeFalsy();

// Check that img element is rendered
const img = await page.find('bds-image >>> img');
expect(img).toBeTruthy();
});

it('should handle PNG data URL', async () => {
// 1x1 red pixel PNG
const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==';

const page = await newE2EPage({
html: `<bds-image src="${dataUrl}" alt="PNG data URL"></bds-image>`,
});

await page.waitForChanges();

const img = await page.find('bds-image >>> img');
expect(img).toBeTruthy();

const imgSrc = await img.getAttribute('src');
expect(imgSrc).toBe(dataUrl);
});

it('should render data URL with alt text', async () => {
const dataUrl = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgZmlsbD0iI2ZmMDAwMCIvPjwvc3ZnPg==';

const page = await newE2EPage({
html: `<bds-image src="${dataUrl}" alt="Red circle"></bds-image>`,
});

await page.waitForChanges();

const img = await page.find('bds-image >>> img');
const alt = await img.getAttribute('alt');
expect(alt).toBe('Red circle');
});
});

describe('Properties', () => {
let page;

beforeEach(async () => {
const dataUrl = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjY2NjIi8+PC9zdmc+';
page = await newE2EPage({
html: `<bds-image src="${dataUrl}" alt="Test image"></bds-image>`,
});
});

it('should render image with correct src', async () => {
const image = await page.find('bds-image');
const src = await image.getAttribute('src');
Expand All @@ -28,22 +81,15 @@ describe('bds-image e2e tests', () => {
const alt = await image.getAttribute('alt');
expect(alt).toBe('Test image');
});

it('should render image with correct loading attribute', async () => {
const image = await page.find('bds-image');
const loading = await image.getAttribute('loading');
expect(loading).toBe('lazy');
});

it('should render image with fade effect enabled', async () => {
const image = await page.find('bds-image');
const fade = await image.getAttribute('fade');
expect(fade).toBe('true');
});
});

describe('Interactions', () => {
it('should handle image src changes correctly', async () => {
const initialDataUrl = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjY2NjIi8+PC9zdmc+';
const page = await newE2EPage({
html: `<bds-image src="${initialDataUrl}" alt="Test image"></bds-image>`,
});

const image = await page.find('bds-image');

// Check initial state
Expand All @@ -58,15 +104,5 @@ describe('bds-image e2e tests', () => {
const newSrc = await image.getAttribute('src');
expect(newSrc).toBe(newDataUrl);
});

it('should handle fade property changes', async () => {
const image = await page.find('bds-image');

await image.setProperty('fade', false);
await page.waitForChanges();

const fade = await image.getProperty('fade');
expect(fade).toBe(false);
});
});
});
102 changes: 102 additions & 0 deletions src/components/image/test/image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,4 +445,106 @@ describe('bds-image', () => {
expect(page.rootInstance.loadError).toBe(false);
});
});

describe('Data URL Support', () => {
it('should load data URL without fetching', async () => {
const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';

const page = await newSpecPage({
components: [Image],
html: `<bds-image src="${dataUrl}"></bds-image>`,
});

await page.rootInstance.loadImage();
await page.waitForChanges();

expect(page.rootInstance.imageLoaded).toBe(true);
expect(page.rootInstance.loadError).toBe(false);
expect(page.rootInstance.currentSrc).toBe(dataUrl);
expect(mockFetch).not.toHaveBeenCalled();
});

it('should render img element with data URL src', async () => {
const dataUrl = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMDA3YmZmIi8+PC9zdmc+';

const page = await newSpecPage({
components: [Image],
html: `<bds-image src="${dataUrl}" alt="Data URL image"></bds-image>`,
});

await page.rootInstance.loadImage();
await page.waitForChanges();

const img = page.root.shadowRoot.querySelector('img');
expect(img).toBeTruthy();
expect(img.getAttribute('src')).toBe(dataUrl);
expect(img.getAttribute('alt')).toBe('Data URL image');
});

it('should handle data URL with different MIME types', async () => {
const testCases = [
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==',
'data:image/jpeg;base64,/9j/4AAQSkZJRg==',
'data:image/gif;base64,R0lGODlhAQABAIAAAP==',
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg==',
'data:image/webp;base64,UklGRh4AAABXRUJQVlA4==',
];

for (const dataUrl of testCases) {
mockFetch.mockClear();

const page = await newSpecPage({
components: [Image],
html: `<bds-image src="${dataUrl}"></bds-image>`,
});

await page.rootInstance.loadImage();

expect(page.rootInstance.imageLoaded).toBe(true);
expect(page.rootInstance.loadError).toBe(false);
expect(page.rootInstance.currentSrc).toBe(dataUrl);
expect(mockFetch).not.toHaveBeenCalled();
}
});

it('should handle data URL without base64 encoding', async () => {
// Use a simpler URL-encoded data URL to avoid quote issues in HTML
const dataUrl = "data:image/svg+xml,%3Csvg%20width%3D%22100%22%20height%3D%22100%22%3E%3C%2Fsvg%3E";

const page = await newSpecPage({
components: [Image],
html: `<bds-image></bds-image>`,
});

// Set the src property directly to avoid HTML parsing issues
page.rootInstance.src = dataUrl;
await page.rootInstance.loadImage();

expect(page.rootInstance.imageLoaded).toBe(true);
expect(page.rootInstance.loadError).toBe(false);
expect(page.rootInstance.currentSrc).toBe(dataUrl);
expect(mockFetch).not.toHaveBeenCalled();
});

it('should not show skeleton when loading data URL', async () => {
const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';

const page = await newSpecPage({
components: [Image],
html: `<bds-image src="${dataUrl}"></bds-image>`,
});

// Start loading
const loadPromise = page.rootInstance.loadImage();

// Data URLs load synchronously, so skeleton should not appear
await loadPromise;
await page.waitForChanges();

expect(page.rootInstance.imageLoaded).toBe(true);

const img = page.root.shadowRoot.querySelector('img');
expect(img).toBeTruthy();
});
});
});
Loading