/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { describe, it } from 'node:test'; import assert from 'node:assert'; import { setOpMatches, stringOpMatches, matchStringAnyNone, numericOpMatches, valueOpMatches, combineRuleResults, isFilterRule, parseFilterRules, Rule, } from './filter-rules.js'; describe('op helpers tolerate an undefined candidate (#1195)', () => { // getTypeName / property accessors are typed `string` but return undefined // for untyped entities at runtime; the helpers must not throw on it. const undef = undefined as unknown as string; it('setOpMatches treats undefined as "not one of the values"', () => { assert.doesNotThrow(() => setOpMatches('in', undef, ['IfcWall'])); assert.strictEqual(setOpMatches('in', undef, ['IfcWall']), false); assert.strictEqual(setOpMatches('notIn', undef, ['IfcWall']), true); }); it('stringOpMatches does not throw on an undefined candidate', () => { assert.doesNotThrow(() => stringOpMatches('contains', undef, 'wall')); assert.strictEqual(stringOpMatches('contains', undef, 'wall'), false); }); it('valueOpMatches treats undefined as not set', () => { assert.strictEqual(valueOpMatches('isNotSet', undef, ''), true); assert.strictEqual(valueOpMatches('isSet', undef, ''), false); assert.doesNotThrow(() => valueOpMatches('eq', undef, 'x')); }); }); describe('setOpMatches', () => { it('matches case-insensitively for "in"', () => { assert.strictEqual(setOpMatches('in', 'IfcWall', ['ifcwall', 'IfcDoor']), true); assert.strictEqual(setOpMatches('in', 'IfcSlab', ['IfcWall', 'IfcDoor']), false); }); it('inverts for "notIn"', () => { assert.strictEqual(setOpMatches('notIn', 'IfcSlab', ['IfcWall']), true); assert.strictEqual(setOpMatches('notIn', 'IfcWall', ['IfcWall']), false); }); it('treats an empty values list as no match for "in"', () => { assert.strictEqual(setOpMatches('in', 'IfcWall', []), false); assert.strictEqual(setOpMatches('notIn', 'IfcWall', []), true); }); }); describe('stringOpMatches', () => { it('eq / ne are case-insensitive', () => { assert.strictEqual(stringOpMatches('eq', 'Foo', 'FOO'), true); assert.strictEqual(stringOpMatches('ne', 'Foo', 'bar'), true); assert.strictEqual(stringOpMatches('ne', 'Foo', 'foo'), false); }); it('contains / notContains ignore case', () => { assert.strictEqual(stringOpMatches('contains', 'Wall-EXT', 'ext'), true); assert.strictEqual(stringOpMatches('notContains', 'Wall-EXT', 'int'), true); }); it('startsWith ignores case', () => { assert.strictEqual(stringOpMatches('startsWith', 'IfcWallStandardCase', 'ifcwall'), true); assert.strictEqual(stringOpMatches('startsWith', 'IfcWall', 'wall'), false); }); }); describe('matchStringAnyNone', () => { const layers = ['Concrete C30/37', 'Rigid Insulation', 'Gypsum Board']; it('positive ops match if ANY candidate satisfies them', () => { assert.strictEqual(matchStringAnyNone('contains', layers, 'insulation'), true); assert.strictEqual(matchStringAnyNone('eq', layers, 'gypsum board'), true); assert.strictEqual(matchStringAnyNone('startsWith', layers, 'concrete'), true); assert.strictEqual(matchStringAnyNone('contains', layers, 'timber'), false); }); it('negative ops match only if NO candidate violates them', () => { assert.strictEqual(matchStringAnyNone('notContains', layers, 'timber'), true); assert.strictEqual(matchStringAnyNone('notContains', layers, 'concrete'), false); assert.strictEqual(matchStringAnyNone('ne', layers, 'steel'), true); assert.strictEqual(matchStringAnyNone('ne', layers, 'gypsum board'), false); }); it('an empty candidate set never matches — including negative ops', () => { assert.strictEqual(matchStringAnyNone('contains', [], 'concrete'), false); assert.strictEqual(matchStringAnyNone('notContains', [], 'concrete'), false); assert.strictEqual(matchStringAnyNone('ne', [], 'concrete'), false); }); }); describe('numericOpMatches', () => { it('eq uses 1e-9 epsilon (matches Rust impl)', () => { assert.strictEqual(numericOpMatches('eq', 1.0 + 1e-12, 1.0), true); assert.strictEqual(numericOpMatches('eq', 1.0 + 1e-7, 1.0), false); }); it('gt/gte/lt/lte are exact', () => { assert.strictEqual(numericOpMatches('gt', 5, 5), false); assert.strictEqual(numericOpMatches('gte', 5, 5), true); assert.strictEqual(numericOpMatches('lt', 5, 5), false); assert.strictEqual(numericOpMatches('lte', 5, 5), true); }); }); describe('valueOpMatches', () => { it('isSet / isNotSet check string presence', () => { assert.strictEqual(valueOpMatches('isSet', 'foo', ''), true); assert.strictEqual(valueOpMatches('isSet', '', ''), false); assert.strictEqual(valueOpMatches('isNotSet', '', ''), true); }); it('eq / ne / contains pass through case-insensitive', () => { assert.strictEqual(valueOpMatches('eq', 'Concrete', 'concrete'), true); assert.strictEqual(valueOpMatches('contains', 'C30/37', '30'), true); assert.strictEqual(valueOpMatches('notContains', 'C30/37', '50'), true); }); it('numeric ops parse both sides as floats; NaN parses fail closed', () => { assert.strictEqual(valueOpMatches('gt', '12.5', '10'), true); assert.strictEqual(valueOpMatches('lt', '12.5', '10'), false); assert.strictEqual(valueOpMatches('gt', 'abc', '10'), false); assert.strictEqual(valueOpMatches('gt', '12', 'abc'), false); }); }); describe('combineRuleResults', () => { it('AND requires all true', () => { assert.strictEqual(combineRuleResults('AND', [true, true]), true); assert.strictEqual(combineRuleResults('AND', [true, false]), false); }); it('OR requires any true', () => { assert.strictEqual(combineRuleResults('OR', [false, true]), true); assert.strictEqual(combineRuleResults('OR', [false, false]), false); }); it('returns false on an empty list (no rule = no match)', () => { assert.strictEqual(combineRuleResults('AND', []), false); assert.strictEqual(combineRuleResults('OR', []), false); }); }); describe('isFilterRule / parseFilterRules', () => { it('accepts every known kind', () => { assert.strictEqual(isFilterRule(Rule.storey(['L1'])), true); assert.strictEqual(isFilterRule(Rule.ifcType(['IfcWall'])), true); assert.strictEqual(isFilterRule(Rule.predefinedType(['SOLID'])), true); assert.strictEqual(isFilterRule(Rule.name('contains', 'wall')), true); assert.strictEqual(isFilterRule(Rule.property('Pset_X', 'P', 'eq', 'v')), true); assert.strictEqual(isFilterRule(Rule.quantity('Qto_X', 'Q', 'gt', 1)), true); assert.strictEqual(isFilterRule(Rule.material('contains', 'Concrete')), true); assert.strictEqual(isFilterRule(Rule.classification('Uniclass', 'contains', 'Pr_')), true); assert.strictEqual(isFilterRule(Rule.elevation('gt', 3)), true); }); it('rejects unknown kinds and non-objects', () => { assert.strictEqual(isFilterRule({ kind: 'bogus' }), false); assert.strictEqual(isFilterRule(null), false); assert.strictEqual(isFilterRule('storey'), false); }); it('parseFilterRules drops invalid entries', () => { const parsed = parseFilterRules([ { kind: 'ifcType', values: ['IfcWall'], op: 'in' }, { kind: 'unknown' }, 'nope', ]); assert.strictEqual(parsed.length, 1); assert.strictEqual(parsed[0].kind, 'ifcType'); }); });