Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
* under the License.
*/
import fetchMock from 'fetch-mock';
import { logging } from '@apache-superset/core/utils';

import { render, screen } from '@superset-ui/core/spec';
import { render, screen, waitFor } from '@superset-ui/core/spec';
import { SupersetClient } from '../../connection';
import { ImageLoader, type BackgroundPosition } from './ImageLoader';

global.URL.createObjectURL = jest.fn(() => '/local_url');
Expand Down Expand Up @@ -63,12 +65,65 @@ describe('ImageLoader', () => {
'src',
'/fallback',
);
expect(fetchMock.callHistory.calls(/thumbnail/)).toHaveLength(1);
expect(global.URL.createObjectURL).toHaveBeenCalled();
expect(await screen.findByTestId('image-loader')).toHaveAttribute(
'src',
'/local_url',
);
expect(fetchMock.callHistory.calls(/thumbnail/)).toHaveLength(1);
expect(global.URL.createObjectURL).toHaveBeenCalled();
});

test('prefixes src with the configured app root', async () => {
const prefixedUrl = 'http://localhost/analytics/app-root-thumbnail';
fetchMock.get(
prefixedUrl,
{ body: blob, headers: { 'Content-Type': 'image/png' } },
{ name: 'app-root-thumbnail' },
);
await SupersetClient.configure({
protocol: 'http:',
host: 'localhost',
appRoot: '/analytics',
}).init();

try {
setup({ src: '/app-root-thumbnail' });
expect(await screen.findByTestId('image-loader')).toHaveAttribute(
'src',
'/local_url',
);
expect(fetchMock.callHistory.called(prefixedUrl)).toBe(true);
expect(
fetchMock.callHistory.calls(/app-root-thumbnail/).map(({ url }) => url),
).toEqual([prefixedUrl]);
} finally {
fetchMock.removeRoute('app-root-thumbnail');
await SupersetClient.configure({
protocol: 'http:',
host: 'localhost',
}).init();
}
});

test('displays fallback image when SupersetClient is not configured', async () => {
SupersetClient.reset();
const loggingSpy = jest.spyOn(logging, 'error').mockImplementation();

try {
setup({ src: 'https://picsum.photos/seed/superset/300/200' });
await waitFor(() => expect(loggingSpy).toHaveBeenCalled());
expect(screen.getByTestId('image-loader')).toHaveAttribute(
'src',
'/fallback',
);
expect(fetchMock.callHistory.calls(/picsum/)).toHaveLength(0);
} finally {
loggingSpy.mockRestore();
await SupersetClient.configure({
protocol: 'http:',
host: 'localhost',
}).init();
}
});

test('displays fallback image when response is not an image', async () => {
Expand All @@ -79,7 +134,9 @@ describe('ImageLoader', () => {
'src',
'/fallback',
);
expect(fetchMock.callHistory.calls(/thumbnail2/)).toHaveLength(1);
await waitFor(() =>
expect(fetchMock.callHistory.calls(/thumbnail2/)).toHaveLength(1),
);
expect(await screen.findByTestId('image-loader')).toHaveAttribute(
'src',
'/fallback',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useEffect, useState, DetailedHTMLProps, HTMLAttributes } from 'react';

import { logging } from '@apache-superset/core/utils';
import { styled } from '@apache-superset/core/theme';
import { SupersetClient } from '../../connection';

export type BackgroundPosition = 'top' | 'bottom';
interface ImageContainerProps {
Expand Down Expand Up @@ -57,7 +58,10 @@ export function ImageLoader({

useEffect(() => {
if (src) {
fetch(src)
// Start the request inside the chain so an unconfigured client
// (e.g. Storybook) rejects into the fallback instead of throwing.
Promise.resolve()
.then(() => SupersetClient.get({ endpoint: src, parseMethod: 'raw' }))
.then(response => response.blob())
.then(blob => {
if (/image/.test(blob.type)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export const InteractiveListViewCard: Story = {
title: 'Superset Card Title',
loading: false,
url: '/superset/dashboard/births/',
imgURL: 'https://picsum.photos/seed/superset/300/200',
imgURL: '',
imgFallbackURL: 'https://picsum.photos/seed/superset/300/200',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit...',
coverLeft: 'Left Section',
coverRight: 'Right Section',
Expand All @@ -61,7 +62,14 @@ export const InteractiveListViewCard: Story = {
imgURL: {
name: 'imgURL',
control: { type: 'text' },
description: 'Primary image URL for the card.',
description:
'Superset endpoint for the card image, such as a chart or dashboard thumbnail URL. The fallback image shows while it loads or when it is empty.',
},
imgFallbackURL: {
name: 'imgFallbackURL',
control: { type: 'text' },
description:
'Image URL shown while imgURL loads, or when imgURL is empty or fails.',
},
description: {
control: { type: 'text' },
Expand All @@ -88,7 +96,7 @@ export const InteractiveListViewCard: Story = {
title="My Dashboard"
description="A sample dashboard card"
url="/dashboard/1"
imgURL="https://picsum.photos/seed/demo/300/200"
imgFallbackURL="https://picsum.photos/seed/demo/300/200"
coverLeft="Created: Jan 2024"
coverRight="Views: 1,234"
/>
Expand All @@ -104,7 +112,7 @@ export const SupersetListViewCard: Story = {
title: 'Superset Card Title',
loading: false,
url: '/superset/dashboard/births/',
imgURL: 'https://picsum.photos/seed/superset2/300/200',
imgFallbackURL: 'https://picsum.photos/seed/superset2/300/200',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit...',
coverLeft: 'Left Section',
coverRight: 'Right Section',
Expand Down
23 changes: 17 additions & 6 deletions superset-frontend/src/features/dashboards/DashboardCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
render,
screen,
fireEvent,
waitFor,
within,
} from 'spec/helpers/testing-library';
import { SubjectType } from 'src/types/Subject';
Expand Down Expand Up @@ -240,7 +241,7 @@ describe('thumbnail URL construction', () => {
</MemoryRouter>,
);

test('constructs thumbnail URL from dashboard id and changed_on_utc', () => {
test('constructs thumbnail URL from dashboard id and changed_on_utc', async () => {
renderCard({
id: 2,
changed_by_name: '',
Expand All @@ -252,12 +253,17 @@ describe('thumbnail URL construction', () => {
changed_on_utc: '2024-01-01T00:00:00',
});

expect(fetchSpy).toHaveBeenCalledWith(
'/api/v1/dashboard/2/thumbnail/2024-01-01T00%3A00%3A00/',
await waitFor(() =>
expect(fetchSpy).toHaveBeenCalledWith(
expect.stringMatching(
/\/api\/v1\/dashboard\/2\/thumbnail\/2024-01-01T00%3A00%3A00\/$/,
),
expect.objectContaining({ method: 'GET' }),
),
);
});

test('falls back to changed_on when changed_on_utc is absent', () => {
test('falls back to changed_on when changed_on_utc is absent', async () => {
renderCard({
id: 3,
changed_by_name: '',
Expand All @@ -269,8 +275,13 @@ describe('thumbnail URL construction', () => {
changed_on: '2024-06-01T12:00:00',
});

expect(fetchSpy).toHaveBeenCalledWith(
'/api/v1/dashboard/3/thumbnail/2024-06-01T12%3A00%3A00/',
await waitFor(() =>
expect(fetchSpy).toHaveBeenCalledWith(
expect.stringMatching(
/\/api\/v1\/dashboard\/3\/thumbnail\/2024-06-01T12%3A00%3A00\/$/,
),
expect.objectContaining({ method: 'GET' }),
),
);
});

Expand Down
Loading