feat: support matches per breakpoints

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2024-10-18 11:45:11 +02:00
parent 77ca3b7f53
commit af9097e1c0
5 changed files with 240 additions and 12 deletions
+21
View File
@@ -0,0 +1,21 @@
---
'@backstage/core-components': patch
---
Adds the ability to mock a media query per break point and to change the active break point during a test. Usage example:
```ts
const { set } = mockBreakpoint({
initialBreakpoint: 'md',
queryBreakpointMap: {
'(min-width:1500px)': 'xl',
'(min-width:1000px)': 'lg',
'(min-width:700px)': 'md',
'(min-width:400px)': 'sm',
'(min-width:0px)': 'xs',
},
});
// assertions for when the active break point is "md"
set('lg');
// assertions for when the active break point is "lg"
```
+1 -1
View File
@@ -65,6 +65,7 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"@react-hookz/web": "^24.0.0",
"@testing-library/react": "^16.0.0",
"@types/react-sparklines": "^1.7.0",
"ansi-regex": "^6.0.1",
"classnames": "^2.2.6",
@@ -98,7 +99,6 @@
"@backstage/test-utils": "workspace:^",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.0.0",
"@types/ansi-regex": "^5.0.0",
"@types/classnames": "^2.2.9",
@@ -3,8 +3,19 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Breakpoint } from '@material-ui/core/styles/createBreakpoints';
// @public
export function mockBreakpoint(options: { matches: boolean }): void;
// @public
export function mockBreakpoint(options: {
initialBreakpoint?: Breakpoint;
queryBreakpointMap?: Record<string, Breakpoint>;
}): {
set(value: string): void;
remove(): void;
};
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,73 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Breakpoint } from '@material-ui/core/styles/createBreakpoints';
import { mockBreakpoint } from './testUtils';
describe('mockBreakpoint', () => {
const originalMatchMedia = window.matchMedia;
afterAll(() => {
window.matchMedia = originalMatchMedia;
});
it('should remove the mock', () => {
const { remove } = mockBreakpoint({ initialBreakpoint: 'md' });
expect(originalMatchMedia).not.toBe(window.matchMedia);
remove();
expect(window.matchMedia).toBe(originalMatchMedia);
});
it('should mock matchMedia with initialBreakpoint', () => {
const { set } = mockBreakpoint({ initialBreakpoint: 'md' });
expect(window.matchMedia('(min-width:960px)').matches).toBe(true);
expect(window.matchMedia('(min-width:1280px)').matches).toBe(false);
set('lg');
expect(window.matchMedia('(min-width:1280px)').matches).toBe(true);
});
it('should mock matchMedia with matches option', () => {
mockBreakpoint({ matches: true });
expect(window.matchMedia('(min-width:1920px)').matches).toBe(true);
expect(window.matchMedia('(min-width:1280px)').matches).toBe(true);
expect(window.matchMedia('(min-width:960px)').matches).toBe(true);
expect(window.matchMedia('(min-width:600px)').matches).toBe(true);
expect(window.matchMedia('(min-width:0px)').matches).toBe(true);
});
it('should mock matchMedia with custom queryBreakpointMap', () => {
const customMap: Record<string, Breakpoint> = {
'(min-width:1500px)': 'xl',
'(min-width:1000px)': 'lg',
'(min-width:700px)': 'md',
'(min-width:400px)': 'sm',
'(min-width:0px)': 'xs',
};
const { set } = mockBreakpoint({
initialBreakpoint: 'sm',
queryBreakpointMap: customMap,
});
expect(window.matchMedia('(min-width:400px)').matches).toBe(true);
expect(window.matchMedia('(min-width:700px)').matches).toBe(false);
set('md');
expect(window.matchMedia('(min-width:700px)').matches).toBe(true);
});
});
+134 -11
View File
@@ -14,6 +14,11 @@
* limitations under the License.
*/
import { Breakpoint } from '@material-ui/core/styles/createBreakpoints';
import { act } from '@testing-library/react';
const originalMatchMedia = window.matchMedia;
/**
* This is a mocking method suggested in the Jest docs, as it is not implemented in JSDOM yet.
* It can be used to mock values for the Material UI `useMediaQuery` hook if it is used in a tested component.
@@ -25,19 +30,137 @@
* https://mui.com/material-ui/react-use-media-query/#testing
*
* @public
*
* @example
* Match with any media query:
* ```ts
* mockBreakpoint({ matches: true });
* ```
*/
export function mockBreakpoint(options: { matches: boolean }) {
export function mockBreakpoint(options: { matches: boolean }): void;
/**
* This is a mocking method suggested in the Jest docs, as it is not implemented in JSDOM yet.
* It can be used to mock values for the Material UI `useMediaQuery` hook if it is used in a tested component.
*
* For issues checkout the documentation:
* https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
*
* If there are any updates from Material UI React on testing `useMediaQuery` this mock should be replaced
* https://mui.com/material-ui/react-use-media-query/#testing
*
* @public
*
* @example
* Set the initial breakpoint:
* ```ts
* mockBreakpoint({ initialBreakpoint: 'md' });
* ```
*
* @example
* Map media queries to breakpoints:
* ```ts
* mockBreakpoint({ queryBreakpointMap: { '(min-width:1500px)': 'xl', '(min-width:1000px)': 'lg', '(min-width:700px)': 'md', '(min-width:400px)': 'sm', '(min-width:0px)': 'xs', } });
* ```
*
* @example
* Change the breakpoint during the test:
* ```ts
* const { set } = mockBreakpoint({ initialBreakpoint: 'md' });
* set('lg');
* ```
**/
export function mockBreakpoint(options: {
/** Defaults to 'lg' */
initialBreakpoint?: Breakpoint;
/** Defaults to \{ '(min-width:1920px)': 'xl', '(min-width:1280px)': 'lg', '(min-width:960px)': 'md', '(min-width:600px)': 'sm', '(min-width:0px)': 'xs' \} */
queryBreakpointMap?: Record<string, Breakpoint>;
}): {
set(value: string): void;
remove(): void;
};
export function mockBreakpoint(
options:
| {
matches: boolean;
}
| {
initialBreakpoint?: Breakpoint;
queryBreakpointMap?: Record<string, Breakpoint>;
},
): {
set(value: string): void;
remove(): void;
} {
const mediaQueries: {
mediaQueryString: string;
mediaQueryList: { matches: boolean };
mediaQueryListeners: Set<(event: { matches: boolean }) => void>;
}[] = [];
let breakpoint: Breakpoint = 'lg';
if ('initialBreakpoint' in options && options.initialBreakpoint) {
breakpoint = options.initialBreakpoint;
}
const breakpoints: Record<string, Breakpoint> =
'queryBreakpointMap' in options &&
typeof options.queryBreakpointMap === 'object'
? options.queryBreakpointMap
: {
'(min-width:1920px)': 'xl',
'(min-width:1280px)': 'lg',
'(min-width:960px)': 'md',
'(min-width:600px)': 'sm',
'(min-width:0px)': 'xs',
};
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: options.matches ?? false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
value: jest.fn().mockImplementation(mediaQueryString => {
const mediaQueryListeners = new Set<
(event: { matches: boolean }) => void
>();
const mediaQueryList = {
matches:
'matches' in options
? options.matches
: breakpoints[mediaQueryString] === breakpoint,
media: mediaQueryString,
onchange: null,
addListener: jest.fn(listener => {
mediaQueryListeners.add(listener);
}),
removeListener: jest.fn(listener => {
mediaQueryListeners.delete(listener);
}),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
};
mediaQueries.push({
mediaQueryString,
mediaQueryList,
mediaQueryListeners,
});
return mediaQueryList;
}),
});
return {
set(newBreakpoint: Breakpoint) {
breakpoint = newBreakpoint;
mediaQueries.forEach(
({ mediaQueryString, mediaQueryList, mediaQueryListeners }) => {
act(() => {
const matches =
'matches' in options
? options.matches
: breakpoints[mediaQueryString] === breakpoint;
mediaQueryList.matches = matches;
mediaQueryListeners.forEach(listener => listener({ matches }));
});
},
);
},
remove() {
window.matchMedia = originalMatchMedia;
},
};
}