diff --git a/package.json b/package.json index 2cffc26250..47916c3171 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "liquidjs", - "version": "3.1.0", + "version": "3.1.1", "description": "Liquid template engine by pure JavaScript: compatible to shopify, easy to extend.", "main": "index.js", "scripts": { diff --git a/sd-custom/custom-operator/equals.js b/sd-custom/custom-operator/equals.js index d9672bf3b9..1e509fb04f 100644 --- a/sd-custom/custom-operator/equals.js +++ b/sd-custom/custom-operator/equals.js @@ -95,7 +95,32 @@ function arrayEquals(l, r) { }); } +/** + * Default equality handler for primitive values. + * + * Treats `null` and `undefined` as equivalent — both represent an absent/nil value. + * This aligns with JavaScript's loose-equality semantics for nil (`null == undefined`) + * and fixes the IS_PRESENT computation bug where a questionnaire field initialised as + * `null` (never filled) was being treated as "present" because: + * `NIL` (undefined variable) → `undefined` + * `null !== undefined` → `true` ← incorrect: null should equal NIL + * + * Examples: + * defaultHandler(null, undefined) → true (both are nil) + * defaultHandler(null, null) → true + * defaultHandler(undefined, undefined) → true + * defaultHandler(0, null) → false (0 is a real value, not nil) + * defaultHandler(0, false) → false (unchanged strict equality) + */ function defaultHandler(l, r) { + const lIsNil = l === null || l === undefined; + const rIsNil = r === null || r === undefined; + + // Both nil → equal + if (lIsNil && rIsNil) return true; + // Only one nil → not equal + if (lIsNil !== rIsNil) return false; + // Neither nil → strict equality return l === r; } diff --git a/test/sd-custom/custom-operator/equals.js b/test/sd-custom/custom-operator/equals.js index 2493ba70c3..2dcbad271e 100644 --- a/test/sd-custom/custom-operator/equals.js +++ b/test/sd-custom/custom-operator/equals.js @@ -159,3 +159,32 @@ describe("Custom equals operator", () => { }); }); }); + + describe("nil equality — null and undefined should be treated as equal", () => { + it("should return true for null == null", () => { + expect(equals(null, null)).to.equal(true); + }); + it("should return true for undefined == undefined", () => { + expect(equals(undefined, undefined)).to.equal(true); + }); + it("should return true for null == undefined (NIL comparison fix)", () => { + // NIL in LiquidJS resolves to `undefined` (undefined variable). + // A questionnaire field that was never filled is sent as `null` by Django. + // Without this fix, `null != NIL` evaluated to `true` causing IS_PRESENT + // to incorrectly return true for blank fields. + expect(equals(null, undefined)).to.equal(true); + expect(equals(undefined, null)).to.equal(true); + }); + it("should return false for null == 0 (0 is a real number value, not nil)", () => { + expect(equals(null, 0)).to.equal(false); + expect(equals(0, null)).to.equal(false); + }); + it("should return false for null == false (false is a real boolean value, not nil)", () => { + expect(equals(null, false)).to.equal(false); + expect(equals(false, null)).to.equal(false); + }); + it("should return false for null == empty string (empty string is not nil)", () => { + expect(equals(null, "")).to.equal(false); + expect(equals("", null)).to.equal(false); + }); + });