diff --git a/js-api-spec/utils.ts b/js-api-spec/utils.ts index 29dabfa23..3a464a87c 100644 --- a/js-api-spec/utils.ts +++ b/js-api-spec/utils.ts @@ -4,13 +4,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import {info} from 'sass'; +import * as sass from 'sass'; /* Whether the tests are running in a browser context. */ export const isBrowser = !global.process; /** The name of the implementation of Sass being tested. */ -export const sassImpl = info.split('\t')[0] as 'dart-sass' | 'sass-embedded'; +export const sassImpl = sass.info.split('\t')[0] as + | 'dart-sass' + | 'sass-embedded'; type Implementation = 'dart-sass' | 'sass-embedded' | 'browser'; @@ -116,3 +118,41 @@ export async function captureStdioAsync( return {out, err}; } + +/** + * Parses {@link expression} as a Sass expression, evaluates it, and returns its + * value. The expression has access to all the built-in modules at their usual + * URLs. + */ +export function evaluateExpression(expression: string): sass.Value { + let value: sass.Value | undefined; + sass.compileString( + ` + @use "sass:color"; + @use "sass:list"; + @use "sass:map"; + @use "sass:math"; + @use "sass:meta"; + @use "sass:selector"; + @use "sass:string"; + $_: fn((${expression})); + `, + { + functions: { + 'fn($arg)': args => { + value = args[0]; + return sass.sassNull; + }, + }, + } + ); + return value!; +} + +/** Converts {@link value} to its serialized Sass representation. */ +export function serializeValue(value: sass.Value): string { + const result = sass.compileString('a {b: fn()}', { + functions: {'fn()': () => value}, + }); + return result.css.match(/b: ?(.*);/)![1]; +} diff --git a/js-api-spec/value/color/color-4-channels.test.ts b/js-api-spec/value/color/color-4-channels.test.ts index 9bcdee6b9..4c27acd87 100644 --- a/js-api-spec/value/color/color-4-channels.test.ts +++ b/js-api-spec/value/color/color-4-channels.test.ts @@ -10,6 +10,7 @@ import {List} from 'immutable'; import {spaces} from './spaces'; import {channelCases, channelNames} from './utils'; +import {evaluateExpression, serializeValue} from '../../utils'; const spaceNames = Object.keys(spaces) as KnownColorSpace[]; @@ -311,6 +312,52 @@ describe('Color 4 SassColor Channels', () => { ); }); }); + + describe('parsed from Sass code', () => { + it('parses defined values', () => { + const color = evaluateExpression( + `color(display-p3 ${spaces['display-p3'].pink.join(' ')} / 0.5)` + ) as SassColor; + expect(color.channels).toFuzzyEqualList(spaces['display-p3'].pink); + expect(color.alpha).toFuzzyEqual(0.5); + }); + + // Regression test for sass/sass#3950 + it('parses missing values', () => { + const color = evaluateExpression( + 'color(display-p3 0 none 1 / none)' + ) as SassColor; + expect(color.channelsOrNull).toFuzzyEqualList([0, null, 1]); + expect(color.isChannelMissing('alpha')).toBeTrue(); + }); + }); + + describe('serialized to CSS', () => { + it('with defined values', () => + expect( + serializeValue( + spaces['display-p3'] + .constructor(...spaces['display-p3'].pink) + .change({alpha: 0.5}) + ) + ).toEqual( + 'color(display-p3 0.9510333334 0.6749909746 0.7568568354 / 0.5)' + )); + + // Regression test for sass/sass#3950 + it('with missing values', () => + expect( + serializeValue( + new SassColor({ + red: 0, + green: null, + blue: 1, + alpha: null, + space: 'display-p3', + }) + ) + ).toEqual('color(display-p3 0 none 1 / none)')); + }); }); /**