mediawiki-skins-Vector/tests/jest/pinnableElement.test.js
Jan Drewniak 4d1c0b8940 Override PinnableElement behaviour at low resolutions.
At resolutions below 1000px, we want pinned elements
such as the Page Tools menu and Main Menu to collapse.
This behaviour is temporary and when the browser is resized,
the pinned elements should revert to their previous pinned state.

We also want to remove the ability to pin these menus at
low resolutions, so the "hide/move" button is hidden.

A new matchMedia event handler is added to PinnableElement.js
to handle this behaviour.

CSS is also added to hide the pinned menus at low resolution.
This is to account for the situation where the page is loaded
at narrow widths, with pinned elements,
and the JS hasn't loaded yet.

features.js is refactors so that class toggling can happen
independently of saving the state to user preferences
(since we want to toggle the classes but not save the state
at lower resolutions).

Bug: T326364
Change-Id: I3113ab83deb15843e04ed63ec767a85c522517b5
2023-01-13 18:09:11 +00:00

177 lines
7.6 KiB
JavaScript

jest.mock( '../../resources/skins.vector.es6/features.js' );
const features = require( '../../resources/skins.vector.es6/features.js' );
const mustache = require( 'mustache' );
const fs = require( 'fs' );
const pinnableHeaderTemplate = fs.readFileSync( 'includes/templates/PinnableHeader.mustache', 'utf8' );
const pinnableElement = require( '../../resources/skins.vector.es6/pinnableElement.js' );
/**
* Mock for matchMedia, which is not included in JSDOM.
* https://jestjs.io/docs/26.x/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
*/
Object.defineProperty( window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation( query => ( {
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn()
} ) )
} );
const simpleData = {
'is-pinned': false,
'data-name': 'simple',
'data-pinnable-element-id': 'pinnable-element',
label: 'simple pinnable element',
'label-tag-name': 'div',
'pin-label': 'pin',
'unpin-label': 'unpin'
};
const movableData = {
'is-pinned': false,
'data-name': 'movable',
'data-pinnable-element-id': 'pinnable-element',
'data-pinned-container-id': 'pinned-container',
'data-unpinned-container-id': 'unpinned-container',
label: 'moveable pinnable element',
'label-tag-name': 'div',
'pin-label': 'pin',
'unpin-label': 'unpin'
};
// @ts-ignore
const initializeHTML = ( headerData ) => {
const pinnableHeaderHTML = mustache.render( pinnableHeaderTemplate, headerData );
const pinnableElementHTML = `<div id="pinnable-element"> ${ pinnableHeaderHTML } </div>`;
document.body.innerHTML = `
<div id="pinned-container">
${ headerData[ 'is-pinned' ] ? pinnableElementHTML : '' }
</div>
<div id="unpinned-container">
${ !headerData[ 'is-pinned' ] ? pinnableElementHTML : '' }
</div>
`;
if ( headerData[ 'data-feature-name' ] ) {
// Return early if the persistent option is enabled as features.js will
// manage the body classes instead of pinnableElement.
return;
}
if ( headerData[ 'is-pinned' ] ) {
document.body.classList.add( `${headerData[ 'data-name' ]}-pinned` );
document.body.classList.remove( `${headerData[ 'data-name' ]}-unpinned` );
} else {
document.body.classList.remove( `${headerData[ 'data-name' ]}-pinned` );
document.body.classList.add( `${headerData[ 'data-name' ]}-unpinned` );
}
};
describe( 'Pinnable header', () => {
test( 'renders', () => {
initializeHTML( simpleData );
expect( document.body.innerHTML ).toMatchSnapshot();
} );
test( 'updates pinnable classes when toggle is pressed', () => {
initializeHTML( simpleData );
pinnableElement.initPinnableElement();
const pinButton = /** @type {HTMLElement} */ ( document.querySelector( '.vector-pinnable-header-pin-button' ) );
const unpinButton = /** @type {HTMLElement} */ ( document.querySelector( '.vector-pinnable-header-unpin-button' ) );
const header = /** @type {HTMLElement} */ ( document.querySelector( `.${simpleData[ 'data-name' ]}-pinnable-header` ) );
expect( header.classList.contains( pinnableElement.PINNED_HEADER_CLASS ) ).toBe( false );
expect( document.body.classList.contains( `${simpleData[ 'data-name' ]}-pinned` ) ).toBe( false );
expect( header.classList.contains( pinnableElement.UNPINNED_HEADER_CLASS ) ).toBe( true );
expect( document.body.classList.contains( `${simpleData[ 'data-name' ]}-unpinned` ) ).toBe( true );
pinButton.click();
expect( header.classList.contains( pinnableElement.PINNED_HEADER_CLASS ) ).toBe( true );
expect( document.body.classList.contains( `${simpleData[ 'data-name' ]}-pinned` ) ).toBe( true );
expect( header.classList.contains( pinnableElement.UNPINNED_HEADER_CLASS ) ).toBe( false );
expect( document.body.classList.contains( `${simpleData[ 'data-name' ]}-unpinned` ) ).toBe( false );
unpinButton.click();
expect( header.classList.contains( pinnableElement.PINNED_HEADER_CLASS ) ).toBe( false );
expect( document.body.classList.contains( `${simpleData[ 'data-name' ]}-pinned` ) ).toBe( false );
expect( header.classList.contains( pinnableElement.UNPINNED_HEADER_CLASS ) ).toBe( true );
expect( document.body.classList.contains( `${simpleData[ 'data-name' ]}-unpinned` ) ).toBe( true );
} );
test( 'doesnt move pinnable element when data attributes arent defined', () => {
initializeHTML( simpleData );
pinnableElement.initPinnableElement();
const pinButton = /** @type {HTMLElement} */ ( document.querySelector( '.vector-pinnable-header-pin-button' ) );
const unpinButton = /** @type {HTMLElement} */ ( document.querySelector( '.vector-pinnable-header-unpin-button' ) );
const pinnableElem = /** @type {HTMLElement} */ ( document.getElementById( simpleData[ 'data-pinnable-element-id' ] ) );
/* eslint-disable no-restricted-properties */
expect( pinnableElem.parentElement && pinnableElem.parentElement.id ).toBe( 'unpinned-container' );
pinButton.click();
expect( pinnableElem.parentElement && pinnableElem.parentElement.id ).toBe( 'unpinned-container' );
unpinButton.click();
expect( pinnableElem.parentElement && pinnableElem.parentElement.id ).toBe( 'unpinned-container' );
/* eslint-enable no-restricted-properties */
} );
test( 'moves pinnable element when data attributes are defined', () => {
initializeHTML( movableData );
pinnableElement.initPinnableElement();
const pinButton = /** @type {HTMLElement} */ ( document.querySelector( '.vector-pinnable-header-pin-button' ) );
const unpinButton = /** @type {HTMLElement} */ ( document.querySelector( '.vector-pinnable-header-unpin-button' ) );
const pinnableElem = /** @type {HTMLElement} */ ( document.getElementById( movableData[ 'data-pinnable-element-id' ] ) );
/* eslint-disable no-restricted-properties */
expect( pinnableElem.parentElement && pinnableElem.parentElement.id ).toBe( 'unpinned-container' );
pinButton.click();
expect( pinnableElem.parentElement && pinnableElem.parentElement.id ).toBe( 'pinned-container' );
unpinButton.click();
expect( pinnableElem.parentElement && pinnableElem.parentElement.id ).toBe( 'unpinned-container' );
/* eslint-enable no-restricted-properties */
} );
test( 'calls features.js when data-feature-name is set', () => {
initializeHTML( {
...simpleData,
'data-name': 'vector-page-tools',
'data-feature-name': 'page-tools-pinned'
} );
pinnableElement.initPinnableElement();
const pinButton = /** @type {HTMLElement} */ ( document.querySelector( '.vector-pinnable-header-pin-button' ) );
const unpinButton = /** @type {HTMLElement} */ ( document.querySelector( '.vector-pinnable-header-unpin-button' ) );
pinButton.click();
expect( features.toggle ).toHaveBeenCalledTimes( 1 );
expect( features.toggle ).toHaveBeenCalledWith( 'page-tools-pinned' );
// @ts-ignore
features.toggle.mockClear();
unpinButton.click();
expect( features.toggle ).toHaveBeenCalledTimes( 1 );
expect( features.toggle ).toHaveBeenCalledWith( 'page-tools-pinned' );
} );
test( 'isPinned() returns whether the element is pinned or not', () => {
initializeHTML( simpleData );
pinnableElement.initPinnableElement();
const pinButton = /** @type {HTMLElement} */ ( document.querySelector( '.vector-pinnable-header-pin-button' ) );
const unpinButton = /** @type {HTMLElement} */ ( document.querySelector( '.vector-pinnable-header-unpin-button' ) );
const header = /** @type {HTMLElement} */ ( document.querySelector( `.${simpleData[ 'data-name' ]}-pinnable-header` ) );
pinButton.click();
expect( pinnableElement.isPinned( header ) ).toBe( true );
unpinButton.click();
expect( pinnableElement.isPinned( header ) ).toBe( false );
} );
} );