From 2e53e495fdfe4489d42db02e5162fab4dcee07c2 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 16 May 2024 16:11:19 +0800 Subject: [PATCH] WIP tests --- .../src/Slider2/Root/SliderRoot.test.tsx | 327 ++++++++++++++++++ .../mui-base/src/Slider2/Root/SliderRoot.tsx | 4 + .../src/Slider2/SliderThumb/useSliderThumb.ts | 1 + .../Slider2/SliderTrack/SliderTrack.types.ts | 13 + .../src/Slider2/SliderTrack/useSliderTrack.ts | 3 +- packages/mui-base/src/Slider2/index.barrel.ts | 2 +- 6 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 packages/mui-base/src/Slider2/Root/SliderRoot.test.tsx diff --git a/packages/mui-base/src/Slider2/Root/SliderRoot.test.tsx b/packages/mui-base/src/Slider2/Root/SliderRoot.test.tsx new file mode 100644 index 0000000000..d961e9e588 --- /dev/null +++ b/packages/mui-base/src/Slider2/Root/SliderRoot.test.tsx @@ -0,0 +1,327 @@ +import { expect } from 'chai'; +import * as React from 'react'; +import { spy, stub } from 'sinon'; +import { createRenderer, fireEvent, screen } from '@mui/internal-test-utils'; +import * as Slider from '@base_ui/react/Slider2'; +import { describeConformance } from '../../../test/describeConformance'; + +type Touches = Array>; + +function createTouches(touches: Touches) { + return { + changedTouches: touches.map( + (touch) => + new Touch({ + target: document.body, + ...touch, + }), + ), + }; +} + +describe('', () => { + before(function beforeHook() { + if (typeof Touch === 'undefined') { + this.skip(); + } + + // PointerEvent not fully implemented in jsdom, causing + // fireEvent.pointer* to ignore options + // https://github.com/jsdom/jsdom/issues/2527 + (window as any).PointerEvent = window.MouseEvent; + }); + + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'div', + render, + refInstanceof: window.HTMLDivElement, + })); + + it('renders a slider', () => { + render( + + + + + + , + ); + + expect(screen.getByRole('slider')).to.have.attribute('aria-valuenow', '30'); + }); + + describe('ARIA attributes', () => { + it('it has the correct aria attributes', () => { + const { container, getByRole, getByTestId } = render( + + + + + + , + ); + + const root = getByTestId('root'); + const slider = getByRole('slider'); + const input = container.querySelector('input'); + + expect(root).not.to.have.attribute('aria-labelledby'); + + expect(slider).to.have.attribute('aria-valuenow', '30'); + expect(slider).to.have.attribute('aria-valuemin', '0'); + expect(slider).to.have.attribute('aria-valuemax', '100'); + expect(slider).to.have.attribute('aria-orientation', 'horizontal'); + + expect(input).to.have.attribute('aria-labelledby', 'labelId'); + expect(input).to.have.attribute('aria-valuenow', '30'); + + // TODO: aria-label should be somewhere + }); + }); + + describe('prop: disabled', () => { + it('should render data-disabled on the root, track, output and thumb', () => { + const { getByTestId } = render( + + + + + + , + ); + + const root = getByTestId('root'); + const output = getByTestId('output'); + const track = getByTestId('track'); + const thumb = getByTestId('thumb'); + + expect(root).to.have.attribute('data-disabled', 'true'); + expect(output).to.have.attribute('data-disabled', 'true'); + expect(track).to.have.attribute('data-disabled', 'true'); + expect(thumb).to.have.attribute('data-disabled', 'true'); + }); + + function TestSlider(props) { + return ( + + + + + + ); + } + + it('should not respond to drag events after becoming disabled', function test() { + // TODO: Don't skip once a fix for https://github.com/jsdom/jsdom/issues/3029 is released. + if (/jsdom/.test(window.navigator.userAgent)) { + this.skip(); + } + + const { getByRole, setProps, getByTestId } = render( + , + ); + + const sliderTrack = getByTestId('track'); + + stub(sliderTrack, 'getBoundingClientRect').callsFake(() => ({ + width: 100, + height: 10, + bottom: 10, + left: 0, + x: 0, + y: 0, + top: 0, + right: 0, + toJSON() {}, + })); + fireEvent.touchStart( + sliderTrack, + createTouches([{ identifier: 1, clientX: 21, clientY: 0 }]), + ); + + const thumb = getByRole('slider'); + + expect(thumb).to.have.attribute('aria-valuenow', '21'); + expect(thumb).toHaveFocus(); + + setProps({ disabled: true }); + expect(thumb).not.toHaveFocus(); + // expect(thumb).not.to.have.class(classes.active); + + fireEvent.touchMove(sliderTrack, createTouches([{ identifier: 1, clientX: 30, clientY: 0 }])); + + expect(thumb).to.have.attribute('aria-valuenow', '21'); + }); + + it('should not respond to drag events if disabled', function test() { + // TODO: Don't skip once a fix for https://github.com/jsdom/jsdom/issues/3029 is released. + if (/jsdom/.test(window.navigator.userAgent)) { + this.skip(); + } + + const { getByRole, getByTestId } = render( + , + ); + + const thumb = getByRole('slider'); + const sliderTrack = getByTestId('track'); + + stub(sliderTrack, 'getBoundingClientRect').callsFake(() => ({ + width: 100, + height: 10, + bottom: 10, + left: 0, + x: 0, + y: 0, + top: 0, + right: 0, + toJSON() {}, + })); + + fireEvent.touchStart( + sliderTrack, + createTouches([{ identifier: 1, clientX: 21, clientY: 0 }]), + ); + + fireEvent.touchMove( + document.body, + createTouches([{ identifier: 1, clientX: 30, clientY: 0 }]), + ); + + fireEvent.touchEnd( + document.body, + createTouches([{ identifier: 1, clientX: 30, clientY: 0 }]), + ); + + expect(thumb).to.have.attribute('aria-valuenow', '21'); + }); + }); + + describe('prop: marks', () => { + it('does not cause unknown-prop error', () => { + const marks = [ + { + value: 33, + }, + ]; + expect(() => { + render(); + }).not.to.throw(); + }); + }); + + describe('prop: orientation', () => { + function VerticalSlider() { + return ( + + + + + + ); + } + + it('sets the orientation via ARIA', () => { + render(); + + const sliderRoot = screen.getByRole('slider'); + expect(sliderRoot).to.have.attribute('aria-orientation', 'vertical'); + }); + + it('does not set the orientation via appearance for WebKit browsers', function test() { + if (/jsdom/.test(window.navigator.userAgent) || !/WebKit/.test(window.navigator.userAgent)) { + this.skip(); + } + + render(); + + const slider = screen.getByRole('slider'); + + expect(slider).to.have.property('tagName', 'INPUT'); + expect(slider).to.have.property('type', 'range'); + // Only relevant if we implement `[role="slider"]` with `input[type="range"]` + // We're not setting this by default because it changes horizontal keyboard navigation in WebKit: https://bugs.chromium.org/p/chromium/issues/detail?id=1162640 + expect(slider).not.toHaveComputedStyle({ webkitAppearance: 'slider-vertical' }); + }); + }); + + describe('prop: onValueChange', () => { + function TestSlider(props: { onValueChange: () => void }) { + return ( + + + + + + ); + } + + it('is called when clicking on the track', () => { + const handleValueChange = spy(); + render(); + + const sliderTrack = screen.getByTestId('track'); + + stub(sliderTrack, 'getBoundingClientRect').callsFake(() => ({ + width: 100, + height: 10, + bottom: 10, + left: 0, + x: 0, + y: 0, + right: 0, + top: 0, + toJSON() {}, + })); + + fireEvent.pointerDown(sliderTrack, { + buttons: 1, + clientX: 41, + clientY: 5, + }); + + expect(handleValueChange.callCount).to.equal(1); + }); + + it('is not called when clicking on the thumb', () => { + const handleValueChange = spy(); + render(); + + const sliderTrack = screen.getByTestId('track'); + const sliderThumb = screen.getByTestId('thumb'); + + stub(sliderTrack, 'getBoundingClientRect').callsFake(() => ({ + width: 100, + height: 10, + bottom: 10, + left: 0, + x: 0, + y: 0, + right: 0, + top: 0, + toJSON() {}, + })); + + fireEvent.pointerDown(sliderThumb, { + buttons: 1, + clientX: 51, + }); + + expect(handleValueChange.callCount).to.equal(0); + }); + }); + + describe('keyboard interactions', () => { + it('should support Shift + Left Arrow / Right Arrow keys', () => {}); + + it('should support Shift + Up Arrow / Down Arrow keys', () => {}); + + it('should support PageUp / PageDown keys', () => {}); + + it('should support Shift + Left Arrow / Right Arrow keys by taking acount step and shiftStep', () => {}); + + it('should stop at max/min when using Shift + Left Arrow / Right Arrow keys', () => {}); + }); +}); diff --git a/packages/mui-base/src/Slider2/Root/SliderRoot.tsx b/packages/mui-base/src/Slider2/Root/SliderRoot.tsx index 0f7cc78e06..dc8e75ffaf 100644 --- a/packages/mui-base/src/Slider2/Root/SliderRoot.tsx +++ b/packages/mui-base/src/Slider2/Root/SliderRoot.tsx @@ -17,11 +17,13 @@ const SliderRoot = React.forwardRef(function SliderRoot( forwardedRef: React.ForwardedRef, ) { const { + 'aria-labelledby': ariaLabelledby, className, defaultValue, disabled = false, render: renderProp, onValueChange, + orientation, value, ...otherProps } = props; @@ -31,9 +33,11 @@ const SliderRoot = React.forwardRef(function SliderRoot( const mergedRef = useRenderPropForkRef(render, forwardedRef); const { getRootProps, ...slider } = useSliderRoot({ + 'aria-labelledby': ariaLabelledby, defaultValue, disabled, onValueChange, + orientation, rootRef: mergedRef, value, ...otherProps, diff --git a/packages/mui-base/src/Slider2/SliderThumb/useSliderThumb.ts b/packages/mui-base/src/Slider2/SliderThumb/useSliderThumb.ts index 3ded05be6f..6ff682724a 100644 --- a/packages/mui-base/src/Slider2/SliderThumb/useSliderThumb.ts +++ b/packages/mui-base/src/Slider2/SliderThumb/useSliderThumb.ts @@ -102,6 +102,7 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) { 'aria-orientation': orientation, 'aria-valuemax': scale(max), 'aria-valuemin': scale(min), + 'aria-valuenow': scale(thumbValue), disabled, name, id: compoundItemId, diff --git a/packages/mui-base/src/Slider2/SliderTrack/SliderTrack.types.ts b/packages/mui-base/src/Slider2/SliderTrack/SliderTrack.types.ts index 7485312aa9..2641d5e053 100644 --- a/packages/mui-base/src/Slider2/SliderTrack/SliderTrack.types.ts +++ b/packages/mui-base/src/Slider2/SliderTrack/SliderTrack.types.ts @@ -2,3 +2,16 @@ import { BaseUIComponentProps } from '../../utils/BaseUI.types'; import { SliderRootOwnerState } from '../Root/SliderRoot.types'; export interface SliderTrackProps extends BaseUIComponentProps<'div', SliderRootOwnerState> {} + +export interface UseSliderTrackParameters { + /** + * The ref attached to the track of the Slider. + */ + rootRef?: React.Ref; +} + +export interface UseSliderTrackReturnValue { + getRootProps: ( + externalProps?: React.ComponentPropsWithRef<'div'>, + ) => React.ComponentPropsWithRef<'div'>; +} diff --git a/packages/mui-base/src/Slider2/SliderTrack/useSliderTrack.ts b/packages/mui-base/src/Slider2/SliderTrack/useSliderTrack.ts index 471f92babf..3b51d43637 100644 --- a/packages/mui-base/src/Slider2/SliderTrack/useSliderTrack.ts +++ b/packages/mui-base/src/Slider2/SliderTrack/useSliderTrack.ts @@ -6,10 +6,11 @@ import { useSliderContext } from '../Root/SliderContext'; import { useEventCallback } from '../../utils/useEventCallback'; import { roundValueToStep, valueToPercent } from '../utils'; import { areValuesEqual, focusThumb, trackFinger } from '../Root/useSliderRoot'; +import { UseSliderTrackParameters, UseSliderTrackReturnValue } from './SliderTrack.types'; const INTENTIONAL_DRAG_COUNT_THRESHOLD = 2; -export function useSliderTrack(parameters: any) { +export function useSliderTrack(parameters: UseSliderTrackParameters): UseSliderTrackReturnValue { const { rootRef: externalRef } = parameters; const trackRef = React.useRef(null); diff --git a/packages/mui-base/src/Slider2/index.barrel.ts b/packages/mui-base/src/Slider2/index.barrel.ts index c90163f55f..fe49fbdd5a 100644 --- a/packages/mui-base/src/Slider2/index.barrel.ts +++ b/packages/mui-base/src/Slider2/index.barrel.ts @@ -1,5 +1,5 @@ export { Slider } from './Root/SliderRoot'; -export { +export type { SliderRootOwnerState, SliderRootProps, UseSliderParameters,