From cc25a81c66534dcc076f08729ad84eed019046d7 Mon Sep 17 00:00:00 2001 From: jurra Date: Tue, 7 Jan 2025 07:42:17 +0100 Subject: [PATCH] With this commit we can assign flexibily properties to an instance of an element. Labels can be added to any element as well. An element's position is defined by its terminals currently. --- src/domain/entities/Element.js | 35 +++++- src/domain/valueObjects/Label.js | 51 +++++++++ .../Position.js | 0 src/domain/valueObjects/Properties.js | 28 +++++ .../Resistance.js | 0 tests/domain/Element.test.js | 106 +++++++++--------- 6 files changed, 162 insertions(+), 58 deletions(-) create mode 100644 src/domain/valueObjects/Label.js rename src/domain/{value-objects => valueObjects}/Position.js (100%) create mode 100644 src/domain/valueObjects/Properties.js rename src/domain/{value-objects => valueObjects}/Resistance.js (100%) diff --git a/src/domain/entities/Element.js b/src/domain/entities/Element.js index f9be3b7..69e3b65 100644 --- a/src/domain/entities/Element.js +++ b/src/domain/entities/Element.js @@ -1,3 +1,6 @@ +import { Label } from '../valueObjects/Label.js'; +import { Position } from '../valueObjects/Position.js'; +import { Properties } from '../valueObjects/Properties.js'; /** * Represents an abstract element in a circuit. @@ -5,21 +8,45 @@ * @abstract */ export class Element { - constructor(id, position) { + /** + * Creates an instance of an Element. + * + * @param {string} id - The unique identifier for the element. + * @param {Position[]} terminals - The list of terminal positions. + * @param {Label|null} label - The label of the element (optional). + * @param {Properties} properties - A container for the element's specific properties. + * @throws {Error} If attempting to instantiate the abstract class directly. + */ + constructor(id, terminals, label = null, properties) + { if (new.target === Element) { throw new Error("Cannot instantiate abstract class Element directly."); } + if (!Array.isArray(terminals) || !terminals.every(t => t instanceof Position)) { + throw new Error("Terminals must be an array of Position instances."); + } + if (label !== null && !(label instanceof Label)) { + throw new Error("Label must be an instance of Label or null."); + } + if (!(properties instanceof Properties)) { + throw new Error("Properties must be an instance of Properties."); + } + this.id = id; - this.position = position; // { x, y } + this.terminals = terminals; + this.label = label; this.type = null; // Each subclass must define its type + this.properties = properties; // Properties container } /** - * Describes the element with its type, id, and position. + * Describes the element with its type, id, terminals, label, and properties. * * @returns {string} A string description of the element. */ describe() { - return `${this.type} (${this.id}) at position (${this.position.x}, ${this.position.y})`; + const labelText = this.label ? `, label: "${this.label}"` : ''; + const terminalsText = this.terminals.map(t => `(${t.x}, ${t.y})`).join(', '); + return `${this.type} (${this.id}): terminals: [${terminalsText}]${labelText}, properties: ${this.properties.describe()}`; } } diff --git a/src/domain/valueObjects/Label.js b/src/domain/valueObjects/Label.js new file mode 100644 index 0000000..d7dc9f3 --- /dev/null +++ b/src/domain/valueObjects/Label.js @@ -0,0 +1,51 @@ + +/** + * Represents a label value object. + * + * @class + */ +export class Label { + /** + * Creates an instance of Label. + * + * @constructor + * @param {string} value - The label value. + * @throws {Error} If the label is invalid. + */ + constructor(value) { + if (!Label.isValid(value)) { + throw new Error("Invalid label: Must be non-empty and less than 50 characters."); + } + this.value = value; + } + + /** + * Validates the label value. + * + * @static + * @param {string} value - The label value to validate. + * @returns {boolean} True if the value is valid, otherwise false. + */ + static isValid(value) { + return typeof value === 'string' && value.trim().length > 0 && value.length <= 50; + } + + /** + * Returns the string representation of the label. + * + * @returns {string} The label value. + */ + toString() { + return this.value; + } + + /** + * Checks if this label is equal to another label. + * + * @param {Label} other - The other label to compare. + * @returns {boolean} True if the labels are equal, otherwise false. + */ + equals(other) { + return other instanceof Label && this.value === other.value; + } +} diff --git a/src/domain/value-objects/Position.js b/src/domain/valueObjects/Position.js similarity index 100% rename from src/domain/value-objects/Position.js rename to src/domain/valueObjects/Position.js diff --git a/src/domain/valueObjects/Properties.js b/src/domain/valueObjects/Properties.js new file mode 100644 index 0000000..5f367f5 --- /dev/null +++ b/src/domain/valueObjects/Properties.js @@ -0,0 +1,28 @@ +/** + * A container for element-specific properties. + */ +export class Properties { + /** + * Creates a properties container. + * + * @param {Object} values - An object containing the value objects for the properties. + */ + constructor(values = {}) { + if (typeof values !== 'object') { + throw new Error("Properties must be an object."); + } + + this.values = values; // Store value objects (e.g., Resistance, Capacitance) + } + + /** + * Describes the properties in the container. + * + * @returns {string} A string description of the properties. + */ + describe() { + return Object.entries(this.values) + .map(([key, value]) => `${key}: ${value.toString()}`) + .join(', '); + } +} diff --git a/src/domain/value-objects/Resistance.js b/src/domain/valueObjects/Resistance.js similarity index 100% rename from src/domain/value-objects/Resistance.js rename to src/domain/valueObjects/Resistance.js diff --git a/tests/domain/Element.test.js b/tests/domain/Element.test.js index 9e0d417..b614d6a 100644 --- a/tests/domain/Element.test.js +++ b/tests/domain/Element.test.js @@ -1,85 +1,83 @@ import { expect } from 'chai'; import { Element } from '../../src/domain/entities/Element.js'; -import { Position } from '../../src/domain/value-objects/Position.js'; -import { Resistance } from '../../src/domain/value-objects/Resistance.js'; +import { Position } from '../../src/domain/valueObjects/Position.js'; +import { Label } from '../../src/domain/valueObjects/Label.js'; +import { Properties } from '../../src/domain/valueObjects/Properties.js'; +console.log("This test runs successfully") + +describe('Basic Import Test', () => { + it('should import Element successfully', () => { + expect(Element).to.be.a('function'); + }); +}); /** - * MockElement class extends the Element class to create a mock element for testing purposes. - * - * @class - * @extends Element - * - * @param {string} id - The unique identifier for the element. - * @param {Array} [terminals=[]] - The terminals associated with the element. - * @param {Object} [properties={}] - The properties of the element. - * @param {string} [label=''] - The label for the element. + * MockElement class extends the Element class for testing purposes. */ class MockElement extends Element { - constructor(id, terminals = [], properties = {}, label = '') { - super(id, terminals, properties, label); + constructor(id, terminals = [], label = null, properties = new Properties()) { + super(id, terminals, label, properties); this.type = 'mock'; - this.terminals = terminals; - this.properties = { ...properties }; } } describe('Element Class Tests', () => { it('An element should have a unique identifier', () => { - const element = new MockElement('E1', []); + const element = new MockElement('E1', [new Position(10, 20)], null, new Properties()); expect(element.id).to.equal('E1'); }); - it('An element should have default properties', () => { - const element = new MockElement('E2', [], { resistance: new Resistance(100) }); - expect(element.properties.resistance.value).to.equal(100); + it('An element should validate terminals as an array of Position instances', () => { + const terminals = [new Position(10, 20), new Position(30, 40)]; + const element = new MockElement('E2', terminals, null, new Properties()); + expect(element.terminals).to.deep.equal(terminals); }); - it('An element should support editable properties', () => { - const element = new MockElement('E3', [], { resistance: new Resistance(100) }); - element.properties.resistance = new Resistance(200); - expect(element.properties.resistance.value).to.equal(200); + it('An element should throw an error if terminals are invalid', () => { + expect(() => new MockElement('E3', [10, 20], null, new Properties())).to.throw( + "Terminals must be an array of Position instances." + ); }); - it('An element should be abstract and not directly instantiable', () => { - expect(() => new Element('E4', [])).to.throw('Cannot instantiate abstract class Element directly.'); + it('An element should accept a label of type Label or null', () => { + const label = new Label('Test Label'); + const element = new MockElement('E4', [new Position(10, 20)], label, new Properties()); + expect(element.label).to.equal(label); }); - it('An element should support multiple properties', () => { - const element = new MockElement('E5', [], { resistance: new Resistance(100), capacitance: 0.01 }); - expect(element.properties.resistance.value).to.equal(100); - expect(element.properties.capacitance).to.equal(0.01); + it('An element should throw an error if label is invalid', () => { + expect(() => new MockElement('E5', [new Position(10, 20)], 'Invalid Label', new Properties())).to.throw( + "Label must be an instance of Label or null." + ); }); - it('An element should use value objects for shared properties', () => { - const position = new Position(10, 20); - const element = new MockElement('E6', [{ name: 'A', position }]); - expect(element.terminals[0].position.equals(position)).to.be.true; + it('An element should accept properties of type Properties', () => { + const properties = new Properties({ resistance: 100 }); + const element = new MockElement('E6', [new Position(10, 20)], null, properties); + expect(element.properties).to.equal(properties); }); - it('The position of an element should depend on its terminals', () => { - const position1 = new Position(10, 20); - const position2 = new Position(30, 40); - const element = new MockElement('E7', [ - { name: 'A', position: position1 }, - { name: 'B', position: position2 }, - ]); - expect(element.terminals.length).to.equal(2); - expect(element.terminals[0].position.equals(position1)).to.be.true; - expect(element.terminals[1].position.equals(position2)).to.be.true; + it('An element should throw an error if properties are invalid', () => { + expect(() => new MockElement('E7', [new Position(10, 20)], null, { invalid: 'properties' })).to.throw( + "Properties must be an instance of Properties." + ); }); - it('An element should support labeling', () => { - const element = new MockElement('E8', [], {}, 'Test Label'); - expect(element.label).to.equal('Test Label'); + it('An element should be abstract and not directly instantiable', () => { + expect(() => new Element('E8', [new Position(10, 20)], null, new Properties())).to.throw( + "Cannot instantiate abstract class Element directly." + ); }); - it('A junction should be a valid element', () => { - const junction = new MockElement('J1', [ - { name: 'T1', position: new Position(10, 20) }, - { name: 'T2', position: new Position(30, 40) }, - { name: 'T3', position: new Position(50, 60) }, - ]); - expect(junction.type).to.equal('mock'); - expect(junction.terminals.length).to.equal(3); + it('An element should support the describe method', () => { + const terminals = [new Position(10, 20), new Position(30, 40)]; + const label = new Label('Test Label'); + const properties = new Properties({ resistance: 100 }); + const element = new MockElement('E9', terminals, label, properties); + + const description = element.describe(); + expect(description).to.equal( + 'mock (E9): terminals: [(10, 20), (30, 40)], label: "Test Label", properties: resistance: 100' + ); }); });