/
githubmirror
/
gutenberg
Обзор
Документация
Войти
/
githubmirror
/
gutenberg
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
trunk
packages/global-styles-engine/src/test/render.test.ts
1 996 строк
47 KB
tellthemachines
Render viewport state element styles in the editor (#81307)
07 авг 2026, 08:14
Не верифицирован
07 авг 2026, 08:14
e898723
Код
Авторство
О чём код?
import { getNodesWithStyles, getNodesWithSettings, generateCustomProperties, transformToStyles, getBlockSelectors, generateGlobalStyles, } from '../core/render'; import type { GlobalStylesConfig } from '../types'; import { ROOT_BLOCK_SELECTOR, ROOT_CSS_PROPERTIES_SELECTOR, } from '../utils/common'; // Mock WordPress data store jest.mock( '@wordpress/data', () => ( { select: jest.fn(), } ) ); // Mock WordPress blocks store jest.mock( '@wordpress/blocks', () => ( { __EXPERIMENTAL_STYLE_PROPERTY: { filter: { value: [ 'filter', 'duotone' ], support: [ 'filter', 'duotone' ], }, }, __EXPERIMENTAL_ELEMENTS: { link: 'a:where(:not(.wp-element-button))', heading: 'h1, h2, h3, h4, h5, h6', h1: 'h1', h2: 'h2', h3: 'h3', h4: 'h4', h5: 'h5', h6: 'h6', button: '.wp-element-button', caption: '.wp-element-caption', }, getBlockSupport: jest.fn(), getBlockTypes: jest.fn(), store: 'core/blocks', } ) ); // Mock WordPress elements (minimal, no mocking of complex APIs) const ELEMENTS = { link: 'a:where(:not(.wp-element-button))', h1: 'h1', h2: 'h2', h3: 'h3', h4: 'h4', h5: 'h5', h6: 'h6', button: '.wp-element-button', caption: '.wp-element-caption', }; describe( 'global styles renderer', () => { describe( 'getNodesWithStyles', () => { it( 'should return the nodes with styles', () => { const tree = { styles: { color: { background: 'red', text: 'red', }, blocks: { 'core/heading': { color: { background: 'blue', text: 'blue', }, elements: { h1: { typography: { fontSize: '42px', }, }, h2: { typography: { fontSize: '23px', }, }, link: { ':hover': { color: { background: 'green', text: 'yellow', }, }, ':focus': { color: { background: 'green', text: 'yellow', }, }, }, }, }, 'core/image': { border: { radius: '9999px', }, }, }, elements: { link: { color: { background: 'yellow', text: 'yellow', }, ':hover': { color: { background: 'hotpink', text: 'black', }, }, ':focus': { color: { background: 'hotpink', text: 'black', }, }, }, }, }, }; const blockSelectors = { 'core/heading': { selector: '.my-heading1, .my-heading2', }, 'core/image': { selector: '.my-image', featureSelectors: '.my-image img, .my-image .crop-area', }, }; expect( getNodesWithStyles( tree, blockSelectors ) ).toEqual( [ { styles: { color: { background: 'red', text: 'red', }, }, selector: ROOT_BLOCK_SELECTOR, skipSelectorWrapper: true, }, { styles: { color: { background: 'yellow', text: 'yellow', }, ':hover': { color: { background: 'hotpink', text: 'black', }, }, ':focus': { color: { background: 'hotpink', text: 'black', }, }, }, selector: ELEMENTS.link, elementName: 'link', skipSelectorWrapper: true, }, { styles: { color: { background: 'blue', text: 'blue', }, }, selector: '.my-heading1, .my-heading2', name: 'core/heading', }, { styles: { typography: { fontSize: '42px', }, }, selector: '.my-heading1 h1, .my-heading2 h1', elementName: 'h1', }, { styles: { typography: { fontSize: '23px', }, }, selector: '.my-heading1 h2, .my-heading2 h2', elementName: 'h2', }, { styles: { ':hover': { color: { background: 'green', text: 'yellow', }, }, ':focus': { color: { background: 'green', text: 'yellow', }, }, }, selector: '.my-heading1 a:where(:not(.wp-element-button)), .my-heading2 a:where(:not(.wp-element-button))', elementName: 'link', }, { styles: { border: { radius: '9999px', }, }, selector: '.my-image', name: 'core/image', featureSelectors: '.my-image img, .my-image .crop-area', }, ] ); } ); } ); describe( 'getNodesWithSettings', () => { it( 'should return nodes with settings', () => { const tree: GlobalStylesConfig = { styles: { color: { background: 'red', text: 'red', }, }, settings: { color: { palette: [ { name: 'White', slug: 'white', color: 'white' }, { name: 'Black', slug: 'black', color: 'black' }, ], }, blocks: { 'core/paragraph': { typography: { fontSizes: [ { name: 'small', slug: 'small', size: '12px', }, { name: 'medium', slug: 'medium', size: '23px', }, ], }, }, }, }, }; const blockSelectors = { 'core/paragraph': { selector: 'p', elements: { link: 'p a', h1: 'p h1', h2: 'p h2', h3: 'p h3', h4: 'p h4', h5: 'p h5', h6: 'p h6', }, }, }; expect( getNodesWithSettings( tree, blockSelectors ) ).toEqual( [ { presets: { color: { palette: [ { name: 'White', slug: 'white', color: 'white', }, { name: 'Black', slug: 'black', color: 'black', }, ], }, }, selector: ROOT_CSS_PROPERTIES_SELECTOR, }, { presets: { typography: { fontSizes: [ { name: 'small', slug: 'small', size: '12px', }, { name: 'medium', slug: 'medium', size: '23px', }, ], }, }, selector: 'p', }, ] ); } ); } ); describe( 'generateCustomProperties', () => { it( 'should return a ruleset', () => { const tree: GlobalStylesConfig = { settings: { color: { palette: { custom: [ { name: 'White', slug: 'white', color: 'white', }, { name: 'Black', slug: 'black', color: 'black', }, { name: 'White to Black', slug: 'white2black', color: 'value', }, ], }, }, custom: { white2black: 'value', 'font-primary': 'value', 'line-height': { body: 1.7, heading: 1.3, }, }, blocks: { 'core/heading': { typography: { fontSizes: { theme: [ { name: 'small', slug: 'small', size: '12px', }, { name: 'medium', slug: 'medium', size: '23px', }, ], }, }, }, }, }, }; const blockSelectors = { 'core/heading': { selector: 'h1,h2,h3,h4,h5,h6', }, }; expect( generateCustomProperties( tree, blockSelectors ) ).toEqual( ':root{--wp--preset--color--white: white;--wp--preset--color--black: black;--wp--preset--color--white-2-black: value;--wp--custom--white-2-black: value;--wp--custom--font-primary: value;--wp--custom--line-height--body: 1.7;--wp--custom--line-height--heading: 1.3;}h1,h2,h3,h4,h5,h6{--wp--preset--font-size--small: 12px;--wp--preset--font-size--medium: 23px;}' ); } ); } ); describe( 'transformToStyles', () => { const minimalStyleOptions = { blockGap: false, blockStyles: true, layoutStyles: false, marginReset: false, presets: false, rootPadding: false, }; it( 'should return a ruleset', () => { const tree = { settings: { color: { palette: { default: [ { name: 'White', slug: 'white', color: 'white', }, { name: 'Black', slug: 'black', color: 'black', }, ], }, }, blocks: { 'core/heading': { color: { palette: { default: [ { name: 'Blue', slug: 'blue', color: 'blue', }, ], }, }, }, }, }, styles: { spacing: { margin: '10px', padding: '10px', }, color: { background: 'red', }, elements: { h1: { typography: { fontSize: '42px', }, }, link: { color: { text: 'blue', }, ':hover': { color: { text: 'orange', }, }, ':focus': { color: { text: 'orange', }, }, }, }, blocks: { 'core/group': { spacing: { margin: { top: '10px', right: '20px', bottom: '30px', left: '40px', }, padding: { top: '11px', right: '22px', bottom: '33px', left: '44px', }, }, }, 'core/heading': { color: { text: 'orange', }, elements: { link: { color: { text: 'hotpink', }, ':hover': { color: { text: 'red', }, }, ':focus': { color: { text: 'red', }, }, }, }, }, 'core/image': { color: { text: 'red', }, border: { radius: '9999px', }, }, }, }, }; const blockSelectors = { 'core/group': { selector: '.wp-block-group', }, 'core/heading': { selector: 'h1,h2,h3,h4,h5,h6', }, 'core/image': { selector: '.wp-block-image', featureSelectors: { border: '.wp-block-image img, .wp-block-image .wp-crop-area', }, }, }; expect( transformToStyles( tree, blockSelectors ) ).toEqual( ':where(body) {margin: 0;}.is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }.is-layout-constrained > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-constrained > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-constrained > .aligncenter { margin-left: auto !important; margin-right: auto !important; }.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)) { max-width: var(--wp--style--global--content-size); margin-left: auto !important; margin-right: auto !important; }.is-layout-constrained > .alignwide { max-width: var(--wp--style--global--wide-size); }body .is-layout-flex { display:flex; }.is-layout-flex { flex-wrap: wrap; align-items: center; }.is-layout-flex > :is(*, div) { margin: 0; }body .is-layout-grid { display:grid; }.is-layout-grid > :is(*, div) { margin: 0; }body{background-color: red;margin: 10px;padding: 10px;}a:where(:not(.wp-element-button)){color: blue;}:root :where(a:where(:not(.wp-element-button)):hover){color: orange;}:root :where(a:where(:not(.wp-element-button)):focus){color: orange;}h1{font-size: 42px;}:root :where(.wp-block-group){margin-top: 10px;margin-right: 20px;margin-bottom: 30px;margin-left: 40px;padding-top: 11px;padding-right: 22px;padding-bottom: 33px;padding-left: 44px;}:root :where(h1,h2,h3,h4,h5,h6){color: orange;}:root :where(h1 a:where(:not(.wp-element-button)),h2 a:where(:not(.wp-element-button)),h3 a:where(:not(.wp-element-button)),h4 a:where(:not(.wp-element-button)),h5 a:where(:not(.wp-element-button)),h6 a:where(:not(.wp-element-button))){color: hotpink;}:root :where(h1 a:where(:not(.wp-element-button)):hover,h2 a:where(:not(.wp-element-button)):hover,h3 a:where(:not(.wp-element-button)):hover,h4 a:where(:not(.wp-element-button)):hover,h5 a:where(:not(.wp-element-button)):hover,h6 a:where(:not(.wp-element-button)):hover){color: red;}:root :where(h1 a:where(:not(.wp-element-button)):focus,h2 a:where(:not(.wp-element-button)):focus,h3 a:where(:not(.wp-element-button)):focus,h4 a:where(:not(.wp-element-button)):focus,h5 a:where(:not(.wp-element-button)):focus,h6 a:where(:not(.wp-element-button)):focus){color: red;}:root :where(.wp-block-image img, .wp-block-image .wp-crop-area){border-radius: 9999px;}:root :where(.wp-block-image){color: red;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.has-white-color{color: var(--wp--preset--color--white) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}:where(h1,h2,h3,h4,h5,h6).has-blue-color{color: var(--wp--preset--color--blue) !important;}:where(h1,h2,h3,h4,h5,h6).has-blue-background-color{background-color: var(--wp--preset--color--blue) !important;}:where(h1,h2,h3,h4,h5,h6).has-blue-border-color{border-color: var(--wp--preset--color--blue) !important;}' ); } ); it( 'should handle feature selectors', () => { const tree = { styles: { blocks: { 'core/image': { color: { text: 'red', }, spacing: { padding: { top: '1px', }, }, border: { color: 'red', radius: '9999px', }, }, }, }, }; const blockSelectors = { 'core/image': { selector: '.wp-image', featureSelectors: { spacing: '.wp-image-spacing', border: { root: '.wp-image-border', color: '.wp-image-border-color', }, }, }, }; expect( transformToStyles( Object.freeze( tree ), blockSelectors ) ).toEqual( ':where(body) {margin: 0;}.is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }.is-layout-constrained > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-constrained > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-constrained > .aligncenter { margin-left: auto !important; margin-right: auto !important; }.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)) { max-width: var(--wp--style--global--content-size); margin-left: auto !important; margin-right: auto !important; }.is-layout-constrained > .alignwide { max-width: var(--wp--style--global--wide-size); }body .is-layout-flex { display:flex; }.is-layout-flex { flex-wrap: wrap; align-items: center; }.is-layout-flex > :is(*, div) { margin: 0; }body .is-layout-grid { display:grid; }.is-layout-grid > :is(*, div) { margin: 0; }:root :where(.wp-image-spacing){padding-top: 1px;}:root :where(.wp-image-border-color){border-color: red;}:root :where(.wp-image-border){border-radius: 9999px;}:root :where(.wp-image){color: red;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' ); } ); it( 'should handle block variations', () => { const tree: GlobalStylesConfig = { styles: { blocks: { 'core/image': { variations: { foo: { color: { text: 'blue', }, spacing: { padding: { top: '2px', }, }, border: { color: 'blue', }, }, }, }, }, }, }; const blockSelectors = { 'core/image': { selector: '.wp-image', featureSelectors: { spacing: '.wp-image-spacing', border: { root: '.wp-image-border', color: '.wp-image-border-color', }, }, styleVariationSelectors: { foo: '.is-style-foo.wp-image', }, }, }; const hasBlockGapSupport = false; const hasFallbackGapSupport = true; const disableLayoutStyles = true; const disableRootPadding = true; const styleOptions: Record< string, boolean > = { blockGap: false, blockStyles: true, layoutStyles: false, marginReset: false, presets: false, rootPadding: false, }; // Confirm no variation styles by default. const withoutVariations = transformToStyles( Object.freeze( tree ), blockSelectors, hasBlockGapSupport, hasFallbackGapSupport, disableLayoutStyles, disableRootPadding, styleOptions ); expect( withoutVariations ).toEqual( '' ); // Includes variation styles when requested. styleOptions.variationStyles = true; const withVariations = transformToStyles( Object.freeze( tree ), blockSelectors, hasBlockGapSupport, hasFallbackGapSupport, disableLayoutStyles, disableRootPadding, styleOptions ); expect( withVariations ).toEqual( ':root :where(.wp-image-spacing.is-style-foo){padding-top: 2px;}:root :where(.wp-image-border-color.is-style-foo){border-color: blue;}:root :where(.is-style-foo.wp-image){color: blue;}' ); } ); it( 'handles variation inner block states', () => { const tree = { styles: { blocks: { 'core/group': { variations: { foo: { blocks: { 'core/button': { color: { text: 'red', }, ':hover': { color: { text: 'blue', }, }, '@mobile': { color: { text: 'green', }, ':hover': { color: { text: 'yellow', }, }, }, }, }, }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/group': { selector: '.wp-block-group', styleVariationSelectors: { foo: '.is-style-foo.wp-block-group', }, }, 'core/button': { selector: '.wp-block-button', }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, { ...minimalStyleOptions, variationStyles: true, } ); expect( result ).toEqual( ':root :where(.is-style-foo.wp-block-group .wp-block-button){color: red;}@media (width <= 480px){:root :where(.is-style-foo.wp-block-group .wp-block-button){color: green;}}:root :where(.is-style-foo.wp-block-group .wp-block-button:hover){color: blue;}@media (width <= 480px){:root :where(.is-style-foo.wp-block-group .wp-block-button:hover){color: yellow;}}' ); } ); it( 'should handle block pseudo selectors', () => { const tree = { styles: { blocks: { 'core/button': { color: { text: 'red', }, ':hover': { color: { text: 'blue', }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/button': { selector: '.wp-block-button', }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, { blockGap: false, blockStyles: true, layoutStyles: false, marginReset: false, presets: false, rootPadding: false, } ); expect( result ).toEqual( ':root :where(.wp-block-button){color: red;}:root :where(.wp-block-button:hover){color: blue;}' ); } ); it( 'outputs default pseudo styles after responsive base styles', () => { const tree = { styles: { blocks: { 'core/button': { color: { text: 'black', }, ':hover': { color: { text: 'blue', }, }, '@mobile': { color: { text: 'red', }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/button': { selector: '.wp-block-button', }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, minimalStyleOptions ); expect( result ).toEqual( ':root :where(.wp-block-button){color: black;}@media (width <= 480px){:root :where(.wp-block-button){color: red;}}:root :where(.wp-block-button:hover){color: blue;}' ); } ); it( 'ignores root-level state styles', () => { const tree = { styles: { color: { text: 'red', }, ':hover': { color: { text: 'blue', }, }, '@mobile': { color: { text: 'green', }, ':hover': { color: { text: 'yellow', }, }, }, }, } as unknown as GlobalStylesConfig; const result = transformToStyles( Object.freeze( tree ), {}, false, false, true, true, minimalStyleOptions ); expect( result ).toEqual( 'body{color: red;}' ); } ); it( 'should handle style variation pseudo selectors', () => { const tree = { styles: { blocks: { 'core/button': { variations: { foo: { color: { text: 'green', }, dimensions: { width: '10rem', }, ':hover': { color: { text: 'yellow', }, dimensions: { width: '20rem', }, }, }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/button': { selector: '.wp-block-button .wp-block-button__link', featureSelectors: { dimensions: { root: '.wp-block-button', width: '.wp-block-button', }, }, styleVariationSelectors: { foo: '.wp-block-button.is-style-foo .wp-block-button__link', }, }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, { blockGap: false, blockStyles: true, layoutStyles: false, marginReset: false, presets: false, rootPadding: false, variationStyles: true, } ); expect( result ).toEqual( ':root :where(.wp-block-button.is-style-foo){width: 10rem;}:root :where(.wp-block-button.is-style-foo .wp-block-button__link){color: green;}:root :where(.wp-block-button.is-style-foo:hover){width: 20rem;}:root :where(.wp-block-button.is-style-foo .wp-block-button__link:hover){color: yellow;}' ); } ); it( 'outputs variation responsive pseudo styles after default pseudo styles', () => { const tree = { styles: { blocks: { 'core/button': { variations: { foo: { color: { text: 'green', }, dimensions: { width: '10rem', }, ':hover': { color: { text: 'blue', }, dimensions: { width: '20rem', }, }, '@tablet': { color: { text: 'red', }, dimensions: { width: '30rem', }, ':hover': { color: { text: 'orange', }, dimensions: { width: '40rem', }, }, }, }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/button': { selector: '.wp-block-button .wp-block-button__link', featureSelectors: { dimensions: { root: '.wp-block-button', width: '.wp-block-button', }, }, styleVariationSelectors: { foo: '.wp-block-button.is-style-foo .wp-block-button__link', }, }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, { ...minimalStyleOptions, variationStyles: true, } ); expect( result ).toEqual( ':root :where(.wp-block-button.is-style-foo){width: 10rem;}:root :where(.wp-block-button.is-style-foo .wp-block-button__link){color: green;}@media (480px < width <= 782px){:root :where(.wp-block-button.is-style-foo){width: 30rem;}:root :where(.wp-block-button.is-style-foo .wp-block-button__link){color: red;}}:root :where(.wp-block-button.is-style-foo:hover){width: 20rem;}:root :where(.wp-block-button.is-style-foo .wp-block-button__link:hover){color: blue;}@media (480px < width <= 782px){:root :where(.wp-block-button.is-style-foo:hover){width: 40rem;}:root :where(.wp-block-button.is-style-foo .wp-block-button__link:hover){color: orange;}}' ); } ); it( 'handles responsive block styles', () => { const tree = { styles: { blocks: { 'core/button': { color: { text: 'red', }, '@mobile': { color: { text: 'blue', }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/button': { selector: '.wp-block-button', }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, minimalStyleOptions ); expect( result ).toEqual( ':root :where(.wp-block-button){color: red;}@media (width <= 480px){:root :where(.wp-block-button){color: blue;}}' ); } ); it( 'handles responsive block gap styles', () => { const tree = { styles: { blocks: { 'core/group': { '@mobile': { spacing: { blockGap: '24px', }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/group': { selector: '.wp-block-group', hasLayoutSupport: true, }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, true, false, false, true, minimalStyleOptions ); expect( result ).toContain( '@media (width <= 480px)' ); expect( result ).toContain( ':root :where(.wp-block-group-is-layout-flow) > * { margin-block-start: 24px; margin-block-end: 0; }' ); expect( result ).toContain( ':root :where(.wp-block-group-is-layout-flex) { gap: 24px; }' ); } ); it( 'handles legacy responsive block styles', () => { const tree = { styles: { blocks: { 'core/button': { color: { text: 'red', }, mobile: { color: { text: 'blue', }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/button': { selector: '.wp-block-button', }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, minimalStyleOptions ); expect( result ).toEqual( ':root :where(.wp-block-button){color: red;}@media (width <= 480px){:root :where(.wp-block-button){color: blue;}}' ); } ); it( 'uses custom viewport breakpoints for responsive block styles', () => { const tree = { settings: { viewport: { mobile: '640px', tablet: '960px', }, }, styles: { blocks: { 'core/button': { '@mobile': { color: { text: 'blue', }, }, '@tablet': { color: { text: 'green', }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/button': { selector: '.wp-block-button', }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, minimalStyleOptions ); expect( result ).toEqual( '@media (width <= 640px){:root :where(.wp-block-button){color: blue;}}@media (640px < width <= 960px){:root :where(.wp-block-button){color: green;}}' ); } ); it( 'omits tablet responsive styles when the tablet breakpoint is not larger than mobile', () => { const tree = { settings: { viewport: { mobile: '960px', tablet: '640px', }, }, styles: { blocks: { 'core/button': { '@mobile': { color: { text: 'blue', }, }, '@tablet': { color: { text: 'green', }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/button': { selector: '.wp-block-button', }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, minimalStyleOptions ); expect( result ).toEqual( '@media (width <= 960px){:root :where(.wp-block-button){color: blue;}}' ); } ); it( 'uses a single max-width tablet query when only the tablet breakpoint is valid', () => { const tree = { settings: { viewport: { mobile: '100%', tablet: '64rem', }, }, styles: { blocks: { 'core/button': { '@mobile': { color: { text: 'blue', }, }, '@tablet': { color: { text: 'green', }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/button': { selector: '.wp-block-button', }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, minimalStyleOptions ); expect( result ).toEqual( '@media (width <= 64rem){:root :where(.wp-block-button){color: green;}}' ); } ); it( 'handles responsive pseudo selector styles', () => { const tree = { styles: { blocks: { 'core/button': { ':hover': { color: { text: 'blue', }, }, '@mobile': { color: { text: 'red', }, ':hover': { color: { text: 'orange', }, }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/button': { selector: '.wp-block-button', }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, minimalStyleOptions ); expect( result ).toEqual( '@media (width <= 480px){:root :where(.wp-block-button){color: red;}}:root :where(.wp-block-button:hover){color: blue;}@media (width <= 480px){:root :where(.wp-block-button:hover){color: orange;}}' ); } ); it( 'handles responsive feature selector styles', () => { const tree = { settings: {}, styles: { blocks: { 'core/button': { dimensions: { width: '25%', }, '@mobile': { dimensions: { width: '50%', }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/button': { selector: '.wp-block-button .wp-block-button__link', featureSelectors: { dimensions: { root: '.wp-block-button', width: '.wp-block-button', }, }, }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, minimalStyleOptions ); expect( result ).toEqual( ':root :where(.wp-block-button){width: calc(25 * 1% - (var(--wp--style--block-gap, 0.5em) * (1 - 25 / 100)));}@media (width <= 480px){:root :where(.wp-block-button){width: calc(50 * 1% - (var(--wp--style--block-gap, 0.5em) * (1 - 50 / 100)));}}' ); } ); it( 'handles responsive element styles', () => { const tree = { styles: { elements: { link: { color: { text: 'blue', }, '@mobile': { color: { text: 'red', }, ':hover': { color: { text: 'orange', }, }, }, }, }, }, } as unknown as GlobalStylesConfig; const result = transformToStyles( Object.freeze( tree ), {}, false, false, true, true, minimalStyleOptions ); expect( result ).toEqual( 'a:where(:not(.wp-element-button)){color: blue;}@media (width <= 480px){:root :where(a:where(:not(.wp-element-button))){color: red;}}@media (width <= 480px){:root :where(a:where(:not(.wp-element-button)):hover){color: orange;}}' ); } ); it( 'renders block element styles defined only in a viewport', () => { const tree = { styles: { blocks: { 'core/group': { '@mobile': { elements: { heading: { color: { text: 'red', }, }, }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/group': { selector: '.wp-block-group', }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, minimalStyleOptions ); expect( result ).toEqual( '@media (width <= 480px){:root :where(.wp-block-group h1,.wp-block-group h2,.wp-block-group h3,.wp-block-group h4,.wp-block-group h5,.wp-block-group h6){color: red;}}' ); } ); it( 'renders block element pseudo styles defined only in a viewport', () => { const tree = { styles: { blocks: { 'core/group': { '@mobile': { elements: { link: { ':hover': { color: { text: 'red', }, }, }, }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/group': { selector: '.wp-block-group', }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, minimalStyleOptions ); expect( result ).toEqual( '@media (width <= 480px){:root :where(.wp-block-group a:where(:not(.wp-element-button)):hover){color: red;}}' ); } ); it( 'renders block element styles defined in separate viewports', () => { const tree = { styles: { blocks: { 'core/group': { '@mobile': { elements: { link: { color: { text: 'red', }, }, }, }, '@tablet': { elements: { link: { color: { text: 'blue', }, }, }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/group': { selector: '.wp-block-group', }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, minimalStyleOptions ); expect( result ).toEqual( '@media (width <= 480px){:root :where(.wp-block-group a:where(:not(.wp-element-button))){color: red;}}@media (480px < width <= 782px){:root :where(.wp-block-group a:where(:not(.wp-element-button))){color: blue;}}' ); } ); it( 'handles responsive style variation styles', () => { const tree = { styles: { blocks: { 'core/button': { variations: { foo: { color: { text: 'green', }, '@mobile': { color: { text: 'yellow', }, }, }, }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/button': { selector: '.wp-block-button', styleVariationSelectors: { foo: '.is-style-foo.wp-block-button', }, }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors, false, false, true, true, { ...minimalStyleOptions, variationStyles: true, } ); expect( result ).toEqual( ':root :where(.is-style-foo.wp-block-button){color: green;}@media (width <= 480px){:root :where(.is-style-foo.wp-block-button){color: yellow;}}' ); } ); it( 'should handle duotone filter', () => { const tree = { styles: { blocks: { 'core/image': { filter: { duotone: 'blur(5px)', }, }, }, }, }; const blockSelectors = { 'core/image': { selector: '.wp-image', duotoneSelector: '.wp-image img', }, }; expect( transformToStyles( Object.freeze( tree ), blockSelectors ) ).toEqual( ':where(body) {margin: 0;}.is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }.is-layout-constrained > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-constrained > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-constrained > .aligncenter { margin-left: auto !important; margin-right: auto !important; }.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)) { max-width: var(--wp--style--global--content-size); margin-left: auto !important; margin-right: auto !important; }.is-layout-constrained > .alignwide { max-width: var(--wp--style--global--wide-size); }body .is-layout-flex { display:flex; }.is-layout-flex { flex-wrap: wrap; align-items: center; }.is-layout-flex > :is(*, div) { margin: 0; }body .is-layout-grid { display:grid; }.is-layout-grid > :is(*, div) { margin: 0; }.wp-image img{filter: blur(5px);}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' ); } ); it( 'should output content and wide size variables if values are specified', () => { const tree = { settings: { layout: { contentSize: '840px', wideSize: '1100px', }, }, }; expect( transformToStyles( Object.freeze( tree ), 'body' ) ).toEqual( ':root { --wp--style--global--content-size: 840px; --wp--style--global--wide-size: 1100px;}:where(body) {margin: 0;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' ); } ); } ); describe( 'generateGlobalStyles', () => { beforeEach( () => { jest.clearAllMocks(); const mockSelect = require( '@wordpress/data' ).select as jest.Mock; mockSelect.mockReturnValue( { getBlockStyles: () => [], } ); } ); it( 'should use css feature selector for block custom CSS when defined', () => { const config = { version: 3, settings: {}, styles: { blocks: { 'core/paragraph': { css: 'color:red;', }, }, }, }; const blockTypes = [ { name: 'core/paragraph', selectors: { root: 'p', css: '.custom-p', }, }, ]; const [ styles ] = generateGlobalStyles( config, blockTypes ); const customCssStylesheet = styles.find( ( s: any ) => s.css && s.css.includes( 'color:red;' ) ); expect( customCssStylesheet ).toBeDefined(); expect( customCssStylesheet.css ).toContain( ':root :where(.custom-p){color:red;}' ); expect( customCssStylesheet.css ).not.toContain( ':root :where(p){color:red;}' ); } ); it( 'should use css feature selector object form with root subkey for block custom CSS', () => { const config = { version: 3, settings: {}, styles: { blocks: { 'core/paragraph': { css: 'color:red;', }, }, }, }; const blockTypes = [ { name: 'core/paragraph', selectors: { root: 'p', css: { root: '.custom-p' }, }, }, ]; const [ styles ] = generateGlobalStyles( config, blockTypes ); const customCssStylesheet = styles.find( ( s: any ) => s.css && s.css.includes( 'color:red;' ) ); expect( customCssStylesheet ).toBeDefined(); expect( customCssStylesheet.css ).toContain( ':root :where(.custom-p){color:red;}' ); expect( customCssStylesheet.css ).not.toContain( ':root :where(p){color:red;}' ); } ); it( 'should fall back to root selector for block custom CSS when no css feature selector is defined', () => { const config = { version: 3, settings: {}, styles: { blocks: { 'core/paragraph': { css: 'color:red;', }, }, }, }; const blockTypes = [ { name: 'core/paragraph', selectors: { root: 'p', }, }, ]; const [ styles ] = generateGlobalStyles( config, blockTypes ); const customCssStylesheet = styles.find( ( s: any ) => s.css && s.css.includes( 'color:red;' ) ); expect( customCssStylesheet ).toBeDefined(); expect( customCssStylesheet.css ).toContain( ':root :where(p){color:red;}' ); } ); it( 'should output duotone SVG filters with __unstableType of svgs', () => { const mockSelect = require( '@wordpress/data' ).select as jest.Mock; mockSelect.mockReturnValue( { getBlockStyles: () => [], } ); const config = { version: 3, settings: { color: { duotone: { theme: [ { slug: 'midnight', name: 'Midnight', colors: [ '#263135', '#69a8a7' ], }, ], }, }, }, styles: {}, }; const blockTypes = [ { name: 'core/image', selectors: { root: '.wp-block-image', filter: { duotone: '.wp-block-image img' }, }, }, ]; const [ styles ] = generateGlobalStyles( config, blockTypes ); const svgStyle = styles.find( ( s: any ) => s.__unstableType === 'svgs' ); expect( svgStyle ).toBeDefined(); expect( svgStyle.__unstableType ).toBe( 'svgs' ); expect( svgStyle.assets.join( '' ) ).toContain( 'wp-duotone-midnight' ); } ); } ); describe( 'getBlockSelectors', () => { beforeEach( () => { // Reset mocks before each test jest.clearAllMocks(); } ); it( 'should return block selectors data', () => { // Mock the select function to return getBlockStyles const mockSelect = require( '@wordpress/data' ).select as jest.Mock; mockSelect.mockReturnValue( { getBlockStyles: () => [ { name: 'foo', label: 'foo' } ], } ); const imageSelectors = { root: '.my-image', border: '.my-image img, .my-image .crop-area', filter: { duotone: 'img' }, }; const imageBlock = { name: 'core/image', selectors: imageSelectors, title: 'My Image', category: 'media', }; const blockTypes = [ imageBlock ]; expect( getBlockSelectors( blockTypes ) ).toEqual( { 'core/image': { name: imageBlock.name, selector: imageSelectors.root, duotoneSelector: imageSelectors.filter.duotone, fallbackGapValue: undefined, featureSelectors: { root: '.my-image', border: '.my-image img, .my-image .crop-area', filter: { duotone: 'img' }, }, styleVariationSelectors: { foo: '.my-image.is-style-foo', }, hasLayoutSupport: false, }, } ); } ); it( 'should return block selectors data with old experimental selectors', () => { // Mock the select function to return getBlockStyles with empty array const mockSelect = require( '@wordpress/data' ).select as jest.Mock; mockSelect.mockReturnValue( { getBlockStyles: () => [], } ); // Mock getBlockSupport to handle experimental duotone support const mockGetBlockSupport = require( '@wordpress/blocks' ) .getBlockSupport as jest.Mock; mockGetBlockSupport.mockImplementation( ( blockType, path, defaultValue ) => { if ( path === 'color.__experimentalDuotone' ) { return 'img'; } return defaultValue; } ); const imageSupports = { __experimentalBorder: { radius: true, __experimentalSelector: 'img, .crop-area', }, color: { __experimentalDuotone: 'img', }, __experimentalSelector: '.my-image', }; const imageBlock = { name: 'core/image', supports: imageSupports, title: 'My Image', category: 'media', }; const blockTypes = [ imageBlock ]; expect( getBlockSelectors( blockTypes ) ).toEqual( { 'core/image': { name: imageBlock.name, selector: imageSupports.__experimentalSelector, duotoneSelector: '.my-image img', fallbackGapValue: undefined, featureSelectors: { root: '.my-image', border: '.my-image img, .my-image .crop-area', }, hasLayoutSupport: false, }, } ); } ); } ); describe( 'button width declarations', () => { it( 'should convert direct percentage width to calc() formula', () => { const tree: GlobalStylesConfig = { settings: {}, styles: { blocks: { 'core/button': { dimensions: { width: '25%', }, }, }, }, }; const blockSelectors = { 'core/button': { selector: '.wp-block-button .wp-block-button__link', featureSelectors: { dimensions: { root: '.wp-block-button', width: '.wp-block-button', }, }, }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors ); expect( result ).toContain( ':root :where(.wp-block-button){width: calc(25 * 1% - (var(--wp--style--block-gap, 0.5em) * (1 - 25 / 100)));}' ); } ); it( 'should convert preset percentage width to calc() formula', () => { const tree = { settings: { blocks: { 'core/button': { dimensions: { dimensionSizes: { default: [ { slug: '50', name: '50%', size: '50%', }, ], }, }, }, }, }, styles: { blocks: { 'core/button': { dimensions: { width: 'var:preset|dimension|50', }, }, }, }, } as unknown as GlobalStylesConfig; const blockSelectors = { 'core/button': { selector: '.wp-block-button .wp-block-button__link', featureSelectors: { dimensions: { root: '.wp-block-button', width: '.wp-block-button', }, }, }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors ); expect( result ).toContain( ':root :where(.wp-block-button){width: calc(50 * 1% - (var(--wp--style--block-gap, 0.5em) * (1 - 50 / 100)));}' ); } ); it( 'should not convert non-percentage width', () => { const tree: GlobalStylesConfig = { settings: {}, styles: { blocks: { 'core/button': { dimensions: { width: '200px', }, }, }, }, }; const blockSelectors = { 'core/button': { selector: '.wp-block-button .wp-block-button__link', featureSelectors: { dimensions: { root: '.wp-block-button', width: '.wp-block-button', }, }, }, }; const result = transformToStyles( Object.freeze( tree ), blockSelectors ); expect( result ).toContain( ':root :where(.wp-block-button){width: 200px;}' ); } ); } ); } );