Skip to content

Commit

Permalink
Merge pull request #411 from visdesignlab/17-query-interface
Browse files Browse the repository at this point in the history
Add interface for string element queries based on attributes
  • Loading branch information
NateLanza authored Oct 23, 2024
2 parents 923ab3c + d27a871 commit 9479420
Show file tree
Hide file tree
Showing 17 changed files with 731 additions and 273 deletions.
149 changes: 147 additions & 2 deletions e2e-tests/elementView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { beforeTest } from './common';
test.beforeEach(beforeTest);

/**
* Drags the mouse from the center of the element to the specified offset
* Drags the mouse from the center of the element to the specified offset.
* Doesn't quite work in firefox; use caution
* @see https://stackoverflow.com/a/71147367
* @param element The element to drag over
* @param xOffset The x offset to drag to
Expand All @@ -28,7 +29,7 @@ async function dragElement(element: Locator, xOffset: number, yOffset: number, p
await page.mouse.up();
}

test('Element View', async ({ page }) => {
test('Element View', async ({ page, browserName }) => {
await page.goto('http://localhost:3000/?workspace=Upset+Examples&table=simpsons&sessionId=193');

// Make selection
Expand Down Expand Up @@ -112,6 +113,10 @@ test('Element View', async ({ page }) => {
// Check that the selection chip is visible after selecting
await elementViewToggle.click();
await dragElement(page.locator('canvas'), 150, 0, page);
// For some reason, in firefox, the dragElement() method doesn't quite work right and the end of the drag
// is off the element, which doesn't fire the select handler. Clicking the canvas in firefox fires the event,
// but clicking in chromium/webkit resets the selection & breaks the test
if (browserName === 'firefox') await page.locator('canvas').first().click();
const elementSelectionChip = await page.getByLabel('Selected elements Atts: Age');
await expect(elementSelectionChip).toBeVisible();

Expand Down Expand Up @@ -155,3 +160,143 @@ test('Element View', async ({ page }) => {
await expect(schoolBlueHairMaleSelectionRect).toBeVisible();
await expect(schoolMale3rdPoly).toBeVisible();
});

/**
* Clears the selection in the element view; element view must be open
* @param page The page to perform the clear selection on
*/
async function clearSelection(page: Page): Promise<void> {
await page.getByRole('button', { name: 'Clear' }).click();
}

/**
* Sets a query in the element view; element view must be open
* @param page The current testing page
* @param att The attribute to query
* @param type The type of query
* @param query The query string
*/
async function setQuery(page: Page, att: string, type: string, query: string): Promise<void> {
await page.getByLabel('Attribute Name').click();
await page.getByRole('option', { name: att }).click();
await page.getByLabel('Query Type').click();
await page.getByRole('option', { name: type, exact: true }).click();
await page.getByPlaceholder('Query').click();
await page.getByPlaceholder('Query').fill(query);
await page.getByRole('button', { name: 'Apply' }).click();
}

test('Query Selection', async ({ page }) => {
await page.goto('http://localhost:3000/?workspace=Upset+Examples&table=simpsons&sessionId=193');
await page.getByLabel('Element View Sidebar Toggle').click();
await page.locator('[id="Subset_School\\~\\&\\~Male"] g').filter({ hasText: /^Blue Hair$/ }).locator('circle').click();
await page.getByLabel('Selected intersection School').click();

// Selected elements for testing
const ralphCell = page.getByRole('cell', { name: 'Ralph' });
const age8Cell = page.getByRole('cell', { name: '8' });
const bartCell = page.getByRole('cell', { name: 'Bart' });
const age10Cell1 = page.getByRole('cell', { name: '10' }).first();
const age10Cell2 = page.getByRole('cell', { name: '10' }).nth(1);
const martinCell = page.getByRole('cell', { name: 'Martin Prince' });
const schoolSelectPoly = page.locator('#Subset_School polygon').nth(1);
const maleSelectPoly = page.locator('#Subset_Male polygon').nth(1);
const schoolMaleSelectPoly = page.locator('[id="Subset_School\\~\\&\\~Male"] polygon').nth(1);

// Test less than query
await setQuery(page, 'Age', 'less than', '10');

await expect(schoolMaleSelectPoly).toBeVisible();
await expect(ralphCell).toBeVisible();
await expect(age8Cell).toBeVisible();
await expect(bartCell).not.toBeVisible();
await expect(age10Cell1).not.toBeVisible();
await expect(age10Cell2).not.toBeVisible();
await expect(martinCell).not.toBeVisible();
await expect(schoolSelectPoly).toBeVisible();
await expect(maleSelectPoly).not.toBeVisible();

// Test greater than query
await clearSelection(page);
await setQuery(page, 'Age', 'greater than', '8');

await expect(bartCell).toBeVisible();
await expect(age10Cell1).toBeVisible();
await expect(age10Cell2).toBeVisible();
await expect(martinCell).toBeVisible();
await expect(maleSelectPoly).toBeVisible();
await expect(schoolMaleSelectPoly).toBeVisible();
await expect(ralphCell).not.toBeVisible();
await expect(age8Cell).not.toBeVisible();
await expect(schoolSelectPoly).not.toBeVisible();

// Test contains query
await clearSelection(page);
await setQuery(page, 'Name', 'contains', 't');

await expect(bartCell).toBeVisible();
await expect(age10Cell1).toBeVisible();
await expect(age10Cell2).toBeVisible();
await expect(martinCell).toBeVisible();
await expect(maleSelectPoly).toBeVisible();
await expect(schoolMaleSelectPoly).toBeVisible();
await expect(ralphCell).not.toBeVisible();
await expect(age8Cell).not.toBeVisible();
await expect(schoolSelectPoly).not.toBeVisible();

// Test equals query
await clearSelection(page);
await setQuery(page, 'Name', 'equals', 'Bart');

await expect(bartCell).toBeVisible();
await expect(age10Cell1).toBeVisible();
await expect(schoolMaleSelectPoly).toBeVisible();
await expect(age10Cell2).not.toBeVisible();
await expect(martinCell).not.toBeVisible();
await expect(maleSelectPoly).not.toBeVisible();
await expect(ralphCell).not.toBeVisible();
await expect(age8Cell).not.toBeVisible();
await expect(schoolSelectPoly).not.toBeVisible();

// Test length equals query
await clearSelection(page);
await setQuery(page, 'Name', 'length equals', '5');

await expect(ralphCell).toBeVisible();
await expect(age8Cell).toBeVisible();
await expect(schoolMaleSelectPoly).toBeVisible();
await expect(bartCell).not.toBeVisible();
await expect(age10Cell1).not.toBeVisible();
await expect(age10Cell2).not.toBeVisible();
await expect(martinCell).not.toBeVisible();
await expect(schoolSelectPoly).not.toBeVisible();
await expect(maleSelectPoly).not.toBeVisible();

// Test regex query
await clearSelection(page);
await setQuery(page, 'Name', 'regex', '^([A-z]+)$');

await expect(page.locator('[id="Subset_Evil\\~\\&\\~Male"] polygon').nth(1)).not.toBeVisible();
await expect(bartCell).toBeVisible();
await expect(age10Cell1).toBeVisible();
await expect(ralphCell).toBeVisible();
await expect(age8Cell).toBeVisible();
await expect(martinCell).not.toBeVisible();
await expect(age10Cell2).not.toBeVisible();
await expect(schoolMaleSelectPoly).toBeVisible();
await expect(schoolSelectPoly).toBeVisible();
await expect(maleSelectPoly).toBeVisible();

// Test clear selection
await clearSelection(page);
await expect(bartCell).toBeVisible();
await expect(age10Cell1).toBeVisible();
await expect(ralphCell).toBeVisible();
await expect(age8Cell).toBeVisible();
await expect(martinCell).toBeVisible();
await expect(age10Cell2).toBeVisible();
// Only visible because the intersection is selected
await expect(schoolMaleSelectPoly).toBeVisible();
await expect(schoolSelectPoly).not.toBeVisible();
await expect(maleSelectPoly).not.toBeVisible();
});
4 changes: 2 additions & 2 deletions packages/core/src/convertConfig.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
AggregateBy, Bookmark, BookmarkedSelection, Column, ColumnName, Histogram, PlotInformation, Row, Scatterplot,
AggregateBy, Bookmark, NumericalBookmark, Column, ColumnName, Histogram, PlotInformation, Row, Scatterplot,
SortByOrder, SortVisibleBy, UpsetConfig,
} from './types';
import { isUpsetConfig } from './typecheck';
Expand Down Expand Up @@ -52,7 +52,7 @@ type PreVersionConfig = {
};
allSets: Column[];
selected: Row | null;
elementSelection: BookmarkedSelection | null;
elementSelection: NumericalBookmark | null;
};

/**
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './defaultConfig';
export * from './convertConfig';
export * from './typecheck';
export * from './typeutils';
export * from './utils';
66 changes: 56 additions & 10 deletions packages/core/src/typecheck.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import {
Aggregate, AggregateBy, aggregateByList, AltText, AttributePlots, AttributePlotType, BaseElement, BaseIntersection, Bookmark, BookmarkedSelection, Column, ElementSelection, Histogram, PlotInformation, Row, RowType, Scatterplot, Subset, Subsets, UpsetConfig,
Aggregate, AggregateBy, aggregateByList, AltText, AttributePlots, AttributePlotType, BaseElement, BaseIntersection, Bookmark, NumericalBookmark, Column, NumericalQuery, Histogram, PlotInformation, Row, RowType, Scatterplot, Subset, Subsets, UpsetConfig,
ElementQuery,
ElementQueryType,
ElementBookmark,
ElementSelection,
} from './types';
import { deepCopy } from './utils';

Expand Down Expand Up @@ -103,11 +107,11 @@ export function isAltText(val: unknown): val is AltText {
}

/**
* Validates that the given value is an ElementSelection.
* Validates that the given value is a NumericalQuery.
* @param value The value to check.
* @returns whether the value is an ElementSelection.
* @returns whether the value is a NumericalQuery.
*/
export function isElementSelection(value: unknown): value is ElementSelection {
export function isNumericalQuery(value: unknown): value is NumericalQuery {
return (
isObject(value)
&& Object.values(value).every((v) => Array.isArray(v)
Expand All @@ -117,6 +121,23 @@ export function isElementSelection(value: unknown): value is ElementSelection {
);
}

/**
* Type guard for ElementQuery
* @param val The value to check.
* @returns whether the value is a ElementQuery
*/
export function isElementQuery(val: unknown): val is ElementQuery {
return (
isObject(val)
&& Object.hasOwn(val, 'att')
&& Object.hasOwn(val, 'type')
&& Object.hasOwn(val, 'query')
&& typeof (val as ElementQuery).att === 'string'
&& Object.values(ElementQueryType).includes((val as ElementQuery).type)
&& typeof (val as ElementQuery).query === 'string'
);
}

/**
* Type guard for Scatterplot
* @param s variable to check
Expand Down Expand Up @@ -163,7 +184,11 @@ export function isBookmark(b: unknown): b is Bookmark {
&& Object.hasOwn(b, 'type')
&& typeof (b as Bookmark).id === 'string'
&& typeof (b as Bookmark).label === 'string'
&& ((b as Bookmark).type === 'intersection' || (b as Bookmark).type === 'elements');
&& (
(b as Bookmark).type === 'intersection'
|| (b as Bookmark).type === 'numerical'
|| (b as Bookmark).type === 'element'
);
}

/**
Expand Down Expand Up @@ -246,15 +271,36 @@ export function isRow(r: unknown): r is Row {
}

/**
* Type guard for BookmarkedSelection
* Type guard for NumericalBookmark
* @param b variable to check
* @returns {boolean}
*/
export function isNumericalBookmark(b: unknown): b is NumericalBookmark {
return isBookmark(b)
&& b.type === 'numerical'
&& Object.hasOwn(b, 'selection')
&& isNumericalQuery((b as NumericalBookmark).selection);
}

/**
* Type guard for ElementBookmark
* @param b variable to check
* @returns {boolean}
*/
export function isBookmarkedSelection(b: unknown): b is BookmarkedSelection {
export function isElementBookmark(b: unknown): b is ElementBookmark {
return isBookmark(b)
&& b.type === 'elements'
&& b.type === 'element'
&& Object.hasOwn(b, 'selection')
&& isElementSelection((b as BookmarkedSelection).selection);
&& isElementQuery((b as ElementBookmark).selection);
}

/**
* Determines if the given object is an ElementSelection.
* @param e The object to check.
* @returns {boolean} Whether the object is an ElementSelection.
*/
export function isElementSelection(e: unknown): e is ElementSelection {
return isNumericalBookmark(e) || isElementBookmark(e);
}

/**
Expand Down Expand Up @@ -494,7 +540,7 @@ export function isUpsetConfig(config: unknown): config is UpsetConfig {
}

// elementSelection
if (!(elementSelection === null || isBookmarkedSelection(elementSelection))) {
if (!(elementSelection === null || isElementSelection(elementSelection))) {
console.warn('Upset config error: Element selection is not a bookmarked selection');
return false;
}
Expand Down
Loading

0 comments on commit 9479420

Please sign in to comment.