diff --git a/e2e/src/navigating/navigateFromRegion.e2e-spec.js b/e2e/src/navigating/navigateFromRegion.e2e-spec.js index 0cbe20e896a45c7dd16d2392acbe49a1e6b9efeb..d6a1ba4ab0c0e5c0cacf6e2d4c582197b5f6520f 100644 --- a/e2e/src/navigating/navigateFromRegion.e2e-spec.js +++ b/e2e/src/navigating/navigateFromRegion.e2e-spec.js @@ -51,61 +51,114 @@ const TEST_DATA = [ }, ] +const getBeforeEachFn = iavPage => ({ + url, + templateName, + position, + }) => async () => { -describe('explore same region in different templates', () => { + if (url) { + await iavPage.goto(url) + } else { + await iavPage.goto() + await iavPage.selectTitleTemplateParcellation(templateName) + await iavPage.wait(500) + await iavPage.waitForAsync() + } + + const tag = await iavPage.getSideNavTag() + await tag.click() + await iavPage.wait(1000) + await iavPage.waitUntilAllChunksLoaded() + await iavPage.cursorMoveToAndClick({ position }) + + await iavPage.showOtherTemplateMenu() + await iavPage.wait(500) +} + +describe('> explore same region in different templates', () => { let iavPage - beforeAll(async () => { + beforeEach(async () => { iavPage = new AtlasPage() await iavPage.init() }) - TEST_DATA.forEach(template => { - template.expectedTemplateLabels.forEach(expectedTemplate => { - it (`testing ${template.expectedRegion} exploring at: ${template.name}`, async () => { - if (template.url) { - await iavPage.goto(template.url) - } else { - await iavPage.goto() - await iavPage.selectTitleTemplateParcellation(template.name) - } - const {position} = template - - const tag = await iavPage.getSideNavTag() - await tag.click() - await iavPage.wait(1000) - await iavPage.waitUntilAllChunksLoaded() - await iavPage.cursorMoveToAndClick({ position }) - - await iavPage.showOtherTemplateMenu() - await iavPage.wait(500) + describe('> moving to different reference template works', () => { + for (const template of TEST_DATA) { + const { + url, + templateName, + position, + expectedRegion, + expectedTemplateLabels, + } = template - const otherTemplates = await iavPage.getAllOtherTemplates() - const { name, hemisphere, expectedPosition } = expectedTemplate - const idx = otherTemplates.findIndex(template => { - if (hemisphere) { - if (template.indexOf(hemisphere) < 0) return false - } - return template.indexOf(name) >= 0 + describe(`> testing ${templateName}`, () => { + beforeEach(async () => { + await getBeforeEachFn(iavPage)(template)() }) - expect(idx).toBeGreaterThanOrEqual(0) + for (const tmplLabel of expectedTemplateLabels) { + const { expectedPosition, name, hemisphere } = tmplLabel + describe(`> moving to ${name}`, () => { + it('> works as expected', async () => { + + const otherTemplates = await iavPage.getAllOtherTemplates() + const idx = otherTemplates.findIndex(template => { + if (hemisphere) { + if (template.indexOf(hemisphere) < 0) return false + } + return template.indexOf(name) >= 0 + }) + + expect(idx).toBeGreaterThanOrEqual(0) + + await iavPage.clickNthItemAllOtherTemplates(idx) + + await iavPage.wait(500) + await iavPage.waitUntilAllChunksLoaded() + + const navState = await iavPage.getNavigationState() + + // somehow there are slight deviations (1nm in most cases) + // giving a tolerance of 0.1um + for (const idx in navState.position) { + expect( + Math.abs(navState.position[idx] - expectedPosition[idx]) + ).toBeLessThanOrEqual(100) + } + }) + }) + } + }) + } + }) - await iavPage.clickNthItemAllOtherTemplates(idx) + describe('> menu UI', () => { + const data = TEST_DATA[0] + + beforeEach(async () => { + await getBeforeEachFn(iavPage)(data)() + }) - await iavPage.wait(500) - await iavPage.waitUntilAllChunksLoaded() - - const navState = await iavPage.getNavigationState() + it('> dismisses when user clicks/drags outside', async () => { + const { expectedRegion, expectedTemplateLabels, position, url, templateName } = data - // somehow there are slight deviations (1nm in most cases) - // giving a tolerance of 0.1um - for (const idx in navState.position) { - expect( - Math.abs(navState.position[idx] - expectedPosition[idx]) - ).toBeLessThanOrEqual(100) - } + await iavPage.cursorMoveToAndDrag({ + position: [position[0], position[1] - 100], + delta: [50, 1] }) + + await iavPage.wait(1000) + + // should throw + try { + const otherTemplates = await iavPage.getAllOtherTemplates() + expect(true).toBe(false) + } catch(e) { + expect(true).toBe(true) + } }) }) }) diff --git a/e2e/src/util.js b/e2e/src/util.js index 0b8e60e8e38d0ef98ce089bd581b83d37ef8a70b..679cbffe203f306c6acbd7e20c42c6a719901a28 100644 --- a/e2e/src/util.js +++ b/e2e/src/util.js @@ -23,7 +23,7 @@ async function _getIndexFromArrayOfWebElements(search, webElements) { const regionSearchAriaLabelText = 'Search for any region of interest in the atlas selected' -const vertifyPos = position => { +const verifyPosition = position => { if (!position) throw new Error(`cursorGoto: position must be defined!`) const x = Array.isArray(position) ? position[0] : position.x @@ -93,7 +93,7 @@ class WdBase{ } async cursorMoveTo({ position }) { - const { x, y } = vertifyPos(position) + const { x, y } = verifyPosition(position) return this._driver.actions() .move() .move({ @@ -105,7 +105,7 @@ class WdBase{ } async cursorMoveToAndClick({ position }) { - const { x, y } = vertifyPos(position) + const { x, y } = verifyPosition(position) return this._driver.actions() .move() .move({ @@ -117,6 +117,26 @@ class WdBase{ .perform() } + async cursorMoveToAndDrag({ position, delta }) { + const { x, y } = verifyPosition(position) + const { x: deltaX, y: deltaY } = verifyPosition(delta) + return this._driver.actions() + .move() + .move({ + x, + y, + duration: 1000 + }) + .press() + .move({ + x: x + deltaX, + y: y + deltaY, + duration: 1000 + }) + .release() + .perform() + } + async initHttpInterceptor(){ await this._browser.executeScript(() => { if (window.__isIntercepting__) return diff --git a/src/main.module.ts b/src/main.module.ts index 3b911d31fd2d5a0b27f52f69aed3d589c6288bd2..e92737b4ba2fd5594a9a8f1247f0b2fac8015e03 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -14,7 +14,6 @@ import { GetNamesPipe } from "./util/pipes/getNames.pipe"; import { HttpClientModule } from "@angular/common/http"; import { EffectsModule } from "@ngrx/effects"; -import { CaptureClickListenerDirective } from "src/util/directives/captureClickListener.directive"; import { AtlasViewerAPIServices } from "./atlasViewer/atlasViewer.apiService.service"; import { AtlasWorkerService } from "./atlasViewer/atlasViewer.workerService.service"; import { ModalUnit } from "./atlasViewer/modalUnit/modalUnit.component"; @@ -100,7 +99,6 @@ import 'src/theme.scss' FloatingContainerDirective, FloatingMouseContextualContainerDirective, DragDropDirective, - CaptureClickListenerDirective, /* pipes */ GetNamesPipe, diff --git a/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts b/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts index 6d58ff5be48a65adbae8a72d1200e7dae2e7167c..4890920d3ea4fc398d7de4bc0dc1f26eefa10e8e 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts @@ -1,7 +1,6 @@ -import {Component, ElementRef, OnDestroy, OnInit, ViewChild} from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { Store } from "@ngrx/store"; -import {MatMenuTrigger} from "@angular/material/menu"; -import {Subscription} from "rxjs"; +import { Subscription } from "rxjs"; import { IavRootStoreInterface } from "src/services/stateStore.service"; import { RegionBase } from '../region.base' @@ -12,8 +11,6 @@ import { RegionBase } from '../region.base' }) export class RegionMenuComponent extends RegionBase implements OnInit, OnDestroy { - @ViewChild('additionalActionsPanel', {read: ElementRef}) additionalActionsPanelElement: ElementRef - private subscriptions: Subscription[] = [] constructor( diff --git a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html index d5234089fd8ef778a51966485d297d9152a84d6d..b078e810dfbe977a5427559bcd29ba9391715333 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html @@ -27,9 +27,12 @@ </span> </button> <button *ngIf="hasConnectivity" - mat-button - [matMenuTriggerFor]="connectivitySourceDatasets" - #connectivityMenuButton="matMenuTrigger"> + mat-button + [matMenuTriggerFor]="connectivitySourceDatasets" + #connectivityMenuButton="matMenuTrigger" + iav-captureClickListenerDirective + [iav-captureClickListenerDirective-captureDocument]="true" + (iav-captureClickListenerDirective-onMousedown)="connectivityMenuButton.closeMenu()"> <i class="fab fa-connectdevelop"></i> <span> Connectivity @@ -37,15 +40,6 @@ <i class="fas fa-angle-right"></i> </button> - <!-- ToDo make dynamic with AVAILABLE CONNECTIVITY DATASETS data - get info from atlas viewer core --> - <mat-menu #connectivitySourceDatasets="matMenu" xPosition="before" (click)="$event.stopPropagation()" hasBackdrop="false"> - <div> - <button mat-menu-item - (click)="showConnectivity(region.name)"> - <span>1000 Brain Study - DTI connectivity</span> - </button> - </div> - </mat-menu> <!-- Menu to navigate between template spaces to explore same region --> @@ -53,7 +47,11 @@ <button mat-button aria-label="Show availability in other reference spaces" *ngIf="sameRegionTemplate.length" - [matMenuTriggerFor]="additionalActions"> + [matMenuTriggerFor]="additionalActions" + #changeTmplTrigger="matMenuTrigger" + iav-captureClickListenerDirective + [iav-captureClickListenerDirective-captureDocument]="true" + (iav-captureClickListenerDirective-onMousedown)="changeTmplTrigger.closeMenu()"> <i class="fas fa-brain"></i> <span> Change template @@ -62,18 +60,32 @@ </button> </div> - <mat-menu - [aria-label]="'Availability in other reference spaces'" - #additionalActions="matMenu" - xPosition="before" - hasBackdrop="false"> - <div> - <button mat-menu-item *ngFor="let sameRegion of sameRegionTemplate; let i = index" (click)="changeView(i)" class="d-flex"> - <span class="overflow-x-hidden text-truncate"> {{sameRegion.template.name}} </span> - <span *ngIf="sameRegion.hemisphere"> - {{sameRegion.hemisphere}}</span> - </button> - </div> - </mat-menu> - </div> </mat-card> + + +<!-- ToDo make dynamic with AVAILABLE CONNECTIVITY DATASETS data - get info from atlas viewer core --> +<mat-menu + #connectivitySourceDatasets="matMenu" + xPosition="before" + hasBackdrop="false"> + <div> + <button mat-menu-item (mousedown)="showConnectivity(region.name)"> + <span>1000 Brain Study - DTI connectivity</span> + </button> + </div> +</mat-menu> + + +<mat-menu + [aria-label]="'Availability in other reference spaces'" + #additionalActions="matMenu" + xPosition="before" + hasBackdrop="false"> + <div> + <button mat-menu-item *ngFor="let sameRegion of sameRegionTemplate; let i = index" (mousedown)="changeView(i)" class="d-flex"> + <span class="overflow-x-hidden text-truncate"> {{sameRegion.template.name}} </span> + <span *ngIf="sameRegion.hemisphere"> - {{sameRegion.hemisphere}}</span> + </button> + </div> +</mat-menu> diff --git a/src/util/directives/FixedMouseContextualContainerDirective.directive.ts b/src/util/directives/FixedMouseContextualContainerDirective.directive.ts index 113bc0650d13bbea99dbf3527a979a35b966df91..bf2293466e960dd98bb05020759f9ab0ddce9e8f 100644 --- a/src/util/directives/FixedMouseContextualContainerDirective.directive.ts +++ b/src/util/directives/FixedMouseContextualContainerDirective.directive.ts @@ -1,11 +1,11 @@ -import { Directive, ElementRef, EventEmitter, HostBinding, Input, Output } from "@angular/core"; +import { Directive, ElementRef, EventEmitter, HostBinding, Input, Output, AfterContentChecked, ChangeDetectorRef, AfterViewInit } from "@angular/core"; @Directive({ selector: '[fixedMouseContextualContainerDirective]', exportAs: 'iavFixedMouseCtxContainer' }) -export class FixedMouseContextualContainerDirective { +export class FixedMouseContextualContainerDirective implements AfterContentChecked { private defaultPos: [number, number] = [-1e3, -1e3] public isShown: boolean = false @@ -21,6 +21,7 @@ export class FixedMouseContextualContainerDirective { constructor( private el: ElementRef, + private cdr: ChangeDetectorRef, ) { } @@ -41,8 +42,12 @@ export class FixedMouseContextualContainerDirective { this.transform = `translate(${this.mousePos.map(v => v.toString() + 'px').join(', ')})` } + ngAfterContentChecked(){ + this.recalculatePosition() + this.cdr.markForCheck() + } + public show() { - setTimeout(() => this.recalculatePosition()) this.styleDisplay = 'inline-block' this.isShown = true this.onShow.emit() diff --git a/src/util/directives/captureClickListener.directive.ts b/src/util/directives/captureClickListener.directive.ts index 655fb8b72f6b76f2cf954b95c09a797f9cebad66..e10bb4dc96c32389868b75b986556aae7d7e04ba 100644 --- a/src/util/directives/captureClickListener.directive.ts +++ b/src/util/directives/captureClickListener.directive.ts @@ -1,6 +1,7 @@ -import { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core"; +import { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output, Input, Inject } from "@angular/core"; import { fromEvent, Subscription } from "rxjs"; import { switchMapTo, takeUntil } from "rxjs/operators"; +import { DOCUMENT } from "@angular/common"; @Directive({ selector: '[iav-captureClickListenerDirective]', @@ -8,16 +9,27 @@ import { switchMapTo, takeUntil } from "rxjs/operators"; export class CaptureClickListenerDirective implements OnInit, OnDestroy { + @Input('iav-captureClickListenerDirective-captureDocument') captureDocument: boolean = false + private subscriptions: Subscription[] = [] @Output('iav-captureClickListenerDirective-onClick') public mapClicked: EventEmitter<any> = new EventEmitter() @Output('iav-captureClickListenerDirective-onMousedown') public mouseDownEmitter: EventEmitter<any> = new EventEmitter() - constructor(private el: ElementRef) { } + constructor( + private el: ElementRef, + @Inject(DOCUMENT) private document: Document, + ) {} + + get element(){ + return this.captureDocument + ? this.document + : this.el.nativeElement + } public ngOnInit() { - const mouseDownObs$ = fromEvent(this.el.nativeElement, 'mousedown', { capture: true }) - const mouseMoveObs$ = fromEvent(this.el.nativeElement, 'mousemove', { capture: true }) - const mouseUpObs$ = fromEvent(this.el.nativeElement, 'mouseup', { capture: true }) + const mouseDownObs$ = fromEvent(this.element, 'mousedown', { capture: true }) + const mouseMoveObs$ = fromEvent(this.element, 'mousemove', { capture: true }) + const mouseUpObs$ = fromEvent(this.element, 'mouseup', { capture: true }) this.subscriptions.push( mouseDownObs$.subscribe(event => { diff --git a/src/util/util.module.ts b/src/util/util.module.ts index e8af338f0ab9d60806b3f190d4854152cd91499c..0675aa46ccf623dda7b28d0b3e5e34a5351f9740 100644 --- a/src/util/util.module.ts +++ b/src/util/util.module.ts @@ -7,6 +7,7 @@ import { StopPropagationDirective } from "./directives/stopPropagation.directive import { FilterNullPipe } from "./pipes/filterNull.pipe"; import { IncludesPipe } from "./pipes/includes.pipe"; import { SafeResourcePipe } from "./pipes/safeResource.pipe"; +import { CaptureClickListenerDirective } from "./directives/captureClickListener.directive"; @NgModule({ declarations: [ @@ -20,6 +21,7 @@ import { SafeResourcePipe } from "./pipes/safeResource.pipe"; KeyListner, IncludesPipe, SafeResourcePipe, + CaptureClickListenerDirective, ], exports: [ FilterNullPipe, @@ -32,6 +34,7 @@ import { SafeResourcePipe } from "./pipes/safeResource.pipe"; KeyListner, IncludesPipe, SafeResourcePipe, + CaptureClickListenerDirective, ], providers: [ ]