Skip to content
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

SLVUU-101 improve take screenshot functionality #124

Closed
Closed
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 @@ -8,7 +8,7 @@ import {
saveLocalEntity,
} from "../../../vuu-filters/src/local-config";
import { formatDate } from "@finos/vuu-utils";
import { expectPromiseRejectsWithError } from "./utils";
import { expectPromiseRejectsWithError } from "@finos/vuu-utils/test/utils";

vi.mock("@finos/vuu-filters", async () => {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { LayoutMetadata, LayoutMetadataDto } from "@finos/vuu-shell";
import { LayoutJSON } from "../../src/layout-reducer";
import { v4 as uuidv4 } from "uuid";
import { expectPromiseRejectsWithError } from "./utils";
import { expectPromiseRejectsWithError } from "@finos/vuu-utils/test/utils";

const persistence = new RemoteLayoutPersistenceManager();
const mockFetch = vi.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
--saltText-color: var(--text-secondary-foreground, #606477);
}

.spinner{
width: 100px;
height:100px;
background-image: var(--svg-spinner);
}

.saveLayoutPanel-panelContainer {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -69,13 +75,9 @@
}

.saveLayoutPanel-screenshot {
display: flex;
justify-content: center;
align-items: center;
background: lightgray 50% / cover no-repeat;
width: 273px;
height: 186px;
flex-shrink: 0;
}

.saveLayoutPanel-buttonsContainer {
Expand Down
43 changes: 28 additions & 15 deletions vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChangeEvent, useEffect, useState } from "react";
import { Input, Button, FormField, FormFieldLabel, Text } from "@salt-ds/core";
import { ComboBox, Checkbox, RadioButton } from "@finos/vuu-ui-controls";
import { Checkbox, ComboBox, RadioButton } from "@finos/vuu-ui-controls";
import { takeScreenshot } from "@finos/vuu-utils";
import { Button, FormField, FormFieldLabel, Input, Text } from "@salt-ds/core";
import { ChangeEvent, useEffect, useMemo, useState } from "react";
import { LayoutMetadataDto } from "./layoutTypes";

import "./SaveLayoutPanel.css";
Expand Down Expand Up @@ -32,12 +32,17 @@ export const SaveLayoutPanel = (props: SaveLayoutPanelProps) => {
const [checkValues, setCheckValues] = useState<string[]>([]);
const [radioValue, setRadioValue] = useState<RadioValue>(radioValues[0]);
const [screenshot, setScreenshot] = useState<string | undefined>();
const [screenshotErrorMessage, setScreenshotErrorMessage] = useState<string | undefined>();

useEffect(() => {
if (componentId) {
takeScreenshot(document.getElementById(componentId) as HTMLElement).then(
(screenshot) => setScreenshot(screenshot)
);
takeScreenshot(document.getElementById(componentId) as HTMLElement)
.then((screenshot) => {
setScreenshot(screenshot);
})
.catch((error: Error) => {
setScreenshotErrorMessage(error.message);
});
}
}, [componentId]);

Expand All @@ -50,6 +55,22 @@ export const SaveLayoutPanel = (props: SaveLayoutPanelProps) => {
});
};

const screenshotContent = useMemo(() => {
if (screenshot) {
return (
<img
className={`${classBase}-screenshot`}
src={screenshot}
alt="screenshot of current layout"
/>
);
}
if (screenshotErrorMessage) {
return <Text>{screenshotErrorMessage}</Text>;
}
return <div className="spinner" />;
}, [screenshot, screenshotErrorMessage]);

return (
<div className={`${classBase}-panelContainer`}>
<div className={`${classBase}-panelContent`}>
Expand Down Expand Up @@ -119,15 +140,7 @@ export const SaveLayoutPanel = (props: SaveLayoutPanelProps) => {
</FormField>
</div>
<div className={`${classBase}-screenshotContainer`}>
{screenshot ? (
<img
className={`${classBase}-screenshot`}
src={screenshot}
alt="screenshot of current layout"
/>
) : (
<Text className="screenshot">No screenshot available</Text>
)}
{screenshotContent}
</div>
</div>
<div className={`${classBase}-buttonsContainer`}>
Expand Down
40 changes: 24 additions & 16 deletions vuu-ui/packages/vuu-utils/src/screenshot-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,30 @@ import { toPng } from "html-to-image";

/**
* Takes a screenshot of the given node and returns the base64 encoded image url
* @param node Node to take screenshot of
* @param node HTMLElement to take screenshot of
* @returns Base64 encoded image url
*/
export async function takeScreenshot(node: HTMLElement) {

const screenshot = await toPng(node, { cacheBust: true })
.then((dataUrl) => {
return dataUrl;
export const takeScreenshot = (node: HTMLElement): Promise<string> => {
return new Promise((resolve, reject) => {
toPng(node, {
cacheBust: true,
filter: (child) =>
// remove content of table rows
child.nodeType === Node.TEXT_NODE ||
child.getAttribute("role") !== "row",
Comment on lines +14 to +15

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These look like good filter criteria to me, but I'd like to know what @heswell thinks.

})
.catch((err) => {
console.error("Error taking screenshot", err);
return undefined;
});

if (!screenshot) {
return undefined;
}
return screenshot;
}
.then((screenshot) => {
if (!screenshot) {
reject(new Error("No Screenshot available"));
}
resolve(screenshot);
})
.catch((error: Error) => {
console.error(
"the following error occurred while taking a screenshot of a DOMNode",
error
);
reject(new Error("Error taking screenshot"));
});
});
};
32 changes: 16 additions & 16 deletions vuu-ui/packages/vuu-utils/test/screenshot-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
import { takeScreenshot } from "../src/screenshot-utils";
import { describe, expect, it, vi } from "vitest";
import { expectPromiseRejectsWithError } from "./utils";
import htmlToImage from "html-to-image";

/**
* The default environment in Vitest is a Node.js environment. If you are building a web application, you can use browser-like environment through either jsdom or happy-dom instead
* @vitest-environment happy-dom
*/
const node = document.createElement("div");

describe("takeScreenshot", () => {
it("returns a string when toPng() promise is resolved", async () => {
const placeholderImage =
"data:image/png;base64,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";

// We need to mock the html-to-image package because the web API operations it relies on (e.g. canvas.toDataUrl) are not available in the test environment
const htmlToImage = await import("html-to-image");
htmlToImage.toPng = vi.fn().mockResolvedValue(placeholderImage);

const node = document.createElement("div");

const screenshot = await takeScreenshot(node);

expect(typeof screenshot).toEqual("string");
});

it("returns undefined when toPng() promise is rejected", async () => {
// We need to mock the html-to-image package because the web API operations it relies on (e.g. canvas.toDataUrl) are not available in the test environment
const htmlToImage = await import("html-to-image");
it("rejects with error message when toPng() promise is rejected", async () => {

htmlToImage.toPng = vi.fn().mockRejectedValue({});

const node = document.createElement("div");

const screenshot = await takeScreenshot(node);
expectPromiseRejectsWithError(
() => takeScreenshot(node),
"Error taking screenshot"
);
});

console.log(screenshot);
it("rejects with error message when toPng() resolves with falsey value", async () => {
htmlToImage.toPng = vi.fn().mockResolvedValue(undefined);

expect(screenshot).toBeUndefined();
expectPromiseRejectsWithError(
() => takeScreenshot(node),
"No Screenshot available"
);
});
});