From ae47e055f41529a8fbd7fdcf8bea89f1eb27ab31 Mon Sep 17 00:00:00 2001 From: Maxim Alyoshin Date: Tue, 25 Apr 2023 21:36:27 +0400 Subject: [PATCH] fix(up): remove incorrect validation when breakpoint is equal zero --- core/breakpoints.js | 198 ++++++++++++++++---------------- core/breakpoints.spec.js | 239 ++++++++++++++------------------------- 2 files changed, 184 insertions(+), 253 deletions(-) diff --git a/core/breakpoints.js b/core/breakpoints.js index 2f063169..2c7033dc 100644 --- a/core/breakpoints.js +++ b/core/breakpoints.js @@ -1,114 +1,112 @@ -const { createInvariantWithPrefix } = require('../library'); +const { createInvariantWithPrefix, memoize } = require('../library'); + +const DEFAULT_BREAKPOINTS = { + xs: '0px', + sm: '576px', + md: '768px', + lg: '992px', + xl: '1200px', + xxl: '1400px', +}; -exports.createBreakpoints = ({ breakpoints, errorPrefix }) => { - const keys = Object.keys(Object(breakpoints)); - const values = Object.values(Object(breakpoints)); - const entries = Object.entries(Object(breakpoints)); - const invariant = createInvariantWithPrefix(errorPrefix); +const ERROR_PREFIX = '[breakpoints]: '; + +const defaultOptions = { + breakpoints: DEFAULT_BREAKPOINTS, + errorPrefix: ERROR_PREFIX, +}; - const validation = withValidation({ - invariant, +exports.DEFAULT_BREAKPOINTS = DEFAULT_BREAKPOINTS; +exports.ERROR_PREFIX = ERROR_PREFIX; + +exports.createBreakpoints = ({ breakpoints, errorPrefix } = defaultOptions) => { + const names = Object.keys(breakpoints); + const validation = createValidation({ + names, breakpoints, - keys, + errorPrefix, }); - return { - keys, - entries, - invariant, - ...withBreakpoints({ - validation, - breakpoints, - keys, - values, - }), - }; -}; + const getValueByName = memoize((name) => { + validation.checkIsValidName(name); + validation.checkIsFirstName(name); -function withValidation({ keys, invariant, breakpoints }) { - return { - throwIsInvalidName, - throwIsValueIsZero, - throwIsLastBreakpoint, + return breakpoints[name]; + }); + + const getNextName = (name) => { + const nextIndex = names.indexOf(name) + 1; + + return names[nextIndex]; }; - function throwIsInvalidName(name) { - invariant( - breakpoints[name], - `breakpoint \`${name}\` not found in ${keys.join(', ')}.` - ); - } - - function throwIsValueIsZero(name) { - const value = breakpoints[name]; - const isNotZero = parseFloat(value) !== 0; - - invariant( - isNotZero, - `\`${name}: ${value}\` cannot be assigned as minimum breakpoint.` - ); - } - - function throwIsLastBreakpoint(name) { - const isNotLast = name !== keys.at(-1); - const validName = keys.at(-2); - - invariant( - isNotLast, - `\`${name}\` doesn't have a maximum width. Use \`${validName}\`. See https://github.com/mg901/styled-breakpoints/issues/4 .` - ); - } -} + const getNextValueByName = memoize((name) => { + validation.checkIsValidName(name); + validation.checkIsLastName(name); + + return breakpoints[getNextName(name)]; + }); + + // Maximum breakpoint width. Null for the largest (last) breakpoint. + // The maximum value is calculated as the minimum of the next one less 0.02px + // to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths. + // See https://www.w3.org/TR/mediaqueries-4/#mq-min-max + // Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari. + // See https://bugs.webkit.org/show_bug.cgi?id=178261 + const calcMaxWidth = (value) => { + return `${parseFloat(value) - 0.02}px`; + }; -function withBreakpoints(state) { return { - up, - down, - between, - only, + up: memoize((name) => { + validation.checkIsValidName(name); + + return breakpoints[name]; + }), + + down: (max) => calcMaxWidth(getValueByName(max)), + + between: (min, max) => ({ + min: getValueByName(min), + max: calcMaxWidth(getValueByName(max)), + }), + + only: (name) => ({ + min: getValueByName(name), + max: calcMaxWidth(getNextValueByName(name)), + }), }; +}; - function up(min) { - state.validation.throwIsInvalidName(min); - - return state.breakpoints[min]; - } - - function down(max) { - state.validation.throwIsInvalidName(max); - state.validation.throwIsValueIsZero(max); - state.validation.throwIsLastBreakpoint(max); - - return calcMaxWidth(state.breakpoints[max]); - } - - function between(min, max) { - return { - min: up(min), - max: down(max), - }; - } - - function only(name) { - state.validation.throwIsInvalidName(name); - state.validation.throwIsLastBreakpoint(name); - const nextIndex = state.keys.indexOf(name) + 1; - - return { - min: up(name), - max: calcMaxWidth(state.values[nextIndex]), - }; - } -} +function createValidation({ names, breakpoints, errorPrefix }) { + const invariant = createInvariantWithPrefix(errorPrefix); -// Maximum breakpoint width. Null for the largest (last) breakpoint. -// The maximum value is calculated as the minimum of the next one less 0.02px -// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths. -// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max -// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari. -// See https://bugs.webkit.org/show_bug.cgi?id=178261 -function calcMaxWidth(value) { - return `${parseFloat(value) - 0.02}px`; + return { + checkIsValidName: (name) => { + invariant( + breakpoints[name], + `breakpoint \`${name}\` not found in ${names.join(', ')}.` + ); + }, + checkIsFirstName: (name) => { + const value = breakpoints[name]; + const isNotZero = parseFloat(value) !== 0; + + invariant( + isNotZero, + `\`${name}: ${value}\` cannot be assigned as minimum breakpoint.` + ); + }, + checkIsLastName: (name) => { + const currentIndex = names.indexOf(name); + const nextIndex = currentIndex + 1; + const isNotLast = names.length !== nextIndex; + const validName = names[names.length - 2]; + + invariant( + isNotLast, + `\`${name}\` doesn't have a maximum width. Use \`${validName}\`. See https://github.com/mg901/styled-breakpoints/issues/4 .` + ); + }, + }; } - -exports.calcMaxWidth = calcMaxWidth; diff --git a/core/breakpoints.spec.js b/core/breakpoints.spec.js index a968e382..7fbb8a8a 100644 --- a/core/breakpoints.spec.js +++ b/core/breakpoints.spec.js @@ -1,6 +1,8 @@ -const { createBreakpoints, calcMaxWidth } = require('./breakpoints'); +const { createBreakpoints } = require('./breakpoints'); -describe('createBreakpoints', () => { +const { down, between, only } = createBreakpoints(); + +describe('core/create-breakpoints', () => { let breakpointsApi = null; let INVALID_BREAKPOINT_NAME = null; let ERROR_PREFIX = null; @@ -26,19 +28,12 @@ describe('createBreakpoints', () => { it('should returns an object with expected methods', () => { expect(Object.keys(breakpointsApi)).toEqual([ - 'keys', - 'entries', - 'invariant', 'up', 'down', 'between', 'only', ]); - expect(Array.isArray(breakpointsApi.keys)).toBeTruthy(); - expect(Array.isArray(breakpointsApi.entries)).toBeTruthy(); - expect(typeof breakpointsApi.invariant).toBe('function'); - expect(typeof breakpointsApi.up).toBe('function'); expect(typeof breakpointsApi.down).toBe('function'); expect(typeof breakpointsApi.between).toBe('function'); @@ -53,6 +48,7 @@ describe('createBreakpoints', () => { }); it('should return the correct value for valid breakpoint', () => { + expect(breakpointsApi.up('xs')).toBe(DEFAULT_BREAKPOINTS.xs); expect(breakpointsApi.up('sm')).toBe(DEFAULT_BREAKPOINTS.sm); expect(breakpointsApi.up('md')).toBe(DEFAULT_BREAKPOINTS.md); expect(breakpointsApi.up('lg')).toBe(DEFAULT_BREAKPOINTS.lg); @@ -61,170 +57,107 @@ describe('createBreakpoints', () => { }); }); - describe('down method', () => { - it('should throw an error for an invalid breakpoint name', () => { - expect(() => breakpointsApi.down(INVALID_BREAKPOINT_NAME)).toThrowError( - `${ERROR_PREFIX}breakpoint \`${INVALID_BREAKPOINT_NAME}\` not found in xs, sm, md, lg, xl, xxl.` - ); - }); - - it('should throw an error when the value is equal 0', () => { - expect(() => breakpointsApi.down('xs')).toThrow( - `${ERROR_PREFIX}\`xs: 0px\` cannot be assigned as minimum breakpoint.` - ); + describe('down', () => { + it('should throw exception if the breakpoint name is not found', () => { + try { + down('wtf'); + } catch (error) { + expect(error.message).toEqual( + `${ERROR_PREFIX}breakpoint \`wtf\` not found in xs, sm, md, lg, xl, xxl.` + ); + } }); - it('should calculate the correct maximum breakpoint width', () => { - expect(breakpointsApi.down('sm')).toBe( - calcMaxWidth(DEFAULT_BREAKPOINTS.sm) - ); - expect(breakpointsApi.down('md')).toBe( - calcMaxWidth(DEFAULT_BREAKPOINTS.md) - ); - expect(breakpointsApi.down('lg')).toBe( - calcMaxWidth(DEFAULT_BREAKPOINTS.lg) - ); - expect(breakpointsApi.down('xl')).toBe( - calcMaxWidth(DEFAULT_BREAKPOINTS.xl) - ); + it('should render correctly breakpoints by default', () => { + const results = [ + ['sm', '575.98px'], + ['md', '767.98px'], + ['lg', '991.98px'], + ['xl', '1199.98px'], + ['xxl', '1399.98px'], + ]; + + results.forEach(([key, value]) => { + expect(down(key)).toEqual(value); + }); }); - it('should throw an error when given the last breakpoint name', () => { - const LAST_BREAKPOINT_NAME = 'xxl'; - - expect(() => { - breakpointsApi.down(LAST_BREAKPOINT_NAME); - }).toThrow( - `${ERROR_PREFIX}\`${LAST_BREAKPOINT_NAME}\` doesn't have a maximum width. Use \`xl\`. See https://github.com/mg901/styled-breakpoints/issues/4 .` - ); + it('should throw exception if the last breakpoint is specified as the maximum value', () => { + try { + down('xxl'); + } catch (error) { + expect(error.message).toEqual( + `${ERROR_PREFIX}\`xxl\` doesn't have a maximum width. Use \`xl\`. See https://github.com/mg901/styled-breakpoints/issues/4 .` + ); + } }); }); - describe('between method', () => { - it('should throw an error for an invalid breakpoint names', () => { - expect(() => - breakpointsApi.between(INVALID_BREAKPOINT_NAME, 'sm') - ).toThrowError( - `${ERROR_PREFIX}breakpoint \`${INVALID_BREAKPOINT_NAME}\` not found in xs, sm, md, lg, xl, xxl.` - ); - - expect(() => - breakpointsApi.between('sm', INVALID_BREAKPOINT_NAME) - ).toThrowError( - `${ERROR_PREFIX}breakpoint \`${INVALID_BREAKPOINT_NAME}\` not found in xs, sm, md, lg, xl, xxl.` - ); + describe('between', () => { + it('should throw exception if the breakpoint name is not found', () => { + try { + between('wtf', 'md'); + } catch (error) { + expect(error.message).toEqual( + `${ERROR_PREFIX}breakpoint \`wtf\` not found in xs, sm, md, lg, xl, xxl.` + ); + } }); - it('should calculate the correct breakpoint range', () => { - // xs - expect(breakpointsApi.between('xs', 'sm')).toEqual({ - min: DEFAULT_BREAKPOINTS.xs, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.sm), - }); - - expect(breakpointsApi.between('xs', 'md')).toEqual({ - min: DEFAULT_BREAKPOINTS.xs, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.md), - }); - - expect(breakpointsApi.between('xs', 'lg')).toEqual({ - min: DEFAULT_BREAKPOINTS.xs, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.lg), - }); - - expect(breakpointsApi.between('xs', 'xl')).toEqual({ - min: DEFAULT_BREAKPOINTS.xs, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.xl), - }); - - // sm - expect(breakpointsApi.between('sm', 'md')).toEqual({ - min: DEFAULT_BREAKPOINTS.sm, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.md), - }); - - expect(breakpointsApi.between('sm', 'lg')).toEqual({ - min: DEFAULT_BREAKPOINTS.sm, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.lg), - }); - - expect(breakpointsApi.between('sm', 'xl')).toEqual({ - min: DEFAULT_BREAKPOINTS.sm, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.xl), - }); - - // md - expect(breakpointsApi.between('md', 'lg')).toEqual({ - min: DEFAULT_BREAKPOINTS.md, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.lg), - }); - - expect(breakpointsApi.between('md', 'xl')).toEqual({ - min: DEFAULT_BREAKPOINTS.md, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.xl), - }); - - // lg - expect(breakpointsApi.between('lg', 'xl')).toEqual({ - min: DEFAULT_BREAKPOINTS.lg, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.xl), - }); + it('should throw exception if the breakpoint value is zero ', () => { + try { + between('xs', 'sm'); + } catch (error) { + expect(error.message).toEqual( + `${ERROR_PREFIX}\`xs: 0px\` cannot be assigned as minimum breakpoint.` + ); + } }); - it('should throw an error when given the last breakpoint name as the second parameter', () => { - const LAST_BREAKPOINT_NAME = 'xxl'; - - expect(() => { - breakpointsApi.between('xs', LAST_BREAKPOINT_NAME); - }).toThrow( - `${ERROR_PREFIX}\`${LAST_BREAKPOINT_NAME}\` doesn't have a maximum width. Use \`xl\`. See https://github.com/mg901/styled-breakpoints/issues/4 .` - ); + it('return an object with the minimum and maximum screen width', () => { + expect(between('md', 'xl')).toEqual({ + max: '1199.98px', + min: '768px', + }); }); - it('should throw an error when the last breakpoint is equal 0', () => { - expect(() => breakpointsApi.between('xl', 'xs')).toThrow( - `${ERROR_PREFIX}\`xs: 0px\` cannot be assigned as minimum breakpoint.` - ); + it('should throw exception if the last breakpoint is specified as the maximum value', () => { + try { + between('xl', 'xxl'); + } catch (error) { + expect(error.message).toEqual( + `${ERROR_PREFIX}\`xxl\` doesn't have a maximum width. Use \`xl\`. See https://github.com/mg901/styled-breakpoints/issues/4 .` + ); + } }); }); - describe('only method', () => { - it('should throw an error for an invalid breakpoint name', () => { - expect(() => breakpointsApi.only(INVALID_BREAKPOINT_NAME)).toThrowError( - `${ERROR_PREFIX}breakpoint \`${INVALID_BREAKPOINT_NAME}\` not found in xs, sm, md, lg, xl, xxl.` - ); - }); - - it('should throw an error when given the last breakpoint name', () => { - const LAST_BREAKPOINT_NAME = 'xxl'; - - expect(() => { - breakpointsApi.only(LAST_BREAKPOINT_NAME); - }).toThrow( - `${ERROR_PREFIX}\`${LAST_BREAKPOINT_NAME}\` doesn't have a maximum width. Use \`xl\`. See https://github.com/mg901/styled-breakpoints/issues/4 .` - ); + describe('only', () => { + it('should throw exception if the breakpoint name is not found', () => { + try { + only('wtf'); + } catch (error) { + expect(error.message).toEqual( + `${ERROR_PREFIX}breakpoint \`wtf\` not found in xs, sm, md, lg, xl, xxl.` + ); + } }); - it('should return correct min and max values for given breakpoint name', () => { - expect(breakpointsApi.only('sm')).toEqual({ - min: DEFAULT_BREAKPOINTS.sm, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.md), - }); - - expect(breakpointsApi.only('md')).toEqual({ - min: DEFAULT_BREAKPOINTS.md, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.lg), - }); - - expect(breakpointsApi.only('lg')).toEqual({ - min: DEFAULT_BREAKPOINTS.lg, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.xl), + it('return an object with the minimum and maximum screen width', () => { + expect(only('md')).toEqual({ + max: '991.98px', + min: '768px', }); + }); - expect(breakpointsApi.only('xl')).toEqual({ - min: DEFAULT_BREAKPOINTS.xl, - max: calcMaxWidth(DEFAULT_BREAKPOINTS.xxl), - }); + it('should throw exception if the last breakpoint is specified as the maximum value', () => { + try { + only('xxl'); + } catch (error) { + expect(error.message).toEqual( + `${ERROR_PREFIX}\`xxl\` doesn't have a maximum width. Use \`xl\`. See https://github.com/mg901/styled-breakpoints/issues/4 .` + ); + } }); }); });