Files
gui/pkgs/core/core-test/getSplitStyles.web.test.tsx
zeekay 5c8c108412 rtl: logical layout in gui
RN-style start/end props (paddingStart, start, borderTopStartRadius) emitted
padding-start and start: on web, which no browser parses; they map to CSS
logical properties now, and inline logical longhands outrank the physical
sides padding and borderRadius expand to. On native, inline insets land on
start/end and textAlign start/end on the left/right RN swaps under RTL.

useDirection falls back to <html dir> and follows changes, so slider, tabs,
menus, roving focus, switch and progress read the direction i18n sets. The
menu no longer asks for bottom-end under RTL, which floating-ui flipped back
to the left. Group joins, list item icon gaps, select and menu adornments and
every shell side are logical; directional marks mirror via data-i18n-mirror.
2026-10-22 00:52:00 -07:00

855 lines
26 KiB
TypeScript

import { beforeAll, describe, expect, test, vi } from 'vitest'
import config from '../config-default'
import {
View,
StyleObjectProperty,
StyleObjectRules,
StyleObjectValue,
Text,
createGui,
styled,
} from '../web/src'
import { getGroupPropParts } from '../web/src/helpers/getGroupPropParts'
import { getSplitStyles } from '../web/src'
import { defaultComponentState } from '../web/src/defaultComponentState'
import { simplifiedGetSplitStyles } from './utils'
beforeAll(() => {
createGui(config.getDefaultGuiConfig())
})
describe('getSplitStyles', () => {
test(`styled with variants`, () => {
const ViewVariants = styled(Text, {
color: 'blue',
variants: {
test: {
true: {
color: 'red',
},
},
},
})
const styles = simplifiedGetSplitStyles(ViewVariants, {
test: true,
})
expect(styles.classNames).toEqual({ color: '_col-red' })
})
test(`prop "aria-required" is passed through`, () => {
const { viewProps } = simplifiedGetSplitStyles(
View,
{
'aria-required': true,
},
{
render: 'input',
}
)
expect(viewProps['aria-required']).toEqual(true)
})
test(`prop "paddingStart" value 10 becomes "10px"`, () => {
const out = simplifiedGetSplitStyles(
View,
{
paddingStart: 10,
},
{
render: 'input',
}
)
expect(Object.values(out.rulesToInsert)[0]?.[StyleObjectValue]).toEqual('10px')
})
test(`font props get the font family, regardless of the order`, () => {
const styles = simplifiedGetSplitStyles(Text, {
fontSize: '$1',
}).rulesToInsert
expect(
Object.values(styles).find((rule) => rule[StyleObjectProperty] === 'fontSize')?.[
StyleObjectValue
]
).toEqual('var(--f-size-1)') // no family provided - this is expected
expect(
Object.values(
simplifiedGetSplitStyles(Text, {
fontSize: '$1',
fontFamily: '$body',
}).rulesToInsert
).find((rule) => rule[StyleObjectProperty] === 'fontSize')?.[StyleObjectValue]
).toEqual('var(--f-size-1)')
expect(
Object.values(
simplifiedGetSplitStyles(Text, {
fontFamily: '$body',
fontSize: '$1',
}).rulesToInsert
).find((rule) => rule[StyleObjectProperty] === 'fontSize')?.[StyleObjectValue]
).toEqual('var(--f-size-1)')
})
test(`font props get the font family from a variant, regardless of the order`, () => {
const CustomText = styled(Text, {
variants: {
type: {
myValue: {
fontFamily: '$body',
},
},
} as const,
})
expect(
Object.values(
simplifiedGetSplitStyles(CustomText, {
fontSize: '$1',
type: 'myValue',
}).rulesToInsert
).find((rule) => rule[StyleObjectProperty] === 'fontSize')?.[StyleObjectValue]
).toEqual('var(--f-size-1)')
expect(
Object.values(
simplifiedGetSplitStyles(CustomText, {
type: 'myValue',
fontSize: '$1',
}).rulesToInsert
).find((rule) => rule[StyleObjectProperty] === 'fontSize')?.[StyleObjectValue]
).toEqual('var(--f-size-1)')
})
test(`$theme-light and $theme-dark styles generate the correct CSS selectors`, () => {
// Test light theme styles
const lightThemeStyles = simplifiedGetSplitStyles(View, {
'$theme-light': {
backgroundColor: 'white',
color: 'black',
},
})
// Check the entire structure for expected values
const lightThemeString = JSON.stringify(lightThemeStyles.rulesToInsert)
expect(lightThemeString).toContain('backgroundColor')
expect(lightThemeString).toContain('white')
expect(lightThemeString).toContain('light')
// If possible, find the rule for the light theme
const lightBgRule = Object.values(lightThemeStyles.rulesToInsert).find(
(rule) =>
rule[StyleObjectProperty] === 'backgroundColor' &&
rule[StyleObjectRules]?.[0]?.includes('light')
)
// Rule might exist in a different format
expect(lightBgRule || lightThemeString.includes('white')).toBeTruthy()
// Test dark theme styles
const darkThemeStyles = simplifiedGetSplitStyles(View, {
'$theme-dark': {
backgroundColor: 'black',
color: 'white',
},
})
// Check the entire structure for expected values
const darkThemeString = JSON.stringify(darkThemeStyles.rulesToInsert)
expect(darkThemeString).toContain('backgroundColor')
expect(darkThemeString).toContain('black')
expect(darkThemeString).toContain('dark')
// If possible, find the rule for the dark theme
const darkBgRule = Object.values(darkThemeStyles.rulesToInsert).find(
(rule) =>
rule[StyleObjectProperty] === 'backgroundColor' &&
rule[StyleObjectRules]?.[0]?.includes('dark')
)
// Rule might exist in a different format
expect(darkBgRule || darkThemeString.includes('black')).toBeTruthy()
})
test(`$theme-light and $theme-dark styles are combined in the same component`, () => {
// Test both light and dark theme styles in the same component
const combinedThemeStyles = simplifiedGetSplitStyles(View, {
'$theme-light': {
backgroundColor: 'white',
color: 'black',
},
'$theme-dark': {
backgroundColor: 'black',
color: 'white',
},
})
// Check the entire structure for expected values
const combinedThemeString = JSON.stringify(combinedThemeStyles.rulesToInsert)
expect(combinedThemeString).toContain('backgroundColor')
expect(combinedThemeString).toContain('white')
expect(combinedThemeString).toContain('black')
expect(combinedThemeString).toContain('light')
expect(combinedThemeString).toContain('dark')
})
test(`$theme conditional styles work with nested theme names`, () => {
// Test more specific theme names like dark_blue
const nestedThemeStyles = simplifiedGetSplitStyles(View, {
'$theme-dark_blue': {
backgroundColor: 'darkblue',
color: 'lightblue',
},
})
// Check the entire structure for expected values
const nestedThemeString = JSON.stringify(nestedThemeStyles.rulesToInsert)
expect(nestedThemeString).toContain('backgroundColor')
expect(nestedThemeString).toContain('darkblue')
expect(nestedThemeString).toContain('dark_blue')
})
test(`$theme-dark de-opts to inline style with noClass animation driver`, () => {
// when using an inline animation driver (noClass: true), $theme-dark should
// de-opt to inline styles rather than CSS classes, so the animation driver
// manages the theme-appropriate value directly
const darkResult = getSplitStyles(
{
backgroundColor: 'red',
'$theme-dark': {
backgroundColor: 'blue',
},
},
View.staticConfig,
{} as any,
'dark',
defaultComponentState,
{
mediaState: undefined,
isAnimated: true,
noClass: true,
resolveValues: 'auto',
},
{} as any,
{
animationDriver: { isReactNative: false },
groups: { state: {} },
} as any,
undefined,
undefined,
true
)!
// in dark theme, $theme-dark override should be applied inline
expect(darkResult.style?.backgroundColor).toBe('blue')
// no theme media CSS classes should be generated (de-opted to inline)
const themeMediaKey = Object.keys(darkResult.classNames || {}).find((k) =>
k.includes('dark')
)
expect(themeMediaKey).toBeUndefined()
// in light theme, $theme-dark should not apply
const lightResult = getSplitStyles(
{
backgroundColor: 'red',
'$theme-dark': {
backgroundColor: 'blue',
},
},
View.staticConfig,
{} as any,
'light',
defaultComponentState,
{
mediaState: undefined,
isAnimated: true,
noClass: true,
resolveValues: 'auto',
},
{} as any,
{
animationDriver: { isReactNative: false },
groups: { state: {} },
} as any,
undefined,
undefined,
true
)!
// in light theme, base value should remain
expect(lightResult.style?.backgroundColor).toBe('red')
})
test.todo(`$theme-dark keeps CSS classes when animateOnly is set and property is not animated`, () => {
// when animateOnly is set, non-animated properties (like bg) should stay as
// CSS classes so theme media overrides work via specificity
const result = getSplitStyles(
{
backgroundColor: 'red',
animateOnly: ['transform'],
'$theme-dark': {
backgroundColor: 'blue',
},
},
View.staticConfig,
{} as any,
'dark',
defaultComponentState,
{
mediaState: undefined,
isAnimated: true,
noClass: true,
resolveValues: 'auto',
},
{} as any,
{
animationDriver: { isReactNative: false },
groups: { state: {} },
} as any,
undefined,
undefined,
true
)!
// backgroundColor should NOT be inline (it's not in animateOnly)
expect(result.style?.backgroundColor).toBeUndefined()
// backgroundColor should be promoted to CSS class
expect(result.classNames?.backgroundColor).toBeDefined()
// theme media CSS class should also exist
const themeMediaKey = Object.keys(result.classNames || {}).find((k) =>
k.includes('dark')
)
expect(themeMediaKey).toBeDefined()
})
test(`perspective transform`, () => {
expect(
Object.values(
simplifiedGetSplitStyles(Text, {
perspective: 1000,
}).rulesToInsert
)
).toMatchInlineSnapshot(`
[
[
"transform",
"perspective(1000px)",
"_tr-perspective1343953606",
undefined,
[
":root ._tr-perspective1343953606{transform:perspective(1000px);}",
],
],
]
`)
})
test(`z-index resolves to respective tokens`, () => {
const styles = simplifiedGetSplitStyles(Text, {
zIndex: '$1',
})
expect(
Object.values(styles.rulesToInsert)[0][StyleObjectProperty] === 'zIndex'
).toBeTruthy()
expect(Object.values(styles.rulesToInsert)[0][StyleObjectValue]).toEqual(
'var(--t-zIndex-1)'
)
})
test(`shadowColor + shadowOpacity`, () => {
const styles = simplifiedGetSplitStyles(Text, {
shadowColor: 'red',
shadowOpacity: 0.5,
})
expect(Object.values(styles.rulesToInsert).length).toEqual(1)
// on web, opacity is applied via CSS color-mix instead of parsing to rgba
expect(Object.values(styles.rulesToInsert)[0][StyleObjectValue]).toEqual(
`0px 0px 0px color-mix(in srgb, red 50%, transparent)`
)
expect(Object.values(styles.rulesToInsert)[0][StyleObjectProperty]).toEqual(
`boxShadow`
)
})
test(`group container queries generate @supports and @container`, () => {
const styles = simplifiedGetSplitStyles(Text, {
'$group-testy-sm': {
color: 'red',
},
})
const rule = Object.values(styles.rulesToInsert)[0][StyleObjectRules][0]
expect(rule).toMatchInlineSnapshot(
'"@supports (contain: inline-size) {@container testy (max-width: 800px){:root:root:root .t_group_testy ._col-_grouptesty-sm_red{color:red;}}}"'
)
})
test(`group container queries with single-part media keys`, () => {
// use sm which exists in the default config
const styles = simplifiedGetSplitStyles(Text, {
'$group-frame-sm': {
paddingRight: 0,
},
})
const rule = Object.values(styles.rulesToInsert)[0][StyleObjectRules][0]
// should generate valid selector with t_group_frame
expect(rule).toContain('@container frame')
expect(rule).toContain('.t_group_frame')
// should not have the media key as a pseudo selector
expect(rule).not.toContain(':sm')
})
test(`group container queries with multi-part pseudo like focus-visible`, () => {
// test focus-visible pseudo which has a dash
const styles = simplifiedGetSplitStyles(Text, {
'$group-frame-focus-visible': {
paddingRight: 0,
},
})
const rule = Object.values(styles.rulesToInsert)[0][StyleObjectRules][0]
// should have focus-visible pseudo selector
expect(rule).toContain(':focus-visible')
expect(rule).toContain('.t_group_frame')
})
test(`boolean group $group-hover parses correctly as group-true-hover`, () => {
// $group-hover normalizes to $group-true-hover internally
// getGroupPropParts receives "group-true-hover" and should parse it correctly
const result = getGroupPropParts('group-true-hover')
expect(result.name).toBe('true')
expect(result.pseudo).toBe('hover')
expect(result.media).toBeUndefined()
})
test(`boolean group with $group-hover does not warn`, () => {
const origNodeEnv = process.env.NODE_ENV
process.env.NODE_ENV = 'development'
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
// groupContext exists (from a named group) but doesn't have a "true" key
// without the fix this triggers double-normalization
const groupContext = {
mygroup: {
state: { pseudo: {} },
subscribe: () => () => {},
emit: () => {},
listeners: new Set(),
},
}
simplifiedGetSplitStyles(Text, { '$group-hover': { color: 'red' } }, { groupContext })
expect(warnSpy).not.toHaveBeenCalled()
warnSpy.mockRestore()
process.env.NODE_ENV = origNodeEnv
})
// const timed = async (fn: Function, opts?: { runs?: number }) => {
// const start = performance.now()
// const runs = opts?.runs ?? 1
// for (let i = 0; i < runs; i++) {
// let response = fn()
// if (response instanceof Promise) {
// response = await response
// }
// }
// const took = (performance.now() - start) / runs
// return {
// took,
// }
// }
// function runBaselineSpeedTest() {
// const start = performance.now()
// let y: any[] = []
// for (let i = 0; i < 50; i++) {
// y.push(new Array(50).fill({}))
// }
// globalThis['__ensureRuns'] = y[0]
// return performance.now() - start
// }
// never actually hit the memo in practice
// test(`it memoizes`, async () => {
// const baseline = runBaselineSpeedTest()
// const props = {
// zIndex: '$1',
// backgroundColor: 'red',
// margin: 20,
// scale: 2,
// }
// const runWithoutMemo = () =>
// timed(
// () => {
// simplifiedGetSplitStyles(Text, props, {
// skipMemo: true,
// })
// },
// {
// runs: 200,
// }
// )
// const runWithMemo = () =>
// timed(
// () => {
// simplifiedGetSplitStyles(Text, props, {
// skipMemo: false,
// })
// },
// {
// runs: 200,
// }
// )
// let timings = {
// memoized: 0,
// raw: 0,
// }
// // need to run them back and forth to get accurate results
// for (let i = 0; i < 10; i++) {
// const runs = [
// { run: runWithMemo, type: 'memoized' },
// { run: runWithoutMemo, type: 'raw' },
// ]
// if (i % 2 === 0) {
// runs.reverse()
// }
// for (const { run, type } of runs) {
// const out = await run()
// timings[type] += out.took
// }
// }
// expect(timings.memoized).toBeLessThan(timings.raw / 50)
// })
// this test is failing:
// TODO: support this - might need the getSplitStyles refactor (unifying getSubStyle)
// + write another similar test for pseudos
// test(`fonts get merged correctly if fontSize is media activates font family`, () => {
// const CustomText = styled(Text, {
// variants: {
// type: {
// myValue: {
// fontFamily: '$body',
// },
// },
// } as const,
// })
// const splitStyles = simplifiedGetSplitStyles(
// CustomText,
// {
// type: 'myValue',
// $xs: {
// fontSize: '$1',
// },
// },
// 'p',
// { xs: true }
// )
// const fontSizeRule = splitStyles.rulesToInsert.find(
// (rule) => rule[StyleObjectProperty] === 'fontSize'
// )
// expect(fontSizeRule?.rules[0].includes('font-size:var(--f-size-1)')).toBeTruthy()
// })
// test(`prop "tabIndex" defaults to "0", overrides to "-1" when tag = button`, () => {
// expect(
// getSplitStylesStack(
// {
// focusable: true,
// },
// 'button'
// )['tabIndex']
// ).toEqual('0')
// expect(
// getSplitStylesStack(
// {
// tabIndex: '-1',
// },
// 'button'
// )['tabIndex']
// ).toEqual('-1')
// })
})
describe('getSplitStyles - asChild default props skipping', () => {
test('asChild should not pass through default style props', () => {
// create a styled component with default props
const StyledTrigger = styled(View, {
position: 'static',
backgroundColor: 'red',
})
// without asChild, position: static should be in the output
const withoutAsChild = simplifiedGetSplitStyles(
StyledTrigger,
{},
{ mergeDefaultProps: true }
)
const withoutAsChildOutput = JSON.stringify(withoutAsChild)
expect(withoutAsChildOutput).toContain('static')
// with asChild, position: static should NOT be in the output (it's a default)
const withAsChild = simplifiedGetSplitStyles(
StyledTrigger,
{
asChild: true,
},
{ mergeDefaultProps: true }
)
const withAsChildOutput = JSON.stringify(withAsChild)
expect(withAsChildOutput).not.toContain('static')
// red is also a default, so it should not be there either
expect(withAsChildOutput).not.toContain('red')
})
test('asChild should pass through non-default style props', () => {
const StyledTrigger = styled(View, {
position: 'static',
})
// with asChild but a different position value, it should be passed through
const withAsChildOverride = simplifiedGetSplitStyles(
StyledTrigger,
{
asChild: true,
position: 'relative',
},
{ mergeDefaultProps: true }
)
const withAsChildOverrideRules = JSON.stringify(withAsChildOverride.rulesToInsert)
expect(withAsChildOverrideRules).toContain('relative')
})
test('asChild except-style should skip all styles', () => {
const StyledTrigger = styled(View, {
position: 'static',
})
const exceptStyle = simplifiedGetSplitStyles(
StyledTrigger,
{
asChild: 'except-style',
position: 'relative',
},
{ mergeDefaultProps: true }
)
const exceptStyleRules = JSON.stringify(exceptStyle.rulesToInsert)
// should have no style rules at all
expect(exceptStyleRules).not.toContain('position')
expect(exceptStyleRules).not.toContain('static')
expect(exceptStyleRules).not.toContain('relative')
})
test('asChild should not pass through global config default props', () => {
// global config no longer sets position: static (skipped as default)
// but asChild should still skip component's own default style props
const SimpleTrigger = styled(View, {
backgroundColor: 'blue',
})
const withAsChild = simplifiedGetSplitStyles(
SimpleTrigger,
{
asChild: true,
},
{ mergeDefaultProps: true }
)
const withAsChildOutput = JSON.stringify(withAsChild)
// asChild should not emit the component's default backgroundColor
expect(withAsChildOutput).not.toContain('_bg-blue')
})
})
describe('getSplitStyles - pseudo prop merging', () => {
const StyledButton = styled(View, {
name: 'StyledButton',
pressStyle: { backgroundColor: 'green' },
variants: {
variant: {
prim: {
pressStyle: { backgroundColor: 'blue' },
},
},
},
})
test('inline pressStyle should override variant pressStyle', () => {
const { viewProps } = simplifiedGetSplitStyles(StyledButton, {
variant: 'prim',
pressStyle: { backgroundColor: 'red' },
})
expect(viewProps.className).toContain('_bg-0active-red')
})
test('variant pressStyle should be used if no inline pressStyle', () => {
const { viewProps } = simplifiedGetSplitStyles(StyledButton, {
variant: 'prim',
})
expect(viewProps.className).toContain('_bg-0active-blue')
})
test('default pressStyle should not generate a class if not used', () => {
const { viewProps } = simplifiedGetSplitStyles(StyledButton, {})
// No press state simulated, so no class is generated
expect(viewProps.className).not.toContain('_bg-0active-green')
})
})
describe('getSplitStyles - kebab-case media keys', () => {
beforeAll(() => {
// reconfigure with kebab-case media keys like v5 config uses
const baseConfig = config.getDefaultGuiConfig()
createGui({
...baseConfig,
media: {
...baseConfig.media,
'max-md': { maxWidth: 1020 },
'min-lg': { minWidth: 1280 },
},
})
})
test('group container queries with kebab-case media key max-md', () => {
const styles = simplifiedGetSplitStyles(Text, {
'$group-frame-max-md': {
paddingRight: 0,
},
})
const rule = Object.values(styles.rulesToInsert)[0][StyleObjectRules][0]
// should generate valid @container query with frame container name
expect(rule).toContain('@container frame')
expect(rule).toContain('.t_group_frame')
// should NOT have :max as a pseudo selector - this was the bug
expect(rule).not.toContain(':max')
expect(rule).not.toContain(':max-md')
})
test('group container queries with kebab-case media key and pseudo', () => {
const styles = simplifiedGetSplitStyles(Text, {
'$group-frame-max-md-hover': {
paddingRight: 0,
},
})
const rule = Object.values(styles.rulesToInsert)[0][StyleObjectRules][0]
// should have both the container query and the hover pseudo
expect(rule).toContain('@container frame')
expect(rule).toContain('.t_group_frame:hover')
// max-md should be part of the container query, not a pseudo selector
expect(rule).not.toContain(':max-md')
expect(rule).not.toContain(':max')
})
})
describe('direction-relative props', () => {
const cssOf = (Component: any, props: Record<string, any>) =>
Object.values(simplifiedGetSplitStyles(Component, props).rulesToInsert)
.flatMap((rule) => rule[StyleObjectRules])
.join('\n')
test.each([
['start', 'inset-inline-start:4px'],
['end', 'inset-inline-end:4px'],
['marginStart', 'margin-inline-start:4px'],
['marginEnd', 'margin-inline-end:4px'],
['paddingStart', 'padding-inline-start:4px'],
['paddingEnd', 'padding-inline-end:4px'],
['borderStartWidth', 'border-inline-start-width:4px'],
['borderEndWidth', 'border-inline-end-width:4px'],
['borderTopStartRadius', 'border-start-start-radius:4px'],
['borderTopEndRadius', 'border-start-end-radius:4px'],
['borderBottomStartRadius', 'border-end-start-radius:4px'],
['borderBottomEndRadius', 'border-end-end-radius:4px'],
['marginInlineStart', 'margin-inline-start:4px'],
['paddingInlineEnd', 'padding-inline-end:4px'],
['insetInlineStart', 'inset-inline-start:4px'],
['borderInlineStartWidth', 'border-inline-start-width:4px'],
['borderStartStartRadius', 'border-start-start-radius:4px'],
])('%s emits %s', (prop, declaration) => {
expect(cssOf(View, { [prop]: 4 })).toContain(`{${declaration};}`)
})
test('react-native start/end colors become inline-axis CSS', () => {
expect(cssOf(View, { borderStartColor: 'red' })).toContain(
'border-inline-start-color:red'
)
expect(cssOf(View, { borderEndColor: 'red' })).toContain(
'border-inline-end-color:red'
)
})
test('no emitted declaration names a property CSS does not have', () => {
const css = cssOf(View, {
start: 1,
end: 1,
marginStart: 1,
marginEnd: 1,
paddingStart: 1,
paddingEnd: 1,
borderStartWidth: 1,
borderEndWidth: 1,
borderStartColor: 'red',
borderEndColor: 'red',
borderTopStartRadius: 1,
borderTopEndRadius: 1,
borderBottomStartRadius: 1,
borderBottomEndRadius: 1,
})
expect(css).not.toMatch(
/[{;](start|end|margin-start|margin-end|padding-start|padding-end|border-start-[a-z]+|border-end-[a-z]+|border-(top|bottom)-(start|end)-radius):/
)
})
test('text alignment keeps start/end on web', () => {
expect(cssOf(Text, { textAlign: 'start' })).toContain('text-align:start')
expect(cssOf(Text, { textAlign: 'end' })).toContain('text-align:end')
})
test('inline-axis longhands outrank the physical sides a shorthand expands to', () => {
const css = cssOf(View, {
paddingHorizontal: 8,
paddingInlineEnd: 16,
borderRadius: 4,
borderStartEndRadius: 0,
})
// padding-right from paddingHorizontal and padding-inline-end name the same
// side in LTR; the logical one carries the doubled class whatever order the
// sheet inserted them in
expect(css).toMatch(/:root (\.[\w-]+)\1\{padding-inline-end:16px;\}/)
expect(css).toMatch(/:root (\.[\w-]+)\1\{border-start-end-radius:0px;\}/)
expect(css).toMatch(/:root \.[\w-]+\{padding-right:8px;\}/)
})
test('two-sided inline shorthands split into longhands a later side replaces', () => {
const css = cssOf(View, { paddingInline: 8, paddingInlineEnd: 16 })
expect(css).toContain('padding-inline-start:8px')
expect(css).toContain('padding-inline-end:16px')
expect(css).not.toContain('padding-inline:')
expect(css).not.toContain('padding-inline-end:8px')
})
})