From e441f1d58c312f807940cbb70c69cb0ab137a937 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=B2=20Pretto?= <info@npretto.com>
Date: Tue, 27 Aug 2024 18:09:52 +0200
Subject: [PATCH] poc of using loki to test png/pdf exports (#45650)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* use loki to test png exports on questions

* try to put the img in the body to see if it fixes the scrollbar issues in ci

* fix linting issues

* let's try again in ci

* Attempt to make CI wait before taking a snapshot for downloaded PNGs

* Use the snapshots from CI for PNG downloads

* pdf export test thanks to kelvin suggestion about just testing the png

* refactored the code to extract a util function

* remove pdf tests as they broke in ci 🤷

* Update frontend/src/metabase/env.ts

Co-authored-by: Phoomparin Mano <poom@metabase.com>

* Update frontend/src/metabase/lib/loki-utils.ts

---------

Co-authored-by: Mahatthana (Kelvin) Nomsawadi <me@bboykelvin.dev>
Co-authored-by: Phoomparin Mano <poom@metabase.com>
---
 ...beddedQuestionView_Dark_Theme_Download.png | Bin 0 -> 10200 bytes
 ...eddedQuestionView_Light_Theme_Download.png | Bin 0 -> 10126 bytes
 .storybook/preview.tsx                        |   2 ++
 frontend/src/metabase/{env.js => env.ts}      |   7 +---
 frontend/src/metabase/lib/loki-utils.ts       |  25 +++++++++++++
 .../PublicOrEmbeddedQuestionView.stories.tsx  |  33 ++++++++++++++++++
 .../visualizations/lib/save-chart-image.ts    |  28 +++++++++------
 7 files changed, 79 insertions(+), 16 deletions(-)
 create mode 100644 .loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Dark_Theme_Download.png
 create mode 100644 .loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Light_Theme_Download.png
 rename frontend/src/metabase/{env.js => env.ts} (67%)
 create mode 100644 frontend/src/metabase/lib/loki-utils.ts

diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Dark_Theme_Download.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Dark_Theme_Download.png
new file mode 100644
index 0000000000000000000000000000000000000000..08640a80c27dd2cafe8f65687c670945c1626a6e
GIT binary patch
literal 10200
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
WJ&QXAfp~&IOwU=K&HMe&d;bOTN2cTe

literal 0
HcmV?d00001

diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Light_Theme_Download.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Light_Theme_Download.png
new file mode 100644
index 0000000000000000000000000000000000000000..49c15193c2893324c7d891488d98062987d76190
GIT binary patch
literal 10126
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
auKe8+gPr>zVg<so5B#F_4;A0L-TxObK$H&v

literal 0
HcmV?d00001

diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index cff1963cd1a..cd48e8781ad 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -11,6 +11,7 @@ import { getMetabaseCssVariables } from "metabase/styled-components/theme/css-va
 import { css, Global, useTheme } from "@emotion/react";
 import { baseStyle, rootStyle } from "metabase/css/core/base.styled";
 import { defaultFontFiles } from "metabase/css/core/fonts.styled";
+import { saveDomImageStyles } from "metabase/visualizations/lib/save-chart-image";
 
 export const parameters = {
   actions: { argTypesRegex: "^on[A-Z].*" },
@@ -30,6 +31,7 @@ const globalStyles = css`
     ${rootStyle}
   }
 
+  ${saveDomImageStyles}
   ${baseStyle}
 `;
 
diff --git a/frontend/src/metabase/env.js b/frontend/src/metabase/env.ts
similarity index 67%
rename from frontend/src/metabase/env.js
rename to frontend/src/metabase/env.ts
index 92a9eda0162..5ad8833c1ca 100644
--- a/frontend/src/metabase/env.js
+++ b/frontend/src/metabase/env.ts
@@ -1,20 +1,15 @@
+// @ts-expect-error window.Cypress is not typed
 export const isCypressActive = !!window.Cypress;
 
-// eslint-disable-next-line no-undef
 export const isStorybookActive = !!process.env.STORYBOOK;
 
-// eslint-disable-next-line no-undef
 export const isProduction = process.env.WEBPACK_BUNDLE === "production";
 
-// eslint-disable-next-line no-undef
 export const isTest = process.env.NODE_ENV === "test";
 
-// eslint-disable-next-line no-undef
 export const shouldLogAnalytics = process.env.MB_LOG_ANALYTICS === "true";
 
 export const isChartsDebugLoggingEnabled =
-  // eslint-disable-next-line no-undef
   process.env.MB_LOG_CHARTS_DEBUG === "true";
 
-// eslint-disable-next-line no-undef
 export const isEmbeddingSdk = !!process.env.IS_EMBEDDING_SDK_BUILD;
diff --git a/frontend/src/metabase/lib/loki-utils.ts b/frontend/src/metabase/lib/loki-utils.ts
new file mode 100644
index 00000000000..e26c360da66
--- /dev/null
+++ b/frontend/src/metabase/lib/loki-utils.ts
@@ -0,0 +1,25 @@
+export const openImageBlobOnStorybook = ({
+  canvas,
+  blob,
+}: {
+  canvas: HTMLCanvasElement;
+  blob: Blob;
+}) => {
+  const imgElement = document.createElement("img");
+  imgElement.src = URL.createObjectURL(blob);
+  // scale to /2 to compensate `scale:2` in html2canvas
+  imgElement.width = canvas.width / 2;
+  imgElement.height = canvas.height / 2;
+
+  const root: HTMLElement = document.querySelector("#root")!;
+  const imageDownloaded = document.createElement("div");
+  imageDownloaded.setAttribute("data-testid", "image-downloaded");
+  root.replaceChildren(imgElement);
+
+  // the presence of this element is used to detect when the image is ready
+  // in the storybook you'll need to `await canvas.findByTestId("image-downloaded");`
+  // and then call `asyncCallback()` to continue the story
+  root.appendChild(imageDownloaded);
+
+  window.document.body.style.height = "initial";
+};
diff --git a/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestionView.stories.tsx b/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestionView.stories.tsx
index 21744e18fb5..2b8faa1608d 100644
--- a/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestionView.stories.tsx
+++ b/frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion/PublicOrEmbeddedQuestionView.stories.tsx
@@ -136,6 +136,16 @@ LightThemeDefaultNoResults.args = {
   result: createMockDataset(),
 };
 
+export const LightThemeDownload = Template.bind({});
+LightThemeDownload.args = {
+  ...LightThemeDefault.args,
+  downloadsEnabled: true,
+};
+LightThemeDownload.play = async ({ canvasElement }) => {
+  const asyncCallback = createAsyncCallback();
+  await downloadQuestionAsPng(canvasElement, asyncCallback);
+};
+
 // Dark theme
 export const DarkThemeDefault = Template.bind({});
 DarkThemeDefault.args = {
@@ -150,6 +160,13 @@ DarkThemeDefaultNoResults.args = {
   result: createMockDataset(),
 };
 
+export const DarkThemeDownload = Template.bind({});
+DarkThemeDownload.args = {
+  ...DarkThemeDefault.args,
+  downloadsEnabled: true,
+};
+DarkThemeDownload.play = LightThemeDownload.play;
+
 // Transparent theme
 export const TransparentThemeDefault = Template.bind({});
 TransparentThemeDefault.args = {
@@ -308,3 +325,19 @@ function NarrowContainer(Story: Story) {
     </Box>
   );
 }
+
+const downloadQuestionAsPng = async (
+  canvasElement: HTMLElement,
+  asyncCallback: () => void,
+) => {
+  const canvas = within(canvasElement);
+
+  const downloadButton = await canvas.findByTestId("download-button");
+  await userEvent.click(downloadButton!);
+
+  const documentElement = within(document.documentElement);
+  const pngButton = await documentElement.findByText(".png");
+  await userEvent.click(pngButton);
+  await canvas.findByTestId("image-downloaded");
+  asyncCallback();
+};
diff --git a/frontend/src/metabase/visualizations/lib/save-chart-image.ts b/frontend/src/metabase/visualizations/lib/save-chart-image.ts
index 489fc6a0227..d5f8bcc0706 100644
--- a/frontend/src/metabase/visualizations/lib/save-chart-image.ts
+++ b/frontend/src/metabase/visualizations/lib/save-chart-image.ts
@@ -1,12 +1,14 @@
 import { css } from "@emotion/react";
 
+import { isStorybookActive } from "metabase/env";
+import { openImageBlobOnStorybook } from "metabase/lib/loki-utils";
+import EmbedFrameS from "metabase/public/components/EmbedFrame/EmbedFrame.module.css";
+
 export const SAVING_DOM_IMAGE_CLASS = "saving-dom-image";
 export const SAVING_DOM_IMAGE_HIDDEN_CLASS = "saving-dom-image-hidden";
 export const SAVING_DOM_IMAGE_DISPLAY_NONE_CLASS =
   "saving-dom-image-display-none";
 
-import EmbedFrameS from "metabase/public/components/EmbedFrame/EmbedFrame.module.css";
-
 export const saveDomImageStyles = css`
   .${SAVING_DOM_IMAGE_CLASS} {
     .${SAVING_DOM_IMAGE_HIDDEN_CLASS} {
@@ -41,14 +43,20 @@ export const saveChartImage = async (selector: string, fileName: string) => {
 
   canvas.toBlob(blob => {
     if (blob) {
-      const link = document.createElement("a");
-      const url = URL.createObjectURL(blob);
-      link.rel = "noopener";
-      link.download = fileName;
-      link.href = url;
-      link.click();
-      link.remove();
-      setTimeout(() => URL.revokeObjectURL(url), 60_000);
+      if (isStorybookActive) {
+        // if we're running storybook we open the image in place
+        // so we can test the export result with loki
+        openImageBlobOnStorybook({ canvas, blob });
+      } else {
+        const link = document.createElement("a");
+        const url = URL.createObjectURL(blob);
+        link.rel = "noopener";
+        link.download = fileName;
+        link.href = url;
+        link.click();
+        link.remove();
+        setTimeout(() => URL.revokeObjectURL(url), 60_000);
+      }
     }
   });
 };
-- 
GitLab