Skip to content

Commit c82dec6

Browse files
authored
feat(model-ad): sync pinned items with URL (MG-439) (Sage-Bionetworks#3686)
1 parent 0cbe0ce commit c82dec6

27 files changed

+702
-93
lines changed

apps/model-ad/app/e2e/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const COMPARISON_TOOL_PATHS: Record<string, string> = {
2+
'Model Overview': '/comparison/model',
3+
'Gene Expression': '/comparison/expression',
4+
'Disease Correlation': '/comparison/correlation',
5+
};

apps/model-ad/app/e2e/helpers/comparison-tool.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { Page, expect, test } from '@playwright/test';
2+
import { getUnpinnedTable } from '@sagebionetworks/explorers/testing/e2e';
3+
import { COMPARISON_TOOL_PATHS } from '../constants';
24

35
export const closeVisualizationOverviewDialog = async (page: Page) => {
46
await test.step('close visualization overview dialog', async () => {
@@ -10,3 +12,22 @@ export const closeVisualizationOverviewDialog = async (page: Page) => {
1012
await expect(dialog).toBeHidden();
1113
});
1214
};
15+
16+
export const navigateToComparison = async (
17+
page: Page,
18+
name: string,
19+
shouldCloseVisualizationOverviewDialog = false,
20+
queryParameters?: string,
21+
) => {
22+
const path = COMPARISON_TOOL_PATHS[name];
23+
const url = queryParameters ? `${path}?${queryParameters}` : path;
24+
await page.goto(url);
25+
26+
if (shouldCloseVisualizationOverviewDialog) {
27+
await closeVisualizationOverviewDialog(page);
28+
}
29+
30+
await expect(page.getByRole('heading', { level: 1, name })).toBeVisible();
31+
await expect(page.locator('explorers-base-table')).toHaveCount(2);
32+
await expect(getUnpinnedTable(page).locator('tbody tr').first()).toBeVisible();
33+
};
File renamed without changes.

apps/model-ad/app/e2e/model-details.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, Page, test } from '@playwright/test';
22
import { baseURL } from '../playwright.config';
3-
import { searchAndGetSearchListItems } from './helpers';
3+
import { searchAndGetSearchListItems } from './helpers/search';
44

55
async function isPageAtTop(page: Page) {
66
return await page.evaluate(() => window.pageYOffset === 0);
Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,82 @@
1-
import { expect, test } from '@playwright/test';
1+
import { expect, test, type Page } from '@playwright/test';
2+
import {
3+
expectPinnedParams,
4+
getPinnedTable,
5+
getRowByName,
6+
getUnpinnedTable,
7+
pinByName,
8+
unPinByName,
9+
} from '@sagebionetworks/explorers/testing/e2e';
10+
import { ModelOverview } from '@sagebionetworks/model-ad/api-client';
211
import { baseURL } from '../playwright.config';
3-
import { closeVisualizationOverviewDialog } from './helpers/comparison-tool';
12+
import { COMPARISON_TOOL_PATHS } from './constants';
13+
import { navigateToComparison } from './helpers/comparison-tool';
14+
15+
const MODEL_OVERVIEW_PATH = COMPARISON_TOOL_PATHS['Model Overview'];
16+
const MODEL_OVERVIEW_API_PATH = '/comparison-tools/model-overview';
17+
18+
const fetchModelOverviews = async (page: Page): Promise<ModelOverview[]> => {
19+
const response = await page.request.get(`${baseURL}/api/v1/${MODEL_OVERVIEW_API_PATH}`, {
20+
params: { itemFilterType: 'exclude' },
21+
});
22+
expect(response.ok()).toBeTruthy();
23+
const data = (await response.json()) as ModelOverview[];
24+
return data;
25+
};
426

527
test.describe('model overview', () => {
628
test('share URL button copies URL to clipboard', async ({ page, context }) => {
7-
const path = '/comparison/model';
829
await context.grantPermissions(['clipboard-read']);
930

10-
await page.goto(path);
11-
await expect(page.getByRole('heading', { level: 1, name: 'Model Overview' })).toBeVisible();
31+
await navigateToComparison(page, 'Model Overview', true);
1232

1333
const shareUrlButton = page.getByRole('button', { name: 'Share URL' });
1434
await expect(shareUrlButton).toBeVisible();
1535

16-
await page.waitForURL(path);
17-
18-
// close the visualization overview dialog
19-
await closeVisualizationOverviewDialog(page);
36+
await page.waitForURL(MODEL_OVERVIEW_PATH);
2037

2138
await shareUrlButton.click();
2239

2340
const clipboardContent = await page.evaluate(() => navigator.clipboard.readText());
24-
expect(clipboardContent).toEqual(`${baseURL}${path}`);
41+
expect(clipboardContent).toEqual(`${baseURL}${MODEL_OVERVIEW_PATH}`);
42+
});
43+
44+
test('pinning and unpinning items updates the pinned query param', async ({ page }) => {
45+
const models = await fetchModelOverviews(page);
46+
expect(models.length).toBeGreaterThan(1);
47+
48+
const [firstModel, secondModel] = models;
49+
50+
await navigateToComparison(page, 'Model Overview', true);
51+
52+
const pinnedTable = getPinnedTable(page);
53+
const unpinnedTable = getUnpinnedTable(page);
54+
55+
await pinByName(unpinnedTable, page, firstModel.name);
56+
await expect(getRowByName(pinnedTable, page, firstModel.name)).toHaveCount(1);
57+
await expectPinnedParams(page, [firstModel._id]);
58+
59+
await pinByName(unpinnedTable, page, secondModel.name);
60+
await expect(getRowByName(pinnedTable, page, secondModel.name)).toHaveCount(1);
61+
await expectPinnedParams(page, [firstModel._id, secondModel._id]);
62+
63+
const firstPinnedRow = await unPinByName(pinnedTable, page, firstModel.name);
64+
await expect(firstPinnedRow).toHaveCount(0);
65+
await expectPinnedParams(page, [secondModel._id]);
66+
67+
const secondPinnedRow = await unPinByName(pinnedTable, page, secondModel.name);
68+
await expect(secondPinnedRow).toHaveCount(0);
69+
await expectPinnedParams(page, []);
70+
});
71+
72+
test('pinned items in the URL are restored in the UI', async ({ page }) => {
73+
const [firstModel] = await fetchModelOverviews(page);
74+
expect(firstModel).toBeDefined();
75+
76+
await navigateToComparison(page, 'Model Overview', true, `pinned=${firstModel._id}`);
77+
78+
await expect(page.locator('explorers-base-table')).toHaveCount(2);
79+
await expect(getRowByName(getPinnedTable(page), page, firstModel.name)).toHaveCount(1);
80+
await expectPinnedParams(page, [firstModel._id]);
2581
});
2682
});

apps/model-ad/app/e2e/search.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from '@playwright/test';
2-
import { headerSearchPlaceholder, searchAndGetSearchListItems } from './helpers';
2+
import { headerSearchPlaceholder, searchAndGetSearchListItems } from './helpers/search';
33

44
test.describe('search', () => {
55
test('can search for model and aliases then navigate to model details from search result', async ({

libs/explorers/comparison-tool/src/lib/comparison-tool-table/primary-identifier-controls/primary-identifier-controls.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
<div class="primary-control-icons">
77
<button
8+
type="button"
89
(click)="viewDetailsWasClicked()"
910
[pTooltip]="viewConfig().viewDetailsTooltip"
1011
tooltipPosition="top"
@@ -19,6 +20,7 @@
1920
</button>
2021

2122
<button
23+
type="button"
2224
(click)="pinToggle()"
2325
[pTooltip]="pinTooltip()"
2426
tooltipPosition="top"

libs/explorers/comparison-tool/src/lib/comparison-tool-table/primary-identifier-controls/primary-identifier-controls.component.scss

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,18 @@
1717
font-weight: 700;
1818

1919
.primary-control-icons {
20-
display: none;
20+
display: flex;
21+
align-items: center;
22+
justify-content: center;
23+
gap: 8px;
24+
opacity: 0;
25+
max-width: 0;
26+
overflow: hidden;
27+
margin-left: 0;
28+
transition:
29+
opacity vars.$transition-duration ease,
30+
max-width vars.$transition-duration ease,
31+
margin-left vars.$transition-duration ease;
2132
}
2233

2334
> div:first-child {
@@ -26,19 +37,15 @@
2637
align-items: center;
2738
}
2839

29-
&:not(:hover) {
30-
> div:not(:first-child) {
31-
display: none;
32-
}
33-
}
34-
35-
&:hover {
40+
&:hover,
41+
&:focus-within {
3642
background-color: var(--color-action-primary);
3743
color: #fff;
3844

3945
.primary-control-icons {
4046
margin-left: 14px;
41-
display: flex;
47+
opacity: 1;
48+
max-width: 120px;
4249
}
4350

4451
> div:first-child {
@@ -56,6 +63,12 @@
5663
justify-content: center;
5764
cursor: pointer;
5865

66+
&:focus-visible {
67+
outline: none;
68+
background-color: rgb(255 255 255 / 20%);
69+
border-radius: 4px;
70+
}
71+
5972
&:disabled {
6073
opacity: 0.5;
6174
cursor: default;

libs/explorers/comparison-tool/src/lib/comparison-tool-table/primary-identifier-controls/primary-identifier-controls.component.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,29 @@ describe('PrimaryIdentifierControlsComponent', () => {
149149
'You have already pinned the maximum number of items (2). You must unpin some items before you can pin more.',
150150
);
151151
});
152+
153+
it('should call viewDetailsClick when activated with the keyboard', async () => {
154+
const { user, viewDetailsButton, viewDetailsClickSpy } = await setup();
155+
156+
await user.tab();
157+
expect(viewDetailsButton).toHaveFocus();
158+
159+
await user.keyboard('[Enter]');
160+
expect(viewDetailsClickSpy).toHaveBeenCalledWith('68fff1aaeb12b9674515fd58', '3xTg-AD');
161+
});
162+
163+
it('should toggle pin state when activated with the keyboard', async () => {
164+
const { user, pinButton, service } = await setup();
165+
166+
await user.tab();
167+
await user.tab();
168+
expect(pinButton).toHaveFocus();
169+
expect(service.isPinned('68fff1aaeb12b9674515fd58')).toBe(false);
170+
171+
await user.keyboard('[Space]');
172+
expect(service.isPinned('68fff1aaeb12b9674515fd58')).toBe(true);
173+
174+
await user.keyboard('[Space]');
175+
expect(service.isPinned('68fff1aaeb12b9674515fd58')).toBe(false);
176+
});
152177
});

libs/explorers/comparison-tool/src/lib/comparison-tool-table/primary-identifier-controls/primary-identifier-controls.component.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, computed, inject, input } from '@angular/core';
1+
import { Component, HostBinding, computed, inject, input } from '@angular/core';
22
import { ComparisonToolService } from '@sagebionetworks/explorers/services';
33
import { SvgIconComponent } from '@sagebionetworks/explorers/util';
44
import { TooltipModule } from 'primeng/tooltip';
@@ -15,6 +15,14 @@ export class PrimaryIdentifierControlsComponent {
1515
id = input.required<string>();
1616
label = input.required<string>();
1717

18+
@HostBinding('attr.role')
19+
protected readonly hostRole = 'group';
20+
21+
@HostBinding('attr.aria-label')
22+
protected get hostAriaLabel(): string {
23+
return this.label();
24+
}
25+
1826
maxPinnedItems = this.comparisonToolService.maxPinnedItems;
1927
hasMaxPinnedItems = this.comparisonToolService.hasMaxPinnedItems;
2028
viewConfig = this.comparisonToolService.viewConfig;

0 commit comments

Comments
 (0)