Repository: fabricjs/fabric.js
Pull request #10615: refactor(tests): move resize filter test cases from qunit to playwright
Author: login: Smrtnyk
Created: 2025-05-21T17:38:11Z | Merged: 2025-05-22T12:54:17Z



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 129be156697..41a9256562a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## [next]
 
+- refactor(tests): move resize filter test cases from qunit to playwright [#10615](https://github.com/fabricjs/fabric.js/pull/10615)
 - refactor(tests): move global composite operation tests from qunit to playwright [#10610](https://github.com/fabricjs/fabric.js/pull/10610)
 - refactor(tests): move group layout tests from qunit to playwright [#10609](https://github.com/fabricjs/fabric.js/pull/10609)
 - refactor(tests): move generic rendering tests from qunit to playwright [#10608](https://github.com/fabricjs/fabric.js/pull/10608)
diff --git a/e2e/setup/index.ts b/e2e/setup/index.ts
index 963c86da794..efc99d3b41e 100644
--- a/e2e/setup/index.ts
+++ b/e2e/setup/index.ts
@@ -2,10 +2,32 @@ import setupApp from './setupApp';
 import setupCoverage from './setupCoverage';
 import setupSelectors from './setupSelectors';
 import path from 'node:path';
+import { FabricNamespace } from '../tests/types';
 
 const ASSET_DIR_NODE = path.resolve(process.cwd(), 'test', 'visual', 'assets');
 const FIXTURE_DIR_NODE = path.resolve(process.cwd(), 'test', 'fixtures');
 
+async function getImage(
+  fabric: FabricNamespace,
+  filename: string,
+): Promise<HTMLImageElement> {
+  const fixtureName = globalThis.getFixtureName(filename);
+  return new Promise((resolve, reject) => {
+    const img = fabric.getFabricDocument().createElement('img');
+    img.onload = function () {
+      img.onerror = null;
+      img.onload = null;
+      resolve(img);
+    };
+    img.onerror = function (err) {
+      img.onerror = null;
+      img.onload = null;
+      reject(err);
+    };
+    img.src = fixtureName;
+  });
+}
+
 // Used to resolve assert path for fetching
 // browser equivalent is installed in setupApp in test.beforeEach
 globalThis.getAssetName = function (f: string) {
@@ -16,6 +38,8 @@ globalThis.getFixtureName = function (f: string) {
   return 'file://' + path.join(FIXTURE_DIR_NODE, `${f}`);
 };
 
+globalThis.getImage = getImage;
+
 export default () => {
   // call first
   setupSelectors();
diff --git a/e2e/setup/setupApp.ts b/e2e/setup/setupApp.ts
index 880c743102d..f8426942d3d 100644
--- a/e2e/setup/setupApp.ts
+++ b/e2e/setup/setupApp.ts
@@ -3,12 +3,32 @@ import { existsSync, readFileSync } from 'fs';
 import path from 'path';
 import imports from '../imports';
 import { JSDOM } from 'jsdom';
+import { FabricNamespace } from '../tests/types';
 
 export default () => {
   test.beforeEach(async ({ page }, { file }) => {
     await page.addInitScript(() => {
-      globalThis.getAssetName = (name) => `/test/visual/assets/${name}`;
-      globalThis.getFixtureName = (name) => `/test/fixtures/${name}`;
+      globalThis.getAssetName = (name: string) => `/test/visual/assets/${name}`;
+      globalThis.getFixtureName = (name: string) => `/test/fixtures/${name}`;
+      globalThis.getImage = async (
+        fabric: FabricNamespace,
+        filename: string,
+      ): Promise<HTMLImageElement> => {
+        return new Promise((resolve, reject) => {
+          const img = fabric.getFabricDocument().createElement('img');
+          img.onload = function () {
+            img.onerror = null;
+            img.onload = null;
+            resolve(img);
+          };
+          img.onerror = function (err) {
+            img.onerror = null;
+            img.onload = null;
+            reject(err);
+          };
+          img.src = globalThis.getFixtureName(filename);
+        });
+      };
     });
 
     await page.goto('/e2e/site');
diff --git a/test/visual/golden/parrot.png b/e2e/tests/visual-output/rendering/index.spec.ts-snapshots/resize-filter/parrot.png
similarity index 100%
rename from test/visual/golden/parrot.png
rename to e2e/tests/visual-output/rendering/index.spec.ts-snapshots/resize-filter/parrot.png
diff --git a/test/visual/golden/parrotblend.png b/e2e/tests/visual-output/rendering/index.spec.ts-snapshots/resize-filter/parrotblend.png
similarity index 100%
rename from test/visual/golden/parrotblend.png
rename to e2e/tests/visual-output/rendering/index.spec.ts-snapshots/resize-filter/parrotblend.png
diff --git a/test/visual/golden/parrotblend2.png b/e2e/tests/visual-output/rendering/index.spec.ts-snapshots/resize-filter/parrotblend2.png
similarity index 100%
rename from test/visual/golden/parrotblend2.png
rename to e2e/tests/visual-output/rendering/index.spec.ts-snapshots/resize-filter/parrotblend2.png
diff --git a/test/visual/golden/parrotxy.png b/e2e/tests/visual-output/rendering/index.spec.ts-snapshots/resize-filter/parrotxy.png
similarity index 100%
rename from test/visual/golden/parrotxy.png
rename to e2e/tests/visual-output/rendering/index.spec.ts-snapshots/resize-filter/parrotxy.png
diff --git a/e2e/tests/visual-output/rendering/renderingCases.ts b/e2e/tests/visual-output/rendering/renderingCases.ts
index fb5e9292130..99dac3409b8 100644
--- a/e2e/tests/visual-output/rendering/renderingCases.ts
+++ b/e2e/tests/visual-output/rendering/renderingCases.ts
@@ -12,6 +12,7 @@ import { clipPathRenderingTests } from './testcases/clip-path';
 import { groupLayoutTests } from './testcases/group-layout';
 import { genericRenderingTests } from './testcases/generic-rendering';
 import { globalCompositeOperationTests } from './testcases/global-composite-operation';
+import { resizeFilterTests } from './testcases/resize-filter';
 
 export const renderTests: renderTestType[] = [
   {
@@ -82,4 +83,5 @@ export const renderTests: renderTestType[] = [
   ...groupLayoutTests,
   ...genericRenderingTests,
   ...globalCompositeOperationTests,
+  ...resizeFilterTests,
 ];
diff --git a/e2e/tests/visual-output/rendering/testcases/generic-rendering.ts b/e2e/tests/visual-output/rendering/testcases/generic-rendering.ts
index b298488fb69..da2300bd165 100644
--- a/e2e/tests/visual-output/rendering/testcases/generic-rendering.ts
+++ b/e2e/tests/visual-output/rendering/testcases/generic-rendering.ts
@@ -1,25 +1,4 @@
-import type { FabricNamespace, renderTestType } from '../../../types';
-
-async function getImage(
-  fabric: FabricNamespace,
-  filename: string,
-): Promise<HTMLImageElement> {
-  const fixtureName = globalThis.getFixtureName(filename);
-  return new Promise((resolve, reject) => {
-    const img = fabric.getFabricDocument().createElement('img');
-    img.onload = function () {
-      img.onerror = null;
-      img.onload = null;
-      resolve(img);
-    };
-    img.onerror = function (err) {
-      img.onerror = null;
-      img.onload = null;
-      reject(err);
-    };
-    img.src = fixtureName;
-  });
-}
+import type { renderTestType } from '../../../types';
 
 const generic1: renderTestType = {
   title: 'Rect with strokeUniform: true',
@@ -332,7 +311,7 @@ const canvasPattern: renderTestType = {
   percentage: 0.09,
   size: [500, 500],
   async renderFunction(canvas, fabric) {
-    const img = await getImage(fabric, 'diet.jpeg');
+    const img = await globalThis.getImage(fabric, 'diet.jpeg');
 
     canvas.backgroundColor = new fabric.Pattern({
       source: img,
@@ -350,7 +329,7 @@ const canvasPatternMultiplier: renderTestType = {
   percentage: 0.09,
   size: [500, 500],
   async renderFunction(canvas, fabric) {
-    const img = await getImage(fabric, 'diet.jpeg');
+    const img = await globalThis.getImage(fabric, 'diet.jpeg');
 
     canvas.backgroundColor = new fabric.Pattern({
       source: img,
@@ -369,7 +348,7 @@ const imageSmoothing: renderTestType = {
   percentage: 0.09,
   size: [800, 400],
   async renderFunction(canvas, fabric) {
-    const img = await getImage(fabric, 'greyfloral.png');
+    const img = await globalThis.getImage(fabric, 'greyfloral.png');
 
     const fImg = new fabric.FabricImage(img, {
       imageSmoothing: false,
diff --git a/e2e/tests/visual-output/rendering/testcases/resize-filter.ts b/e2e/tests/visual-output/rendering/testcases/resize-filter.ts
new file mode 100644
index 00000000000..c9b3f1621e1
--- /dev/null
+++ b/e2e/tests/visual-output/rendering/testcases/resize-filter.ts
@@ -0,0 +1,140 @@
+import type { FabricNamespace, renderTestType } from '../../../types';
+
+function configureFabric(fabric: FabricNamespace) {
+  fabric.config.configure({
+    enableGLFiltering: false,
+  });
+}
+
+const imageResizeTest: renderTestType = {
+  title: 'Image resize with canvas zoom',
+  golden: 'parrot.png',
+  percentage: 0.08,
+  size: [200, 200],
+  snapshotSuffix: 'resize-filter',
+  async renderFunction(canvas, fabric) {
+    configureFabric(fabric);
+    fabric.Object.ownDefaults.objectCaching = false;
+
+    const img = await globalThis.getImage(fabric, 'parrot.png');
+    const zoom = 8;
+    const image = new fabric.Image(img);
+    image.resizeFilter = new fabric.filters.Resize({ resizeType: 'lanczos' });
+    canvas.setZoom(zoom);
+    image.scaleToWidth(canvas.width / zoom);
+    canvas.add(image);
+    canvas.renderAll();
+  },
+};
+
+const imageResizeTestNoZoom: renderTestType = {
+  title: 'Image resize without zoom',
+  golden: 'parrot.png',
+  percentage: 0.08,
+  size: [200, 200],
+  snapshotSuffix: 'resize-filter',
+  async renderFunction(canvas, fabric) {
+    configureFabric(fabric);
+
+    const img = await globalThis.getImage(fabric, 'parrot.png');
+    const image = new fabric.FabricImage(img);
+    image.resizeFilter = new fabric.filters.Resize({ resizeType: 'lanczos' });
+    image.scaleToWidth(canvas.width);
+    canvas.add(image);
+    canvas.renderAll();
+  },
+};
+
+const imageResizeTestAnamorphic: renderTestType = {
+  title: 'Image resize with scaleY != scaleX',
+  golden: 'parrotxy.png',
+  percentage: 0.08,
+  size: [200, 200],
+  snapshotSuffix: 'resize-filter',
+  async renderFunction(canvas, fabric) {
+    configureFabric(fabric);
+
+    const img = await globalThis.getImage(fabric, 'parrot.png');
+    const image = new fabric.Image(img);
+    image.resizeFilter = new fabric.filters.Resize({ resizeType: 'lanczos' });
+    image.scaleY = 0.3;
+    image.scaleX = 1;
+    canvas.add(image);
+    canvas.renderAll();
+  },
+};
+
+const imageResizeTestGroup: renderTestType = {
+  title: 'Image resize with scaled group',
+  golden: 'parrot.png',
+  percentage: 0.08,
+  size: [200, 200],
+  snapshotSuffix: 'resize-filter',
+  async renderFunction(canvas, fabric) {
+    configureFabric(fabric);
+
+    const img = await globalThis.getImage(fabric, 'parrot.png');
+    const image = new fabric.Image(img, { strokeWidth: 0 });
+    image.resizeFilter = new fabric.filters.Resize({ resizeType: 'lanczos' });
+    const group = new fabric.Group([image]);
+    group.strokeWidth = 0;
+    group.scaleToWidth(canvas.width);
+    canvas.add(group);
+    canvas.renderAll();
+  },
+};
+
+const blendImageTest2: renderTestType = {
+  title: 'Blend image test with flip',
+  golden: 'parrotblend2.png',
+  percentage: 0.06,
+  size: [400, 400],
+  snapshotSuffix: 'resize-filter',
+  async renderFunction(canvas, fabric) {
+    configureFabric(fabric);
+
+    const img = await globalThis.getImage(fabric, 'parrot.png');
+    const image = new fabric.Image(img);
+    const backdropImage = new fabric.Image(img);
+    backdropImage.left = backdropImage.width;
+    backdropImage.scaleX = -1;
+    image.filters.push(new fabric.filters.BlendImage({ image: backdropImage }));
+    image.applyFilters();
+    image.scaleToWidth(400);
+    canvas.add(image);
+    canvas.renderAll();
+  },
+};
+
+const blendImageTest: renderTestType = {
+  title: 'Blend image test',
+  golden: 'parrotblend.png',
+  percentage: 0.06,
+  size: [400, 400],
+  snapshotSuffix: 'resize-filter',
+  async renderFunction(canvas, fabric) {
+    configureFabric(fabric);
+
+    const img = await globalThis.getImage(fabric, 'parrot.png');
+    const backdrop = await globalThis.getImage(fabric, 'very_large_image.jpg');
+
+    const image = new fabric.Image(img);
+    const backdropImage = new fabric.Image(backdrop);
+    image.filters.push(
+      new fabric.filters.BlendImage({ image: backdropImage, alpha: 0.5 }),
+    );
+    image.scaleToWidth(400);
+    image.applyFilters();
+    canvas.add(image);
+    canvas.renderAll();
+  },
+};
+
+export const resizeFilterTests = [
+  imageResizeTest,
+  imageResizeTestNoZoom,
+  imageResizeTestAnamorphic,
+  imageResizeTestGroup,
+  blendImageTest2,
+  blendImageTest,
+];
diff --git a/test/visual/index.ts b/test/visual/index.ts
index 7e538874136..04d1e2b512d 100644
--- a/test/visual/index.ts
+++ b/test/visual/index.ts
@@ -1,4 +1,3 @@
 import './freedraw';
-import './resize_filter';
 import './svg_import';
 import './text';
diff --git a/test/visual/resize_filter.js b/test/visual/resize_filter.js
deleted file mode 100644
index 8f2694184a2..00000000000
--- a/test/visual/resize_filter.js
+++ /dev/null
@@ -1,169 +0,0 @@
-(function() {
-  fabric.config.configure({
-    enableGLFiltering: false
-  });
-  var visualTestLoop;
-  var getFixture;
-  var isFirefox = false;
-  if (isNode()) {
-    visualTestLoop = global.visualTestLoop;
-    getFixture = global.getFixture;
-  }
-  else {
-    visualTestLoop = window.visualTestLoop;
-    getFixture = window.getFixture;
-    isFirefox = window.navigator.userAgent.includes('Firefox/')
-  }
-
-  var tests = [];
-
-  function imageResizeTest(canvas, callback) {
-    getFixture('parrot.png', false, function(img) {
-      var zoom = 8;
-      var image = new fabric.Image(img);
-      image.resizeFilter = new fabric.filters.Resize({ resizeType: 'lanczos' });
-      canvas.setZoom(zoom);
-      image.scaleToWidth(canvas.width / zoom);
-      canvas.add(image);
-      canvas.renderAll();
-      callback(canvas.lowerCanvasEl);
-      image.dispose();
-    });
-  }
-
-  tests.push({
-    test: 'Image resize with canvas zoom',
-    code: imageResizeTest,
-    golden: 'parrot.png',
-    newModule: 'Image resize filter test',
-    percentage: 0.08,
-    disabled: isFirefox,
-    width: 200,
-    height: 200,
-    beforeEachHandler: function() {
-      fabric.Object.ownDefaults.objectCaching = false;
-    }
-  });
-
-  function imageResizeTestNoZoom(canvas, callback) {
-    getFixture('parrot.png', false, function(img) {
-      var image = new fabric.Image(img);
-      image.resizeFilter = new fabric.filters.Resize({ resizeType: 'lanczos' });
-      image.scaleToWidth(canvas.width);
-      canvas.add(image);
-      canvas.renderAll();
-      callback(canvas.lowerCanvasEl);
-      image.dispose();
-    });
-  }
-
-  tests.push({
-    test: 'Image resize without zoom',
-    code: imageResizeTestNoZoom,
-    golden: 'parrot.png',
-    disabled: isFirefox,
-    percentage: 0.08,
-    width: 200,
-    height: 200,
-  });
-
-  function imageResizeTestAnamorphic(canvas, callback) {
-    getFixture('parrot.png', false, function(img) {
-      var image = new fabric.Image(img);
-      image.resizeFilter = new fabric.filters.Resize({ resizeType: 'lanczos' });
-      image.scaleY = 0.3;
-      image.scaleX = 1;
-      canvas.add(image);
-      canvas.renderAll();
-      callback(canvas.lowerCanvasEl);
-      image.dispose();
-    });
-  }
-
-  tests.push({
-    test: 'Image resize with scaleY != scaleX',
-    code: imageResizeTestAnamorphic,
-    golden: 'parrotxy.png',
-    percentage: 0.08,
-    disabled: isFirefox,
-    width: 200,
-    height: 200,
-  });
-
-  function imageResizeTestGroup(canvas, callback) {
-    getFixture('parrot.png', false, function(img) {
-      var image = new fabric.Image(img, { strokeWidth: 0 });
-      image.resizeFilter = new fabric.filters.Resize({ resizeType: 'lanczos' });
-      var group = new fabric.Group([image]);
-      group.strokeWidth = 0;
-      group.scaleToWidth(canvas.width);
-      canvas.add(group);
-      canvas.renderAll();
-      image.dispose();
-      callback(canvas.lowerCanvasEl);
-    });
-  }
-
-  tests.push({
-    test: 'Image resize with scaled group',
-    code: imageResizeTestGroup,
-    golden: 'parrot.png',
-    disabled: isFirefox,
-    percentage: 0.08,
-    width: 200,
-    height: 200,
-  });
-
-  function blendImageTest2(canvas, callback) {
-    getFixture('parrot.png', false, function(img) {
-      var image = new fabric.Image(img);
-      var backdropImage = new fabric.Image(img);
-      backdropImage.left = backdropImage.width;
-      backdropImage.scaleX = -1;
-      image.filters.push(new fabric.filters.BlendImage({ image: backdropImage }));
-      image.applyFilters();
-      image.scaleToWidth(400);
-      canvas.add(image);
-      canvas.renderAll();
-      image.dispose();
-      backdropImage.dispose();
-      callback(canvas.lowerCanvasEl);
-    });
-  }
-
-  tests.push({
-    test: 'Blend image test with flip',
-    code: blendImageTest2,
-    golden: 'parrotblend2.png',
-    newModule: 'Image Blend test',
-    percentage: 0.06,
-    width: 400,
-    height: 400,
-  });
-
-  function blendImageTest(canvas, callback) {
-    getFixture('parrot.png', false, function(img) {
-      getFixture('very_large_image.jpg', false, function(backdrop) {
-        var image = new fabric.Image(img);
-        var backdropImage = new fabric.Image(backdrop);
-        image.filters.push(new fabric.filters.BlendImage({image: backdropImage, alpha: 0.5 }));
-        image.scaleToWidth(400);
-        image.applyFilters();
-        canvas.add(image);
-        canvas.renderAll();
-        callback(canvas.lowerCanvasEl);
-      });
-    });
-  }
-
-  tests.push({
-    test: 'Blend image test',
-    code: blendImageTest,
-    golden: 'parrotblend.png',
-    percentage: 0.06,
-    width: 400,
-    height: 400,
-  });
-
-  tests.forEach(visualTestLoop(QUnit));
-})();
diff --git a/test/visual/svg_import.js b/test/visual/svg_import.js
index efb88d5f772..22d55377e0b 100644
--- a/test/visual/svg_import.js
+++ b/test/visual/svg_import.js
@@ -2,7 +2,6 @@
   fabric.config.configure({
     enableGLFiltering: false
   });
-  fabric.Object.ownDefaults.objectCaching = true;
   var visualTestLoop;
   var getAsset;
   if (isNode()) {


Reviews:
user: login: Smrtnyk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T17:46:40Z
user: login: asturur
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T12:45:52Z
user: login: asturur
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-22T12:54:09Z

Review comments:
diff_hunk: @@ -2,7 +2,6 @@
   fabric.config.configure({
     enableGLFiltering: false
   });
-  fabric.Object.ownDefaults.objectCaching = true;
path: test/visual/svg_import.js
user: login: Smrtnyk
body: yeah, this proves that qunit is full of sideeffects between tests
that I had to remove this
because some previous test in another suite had this flipped
created_at: 2025-05-21T17:46:40Z
author_association: COLLABORATOR
diff_hunk: @@ -2,7 +2,6 @@
   fabric.config.configure({
     enableGLFiltering: false
   });
-  fabric.Object.ownDefaults.objectCaching = true;
path: test/visual/svg_import.js
user: login: asturur
body: I grew my js expertise and test understanding working of fabricjs during the years before professional js was a carreerr for me. I m sure code written between 2015-2018 is full of poor decisions based on 'i need this to work for work related issues' and 'tests is better than no tests'
created_at: 2025-05-22T12:45:52Z
author_association: MEMBER

PR comments:
user: login: codesandbox[bot]
created_at: 2025-05-21T17:38:14Z
body: #### Review or Edit in CodeSandbox

Open the branch in <a href="https://codesandbox.io/p/github/Smrtnyk/fabric.js/resize-filter?mode=review&utm_source=gh_app">Web Editor</a> &bull; <a href="https://codesandbox.io/p/vscode?owner=Smrtnyk&repo=fabric.js&branch=resize-filter&utm_source=gh_app">VS Code</a> &bull; <a href="https://codesandbox.io/p/vscode?owner=Smrtnyk&repo=fabric.js&branch=resize-filter&insiders=true&utm_source=gh_app">Insiders</a><br>
Open <a href="https://codesandbox.io/p/devtool/preview/Smrtnyk/fabric.js/resize-filter?task=preview&port=51423&redirect=true&utm_source=gh_app">Preview</a>

<!-- open-in-codesandbox:complete -->

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-21T17:40:17Z
body: <!-- build-stats -->
**Build Stats**

 | file / KB (diff) | bundled | minified | 
 | --- | --- | --- | 
 | fabric | 914.662 (**0**) | 301.450 (**0**) |
author_association: CONTRIBUTOR
user: login: asturur
created_at: 2025-05-22T12:54:02Z
body: This is good.
When all is settled if we see there is a need for cases like this getImage to be available on globlaThis and global, we could think of refactoring by injecting an utility object from the test runner so that we have one for browser and one for node.
I understand that having visual tests running in playwright browser and also by the test runner in node is not the standard playwright usage.
author_association: MEMBER
user: login: Smrtnyk
created_at: 2025-05-22T12:56:01Z
body: > This is good. When all is settled if we see there is a need for cases like this getImage to be available on globlaThis and global, we could think of refactoring by injecting an utility object from the test runner so that we have one for browser and one for node. I understand that having visual tests running in playwright browser and also by the test runner in node is not the standard playwright usage.

we could maybe split this into projects
1 project is chromium and another one node
running same suite of tests
to not mix up the stuff
I could have a look later after we clean the repo out of qunit
I also have some idea to speed up playwright tests that I want to try out
author_association: COLLABORATOR