From dc2030ea6a0f763e1233edda63496bafb16b0a07 Mon Sep 17 00:00:00 2001 From: Neo Date: Tue, 23 Jun 2026 11:08:09 +0000 Subject: [PATCH] fix: treat null and undefined as equal in LiquidJS IS_PRESENT computation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The custom equality operator used strict JavaScript equality (===). In the Computation Library, IS_PRESENT checks use '!= NIL' where NIL resolves to undefined (an undefined variable in scope). When Django sends null for a never-filled questionnaire field, the comparison null != NIL(undefined) evaluated to true (null !== undefined) — incorrectly marking the blank field as 'present'. This caused blank questionnaire fields (e.g., a Discount column) to appear in the generated Docx even when no value was entered. Fix: In defaultHandler(), treat null and undefined as equivalent: - null == null → true (no change) - null == undefined → true (fix: was false) - 0 == null → false (no change) - false == null → false (no change) - '' == null → false (no change) This aligns with the isTruthy/isFalsy semantics already used in the 'if' tag evaluation and the LiquidJS runtime, where null and undefined are both considered 'nil'. Affected field types: NUMBER, TEXT, DROPDOWN, CHECKBOX, CURRENCY, DURATION (any type relying on '!= NIL' in IS_PRESENT computations). Bumps version to 3.1.1. Co-authored-by: Shreekaran Gandikota --- package.json | 2 +- sd-custom/custom-operator/equals.js | 25 ++++++++++++++++++++ test/sd-custom/custom-operator/equals.js | 29 ++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) 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); + }); + });