Repository: fabricjs/fabric.js (ts)

Issue:
[Bug]: blurred text in zoom if use paintFirst: "stroke"
### CheckList

- [X] I agree to follow this project's Code of Conduct
- [X] I have read and followed the [Contributing Guide](https://github.com/fabricjs/fabric.js/blob/master/CONTRIBUTING.md)
- [X] I have read and followed the [Issue Tracker Guide](https://github.com/fabricjs/fabric.js/blob/master/CONTRIBUTING.md#%EF%B8%8F-issue-tracker)
- [X] I have searched and referenced existing issues and discussions
- [X] I am filing a **BUG** report.
- [X] I have managed to reproduce the bug **after** upgrading to the **latest** version
- [X] I have created an accurate and minimal reproduction

### Version

6.0.0-beta9

### In What environments are you experiencing the problem?

Chrome

### Node Version (if applicable)

None

### Link To Reproduction

https://codepen.io/jonnas123/pen/poqVQoG

### Steps To Reproduce


Hello, I'm new here, not sure if it's a bug or new feature request. Please go to the link and zoom the text. You will see the text is blurred. After I remove the paintFirst: "stroke" the blurred is gone. How to go with paintFirst: "stroke" without blurred? I need to use paintFirst: "stroke".



### Expected Behavior

After removing paintFirst: "stroke"

![sctwo](https://github.com/fabricjs/fabric.js/assets/90570681/52c1e563-7b46-403e-9611-2b2c58e5a590)



### Actual Behavior

Before removing paintFirst: "stroke"

![scone](https://github.com/fabricjs/fabric.js/assets/90570681/5efc6044-61f2-4d2a-9e75-a8b75b01e296)



### Error Message & Stack Trace

_No response_

Pull request:
fix(Object): Fix detection of falsy shadows in Object.needsItsOwnCache method
## Motivation
@closes #9390

## Description

Fix blur that can happen when zooming in on text. This is due to incorrect caching that should refresh instead.

## Changes

Update shadow on Objects to have undefined instead of null. Also fix cache check to check for not undefined rather than typeof.
Changing to undefined required having to change several tests which were expecting null and also to use an actual JSON object rather than a parsed string (string parse does not support undefined).
Utilized playwright to have an image compare for the test. I tried reverting the code and saw that it had a 0.02 diff so I used 0.01 to ensure it failed if the old code was present.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5752a07f1..7afa18d32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## [next]
 
+- fix(Object): Fix detection of falsy shadows in Object.needsItsOwnCache method [#9469](https://github.com/fabricjs/fabric.js/pull/9469)
 - feat(util): expose `calcPlaneRotation` [#9419](https://github.com/fabricjs/fabric.js/pull/9419)
 - refactor(Canvas): BREAKING remove button from mouse events, delegate to event.button property [#9449](https://github.com/fabricjs/fabric.js/pull/9449)
 - patch(Canvas): move event mouse:up:before earlier in the logic for more control [#9434](https://github.com/fabricjs/fabric.js/pull/9434)
diff --git a/src/shapes/Object/Object.spec.ts b/src/shapes/Object/Object.spec.ts
index 150446937..446886b2d 100644
--- a/src/shapes/Object/Object.spec.ts
+++ b/src/shapes/Object/Object.spec.ts
@@ -1,3 +1,5 @@
+import { Shadow } from '../../Shadow';
+import { Rect } from '../Rect';
 import { FabricObject } from './Object';
 
 describe('Object', () => {
@@ -58,4 +60,44 @@ describe('Object', () => {
     expect(fObj.top).toBe(0);
     expect(fObj.left).toBe(0);
   });
+  describe('needsItsOwnCache', () => {
+    it('returns false for default values', () => {
+      const rect = new Rect({ width: 100, height: 100 });
+      expect(rect.needsItsOwnCache()).toBe(false);
+    });
+    it('returns true when a clipPath is present', () => {
+      const rect = new Rect({ width: 100, height: 100 });
+      rect.clipPath = new Rect({ width: 50, height: 50 });
+      expect(rect.needsItsOwnCache()).toBe(true);
+    });
+    it('returns true when paintFirst is stroke and there is a shadow', () => {
+      const rect = new Rect({ width: 100, height: 100 });
+      rect.paintFirst = 'stroke';
+      rect.stroke = 'black';
+      rect.shadow = new Shadow({ color: 'green' });
+      expect(rect.needsItsOwnCache()).toBe(true);
+    });
+    it('returns false when paintFirst is stroke and there is no shadow', () => {
+      const rect = new Rect({ width: 100, height: 100 });
+      rect.paintFirst = 'stroke';
+      rect.stroke = 'black';
+      rect.shadow = null;
+      expect(rect.needsItsOwnCache()).toBe(false);
+    });
+    it('returns false when paintFirst is stroke but no stroke', () => {
+      const rect = new Rect({ width: 100, height: 100 });
+      rect.paintFirst = 'stroke';
+      rect.stroke = '';
+      rect.shadow = new Shadow({ color: 'green' });
+      expect(rect.needsItsOwnCache()).toBe(false);
+    });
+    it('returns false when paintFirst is stroke but no fill', () => {
+      const rect = new Rect({ width: 100, height: 100 });
+      rect.paintFirst = 'stroke';
+      rect.stroke = 'black';
+      rect.fill = '';
+      rect.shadow = new Shadow({ color: 'green' });
+      expect(rect.needsItsOwnCache()).toBe(false);
+    });
+  });
 });
diff --git a/src/shapes/Object/Object.ts b/src/shapes/Object/Object.ts
index d7d39027f..03f6a2ded 100644
--- a/src/shapes/Object/Object.ts
+++ b/src/shapes/Object/Object.ts
@@ -845,7 +845,7 @@ export class FabricObject<
       this.paintFirst === 'stroke' &&
       this.hasFill() &&
       this.hasStroke() &&
-      typeof this.shadow === 'object'
+      !!this.shadow
     ) {
       return true;
     }


Test patch:
diff --git a/test/unit/object.js b/test/unit/object.js
index 6c6ede2fc..fcd66e557 100644
--- a/test/unit/object.js
+++ b/test/unit/object.js
@@ -1305,34 +1305,6 @@
     assert.equal(object.shouldCache(), true, 'if objectCaching is false, but we have a clipPath, group not cached, we cache anyway');
 
   });
-  QUnit.test('needsItsOwnCache', function(assert) {
-    var object = new fabric.Object();
-    assert.equal(object.needsItsOwnCache(), false, 'default needsItsOwnCache is false');
-    object.clipPath = {};
-    assert.equal(object.needsItsOwnCache(), true, 'with a clipPath is true');
-    delete object.clipPath;
-
-    object.paintFirst = 'stroke';
-    object.stroke = 'black';
-    object.shadow = {};
-    assert.equal(object.needsItsOwnCache(), true, 'if stroke first will return true');
-
-    object.paintFirst = 'stroke';
-    object.stroke = 'black';
-    object.shadow = null;
-    assert.equal(object.needsItsOwnCache(), true, 'if stroke first will return false if no shadow');
-
-    object.paintFirst = 'stroke';
-    object.stroke = '';
-    object.shadow = {};
-    assert.equal(object.needsItsOwnCache(), false, 'if stroke first will return false if no stroke');
-
-    object.paintFirst = 'stroke';
-    object.stroke = 'black';
-    object.fill = '';
-    object.shadow = {};
-    assert.equal(object.needsItsOwnCache(), false, 'if stroke first will return false if no fill');
-  });
   QUnit.test('hasStroke', function(assert) {
     var object = new fabric.Object({ fill: 'blue', width: 100, height: 100, strokeWidth: 3, stroke: 'black' });
     assert.equal(object.hasStroke(), true, 'if strokeWidth is present and stroke is black hasStroke is true');