Skip to content

chore: fixed image not downloading functionality and ImageWidget cypr… #34786

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: release
Choose a base branch
from
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 @@ -10,13 +10,19 @@ import {
import EditorNavigation, {
EntityType,
} from "../../../../../support/Pages/EditorNavigation";
import { urlToBase64 } from "../../../../../../src/widgets/ImageWidget/helper";

describe(
"Image widget - Rotation & Download",
{ tags: ["@tag.Widget", "@tag.Image"] },
function () {
const jpgImg = "https://jpeg.org/images/jpegsystems-home.jpg";
before(() => {
let base64Url: string;
before(function () {
cy.wrap(urlToBase64(jpgImg)).then((url) => {
base64Url = url as string;
});

entityExplorer.DragDropWidgetNVerify(draggableWidgets.IMAGE);
propPane.UpdatePropertyFieldValue("Image", jpgImg);
deployMode.DeployApp(locators._widgetInDeployed(draggableWidgets.IMAGE));
Expand Down Expand Up @@ -63,7 +69,10 @@ describe(
agHelper.GetNClick(locators._widgetInDeployed(draggableWidgets.IMAGE));
agHelper.HoverElement(locators._widgetInDeployed(draggableWidgets.IMAGE));
agHelper.AssertElementVisibility(widgetLocators.imageDownloadBtn);
agHelper.AssertAttribute(widgetLocators.imageDownloadBtn, "href", jpgImg);
cy.wrap(base64Url).then((url) => {
// This is to validate the final base64 url which is used for download as href in the anchor tag
agHelper.AssertAttribute(widgetLocators.imageDownloadBtn, "href", url);
});
});
},
);
115 changes: 115 additions & 0 deletions app/client/src/widgets/ImageWidget/component/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import store from "store";
import React from "react";
import { Provider } from "react-redux";
import type { ImageComponentProps } from "./";
import ImageComponent from "./";
import { render, fireEvent, waitFor } from "@testing-library/react";
import { urlToBase64 } from "../helper";

let container: HTMLDivElement | null;

beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});

afterEach(() => {
document.body.removeChild(container as Node);
container = null;
});

const mockData = "base64data";

jest.mock("../helper.ts", () => ({
...jest.requireActual("../helper.ts"),
urlToBase64: jest.fn(async (url) => {
if (!url) return Promise.resolve("");
return Promise.resolve(`data:image/jpeg;base64,${mockData}`);
}),
}));

describe("<ImageComponent />", () => {
const imageUrl = "https://assets.appsmith.com/widgets/default.png";
const defaultImageProps: ImageComponentProps = {
imageUrl: imageUrl,
enableDownload: true,
defaultImageUrl: imageUrl,
isLoading: false,
maxZoomLevel: 0,
objectFit: "",
disableDrag: () => {},
borderRadius: "",
widgetId: "",
};

it("1. renders the image", async () => {
const { container } = render(
<Provider store={store}>
<ImageComponent {...defaultImageProps} />
</Provider>,
);
const imageContainer = container.querySelector("img");
expect(imageContainer).not.toBeNull();
expect(imageContainer?.getAttribute("src")).toBe(imageUrl);
});

it("2. renders the download button on hover", async () => {
const { container, getByTestId } = render(
<Provider store={store}>
<ImageComponent {...defaultImageProps} />
</Provider>,
);
const imageContainer = container.querySelector("img");
expect(imageContainer).not.toBeNull();
expect(imageContainer?.getAttribute("src")).toBe(imageUrl);
fireEvent.mouseEnter(imageContainer as Element);
expect(getByTestId("t--image-download")).not.toBeNull();
});

it("3. downloads the image on click", async () => {
const { container, getByTestId } = render(
<Provider store={store}>
<ImageComponent {...defaultImageProps} />
</Provider>,
);

const imageContainer = container.querySelector("img");
expect(imageContainer).not.toBeNull();
expect(imageContainer?.getAttribute("src")).toBe(imageUrl);
fireEvent.mouseEnter(imageContainer as Element);
const downloadButton = getByTestId(
"t--image-download",
) as HTMLAnchorElement;
expect(downloadButton).not.toBeNull();

// Wait for the state to be updated
await waitFor(() => expect(urlToBase64).toHaveBeenCalled());

fireEvent.click(downloadButton);
expect(downloadButton.href).toContain(`data:image/jpeg;base64,${mockData}`);
});

it("4. does not render download button if both image URL and default image URL is empty", async () => {
const emptyUrlProps: ImageComponentProps = {
...defaultImageProps,
imageUrl: "",
defaultImageUrl: "",
};

const { container, queryByTestId } = render(
<Provider store={store}>
<ImageComponent {...emptyUrlProps} />
</Provider>,
);

const imageContainer = container.querySelector("img");
expect(imageContainer).not.toBeNull();
expect(imageContainer?.getAttribute("src")).toBe("");

fireEvent.mouseEnter(imageContainer as Element);

await waitFor(() => expect(urlToBase64).toHaveBeenCalledWith(""));

expect(queryByTestId("t--image-download")).toBeNull();
});
});
38 changes: 37 additions & 1 deletion app/client/src/widgets/ImageWidget/component/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as React from "react";
import type { ComponentProps } from "widgets/BaseComponent";
import styled from "styled-components";
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
import { urlToBase64 } from "../helper";
import log from "loglevel";
import { createMessage, IMAGE_LOAD_ERROR } from "ee/constants/messages";
import { importSvg } from "@appsmith/ads-old";

Expand Down Expand Up @@ -141,6 +143,7 @@ class ImageComponent extends React.Component<
showImageControl: boolean;
imageRotation: number;
zoomingState: ZoomingState;
downloadUrl?: string;
}
> {
isPanning: boolean;
Expand All @@ -152,10 +155,23 @@ class ImageComponent extends React.Component<
showImageControl: false,
imageRotation: 0,
zoomingState: ZoomingState.MAX_ZOOMED_OUT,
downloadUrl: "",
};
}

componentDidMount = () => {
this.updateDownloadUrl();
};

componentDidUpdate = (prevProps: ImageComponentProps) => {
// update download url when imageUrl or defaultImageUrl changes
if (
this.props.imageUrl !== prevProps.imageUrl ||
prevProps.defaultImageUrl !== this.props.defaultImageUrl
) {
this.updateDownloadUrl();
}

// reset the imageError flag when the defaultImageUrl or imageUrl changes
if (
(prevProps.imageUrl !== this.props.imageUrl ||
Expand All @@ -166,6 +182,25 @@ class ImageComponent extends React.Component<
}
};

updateDownloadUrl = async () => {
try {
/* This solution only works for images that are hosted on server which allows cross origin request
For images that are hosted on server which doesn't allow cross origin request, the backend should also return base64 url
*/
const { defaultImageUrl, imageUrl } = this.props;
const url = imageUrl || defaultImageUrl;

const base64Url = await urlToBase64(url);
if (base64Url) {
this.setState({ downloadUrl: base64Url });
} else {
this.setState({ downloadUrl: "" });
}
} catch (error) {
log.error("Could not fetch image for download", error);
}
};

render() {
const { imageUrl, maxZoomLevel } = this.props;

Expand Down Expand Up @@ -315,7 +350,8 @@ class ImageComponent extends React.Component<
} = this.props;
const { showImageControl } = this.state;
const showDownloadBtn = enableDownload && (!!imageUrl || !!defaultImageUrl);
const hrefUrl = imageUrl || defaultImageUrl;
const hrefUrl = this.state.downloadUrl || imageUrl || defaultImageUrl;
if (!hrefUrl) return null;

if (showImageControl && (enableRotation || showDownloadBtn)) {
return (
Expand Down
18 changes: 18 additions & 0 deletions app/client/src/widgets/ImageWidget/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Buffer } from "buffer";
import log from "loglevel";
export const urlToBase64 = async (url: string) => {
if (!url) return "";
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const buffer = await response.arrayBuffer();
const contentType = response.headers.get("content-type");
const base64String = Buffer.from(buffer).toString("base64");
return `data:${contentType};base64,${base64String}`;
} catch (error) {
log.error("Failed to fetch image for download", error);
return "";
}
};