Back to home page

EIC code displayed by LXR

 
 

    


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 });