- Add validation system to prevent contradicting conditions: - Same entity in include/exclude detection - Parent-child conflict detection for tree entities - Redundant selection prevention - Toast notifications for validation errors - Fix entity icons (employees: briefcase, taxes: calculator) - Improve tooltip component: - Use Material Icons instead of broken FA4 icons - Fix positioning using getBoundingClientRect for viewport coords - Add click-to-pin functionality with close button - Pinned tooltips show X icon and close button in corner - Add lightweight test suite (31 tests) for validation logic Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
616 lines
22 KiB
JavaScript
616 lines
22 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Lightweight Test Runner for Entity Selector Validation
|
|
* No heavy dependencies - runs directly with Node.js
|
|
*/
|
|
|
|
const { JSDOM } = require('jsdom');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Setup DOM environment with script execution enabled
|
|
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
url: 'http://localhost',
|
|
runScripts: 'dangerously',
|
|
resources: 'usable'
|
|
});
|
|
|
|
const window = dom.window;
|
|
const document = window.document;
|
|
|
|
// Initialize mixin namespace before loading scripts
|
|
window._EntitySelectorMixins = {};
|
|
|
|
// Load jQuery using window.eval to properly share context
|
|
const jqueryCode = fs.readFileSync(
|
|
path.join(__dirname, '../node_modules/jquery/dist/jquery.min.js'),
|
|
'utf8'
|
|
);
|
|
window.eval(jqueryCode);
|
|
|
|
// Make $ available in Node context
|
|
const $ = window.jQuery;
|
|
|
|
// Load and execute the validation mixin using window.eval
|
|
const validationCode = fs.readFileSync(
|
|
path.join(__dirname, '../sources/js/admin/entity-selector/_validation.js'),
|
|
'utf8'
|
|
);
|
|
window.eval(validationCode);
|
|
|
|
// Get the validation mixin
|
|
const validationMixin = window._EntitySelectorMixins.validation;
|
|
|
|
// Test utilities
|
|
let passed = 0;
|
|
let failed = 0;
|
|
let currentGroup = '';
|
|
|
|
function describe(name, fn) {
|
|
currentGroup = name;
|
|
console.log(`\n\x1b[1m${name}\x1b[0m`);
|
|
fn();
|
|
}
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
passed++;
|
|
console.log(` \x1b[32m✓\x1b[0m ${name}`);
|
|
} catch (e) {
|
|
failed++;
|
|
console.log(` \x1b[31m✗\x1b[0m ${name}`);
|
|
console.log(` \x1b[31m${e.message}\x1b[0m`);
|
|
}
|
|
}
|
|
|
|
function expect(actual) {
|
|
return {
|
|
toBe(expected) {
|
|
if (actual !== expected) {
|
|
throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
}
|
|
},
|
|
toEqual(expected) {
|
|
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
}
|
|
},
|
|
toContain(expected) {
|
|
if (typeof actual === 'string') {
|
|
if (!actual.includes(expected)) {
|
|
throw new Error(`Expected "${actual}" to contain "${expected}"`);
|
|
}
|
|
} else if (Array.isArray(actual)) {
|
|
if (!actual.includes(expected)) {
|
|
throw new Error(`Expected array to contain ${expected}`);
|
|
}
|
|
}
|
|
},
|
|
toBeDefined() {
|
|
if (actual === undefined) {
|
|
throw new Error('Expected value to be defined');
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Test fixtures
|
|
function createValidator() {
|
|
const $wrapper = $(`
|
|
<div class="entity-selector-wrapper">
|
|
<div class="target-block" data-block-type="categories">
|
|
<div class="selection-group" data-group-index="0">
|
|
<div class="group-include">
|
|
<div class="value-picker include-picker">
|
|
<div class="entity-chips"></div>
|
|
</div>
|
|
</div>
|
|
<div class="exclude-rows-container">
|
|
<div class="exclude-row" data-exclude-index="0">
|
|
<div class="value-picker exclude-picker">
|
|
<div class="entity-chips"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`);
|
|
|
|
$(document.body).empty().append($wrapper);
|
|
|
|
return Object.assign({}, validationMixin, {
|
|
$wrapper: $wrapper,
|
|
$dropdown: $('<div class="target-search-dropdown"></div>'),
|
|
config: { trans: {} },
|
|
activeGroup: {
|
|
blockType: 'categories',
|
|
groupIndex: 0,
|
|
section: 'include',
|
|
excludeIndex: 0,
|
|
searchEntity: 'categories'
|
|
},
|
|
treeFlatData: null,
|
|
escapeHtml: (str) => str,
|
|
escapeAttr: (str) => str
|
|
});
|
|
}
|
|
|
|
function createMockChips($picker, ids) {
|
|
const $chips = $picker.find('.entity-chips');
|
|
$chips.empty();
|
|
ids.forEach(id => {
|
|
$chips.append(`<span class="entity-chip" data-id="${id}"></span>`);
|
|
});
|
|
}
|
|
|
|
// =========================================================================
|
|
// TESTS
|
|
// =========================================================================
|
|
|
|
console.log('\n\x1b[36m╔════════════════════════════════════════════════════════════╗\x1b[0m');
|
|
console.log('\x1b[36m║ Entity Selector Validation Test Suite ║\x1b[0m');
|
|
console.log('\x1b[36m╚════════════════════════════════════════════════════════════╝\x1b[0m');
|
|
|
|
// GROUP 1: Include/Exclude Conflict Detection
|
|
describe('Include/Exclude Conflict Detection', () => {
|
|
|
|
test('TC-001: Block adding to EXCLUDE when entity is in INCLUDE', () => {
|
|
const validator = createValidator();
|
|
const $includePicker = validator.$wrapper.find('.include-picker');
|
|
createMockChips($includePicker, [5]);
|
|
|
|
validator.activeGroup.section = 'exclude';
|
|
const result = validator.validateSelection(5, 'Electronics', 'exclude', {});
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('include_exclude_conflict');
|
|
expect(result.error).toContain('already in the include list');
|
|
});
|
|
|
|
test('TC-002: Block adding to INCLUDE when entity is in EXCLUDE', () => {
|
|
const validator = createValidator();
|
|
const $excludePicker = validator.$wrapper.find('.exclude-picker');
|
|
createMockChips($excludePicker, [5]);
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(5, 'Electronics', 'include', {});
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('include_exclude_conflict');
|
|
expect(result.error).toContain('already in the exclude list');
|
|
});
|
|
|
|
test('TC-003: Allow adding to INCLUDE when entity is NOT in EXCLUDE', () => {
|
|
const validator = createValidator();
|
|
const $excludePicker = validator.$wrapper.find('.exclude-picker');
|
|
createMockChips($excludePicker, [10]);
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(5, 'Electronics', 'include', {});
|
|
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
test('TC-004: Allow adding to EXCLUDE when entity is NOT in INCLUDE', () => {
|
|
const validator = createValidator();
|
|
const $includePicker = validator.$wrapper.find('.include-picker');
|
|
createMockChips($includePicker, [10]);
|
|
|
|
validator.activeGroup.section = 'exclude';
|
|
const result = validator.validateSelection(5, 'Electronics', 'exclude', {});
|
|
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
test('TC-005: Check across multiple exclude rows', () => {
|
|
const validator = createValidator();
|
|
const $container = validator.$wrapper.find('.exclude-rows-container');
|
|
$container.append(`
|
|
<div class="exclude-row" data-exclude-index="1">
|
|
<div class="value-picker exclude-picker">
|
|
<div class="entity-chips">
|
|
<span class="entity-chip" data-id="5"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`);
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(5, 'Electronics', 'include', {});
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('include_exclude_conflict');
|
|
});
|
|
});
|
|
|
|
// GROUP 2: Redundant Selection Detection
|
|
describe('Redundant Selection Detection', () => {
|
|
|
|
test('TC-010: Block duplicate selection in INCLUDE', () => {
|
|
const validator = createValidator();
|
|
const $includePicker = validator.$wrapper.find('.include-picker');
|
|
createMockChips($includePicker, [5]);
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(5, 'Electronics', 'include', {});
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('redundant');
|
|
expect(result.error).toContain('already selected');
|
|
});
|
|
|
|
test('TC-011: Block duplicate selection in EXCLUDE', () => {
|
|
const validator = createValidator();
|
|
const $excludePicker = validator.$wrapper.find('.exclude-picker');
|
|
createMockChips($excludePicker, [5]);
|
|
|
|
validator.activeGroup.section = 'exclude';
|
|
const result = validator.validateSelection(5, 'Electronics', 'exclude', {});
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('redundant');
|
|
expect(result.error).toContain('already in an exclude list');
|
|
});
|
|
|
|
test('TC-012: Allow selection when not duplicate', () => {
|
|
const validator = createValidator();
|
|
const $includePicker = validator.$wrapper.find('.include-picker');
|
|
createMockChips($includePicker, [10]);
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(5, 'Electronics', 'include', {});
|
|
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
});
|
|
|
|
// GROUP 3: Parent-Child Conflict Detection
|
|
describe('Parent-Child Conflict Detection', () => {
|
|
|
|
const treeData = [
|
|
{ id: 1, name: 'Electronics', parent_id: 0 },
|
|
{ id: 2, name: 'Computers', parent_id: 1 },
|
|
{ id: 3, name: 'Phones', parent_id: 1 },
|
|
{ id: 4, name: 'Laptops', parent_id: 2 },
|
|
{ id: 5, name: 'Desktops', parent_id: 2 },
|
|
{ id: 6, name: 'Smartphones', parent_id: 3 },
|
|
{ id: 7, name: 'Feature Phones', parent_id: 3 }
|
|
];
|
|
|
|
test('TC-020: Block including child when PARENT is EXCLUDED', () => {
|
|
const validator = createValidator();
|
|
validator.treeFlatData = treeData;
|
|
const $excludePicker = validator.$wrapper.find('.exclude-picker');
|
|
createMockChips($excludePicker, [2]); // Computers excluded
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(4, 'Laptops', 'include', { parentId: 2 });
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('parent_excluded');
|
|
expect(result.error).toContain('Computers');
|
|
});
|
|
|
|
test('TC-021: Block including grandchild when GRANDPARENT is EXCLUDED', () => {
|
|
const validator = createValidator();
|
|
validator.treeFlatData = treeData;
|
|
const $excludePicker = validator.$wrapper.find('.exclude-picker');
|
|
createMockChips($excludePicker, [1]); // Electronics excluded
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(4, 'Laptops', 'include', { parentId: 2 });
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('parent_excluded');
|
|
expect(result.error).toContain('Electronics');
|
|
});
|
|
|
|
test('TC-022: Allow including child when SIBLING is EXCLUDED', () => {
|
|
const validator = createValidator();
|
|
validator.treeFlatData = treeData;
|
|
const $excludePicker = validator.$wrapper.find('.exclude-picker');
|
|
createMockChips($excludePicker, [5]); // Desktops (sibling) excluded
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(4, 'Laptops', 'include', { parentId: 2 });
|
|
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
test('TC-030: Block including parent when CHILD is EXCLUDED', () => {
|
|
const validator = createValidator();
|
|
validator.treeFlatData = treeData;
|
|
const $excludePicker = validator.$wrapper.find('.exclude-picker');
|
|
createMockChips($excludePicker, [4]); // Laptops excluded
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(2, 'Computers', 'include', { parentId: 1 });
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('child_excluded');
|
|
expect(result.error).toContain('Laptops');
|
|
});
|
|
|
|
test('TC-031: Block including grandparent when GRANDCHILD is EXCLUDED', () => {
|
|
const validator = createValidator();
|
|
validator.treeFlatData = treeData;
|
|
const $excludePicker = validator.$wrapper.find('.exclude-picker');
|
|
createMockChips($excludePicker, [4]); // Laptops excluded
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(1, 'Electronics', 'include', { parentId: 0 });
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('child_excluded');
|
|
});
|
|
|
|
test('TC-040: Block excluding child when PARENT is INCLUDED', () => {
|
|
const validator = createValidator();
|
|
validator.treeFlatData = treeData;
|
|
const $includePicker = validator.$wrapper.find('.include-picker');
|
|
createMockChips($includePicker, [2]); // Computers included
|
|
|
|
validator.activeGroup.section = 'exclude';
|
|
const result = validator.validateSelection(4, 'Laptops', 'exclude', { parentId: 2 });
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('parent_included');
|
|
});
|
|
|
|
test('TC-050: Block excluding parent when CHILDREN are INCLUDED', () => {
|
|
const validator = createValidator();
|
|
validator.treeFlatData = treeData;
|
|
const $includePicker = validator.$wrapper.find('.include-picker');
|
|
createMockChips($includePicker, [4, 5]); // Laptops & Desktops included
|
|
|
|
validator.activeGroup.section = 'exclude';
|
|
const result = validator.validateSelection(2, 'Computers', 'exclude', { parentId: 1 });
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('children_included');
|
|
});
|
|
|
|
test('TC-051: Block excluding grandparent when GRANDCHILDREN are INCLUDED', () => {
|
|
const validator = createValidator();
|
|
validator.treeFlatData = treeData;
|
|
const $includePicker = validator.$wrapper.find('.include-picker');
|
|
createMockChips($includePicker, [4]); // Laptops included
|
|
|
|
validator.activeGroup.section = 'exclude';
|
|
const result = validator.validateSelection(1, 'Electronics', 'exclude', { parentId: 0 });
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('children_included');
|
|
});
|
|
|
|
test('TC-052: Allow excluding when no children are included', () => {
|
|
const validator = createValidator();
|
|
validator.treeFlatData = treeData;
|
|
const $includePicker = validator.$wrapper.find('.include-picker');
|
|
createMockChips($includePicker, [3]); // Phones (different branch) included
|
|
|
|
validator.activeGroup.section = 'exclude';
|
|
const result = validator.validateSelection(2, 'Computers', 'exclude', { parentId: 1 });
|
|
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
});
|
|
|
|
// GROUP 4: Non-Tree Entities
|
|
describe('Non-Tree Entity Validation', () => {
|
|
|
|
test('TC-060: Products - no tree validation, only include/exclude check', () => {
|
|
const validator = createValidator();
|
|
validator.activeGroup.searchEntity = 'products';
|
|
validator.treeFlatData = null;
|
|
|
|
const $excludePicker = validator.$wrapper.find('.exclude-picker');
|
|
createMockChips($excludePicker, [100]);
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(100, 'Product A', 'include', {});
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('include_exclude_conflict');
|
|
});
|
|
|
|
test('TC-061: Manufacturers - allow selection without tree validation', () => {
|
|
const validator = createValidator();
|
|
validator.activeGroup.searchEntity = 'manufacturers';
|
|
validator.treeFlatData = null;
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(50, 'Nike', 'include', {});
|
|
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
});
|
|
|
|
// GROUP 5: Edge Cases
|
|
describe('Edge Cases', () => {
|
|
|
|
test('TC-070: Empty selections - allow any selection', () => {
|
|
const validator = createValidator();
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(1, 'Category A', 'include', {});
|
|
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
test('TC-071: No activeGroup - return valid (fail-safe)', () => {
|
|
const validator = createValidator();
|
|
validator.activeGroup = null;
|
|
const result = validator.validateSelection(1, 'Category A', 'include', {});
|
|
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
test('TC-072: String ID coercion to integer', () => {
|
|
const validator = createValidator();
|
|
const $includePicker = validator.$wrapper.find('.include-picker');
|
|
$includePicker.find('.entity-chips').append('<span class="entity-chip" data-id="5"></span>');
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(5, 'Category', 'include', {});
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('redundant');
|
|
});
|
|
|
|
test('TC-074: Root category (parent_id = 0)', () => {
|
|
const validator = createValidator();
|
|
validator.treeFlatData = [
|
|
{ id: 1, name: 'Root', parent_id: 0 },
|
|
{ id: 2, name: 'Child', parent_id: 1 }
|
|
];
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(1, 'Root', 'include', { parentId: 0 });
|
|
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
test('TC-075: Deep nesting (6 levels)', () => {
|
|
const validator = createValidator();
|
|
validator.treeFlatData = [
|
|
{ id: 1, name: 'Level 1', parent_id: 0 },
|
|
{ id: 2, name: 'Level 2', parent_id: 1 },
|
|
{ id: 3, name: 'Level 3', parent_id: 2 },
|
|
{ id: 4, name: 'Level 4', parent_id: 3 },
|
|
{ id: 5, name: 'Level 5', parent_id: 4 },
|
|
{ id: 6, name: 'Level 6', parent_id: 5 }
|
|
];
|
|
|
|
const $excludePicker = validator.$wrapper.find('.exclude-picker');
|
|
createMockChips($excludePicker, [1]); // Root excluded
|
|
|
|
validator.activeGroup.section = 'include';
|
|
const result = validator.validateSelection(6, 'Level 6', 'include', { parentId: 5 });
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.type).toBe('parent_excluded');
|
|
});
|
|
});
|
|
|
|
// GROUP 6: Helper Functions
|
|
describe('Helper Functions', () => {
|
|
|
|
test('TC-080: getChipIds returns correct IDs', () => {
|
|
const validator = createValidator();
|
|
const $picker = validator.$wrapper.find('.include-picker');
|
|
createMockChips($picker, [1, 5, 10, 25]);
|
|
|
|
const ids = validator.getChipIds($picker);
|
|
|
|
expect(ids).toEqual([1, 5, 10, 25]);
|
|
});
|
|
|
|
test('TC-081: getAncestorIds returns correct ancestors', () => {
|
|
const validator = createValidator();
|
|
validator.treeFlatData = [
|
|
{ id: 1, name: 'Root', parent_id: 0 },
|
|
{ id: 2, name: 'Child', parent_id: 1 },
|
|
{ id: 3, name: 'Grandchild', parent_id: 2 }
|
|
];
|
|
|
|
const lookup = {};
|
|
validator.treeFlatData.forEach(item => {
|
|
lookup[item.id] = item;
|
|
});
|
|
|
|
const ancestors = validator.getAncestorIds(3, lookup);
|
|
|
|
expect(ancestors).toContain(2);
|
|
expect(ancestors).toContain(1);
|
|
expect(ancestors.length).toBe(2);
|
|
});
|
|
|
|
test('TC-082: getDescendantIds returns correct descendants', () => {
|
|
const validator = createValidator();
|
|
validator.treeFlatData = [
|
|
{ id: 1, name: 'Root', parent_id: 0 },
|
|
{ id: 2, name: 'Child A', parent_id: 1 },
|
|
{ id: 3, name: 'Child B', parent_id: 1 },
|
|
{ id: 4, name: 'Grandchild', parent_id: 2 }
|
|
];
|
|
|
|
const lookup = {};
|
|
validator.treeFlatData.forEach(item => {
|
|
lookup[item.id] = item;
|
|
});
|
|
|
|
const descendants = validator.getDescendantIds(1, lookup);
|
|
|
|
expect(descendants).toContain(2);
|
|
expect(descendants).toContain(3);
|
|
expect(descendants).toContain(4);
|
|
expect(descendants.length).toBe(3);
|
|
});
|
|
});
|
|
|
|
// GROUP 7: Bulk Validation
|
|
describe('Bulk Validation (validatePendingSelections)', () => {
|
|
|
|
test('TC-090: Returns errors for conflicts', () => {
|
|
const validator = createValidator();
|
|
const $excludePicker = validator.$wrapper.find('.exclude-picker');
|
|
createMockChips($excludePicker, [5]);
|
|
|
|
const pendingSelections = [
|
|
{ id: 1, name: 'Category A', data: {} },
|
|
{ id: 5, name: 'Category E', data: {} }, // Conflict
|
|
{ id: 10, name: 'Category J', data: {} }
|
|
];
|
|
|
|
const errors = validator.validatePendingSelections(pendingSelections, 'include');
|
|
|
|
expect(errors.length).toBe(1);
|
|
expect(errors[0].id).toBe(5);
|
|
expect(errors[0].type).toBe('include_exclude_conflict');
|
|
});
|
|
|
|
test('TC-091: Returns empty for valid selections', () => {
|
|
const validator = createValidator();
|
|
|
|
const pendingSelections = [
|
|
{ id: 1, name: 'Category A', data: {} },
|
|
{ id: 2, name: 'Category B', data: {} }
|
|
];
|
|
|
|
const errors = validator.validatePendingSelections(pendingSelections, 'include');
|
|
|
|
expect(errors.length).toBe(0);
|
|
});
|
|
|
|
test('TC-092: Handles empty array', () => {
|
|
const validator = createValidator();
|
|
const errors = validator.validatePendingSelections([], 'include');
|
|
expect(errors.length).toBe(0);
|
|
});
|
|
|
|
test('TC-093: Handles null', () => {
|
|
const validator = createValidator();
|
|
const errors = validator.validatePendingSelections(null, 'include');
|
|
expect(errors.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// SUMMARY
|
|
// =========================================================================
|
|
|
|
console.log('\n\x1b[36m══════════════════════════════════════════════════════════════\x1b[0m');
|
|
console.log(`\x1b[1mTest Results:\x1b[0m`);
|
|
console.log(` \x1b[32m✓ Passed: ${passed}\x1b[0m`);
|
|
if (failed > 0) {
|
|
console.log(` \x1b[31m✗ Failed: ${failed}\x1b[0m`);
|
|
} else {
|
|
console.log('\n \x1b[32m🎉 All tests passed!\x1b[0m');
|
|
}
|
|
console.log(`\n Total: ${passed + failed}`);
|
|
console.log('\x1b[36m══════════════════════════════════════════════════════════════\x1b[0m\n');
|
|
|
|
process.exit(failed > 0 ? 1 : 0);
|