Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/storybook/src/DomainSlider.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ export const Default = {
isAutoMax={isAutoMax}
disabled={disabled}
onChange={setSliderDomain}
onAfterChange={(hasMinChanged, hasMaxChanged) => {
onAfterChange={(hasMinChanged, hasMaxChanged, newValue) => {
setCustomDomain([
hasMinChanged ? sliderDomain[0] : customDomain[0],
hasMaxChanged ? sliderDomain[1] : customDomain[1],
hasMinChanged ? newValue[0] : customDomain[0],
hasMaxChanged ? newValue[1] : customDomain[1],
]);
}}
/>
Expand Down
4 changes: 2 additions & 2 deletions cypress/e2e/app.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,8 @@ describe('/mock', () => {
cy.findByRole('slider', { name: 'D1' })
.as('d1Slider')
.should('have.attr', 'aria-valuenow', 0)
.and('have.attr', 'aria-valuemin', 0)
.and('have.attr', 'aria-valuemax', 8);
.and('have.attr', 'min', 0)
.and('have.attr', 'max', 8);

// Move slider up by three marks and check value
cy.get('@d1Slider').focus();
Expand Down
34 changes: 28 additions & 6 deletions packages/app/src/__tests__/DimensionMapper.browser.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from 'vitest';
import { page } from 'vitest/browser';
import { commands, page } from 'vitest/browser';

import { getDimMappingBtn, renderApp } from '../test-utils';
import { Vis } from '../vis-packs/core/visualizations';
Expand All @@ -25,7 +25,7 @@ test('control mapping for X axis when visualizing 2D dataset as Line', async ()
// Ensure that only one dimension slider for D0 is visible
const d0Slider = page.getByRole('slider', { name: 'D0' });
expect(d0Slider).toBeVisible();
expect(d0Slider).toHaveValue(0);
expect(d0Slider).toHaveValue('0');
expect(page.getByRole('slider', { name: 'D1' })).not.toBeInTheDocument();

// Change mapping from [0, 'x'] to ['x', 0]
Expand All @@ -36,7 +36,7 @@ test('control mapping for X axis when visualizing 2D dataset as Line', async ()
// Ensure that the dimension slider is now for D1
const d1Slider = page.getByRole('slider', { name: 'D1' });
expect(d1Slider).toBeVisible();
expect(d1Slider).toHaveValue(0);
expect(d1Slider).toHaveValue('0');
expect(page.getByRole('slider', { name: 'D0' })).not.toBeInTheDocument();
});

Expand Down Expand Up @@ -91,7 +91,7 @@ test('display one slider and two mappers when visualizing 3D dataset as Matrix',
expect(yDimsButtons.nth(1)).toBeChecked();

// Ensure that only one dimension slider for D0 is visible
expect(page.getByRole('slider', { name: 'D0' })).toHaveValue(0);
expect(page.getByRole('slider', { name: 'D0' })).toHaveValue('0');
expect(page.getByRole('slider', { name: 'D1' })).not.toBeInTheDocument();
});

Expand All @@ -105,7 +105,29 @@ test('slice through 2D dataset', async () => {
const d0Slider = page.getByRole('slider', { name: 'D0' });
await user.type(d0Slider, '{ArrowUp}');

expect(d0Slider).toHaveValue(1);
expect(d0Slider).toHaveValue('1');
});

test('slice through 2D dataset by dragging slider thumb', async () => {
await renderApp({ initialPath: '/arrays/twoD', preferredVis: Vis.Line });

const d0Slider = page.getByRole('slider', { name: 'D0' });
const { left, top, width } = d0Slider.element().getBoundingClientRect();
const [x, y] = [left + width / 2, top + 2]; // grab thumb above its centre

// Thumb doesn't jump when grabbed off-centre
await commands.mouseDrag([
[x, y],
[x, y - 1],
]);
await expect.element(d0Slider).toHaveValue('0');

// Drag thumb to top of slider
await commands.mouseDrag([
[x, y],
[x, y - 1000],
]);
await expect.element(d0Slider).toHaveValue('19');
});

test('slice through 2D opaque dataset', async () => {
Expand All @@ -117,7 +139,7 @@ test('slice through 2D opaque dataset', async () => {
const d0Slider = page.getByRole('slider', { name: 'D0' });
await user.type(d0Slider, '{ArrowUp}');

expect(d0Slider).toHaveValue(1);
expect(d0Slider).toHaveValue('1');
await expect.element(page.getByText('Uint8Array [ 4,5 ]')).toBeVisible();
});

Expand Down
93 changes: 62 additions & 31 deletions packages/app/src/__tests__/DomainWidget.browser.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { expect, test } from 'vitest';
import { page } from 'vitest/browser';

import { renderApp } from '../test-utils';
import { dragHorizontally, renderApp } from '../test-utils';

test('show slider with two thumbs and reveal popup on hover', async () => {
const { user } = await renderApp('/nexus/NXdata');

const thumbs = page.getByRole('slider');
expect(thumbs).toHaveLength(2);
expect(thumbs.nth(0)).toHaveValue(20);
expect(thumbs.nth(1)).toHaveValue(81);
expect(thumbs.nth(0)).toHaveValue('20');
expect(thumbs.nth(1)).toHaveValue('81');

const editBtn = page.getByRole('button', { name: 'Edit domain' });
const popup = page.getByRole('dialog', {
Expand Down Expand Up @@ -59,9 +59,9 @@ test('show min/max and data range in popup', async () => {
test('move thumbs with keyboard to update domain', async () => {
const { user } = await renderApp('/nexus/NXdata');

// Hover min thumb to reveal tooltip
// Hover edit button to reveal tooltip
const minThumb = page.getByRole('slider', { name: /min/ });
await minThumb.hover();
await page.getByRole('button', { name: 'Edit domain' }).hover();

const minInput = page.getByLabelText('min', { exact: true });
const maxInput = page.getByLabelText('max', { exact: true });
Expand All @@ -73,25 +73,64 @@ test('move thumbs with keyboard to update domain', async () => {

// Move min thumb five steps to the left (in a single press)
await user.type(minThumb, '{ArrowLeft>5/}'); // press key and hold for 5 keydown events, then release
await expect.element(minInput).toHaveValue('−2.30818e+2');
expect(page.getByLabelText('Min: −2.308e+2')).toBeVisible();
await expect.element(minInput).toHaveValue('−1.72037e+2'); // slider extent updated after each step
expect(page.getByLabelText('Min: −1.72e+2')).toBeVisible();

expect(minThumb).toHaveValue(20); // still at original position
expect(minThumb).toHaveValue('20'); // still at original position
expect(maxInput).toHaveValue('4e+2'); // unaffected

// Move max thumb ten steps to the left
const maxThumb = page.getByRole('slider', { name: /max/ });
await user.type(maxThumb, '{PageDown}');
expect(maxInput).toHaveValue('5.72182e+1');
expect(page.getByLabelText('Max: 5.722e+1')).toBeVisible();
expect(maxInput).toHaveValue('6.01623e+1');
expect(page.getByLabelText('Max: 6.016e+1')).toBeVisible();

// Move max thumb ten steps to the right
await user.type(maxThumb, '{PageUp}');
expect(maxInput).toHaveValue('3.68841e+2'); // not back to 4e+2 because 4e+2 is not exactly on a slider division
expect(page.getByLabelText('Max: 3.688e+2')).toBeInTheDocument();
expect(maxInput).toHaveValue('3.69607e+2'); // not back to 4e+2 because 4e+2 is not exactly on a slider division
expect(page.getByLabelText('Max: 3.696e+2')).toBeInTheDocument();

expect(maxThumb).toHaveValue('81'); // still at original position
expect(minInput).toHaveValue('−1.72037e+2'); // unaffected
});

test('move thumbs with pointer to update domain', async () => {
await renderApp('/nexus/NXdata');

const minThumb = page.getByRole('slider', { name: /min/ });
const maxThumb = page.getByRole('slider', { name: /max/ });
const minInput = page.getByLabelText('min', { exact: true });
const maxInput = page.getByLabelText('max', { exact: true });
const minBtn = page.getByRole('button', { name: 'Min', exact: true });
const maxBtn = page.getByRole('button', { name: 'Max', exact: true });

expect(maxThumb).toHaveValue(81); // still at original position
expect(minInput).toHaveValue('−2.30818e+2'); // unaffected
// Drag min thumb and bring it back to its initial position
await dragHorizontally(minThumb, [20, 0]);
await expect.element(minInput).toHaveValue('−9.5e+1'); // not changed
expect(minThumb).toHaveValue('20');
expect(minBtn).toHaveAttribute('aria-pressed', 'true'); // still autoscaling

// Drag min thumb past max thumb, then back a little
const { left: minLeft } = minThumb.element().getBoundingClientRect();
const { left: maxLeft } = maxThumb.element().getBoundingClientRect();
await dragHorizontally(minThumb, [maxLeft - minLeft + 20, maxLeft - minLeft]);

// Min thumb pushes max thumb and keeps focus
await expect.element(minBtn).toHaveAttribute('aria-pressed', 'false');
expect(maxBtn).toHaveAttribute('aria-pressed', 'false');
expect(minInput).toHaveValue('3.71154e+2');
expect(maxInput).toHaveValue('4.81114e+3');
expect(minThumb).toHaveFocus();
});

test('toggle popup when clicking left edge of edit button', async () => {
const { user } = await renderApp('/nexus/NXdata');

const editBtn = page.getByRole('button', { name: 'Edit domain' });
await user.click(editBtn, { position: { x: 1, y: 10 } });

expect(editBtn).toHaveAttribute('aria-pressed', 'true');
expect(page.getByRole('slider', { name: /max/ })).toHaveValue('81');
});

test('edit bounds manually', async () => {
Expand Down Expand Up @@ -149,23 +188,23 @@ test('clamp domain in symlog scale', async () => {
await minInput.fill('-1e+1000');
await page.getByRole('button', { name: 'Apply min' }).click();
expect(minInput).toHaveValue('−8.98847e+307');
expect(minThumb).toHaveValue(1);
expect(minThumb).toHaveValue('1');

await maxInput.fill('1e+1000');
await page.getByRole('button', { name: 'Apply max' }).click();
expect(maxInput).toHaveValue('8.98847e+307');
expect(maxThumb).toHaveValue(100);
expect(maxThumb).toHaveValue('100');

await user.type(maxThumb, '{ArrowLeft}');
expect(maxInput).toHaveValue('5.40006e+301');
expect(maxThumb).toHaveValue(99); // does not jump back to 81
expect(maxThumb).toHaveValue('99'); // does not jump back to 81
});

test('control min/max autoscale behaviour', async () => {
const { user } = await renderApp('/nexus/NXdata');

const minThumb = page.getByRole('slider', { name: /min/ });
await minThumb.hover();
await page.getByRole('button', { name: 'Edit domain' }).hover();

const minBtn = page.getByRole('button', { name: 'Min', exact: true });
const maxBtn = page.getByRole('button', { name: 'Max', exact: true });
Expand Down Expand Up @@ -205,23 +244,15 @@ test('handle empty domain', async () => {
// Give min the same value as max
await minInput.fill('400');
await page.getByRole('button', { name: 'Apply min' }).click();
expect(minThumb).toHaveValue(81);
expect(maxThumb).toHaveValue(81);
expect(minThumb).toHaveValue('81');
expect(maxThumb).toHaveValue('81');
expect(page.getByLabelText('Min: −∞')).toBeVisible();
expect(page.getByLabelText('Max: +∞')).toBeVisible();

// Check that pearling works (i.e. that one thumb can push the other)
await user.type(maxThumb, '{ArrowLeft}');
expect(minInput).toHaveValue('3.12772e+2');
expect(maxInput).toHaveValue('3.12772e+2');
expect(minThumb).toHaveValue(80);
expect(maxThumb).toHaveValue(80);

// Ensure thumbs can be separated again
// Ensure thumbs can be separated again (with keyboard, thumbs don't push each other)
await user.type(maxThumb, '{ArrowRight}');
expect(maxInput).toHaveValue('3.71154e+2');
expect(minThumb).toHaveValue(80);
expect(maxThumb).toHaveValue(81);
expect(minInput).toHaveValue('4e+2');
expect(maxInput).toHaveValue('4.40398e+2');
});

test('handle min > max', async () => {
Expand Down
18 changes: 9 additions & 9 deletions packages/app/src/__tests__/NexusPack.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,14 @@ test('follow default slice on NXdata group', async () => {
const { selectVisTab } = await renderApp('/nexus/default_slice');

expect(getSelectedVisTab()).toBe(NxDataVis.NxHeatmap);
expect(page.getByRole('slider', { name: 'D0' })).toHaveValue(1);
expect(page.getByRole('slider', { name: 'D2' })).toHaveValue(2);
expect(page.getByRole('slider', { name: 'D0' })).toHaveValue('1');
expect(page.getByRole('slider', { name: 'D2' })).toHaveValue('2');

// Ignore `default_slice` meant for heatmap
await selectVisTab(NxDataVis.NxLine);
expect(page.getByRole('slider', { name: 'D0' })).toHaveValue(0);
expect(page.getByRole('slider', { name: 'D1' })).toHaveValue(0);
expect(page.getByRole('slider', { name: 'D2' })).toHaveValue(0);
expect(page.getByRole('slider', { name: 'D0' })).toHaveValue('0');
expect(page.getByRole('slider', { name: 'D1' })).toHaveValue('0');
expect(page.getByRole('slider', { name: 'D2' })).toHaveValue('0');
});

test('follow SILX styles on NXdata group', async () => {
Expand Down Expand Up @@ -242,16 +242,16 @@ test('ignore malformed `default_slice` attribute', async () => {
// Check that `default_slice` is not applied
expect(getSelectedVisTab()).toBe(NxDataVis.NxHeatmap);
expect(page.getByRole('figure', { name: 'fourD' })).toBeVisible();
expect(page.getByRole('slider', { name: 'D0' })).toHaveValue(0);
expect(page.getByRole('slider', { name: 'D1' })).toHaveValue(0);
expect(page.getByRole('slider', { name: 'D0' })).toHaveValue('0');
expect(page.getByRole('slider', { name: 'D1' })).toHaveValue('0');
expect(warningSpy).toHaveBeenCalledWith(
"Malformed 'default_slice' attribute: expected same length as signal dimensions",
);

// Slicing index out of bound
await selectNexusExplorerNode('default_slice_out_of_bounds');
expect(page.getByRole('slider', { name: 'D0' })).toHaveValue(0);
expect(page.getByRole('slider', { name: 'D1' })).toHaveValue(0);
expect(page.getByRole('slider', { name: 'D0' })).toHaveValue('0');
expect(page.getByRole('slider', { name: 'D1' })).toHaveValue('0');
expect(warningSpy).toHaveBeenCalledWith(
"Malformed 'default_slice' attribute: expected indices within bounds of signal dimensions",
);
Expand Down
28 changes: 27 additions & 1 deletion packages/app/src/test-utils.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { assertDefined, assertNonNull } from '@h5web/shared/guards';
import { expect, type MockInstance, vi } from 'vitest';
import { type Locator, page, userEvent } from 'vitest/browser';
import { commands, type Locator, page, userEvent } from 'vitest/browser';
import { render, type RenderResult } from 'vitest-browser-react';

import App from './App';
Expand All @@ -9,6 +9,12 @@ import { delay } from './providers/mock/utils';
import { type Vis } from './vis-packs/core/visualizations';
import { type NxDataVis } from './vis-packs/nexus/visualizations';

declare module 'vitest/browser' {
interface BrowserCommands {
mouseDrag: (path: [number, number][]) => Promise<void>;
}
}

interface RenderAppResult extends RenderResult {
user: ReturnType<typeof userEvent.setup>;
selectExplorerNode: (name: string) => Promise<void>;
Expand Down Expand Up @@ -135,6 +141,26 @@ export function getSelectedVisTab(): string {
return selectedTab.textContent;
}

/**
* Drag element with real mouse events: press mouse button at centre of element,
* move pointer horizontally through the given offsets (in px), then release.
*/
export async function dragHorizontally(
locator: Locator,
offsets: number[],
): Promise<void> {
const { left, top, width, height } = locator
.element()
.getBoundingClientRect();

const x = left + width / 2;
const y = top + height / 2;
await commands.mouseDrag([
[x, y],
...offsets.map((dx): [number, number] => [x + dx, y]),
]);
}

export function getDimMappingBtn(axis: 'x' | 'y', dim: number): Locator {
return page
.getByLabelText(`Dimension as ${axis} axis`)
Expand Down
22 changes: 21 additions & 1 deletion packages/app/vitest.browser.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { playwright } from '@vitest/browser-playwright';
import { defineBrowserCommand, playwright } from '@vitest/browser-playwright';
import { defineConfig, mergeConfig } from 'vitest/config';

import viteConfig from './vite.config';
Expand All @@ -7,6 +7,25 @@ const browser = process.env.CI
? 'chromium' // Firefox doesn't work in CI: https://github.com/microsoft/playwright/issues/11566
: process.env.VITE_TEST_BROWSER;

// Press mouse button at first point of path, move through other points, then release
// (coordinates are relative to the viewport of the test iframe)
const mouseDrag = defineBrowserCommand(async (ctx, path) => {
const iframe = ctx.iframe.owner();
const box = await iframe.boundingBox();
const scale = box.width / (await iframe.evaluate((elem) => elem.clientWidth));
const [start, ...rest] = path.map(([x, y]) => [
box.x + x * scale,
box.y + y * scale,
]);

await ctx.page.mouse.move(...start);
await ctx.page.mouse.down();
for (const point of rest) {
await ctx.page.mouse.move(...point, { steps: 5 }); // eslint-disable-line no-await-in-loop -- moves must be sequential
}
await ctx.page.mouse.up();
});

export default mergeConfig(
viteConfig,
defineConfig({
Expand All @@ -24,6 +43,7 @@ export default mergeConfig(
instances: [{ browser }],
viewport: { width: 1920, height: 1080 },
ui: false,
commands: { mouseDrag },
},
},
}),
Expand Down
Loading