UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

339 lines (338 loc) 15.9 kB
import { MultipleSelectionBucket } from "./MultipleSelectionBucket"; import { SelectionService } from "./SelectionService"; import { ESelectionStrategy } from "./types"; describe("SelectionService", () => { let service; let bucket; const entityType = "block"; beforeEach(() => { service = new SelectionService(); bucket = new MultipleSelectionBucket(entityType); service.registerBucket(bucket); }); it("registers and retrieves bucket", () => { expect(service.getBucket(entityType)).toBe(bucket); expect(service.getBucket("unknown")).toBeUndefined(); }); it("selects entities via service", () => { service.select(entityType, ["a", "b"], ESelectionStrategy.REPLACE); expect(bucket.$selected.value).toEqual(new Set(["a", "b"])); }); it("deselects entities via service", () => { service.select(entityType, ["a", "b"], ESelectionStrategy.REPLACE); service.deselect(entityType, ["a"]); expect(bucket.$selected.value).toEqual(new Set(["b"])); }); it("checks isSelected via service", () => { service.select(entityType, ["a"], ESelectionStrategy.REPLACE); expect(service.isSelected(entityType, "a")).toBe(true); expect(service.isSelected(entityType, "b")).toBe(false); }); it("resets selection via service", () => { service.select(entityType, ["a", "b"], ESelectionStrategy.REPLACE); service.resetSelection(entityType); expect(bucket.$selected.value.size).toBe(0); }); it("does nothing if bucket not found", () => { expect(() => service.select("unknown", ["x"], ESelectionStrategy.REPLACE)).not.toThrow(); expect(() => service.deselect("unknown", ["x"])).not.toThrow(); expect(service.isSelected("unknown", "x")).toBe(false); expect(() => service.resetSelection("unknown")).not.toThrow(); }); }); // --- MULTI-BUCKET TESTS --- describe("SelectionService with multiple buckets", () => { class MockBucket extends MultipleSelectionBucket { constructor() { super(...arguments); this.resetCalled = false; this.updateSelectionCalled = []; } reset() { this.resetCalled = true; super.reset(); } updateSelection(ids, select, strategy) { this.updateSelectionCalled.push({ ids, select, strategy }); super.updateSelection(ids, select, strategy); } } let service; let bucketA; let bucketB; beforeEach(() => { service = new SelectionService(); bucketA = new MockBucket("A"); bucketB = new MockBucket("B"); service.registerBucket(bucketA); service.registerBucket(bucketB); }); it("REPLACE strategy resets other buckets", () => { // Preselect in both buckets bucketA.updateSelection(["1"], true, ESelectionStrategy.REPLACE); bucketB.updateSelection(["2"], true, ESelectionStrategy.REPLACE); // Select in bucket A with REPLACE service.select("A", ["3"], ESelectionStrategy.REPLACE); // Bucket B should be reset expect(bucketB.resetCalled).toBe(true); // Bucket A should not be reset expect(bucketA.resetCalled).toBe(false); // Bucket A should have new selection expect(bucketA.isSelected("3")).toBe(true); expect(bucketA.isSelected("1")).toBe(false); }); it("REPLACE strategy does not reset its own bucket", () => { service.select("A", ["x"], ESelectionStrategy.REPLACE); expect(bucketA.resetCalled).toBe(false); }); it("SUBTRACT does not reset other buckets", () => { service.select("A", ["x"], ESelectionStrategy.SUBTRACT); expect(bucketB.resetCalled).toBe(false); service.select("B", ["y"], ESelectionStrategy.SUBTRACT); expect(bucketA.resetCalled).toBe(false); }); }); describe("SelectionService corner-cases", () => { let service; let bucket; const entityType = "block"; beforeEach(() => { service = new SelectionService(); bucket = new MultipleSelectionBucket(entityType); service.registerBucket(bucket); }); it("does nothing if entityType is not registered", () => { expect(() => service.select("unknown", ["a"], ESelectionStrategy.REPLACE)).not.toThrow(); expect(() => service.deselect("unknown", ["a"])).not.toThrow(); expect(() => service.resetSelection("unknown")).not.toThrow(); expect(service.isSelected("unknown", "a")).toBe(false); }); it("REPLACE with already selected ids resets other buckets", () => { const bucket2 = new MultipleSelectionBucket("other"); service.registerBucket(bucket2); bucket.updateSelection(["a"], true, ESelectionStrategy.REPLACE); bucket2.updateSelection(["b"], true, ESelectionStrategy.REPLACE); service.select(entityType, ["a"], ESelectionStrategy.REPLACE); expect(bucket2.$selected.value.size).toBe(0); }); it("SUBTRACT with id not in selection does nothing", () => { service.select(entityType, ["a"], ESelectionStrategy.REPLACE); service.deselect(entityType, ["b"]); expect(bucket.$selected.value).toEqual(new Set(["a"])); }); it("reset does nothing if selection is already empty", () => { service.resetSelection(entityType); expect(bucket.$selected.value.size).toBe(0); }); it("treats '1' and 1 as different ids", () => { service.select(entityType, ["1"], ESelectionStrategy.REPLACE); service.select(entityType, [1], ESelectionStrategy.APPEND); expect(bucket.$selected.value).toEqual(new Set(["1", 1])); }); }); describe("SelectionService $selection signal", () => { it("aggregates selection from all buckets and reacts to changes", () => { const service = new SelectionService(); const bucketA = new MultipleSelectionBucket("A"); const bucketB = new MultipleSelectionBucket("B"); service.registerBucket(bucketA); service.registerBucket(bucketB); // Изначально пусто expect(service.$selection.value).toEqual(new Map([ ["A", new Set()], ["B", new Set()], ])); // Выделяем в одном бакете bucketA.updateSelection(["1", "2"], true, ESelectionStrategy.REPLACE); expect(service.$selection.value.get("A")).toEqual(new Set(["1", "2"])); expect(service.$selection.value.get("B")).toEqual(new Set()); // Выделяем во втором бакете bucketB.updateSelection(["x"], true, ESelectionStrategy.REPLACE); expect(service.$selection.value.get("A")).toEqual(new Set(["1", "2"])); expect(service.$selection.value.get("B")).toEqual(new Set(["x"])); // Снимаем выделение bucketA.updateSelection(["1"], false, ESelectionStrategy.SUBTRACT); expect(service.$selection.value.get("A")).toEqual(new Set(["2"])); }); it("reacts to adding new buckets", () => { const service = new SelectionService(); const bucketA = new MultipleSelectionBucket("A"); service.registerBucket(bucketA); bucketA.updateSelection(["1"], true, ESelectionStrategy.REPLACE); expect(service.$selection.value.get("A")).toEqual(new Set(["1"])); // Добавляем новый бакет const bucketB = new MultipleSelectionBucket("B"); service.registerBucket(bucketB); expect(service.$selection.value.get("B")).toEqual(new Set()); bucketB.updateSelection(["x"], true, ESelectionStrategy.REPLACE); expect(service.$selection.value.get("B")).toEqual(new Set(["x"])); }); it("returns empty Map if no buckets registered", () => { const service = new SelectionService(); expect(service.$selection.value.size).toBe(0); }); it("handles buckets with empty selection", () => { const service = new SelectionService(); const bucketA = new MultipleSelectionBucket("A"); service.registerBucket(bucketA); expect(service.$selection.value.get("A")).toEqual(new Set()); bucketA.updateSelection(["1"], true, ESelectionStrategy.REPLACE); bucketA.updateSelection(["1"], false, ESelectionStrategy.SUBTRACT); expect(service.$selection.value.get("A")).toEqual(new Set()); }); it("handles different id types and multiple buckets", () => { const service = new SelectionService(); const bucketA = new MultipleSelectionBucket("A"); const bucketB = new MultipleSelectionBucket("B"); service.registerBucket(bucketA); service.registerBucket(bucketB); bucketA.updateSelection(["foo", 42], true, ESelectionStrategy.REPLACE); bucketB.updateSelection([7, 8], true, ESelectionStrategy.REPLACE); expect(service.$selection.value.get("A")).toEqual(new Set(["foo", 42])); expect(service.$selection.value.get("B")).toEqual(new Set([7, 8])); }); }); describe("SelectionService registerBucket errors", () => { it("throws if registering two buckets with the same entityType", () => { const service = new SelectionService(); const bucketA1 = new MultipleSelectionBucket("A"); const bucketA2 = new MultipleSelectionBucket("A"); service.registerBucket(bucketA1); expect(() => service.registerBucket(bucketA2)).toThrow(/Selection bucket for entityType 'A' is already registered/); }); }); // --- MULTI-ENTITY SELECTION TESTS --- describe("SelectionService multi-entity selection", () => { let service; let blockBucket; let connectionBucket; beforeEach(() => { service = new SelectionService(); blockBucket = new MultipleSelectionBucket("block"); connectionBucket = new MultipleSelectionBucket("connection"); service.registerBucket(blockBucket); service.registerBucket(connectionBucket); }); it("selects entities across multiple types with REPLACE strategy", () => { const selection = { block: ["b1", "b2"], connection: ["c1", "c2"], }; service.select(selection, ESelectionStrategy.REPLACE); expect(blockBucket.$selected.value).toEqual(new Set(["b1", "b2"])); expect(connectionBucket.$selected.value).toEqual(new Set(["c1", "c2"])); expect(service.$selection.value.get("block")).toEqual(new Set(["b1", "b2"])); expect(service.$selection.value.get("connection")).toEqual(new Set(["c1", "c2"])); }); it("selects entities across multiple types with APPEND strategy", () => { // Initial selection using multi-entity API to avoid cross-bucket reset const initialSelection = { block: ["b1"], connection: ["c1"], }; service.select(initialSelection, ESelectionStrategy.REPLACE); // Check initial state expect(blockBucket.$selected.value).toEqual(new Set(["b1"])); expect(connectionBucket.$selected.value).toEqual(new Set(["c1"])); const selection = { block: ["b2", "b3"], connection: ["c2", "c3"], }; service.select(selection, ESelectionStrategy.APPEND); expect(blockBucket.$selected.value).toEqual(new Set(["b1", "b2", "b3"])); expect(connectionBucket.$selected.value).toEqual(new Set(["c1", "c2", "c3"])); }); it("single-entity API works in multi-entity context", () => { // Use multi-entity API to establish initial state without cross-bucket reset const initialSelection = { block: ["b1"], connection: ["c1"], }; service.select(initialSelection, ESelectionStrategy.REPLACE); expect(blockBucket.$selected.value).toEqual(new Set(["b1"])); expect(connectionBucket.$selected.value).toEqual(new Set(["c1"])); // Now test multi-entity API const selection = { block: ["b2"], connection: ["c2"], }; service.select(selection, ESelectionStrategy.APPEND); expect(blockBucket.$selected.value).toEqual(new Set(["b1", "b2"])); expect(connectionBucket.$selected.value).toEqual(new Set(["c1", "c2"])); }); it("deselects entities across multiple types", () => { // Initial selection using multi-entity API to avoid cross-bucket reset const initialSelection = { block: ["b1", "b2"], connection: ["c1", "c2"], }; service.select(initialSelection, ESelectionStrategy.REPLACE); const deselection = { block: ["b1"], connection: ["c2"], }; service.deselect(deselection); expect(blockBucket.$selected.value).toEqual(new Set(["b2"])); expect(connectionBucket.$selected.value).toEqual(new Set(["c1"])); }); it("checks selection status across multiple types", () => { // Use multi-entity API to establish initial state without cross-bucket reset const initialSelection = { block: ["b1"], connection: ["c1"], }; service.select(initialSelection, ESelectionStrategy.REPLACE); const queries = { block: ["b1", "b2"], connection: ["c1", "c2"], }; const results = service.isSelected(queries); expect(results.block).toBe(true); // b1 is selected, so some() returns true expect(results.connection).toBe(true); // c1 is selected, so some() returns true }); it("resets selection for multiple entity types", () => { service.select("block", ["b1"], ESelectionStrategy.REPLACE); service.select("connection", ["c1"], ESelectionStrategy.REPLACE); service.resetSelection(["block", "connection"]); expect(blockBucket.$selected.value.size).toBe(0); expect(connectionBucket.$selected.value.size).toBe(0); }); it("resets all selections", () => { service.select("block", ["b1"], ESelectionStrategy.REPLACE); service.select("connection", ["c1"], ESelectionStrategy.REPLACE); service.resetAllSelections(); expect(blockBucket.$selected.value.size).toBe(0); expect(connectionBucket.$selected.value.size).toBe(0); }); it("REPLACE strategy with multi-entity selection resets non-selected types", () => { const bucket3 = new MultipleSelectionBucket("other"); service.registerBucket(bucket3); // Initial selections service.select("block", ["b1"], ESelectionStrategy.REPLACE); service.select("connection", ["c1"], ESelectionStrategy.REPLACE); service.select("other", ["o1"], ESelectionStrategy.REPLACE); // Multi-select only block and connection const selection = { block: ["b2"], connection: ["c2"], }; service.select(selection, ESelectionStrategy.REPLACE); // block and connection should be updated expect(blockBucket.$selected.value).toEqual(new Set(["b2"])); expect(connectionBucket.$selected.value).toEqual(new Set(["c2"])); // other should be reset because it's not in the selection expect(bucket3.$selected.value.size).toBe(0); }); it("works with single-entity API after multi-entity API", () => { // Use multi-entity API first const selection = { block: ["b1", "b2"], connection: ["c1"], }; service.select(selection, ESelectionStrategy.REPLACE); // Then use single-entity API service.select("block", ["b3"], ESelectionStrategy.APPEND); expect(blockBucket.$selected.value).toEqual(new Set(["b1", "b2", "b3"])); expect(connectionBucket.$selected.value).toEqual(new Set(["c1"])); }); });