Warning, /firebird/packages/firebird-ng/display/src/lib/components/model-tree/model-tree.component.spec.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * Model tree: a selection that arrives from elsewhere is revealed once, and
0003 * the user stays in control of the tree afterwards (collapse, expand, "more");
0004 * a selection can be cleared from the tree.
0005 */
0006 import { TestBed, type ComponentFixture } from '@angular/core/testing';
0007 import { provideZonelessChangeDetection, signal } from '@angular/core';
0008 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
0009 import type { EntityRef } from '@dexvis/firebird-core';
0010 import { ModelTreeComponent } from './model-tree.component';
0011 import { SelectionService } from '../../services/selection.service';
0012 import { DataModelService } from '../../services/data-model.service';
0013
0014 /** The selection API the tree uses, backed by plain signals (the real service pulls in the 3D scene). */
0015 class SelectionStub {
0016 private readonly current = signal<EntityRef | null>(null);
0017 readonly selection = this.current.asReadonly();
0018 readonly selectedPiece = signal<string | null>(null);
0019 select(ref: EntityRef | null): void {
0020 this.current.set(ref);
0021 if (ref) this.selectedPiece.set(ref.pieceName);
0022 }
0023 clear(): void {
0024 this.select(null);
0025 }
0026 selectPiece(pieceName: string | null): void {
0027 this.selectedPiece.set(pieceName);
0028 }
0029 isSelected(pieceName: string, entityIndex: number): boolean {
0030 const current = this.current();
0031 return current !== null && current.pieceName === pieceName && current.entityIndex === entityIndex;
0032 }
0033 }
0034
0035 function piece(name: string, entityCount: number) {
0036 return {
0037 name,
0038 type: 'spec.Type',
0039 entityCount,
0040 entityLabel: (i: number) => `${name} ${i}`,
0041 entityRefs: () => [],
0042 };
0043 }
0044
0045 describe('ModelTreeComponent', () => {
0046 let fixture: ComponentFixture<ModelTreeComponent>;
0047 let selection: SelectionStub;
0048 let scrolls: ReturnType<typeof vi.fn>;
0049 const originalScroll = Element.prototype.scrollIntoView;
0050
0051 const element = () => fixture.nativeElement as HTMLElement;
0052 const pieceRow = (name: string) =>
0053 [...element().querySelectorAll<HTMLElement>('.piece-row')].find(row => row.textContent?.includes(name))!;
0054 const entityRows = () => element().querySelectorAll<HTMLElement>('.entity-row');
0055 const settle = async () => {
0056 await fixture.whenStable();
0057 // The reveal scrolls in a timeout, after the rows rendered
0058 await new Promise(resolve => setTimeout(resolve));
0059 await fixture.whenStable();
0060 };
0061
0062 beforeEach(async () => {
0063 localStorage.clear();
0064 scrolls = vi.fn();
0065 Element.prototype.scrollIntoView = scrolls as unknown as typeof Element.prototype.scrollIntoView;
0066 selection = new SelectionStub();
0067 TestBed.configureTestingModule({
0068 imports: [ModelTreeComponent],
0069 providers: [
0070 provideZonelessChangeDetection(),
0071 { provide: SelectionService, useValue: selection },
0072 {
0073 provide: DataModelService,
0074 useValue: { currentEntry: signal({ pieces: [piece('Hits', 500), piece('Tracks', 450)] }) },
0075 },
0076 ],
0077 });
0078 fixture = TestBed.createComponent(ModelTreeComponent);
0079 await settle();
0080 });
0081
0082 afterEach(() => {
0083 Element.prototype.scrollIntoView = originalScroll;
0084 });
0085
0086 it('reveals a selection from elsewhere: expands the piece, raises the limit, scrolls once', async () => {
0087 selection.select({ pieceName: 'Hits', entityIndex: 350 });
0088 await settle();
0089
0090 expect(fixture.componentInstance.isExpanded('Hits')).toBe(true);
0091 expect(element().querySelector('#model-tree-Hits-350')).not.toBeNull();
0092 expect(element().querySelector('.entity-row.selected')?.id).toBe('model-tree-Hits-350');
0093 expect(scrolls).toHaveBeenCalledTimes(1);
0094 });
0095
0096 it('lets the user collapse the piece that holds the selection', async () => {
0097 selection.select({ pieceName: 'Hits', entityIndex: 3 });
0098 await settle();
0099 expect(entityRows().length).toBeGreaterThan(0);
0100
0101 pieceRow('Hits').click();
0102 await settle();
0103
0104 expect(fixture.componentInstance.isExpanded('Hits')).toBe(false);
0105 expect(entityRows().length).toBe(0);
0106 expect(scrolls).toHaveBeenCalledTimes(1);
0107 });
0108
0109 it('does not jump back to the selection on expand or "more" elsewhere', async () => {
0110 selection.select({ pieceName: 'Hits', entityIndex: 3 });
0111 await settle();
0112 pieceRow('Hits').click(); // collapse the selected piece
0113 pieceRow('Tracks').click(); // expand another one
0114 await settle();
0115 element().querySelector<HTMLElement>('.more-button')!.click();
0116 await settle();
0117
0118 expect(fixture.componentInstance.isExpanded('Hits')).toBe(false);
0119 expect(fixture.componentInstance.limitFor('Tracks')).toBe(400);
0120 expect(scrolls).toHaveBeenCalledTimes(1);
0121 });
0122
0123 it('clicking the selected entity again deselects it', async () => {
0124 selection.select({ pieceName: 'Hits', entityIndex: 3 });
0125 await settle();
0126
0127 element().querySelector<HTMLElement>('#model-tree-Hits-3')!.click();
0128 await settle();
0129
0130 expect(selection.selection()).toBeNull();
0131 expect(element().querySelector('.entity-row.selected')).toBeNull();
0132 });
0133
0134 it('the selection bar names the selection and clears it', async () => {
0135 expect(element().querySelector('.selection-bar')).toBeNull();
0136 selection.select({ pieceName: 'Tracks', entityIndex: 7 });
0137 await settle();
0138
0139 expect(element().querySelector('.selection-bar')?.textContent).toContain('Tracks #7');
0140 element().querySelector<HTMLElement>('.clear-selection')!.click();
0141 await settle();
0142
0143 expect(selection.selection()).toBeNull();
0144 expect(element().querySelector('.selection-bar')).toBeNull();
0145 });
0146 });