From fca8562e394059583cd6d9de4c5fca0497f0c284 Mon Sep 17 00:00:00 2001 From: Vedant Ere Date: Fri, 9 Oct 2026 17:56:40 +0530 Subject: [PATCH 1/7] CustomGradientPicker: Keep the color interpolation method when editing a gradient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gradient-parser` throws on a color interpolation method such as `in oklch`, so the picker fell back to its default gradient. A gradient like `linear-gradient(in oklch 160deg, …)` showed no color stops, and changing its angle or type replaced it with the default gradient. Take the method out of the first argument before parsing, keep it next to the parsed gradient, and write it back when serializing. Only the color spaces and hue interpolation methods defined by CSS Color are accepted, so anything else still falls back as before. --- .../src/custom-gradient-picker/constants.ts | 31 +++++ .../src/custom-gradient-picker/serializer.ts | 13 +- .../custom-gradient-picker/test/serializer.ts | 32 +++++ .../src/custom-gradient-picker/test/utils.ts | 54 ++++++++ .../src/custom-gradient-picker/types.ts | 12 +- .../src/custom-gradient-picker/utils.ts | 115 +++++++++++++++++- 6 files changed, 248 insertions(+), 9 deletions(-) diff --git a/packages/components/src/custom-gradient-picker/constants.ts b/packages/components/src/custom-gradient-picker/constants.ts index 19d28a0bd734bc..2852a6af8a1511 100644 --- a/packages/components/src/custom-gradient-picker/constants.ts +++ b/packages/components/src/custom-gradient-picker/constants.ts @@ -30,3 +30,34 @@ export const DIRECTIONAL_ORIENTATION_ANGLE_MAP = { 'top left': 315, 'left top': 315, }; + +// The color spaces and hue interpolation methods that a CSS +// `` accepts, e.g. `in oklch longer hue`. +// See https://www.w3.org/TR/css-color-4/#interpolation-space. +export const RECTANGULAR_INTERPOLATION_COLOR_SPACES = [ + 'srgb', + 'srgb-linear', + 'display-p3', + 'a98-rgb', + 'prophoto-rgb', + 'rec2020', + 'lab', + 'oklab', + 'xyz', + 'xyz-d50', + 'xyz-d65', +]; + +export const POLAR_INTERPOLATION_COLOR_SPACES = [ + 'hsl', + 'hwb', + 'lch', + 'oklch', +]; + +export const HUE_INTERPOLATION_METHODS = [ + 'shorter', + 'longer', + 'increasing', + 'decreasing', +]; diff --git a/packages/components/src/custom-gradient-picker/serializer.ts b/packages/components/src/custom-gradient-picker/serializer.ts index 255727556eb5ef..a1551da534a3b4 100644 --- a/packages/components/src/custom-gradient-picker/serializer.ts +++ b/packages/components/src/custom-gradient-picker/serializer.ts @@ -1,4 +1,5 @@ import type gradientParser from 'gradient-parser'; +import type { GradientAST } from './types'; export function serializeGradientColor( { type, @@ -63,10 +64,18 @@ export function serializeGradientOrientation( export function serializeGradient( { type, + colorInterpolation, orientation, colorStops, -}: gradientParser.GradientNode ) { - const serializedOrientation = serializeGradientOrientation( orientation ); +}: GradientAST ) { + // The color interpolation method shares the first argument with the + // orientation, e.g. `linear-gradient(in oklch 160deg, …)`. + const serializedOrientation = [ + colorInterpolation, + serializeGradientOrientation( orientation ), + ] + .filter( Boolean ) + .join( ' ' ); const serializedColorStops = colorStops .sort( ( colorStop1, colorStop2 ) => { const getNumericStopValue = ( diff --git a/packages/components/src/custom-gradient-picker/test/serializer.ts b/packages/components/src/custom-gradient-picker/test/serializer.ts index ef578192dea67f..9bf615c002afb2 100644 --- a/packages/components/src/custom-gradient-picker/test/serializer.ts +++ b/packages/components/src/custom-gradient-picker/test/serializer.ts @@ -204,4 +204,36 @@ describe( 'It should serialize a gradient', () => { 'linear-gradient(0deg,rgba(1,2,3,0.5) 0%,rgba(10,20,30,0.5) 20%,rgba(255,1,1,0.9) 40%,rgba(1,2,3,0.5) 100%)' ); } ); + + test( 'serializeGradient writes the interpolation method before the angle', () => { + const colorStops = [ + { + type: 'hex' as const, + value: '000', + length: { type: '%' as const, value: '0' }, + }, + { + type: 'hex' as const, + value: 'fff', + length: { type: '%' as const, value: '100' }, + }, + ]; + + expect( + serializeGradient( { + type: 'linear-gradient', + colorInterpolation: 'in oklch', + orientation: { type: 'angular', value: '160', unit: 'deg' }, + colorStops, + } ) + ).toBe( 'linear-gradient(in oklch 160deg,#000 0%,#fff 100%)' ); + + expect( + serializeGradient( { + type: 'radial-gradient', + colorInterpolation: 'in oklab', + colorStops, + } ) + ).toBe( 'radial-gradient(in oklab,#000 0%,#fff 100%)' ); + } ); } ); diff --git a/packages/components/src/custom-gradient-picker/test/utils.ts b/packages/components/src/custom-gradient-picker/test/utils.ts index 03a7a87ed6bcb1..5f15e3244ca9d8 100644 --- a/packages/components/src/custom-gradient-picker/test/utils.ts +++ b/packages/components/src/custom-gradient-picker/test/utils.ts @@ -68,3 +68,57 @@ describe( 'CustomGradientPicker gradient parsing', () => { expect( result.gradientAST ).toEqual( fallback.gradientAST ); } ); } ); + +describe( 'getGradientAstWithDefault', () => { + describe( 'with a color interpolation method', () => { + test( 'reads an interpolation method placed before the angle', () => { + const { gradientAST, hasGradient } = getGradientAstWithDefault( + 'linear-gradient(in oklch 160deg, #009ED4 0%, #FFCE00 100%)' + ); + + expect( hasGradient ).toBe( true ); + expect( gradientAST.colorInterpolation ).toBe( 'in oklch' ); + expect( gradientAST.orientation ).toEqual( { + type: 'angular', + value: '160', + unit: 'deg', + } ); + expect( gradientAST.colorStops ).toHaveLength( 2 ); + } ); + + test( 'reads an interpolation method placed after the direction', () => { + const { gradientAST, hasGradient } = getGradientAstWithDefault( + 'linear-gradient(to right in oklab, #009ED4 0%, #FFCE00 100%)' + ); + + expect( hasGradient ).toBe( true ); + expect( gradientAST.colorInterpolation ).toBe( 'in oklab' ); + expect( gradientAST.orientation ).toEqual( { + type: 'angular', + value: '90', + unit: 'deg', + } ); + } ); + + test( 'keeps the hue interpolation method of a polar color space', () => { + const { gradientAST, hasGradient } = getGradientAstWithDefault( + 'linear-gradient(in oklch longer hue 160deg, #009ED4 0%, #FFCE00 100%)' + ); + + expect( hasGradient ).toBe( true ); + expect( gradientAST.colorInterpolation ).toBe( + 'in oklch longer hue' + ); + } ); + + test( 'falls back to the default gradient for an unknown color space', () => { + const { gradientAST, hasGradient } = getGradientAstWithDefault( + 'linear-gradient(in not-a-space 160deg, #009ED4 0%, #FFCE00 100%)' + ); + + expect( console ).toHaveWarned(); + expect( hasGradient ).toBe( false ); + expect( gradientAST.colorInterpolation ).toBeUndefined(); + } ); + } ); +} ); diff --git a/packages/components/src/custom-gradient-picker/types.ts b/packages/components/src/custom-gradient-picker/types.ts index 03add2a860b36d..149158c26bf505 100644 --- a/packages/components/src/custom-gradient-picker/types.ts +++ b/packages/components/src/custom-gradient-picker/types.ts @@ -37,14 +37,22 @@ export type CustomGradientPickerProps = { __experimentalIsRenderedInSidebar?: boolean; }; +/** + * `gradient-parser` can't read the color interpolation method of a gradient + * (e.g. `in oklch`), so the picker keeps it next to the parsed AST. + */ +type WithColorInterpolation = { colorInterpolation?: string }; + +export type GradientAST = gradientParser.GradientNode & WithColorInterpolation; + export type GradientAnglePickerProps = { - gradientAST: gradientParser.LinearGradientNode; + gradientAST: gradientParser.LinearGradientNode & WithColorInterpolation; hasGradient: boolean; onChange: ( gradient: string ) => void; }; export type GradientTypePickerProps = { - gradientAST: gradientParser.GradientNode; + gradientAST: GradientAST; hasGradient: boolean; onChange: ( gradient: string ) => void; }; diff --git a/packages/components/src/custom-gradient-picker/utils.ts b/packages/components/src/custom-gradient-picker/utils.ts index de301cac24afe2..c165025cd6d2c5 100644 --- a/packages/components/src/custom-gradient-picker/utils.ts +++ b/packages/components/src/custom-gradient-picker/utils.ts @@ -5,9 +5,12 @@ import { DEFAULT_GRADIENT, HORIZONTAL_GRADIENT_ORIENTATION, DIRECTIONAL_ORIENTATION_ANGLE_MAP, + HUE_INTERPOLATION_METHODS, + POLAR_INTERPOLATION_COLOR_SPACES, + RECTANGULAR_INTERPOLATION_COLOR_SPACES, } from './constants'; import { serializeGradient } from './serializer'; -import type { ControlPoint } from './types'; +import type { ControlPoint, GradientAST } from './types'; extend( [ namesPlugin ] ); @@ -32,16 +35,118 @@ function hasUnsupportedLength( item: gradientParser.ColorStop ) { return item.length === undefined || item.length.type !== '%'; } +/** + * Reads the first argument of a gradient, the one that holds the orientation + * and the color interpolation method, as a list of space separated tokens. + * + * @param gradient CSS gradient. + * @param start Index right after the opening parenthesis. + */ +function readFirstArgument( gradient: string, start: number ) { + const tokens: string[] = []; + let token = ''; + let depth = 0; + + for ( let index = start; index < gradient.length; index++ ) { + const char = gradient[ index ]; + + if ( depth === 0 && ( char === ',' || char === ')' ) ) { + if ( token ) { + tokens.push( token ); + } + return { tokens, end: index }; + } + + if ( char === '(' ) { + depth++; + } else if ( char === ')' ) { + depth--; + } + + if ( depth === 0 && /\s/.test( char ) ) { + if ( token ) { + tokens.push( token ); + } + token = ''; + } else { + token += char; + } + } + + // The gradient is missing its closing parenthesis. + return undefined; +} + +/** + * `gradient-parser` can't read a color interpolation method such as + * `in oklch`, so it is taken out of the gradient before it is parsed. + * + * Only the first argument is searched, at its top level, so a color stop like + * `color-mix( in srgb, … )` is never mistaken for one. + * + * @param gradient CSS gradient. + */ +function extractColorInterpolation( gradient: string ) { + const start = gradient.indexOf( '(' ) + 1; + const firstArgument = start && readFirstArgument( gradient, start ); + if ( ! firstArgument ) { + return { gradient }; + } + + const { tokens, end } = firstArgument; + const words = tokens.map( ( token ) => token.toLowerCase() ); + const index = words.indexOf( 'in' ); + if ( index === -1 ) { + return { gradient }; + } + + const colorSpace = words[ index + 1 ]; + let length; + if ( + POLAR_INTERPOLATION_COLOR_SPACES.includes( colorSpace ) && + HUE_INTERPOLATION_METHODS.includes( words[ index + 2 ] ) && + words[ index + 3 ] === 'hue' + ) { + length = 4; + } else if ( + RECTANGULAR_INTERPOLATION_COLOR_SPACES.includes( colorSpace ) || + POLAR_INTERPOLATION_COLOR_SPACES.includes( colorSpace ) + ) { + length = 2; + } else { + return { gradient }; + } + + const remainder = [ + ...tokens.slice( 0, index ), + ...tokens.slice( index + length ), + ].join( ' ' ); + // Without an orientation, the comma after the first argument goes too. + const hasEmptyFirstArgument = ! remainder && gradient[ end ] === ','; + + return { + gradient: + gradient.slice( 0, start ) + + remainder + + gradient.slice( hasEmptyFirstArgument ? end + 1 : end ), + colorInterpolation: words.slice( index, index + length ).join( ' ' ), + }; +} + export function getGradientAstWithDefault( value?: string | null ) { // gradientAST will contain the gradient AST as parsed by gradient-parser npm module. // More information of its structure available at https://www.npmjs.com/package/gradient-parser#ast. - let gradientAST: gradientParser.GradientNode | undefined; + let gradientAST: GradientAST | undefined; let hasGradient = !! value; - const valueToParse = value ?? DEFAULT_GRADIENT; + const { gradient: valueToParse, colorInterpolation } = + extractColorInterpolation( value ?? DEFAULT_GRADIENT ); try { gradientAST = gradientParser.parse( valueToParse )[ 0 ]; + if ( colorInterpolation ) { + gradientAST.colorInterpolation = colorInterpolation; + } } catch ( error ) { // eslint-disable-next-line no-console console.warn( @@ -103,7 +208,7 @@ export function getGradientAstWithDefault( value?: string | null ) { } export function getGradientAstWithControlPoints( - gradientAST: gradientParser.GradientNode, + gradientAST: GradientAST, newControlPoints: ControlPoint[] ) { return { @@ -122,7 +227,7 @@ export function getGradientAstWithControlPoints( : [ `${ r }`, `${ g }`, `${ b }` ], }; } ), - } as gradientParser.GradientNode; + } as GradientAST; } export function getStopCssColor( colorStop: gradientParser.ColorStop ) { From c5dc46f3af4a40ae6e101ad970be5a60e5c9d387 Mon Sep 17 00:00:00 2001 From: Vedant Ere Date: Fri, 9 Oct 2026 17:56:52 +0530 Subject: [PATCH 2/7] CustomGradientPicker: Add browser tests for gradients with a color interpolation method Render the picker with a gradient that has a color interpolation method and check that its color stops show up, and that the method survives changing the angle, switching to a radial gradient, and moving a color stop. --- .../test/index.browser.test.tsx | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/packages/components/src/custom-gradient-picker/test/index.browser.test.tsx b/packages/components/src/custom-gradient-picker/test/index.browser.test.tsx index 240748ad8987e4..9653fe7cb9f8e6 100644 --- a/packages/components/src/custom-gradient-picker/test/index.browser.test.tsx +++ b/packages/components/src/custom-gradient-picker/test/index.browser.test.tsx @@ -163,6 +163,88 @@ describe( 'CustomGradientPicker', () => { expect( lastCall ).not.toContain( 'deg' ); } ); } ); + + describe( 'with a color interpolation method', () => { + const INTERPOLATED_GRADIENT = + 'linear-gradient(in oklch 160deg,rgb(0,0,0) 0%,rgb(255,255,255) 100%)'; + + it( 'shows the color stops of a gradient that has an interpolation method', async () => { + await render( + + ); + + expect( + screen.getAllByRole( 'button', { + name: /Gradient control point/, + } ) + ).toHaveLength( 2 ); + } ); + + it( 'keeps the interpolation method when the angle changes', async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + + await render( + + ); + + screen.getByRole( 'spinbutton', { name: 'Angle' } ).focus(); + await user.keyboard( '[ArrowUp]' ); + + const lastCall = + onChange.mock.calls[ onChange.mock.calls.length - 1 ][ 0 ]; + expect( lastCall ).toMatch( /^linear-gradient\(in oklch 161deg,/ ); + } ); + + it( 'keeps the interpolation method when switching to a radial gradient', async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + + await render( + + ); + + await user.selectOptions( + screen.getByRole( 'combobox', { name: /type/i } ), + 'radial-gradient' + ); + + const lastCall = + onChange.mock.calls[ onChange.mock.calls.length - 1 ][ 0 ]; + expect( lastCall ).toMatch( /^radial-gradient\(in oklch,/ ); + } ); + + it( 'keeps the interpolation method when a color stop moves', async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + + await render( + + ); + + const [ firstPoint ] = screen.getAllByRole( 'button', { + name: /Gradient control point/, + } ); + firstPoint.focus(); + await user.keyboard( '[ArrowRight]' ); + + const lastCall = + onChange.mock.calls[ onChange.mock.calls.length - 1 ][ 0 ]; + expect( lastCall ).toMatch( /^linear-gradient\(in oklch 160deg,/ ); + } ); + } ); } ); describe( 'CustomGradientBar', () => { From 99825ef6b320c59984b5635a5de2ae5cb2b964e2 Mon Sep 17 00:00:00 2001 From: Vedant Ere Date: Fri, 9 Oct 2026 18:00:36 +0530 Subject: [PATCH 3/7] CustomGradientPicker: Add changelog entry --- packages/components/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 09c12368d7e87c..22b6bb81203318 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -39,6 +39,7 @@ - `Modal`: Keep an accessible name on the dialog when `title` is combined with `__experimentalHideHeader`. The heading that `aria-labelledby` referenced is not rendered in that case, so the title is now applied as the dialog's `aria-label` instead ([#82645](https://github.com/WordPress/gutenberg/pull/82645)). - `ColorPicker`: Make the HSL and RGB input labels translatable, and name the Hue and Alpha inputs after their color format ([#70214](https://github.com/WordPress/gutenberg/pull/70214)). - `CustomGradientPicker`: Use the bundled `gradient-parser` types, preserve gradient angles when editing, and keep unsupported gradient syntax on the existing fallback path ([#84582](https://github.com/WordPress/gutenberg/pull/84582)). +- `CustomGradientPicker`: Keep the color interpolation method of a gradient, such as `in oklch`, when editing it, instead of replacing the gradient with the default one ([#84383](https://github.com/WordPress/gutenberg/pull/84383)). ### Internal From 0839435ddef6672e0b9efe131bc0aecdf2d9415e Mon Sep 17 00:00:00 2001 From: Vedant Ere Date: Wed, 14 Oct 2026 17:04:49 +0530 Subject: [PATCH 4/7] CustomGradientPicker: Add tests for edge cases of the color interpolation method Cover a color interpolation method that is the whole first argument of a gradient, and a hue interpolation method on a color space that does not support one, which must still fall back to the default gradient. --- .../src/custom-gradient-picker/test/utils.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/components/src/custom-gradient-picker/test/utils.ts b/packages/components/src/custom-gradient-picker/test/utils.ts index 5f15e3244ca9d8..06ca8a67391569 100644 --- a/packages/components/src/custom-gradient-picker/test/utils.ts +++ b/packages/components/src/custom-gradient-picker/test/utils.ts @@ -120,5 +120,25 @@ describe( 'getGradientAstWithDefault', () => { expect( hasGradient ).toBe( false ); expect( gradientAST.colorInterpolation ).toBeUndefined(); } ); + + test( 'reads an interpolation method that is the whole first argument', () => { + const { gradientAST, hasGradient } = getGradientAstWithDefault( + 'linear-gradient(in oklch, #009ED4 0%, #FFCE00 100%)' + ); + + expect( hasGradient ).toBe( true ); + expect( gradientAST.colorInterpolation ).toBe( 'in oklch' ); + expect( gradientAST.colorStops ).toHaveLength( 2 ); + } ); + + test( 'falls back to the default gradient for a hue interpolation method on a non-hue color space', () => { + const { gradientAST, hasGradient } = getGradientAstWithDefault( + 'linear-gradient(in oklab longer hue 160deg, #009ED4 0%, #FFCE00 100%)' + ); + + expect( console ).toHaveWarned(); + expect( hasGradient ).toBe( false ); + expect( gradientAST.colorInterpolation ).toBeUndefined(); + } ); } ); } ); From 620bc3bca5284b8b337715c69574ed41d61bbd7b Mon Sep 17 00:00:00 2001 From: Vedant Ere Date: Wed, 14 Oct 2026 18:57:30 +0530 Subject: [PATCH 5/7] CustomGradientPicker: Link the upstream issue next to the color interpolation workaround Point at the gradient-parser issue that tracks support for color interpolation methods, so the workaround can be removed once an upstream release supports them. --- packages/components/src/custom-gradient-picker/utils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/components/src/custom-gradient-picker/utils.ts b/packages/components/src/custom-gradient-picker/utils.ts index c165025cd6d2c5..b9d3ab4e18d0c0 100644 --- a/packages/components/src/custom-gradient-picker/utils.ts +++ b/packages/components/src/custom-gradient-picker/utils.ts @@ -84,6 +84,10 @@ function readFirstArgument( gradient: string, start: number ) { * Only the first argument is searched, at its top level, so a color stop like * `color-mix( in srgb, … )` is never mistaken for one. * + * This is a workaround until `gradient-parser` supports color interpolation + * methods. It can be removed once an upstream release does: + * https://github.com/rafaelcaricio/gradient-parser/issues/57 + * * @param gradient CSS gradient. */ function extractColorInterpolation( gradient: string ) { From 3afc0f11e7955db19adb82da484998f6cb8e6d23 Mon Sep 17 00:00:00 2001 From: Vedant Ere Date: Wed, 14 Oct 2026 19:28:51 +0530 Subject: [PATCH 6/7] CustomGradientPicker: Accept the display-p3-linear color interpolation space CSS Color 4 includes display-p3-linear among the rectangular color spaces a gradient can interpolate in, but the picker did not recognise it, so a gradient using it still fell back to the default gradient. Add it, and test every color space and hue interpolation method listed in the CSS Color 4 grammar so that a missing keyword is caught. --- .../src/custom-gradient-picker/constants.ts | 1 + .../src/custom-gradient-picker/test/utils.ts | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/packages/components/src/custom-gradient-picker/constants.ts b/packages/components/src/custom-gradient-picker/constants.ts index 2852a6af8a1511..9c6997e058d4f3 100644 --- a/packages/components/src/custom-gradient-picker/constants.ts +++ b/packages/components/src/custom-gradient-picker/constants.ts @@ -38,6 +38,7 @@ export const RECTANGULAR_INTERPOLATION_COLOR_SPACES = [ 'srgb', 'srgb-linear', 'display-p3', + 'display-p3-linear', 'a98-rgb', 'prophoto-rgb', 'rec2020', diff --git a/packages/components/src/custom-gradient-picker/test/utils.ts b/packages/components/src/custom-gradient-picker/test/utils.ts index 06ca8a67391569..f87c07c52c2a04 100644 --- a/packages/components/src/custom-gradient-picker/test/utils.ts +++ b/packages/components/src/custom-gradient-picker/test/utils.ts @@ -111,6 +111,53 @@ describe( 'getGradientAstWithDefault', () => { ); } ); + // The keywords below are copied from the CSS Color 4 grammar for + // ``, `` and + // ``, so that a keyword missing from the + // picker's own lists is caught. + // See https://www.w3.org/TR/css-color-4/#interpolation-space. + test.each( [ + 'srgb', + 'srgb-linear', + 'display-p3', + 'display-p3-linear', + 'a98-rgb', + 'prophoto-rgb', + 'rec2020', + 'lab', + 'oklab', + 'xyz', + 'xyz-d50', + 'xyz-d65', + 'hsl', + 'hwb', + 'lch', + 'oklch', + ] )( 'reads the %s color space', ( colorSpace ) => { + const { gradientAST, hasGradient } = getGradientAstWithDefault( + `linear-gradient(in ${ colorSpace } 160deg, #009ED4 0%, #FFCE00 100%)` + ); + + expect( hasGradient ).toBe( true ); + expect( gradientAST.colorInterpolation ).toBe( + `in ${ colorSpace }` + ); + } ); + + test.each( [ 'shorter', 'longer', 'increasing', 'decreasing' ] )( + 'reads the %s hue interpolation method', + ( method ) => { + const { gradientAST, hasGradient } = getGradientAstWithDefault( + `linear-gradient(in oklch ${ method } hue 160deg, #009ED4 0%, #FFCE00 100%)` + ); + + expect( hasGradient ).toBe( true ); + expect( gradientAST.colorInterpolation ).toBe( + `in oklch ${ method } hue` + ); + } + ); + test( 'falls back to the default gradient for an unknown color space', () => { const { gradientAST, hasGradient } = getGradientAstWithDefault( 'linear-gradient(in not-a-space 160deg, #009ED4 0%, #FFCE00 100%)' From 05df563cbf4ceca623a88c2ba099066a0775f74b Mon Sep 17 00:00:00 2001 From: Vedant Ere Date: Wed, 14 Oct 2026 19:30:27 +0530 Subject: [PATCH 7/7] CustomGradientPicker: Assert the supplied gradient in the color interpolation tests The test for showing the color stops only counted two control points, which the default gradient the picker falls back to also has, so it could pass without showing the supplied gradient. Use the gradient from the issue, which has five color stops, and assert the label of every control point, the angle, and the full gradient after each edit. Do the same for the parsing tests, which now check the whole serialized gradient instead of the number of color stops. --- .../test/index.browser.test.tsx | 39 ++++++++++++++----- .../src/custom-gradient-picker/test/utils.ts | 8 +++- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/components/src/custom-gradient-picker/test/index.browser.test.tsx b/packages/components/src/custom-gradient-picker/test/index.browser.test.tsx index 9653fe7cb9f8e6..e820033063e81f 100644 --- a/packages/components/src/custom-gradient-picker/test/index.browser.test.tsx +++ b/packages/components/src/custom-gradient-picker/test/index.browser.test.tsx @@ -165,10 +165,13 @@ describe( 'CustomGradientPicker', () => { } ); describe( 'with a color interpolation method', () => { + // The gradient from the issue. It has more color stops than the default + // gradient the picker falls back to, so the assertions below can only + // pass when this is the gradient being edited. const INTERPOLATED_GRADIENT = - 'linear-gradient(in oklch 160deg,rgb(0,0,0) 0%,rgb(255,255,255) 100%)'; + 'linear-gradient(in oklch 160deg,#009ED4 0%,#003366 45%,#452955 65%,#F08119 95%,#FFCE00 100%)'; - it( 'shows the color stops of a gradient that has an interpolation method', async () => { + it( 'shows the color stops and angle of a gradient that has an interpolation method', async () => { await render( { ); expect( - screen.getAllByRole( 'button', { - name: /Gradient control point/, - } ) - ).toHaveLength( 2 ); + screen + .getAllByRole( 'button', { + name: /Gradient control point/, + } ) + .map( ( point ) => point.getAttribute( 'aria-label' ) ) + ).toEqual( [ + 'Gradient control point at position 0% with color code #009ED4.', + 'Gradient control point at position 45% with color code #003366.', + 'Gradient control point at position 65% with color code #452955.', + 'Gradient control point at position 95% with color code #F08119.', + 'Gradient control point at position 100% with color code #FFCE00.', + ] ); + expect( + screen.getByRole( 'spinbutton', { name: 'Angle' } ) + ).toHaveValue( 160 ); } ); it( 'keeps the interpolation method when the angle changes', async () => { @@ -199,7 +213,9 @@ describe( 'CustomGradientPicker', () => { const lastCall = onChange.mock.calls[ onChange.mock.calls.length - 1 ][ 0 ]; - expect( lastCall ).toMatch( /^linear-gradient\(in oklch 161deg,/ ); + expect( lastCall ).toBe( + 'linear-gradient(in oklch 161deg,#009ED4 0%,#003366 45%,#452955 65%,#F08119 95%,#FFCE00 100%)' + ); } ); it( 'keeps the interpolation method when switching to a radial gradient', async () => { @@ -220,7 +236,9 @@ describe( 'CustomGradientPicker', () => { const lastCall = onChange.mock.calls[ onChange.mock.calls.length - 1 ][ 0 ]; - expect( lastCall ).toMatch( /^radial-gradient\(in oklch,/ ); + expect( lastCall ).toBe( + 'radial-gradient(in oklch,#009ED4 0%,#003366 45%,#452955 65%,#F08119 95%,#FFCE00 100%)' + ); } ); it( 'keeps the interpolation method when a color stop moves', async () => { @@ -240,9 +258,12 @@ describe( 'CustomGradientPicker', () => { firstPoint.focus(); await user.keyboard( '[ArrowRight]' ); + // Moving a color stop rewrites every color as `rgb()`. const lastCall = onChange.mock.calls[ onChange.mock.calls.length - 1 ][ 0 ]; - expect( lastCall ).toMatch( /^linear-gradient\(in oklch 160deg,/ ); + expect( lastCall ).toBe( + `linear-gradient(in oklch 160deg,rgb(0,158,212) ${ KEYBOARD_CONTROL_POINT_VARIATION }%,rgb(0,51,102) 45%,rgb(69,41,85) 65%,rgb(240,129,25) 95%,rgb(255,206,0) 100%)` + ); } ); } ); } ); diff --git a/packages/components/src/custom-gradient-picker/test/utils.ts b/packages/components/src/custom-gradient-picker/test/utils.ts index f87c07c52c2a04..8a017a6497d686 100644 --- a/packages/components/src/custom-gradient-picker/test/utils.ts +++ b/packages/components/src/custom-gradient-picker/test/utils.ts @@ -83,7 +83,9 @@ describe( 'getGradientAstWithDefault', () => { value: '160', unit: 'deg', } ); - expect( gradientAST.colorStops ).toHaveLength( 2 ); + expect( serializeGradient( gradientAST ) ).toBe( + 'linear-gradient(in oklch 160deg,#009ED4 0%,#FFCE00 100%)' + ); } ); test( 'reads an interpolation method placed after the direction', () => { @@ -175,7 +177,9 @@ describe( 'getGradientAstWithDefault', () => { expect( hasGradient ).toBe( true ); expect( gradientAST.colorInterpolation ).toBe( 'in oklch' ); - expect( gradientAST.colorStops ).toHaveLength( 2 ); + expect( serializeGradient( gradientAST ) ).toBe( + 'linear-gradient(in oklch,#009ED4 0%,#FFCE00 100%)' + ); } ); test( 'falls back to the default gradient for a hue interpolation method on a non-hue color space', () => {