From e6e6762153c8c6a02d289ab078fb1186c10b91f3 Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Wed, 19 Jul 2023 15:36:21 -0600 Subject: [PATCH] Add JS API tests for calculations (#1918) Co-authored-by: Jonny Gerig Meyer --- js-api-spec/function.test.ts | 67 +++- js-api-spec/value/argument-list.test.ts | 1 + js-api-spec/value/boolean.test.ts | 2 + js-api-spec/value/calculation.test.ts | 424 ++++++++++++++++++++++++ js-api-spec/value/color.test.ts | 1 + js-api-spec/value/list.test.ts | 2 + js-api-spec/value/map.test.ts | 1 + js-api-spec/value/null.test.ts | 1 + js-api-spec/value/number.test.ts | 1 + js-api-spec/value/string.test.ts | 1 + 10 files changed, 487 insertions(+), 14 deletions(-) create mode 100644 js-api-spec/value/calculation.test.ts diff --git a/js-api-spec/function.test.ts b/js-api-spec/function.test.ts index e0661444b..c606e5884 100644 --- a/js-api-spec/function.test.ts +++ b/js-api-spec/function.test.ts @@ -8,6 +8,7 @@ import { compileString, compileStringAsync, sassNull, + SassCalculation, } from 'sass'; import {spy} from './utils'; @@ -98,14 +99,27 @@ describe('gracefully handles a custom function', () => { ).toThrowSassException({line: 0}); }); - it('returning a non-Value', () => { - expect(() => - compileString('a {b: foo()}', { - functions: { - 'foo()': (() => 'wrong') as unknown as CustomFunction<'sync'>, - }, - }) - ).toThrowSassException({line: 0}); + describe('returning a non-Value', () => { + it('directly', () => { + expect(() => + compileString('a {b: foo()}', { + functions: { + 'foo()': (() => 'wrong') as unknown as CustomFunction<'sync'>, + }, + }) + ).toThrowSassException({line: 0}); + }); + + it('in a calculation', () => { + expect(() => + compileString('a {b: foo()}', { + functions: { + 'foo()': () => + SassCalculation.calc('wrong' as unknown as SassString), + }, + }) + ).toThrowSassException({line: 0}); + }); }); }); @@ -151,12 +165,37 @@ describe('asynchronously', () => { expect(fn).toHaveBeenCalled(); }); - it('gracefully handles promise rejections', async () => { - await expectAsync(() => - compileStringAsync('a {b: foo(bar)}', { - functions: {'foo($arg)': () => Promise.reject('heck')}, - }) - ).toThrowSassException({line: 0}); + describe('gracefully handles', () => { + it('promise rejections', async () => { + await expectAsync(() => + compileStringAsync('a {b: foo(bar)}', { + functions: {'foo($arg)': () => Promise.reject('heck')}, + }) + ).toThrowSassException({line: 0}); + }); + + describe('returning a non-Value', () => { + it('directly', async () => { + await expectAsync(() => + compileStringAsync('a {b: foo()}', { + functions: { + 'foo()': (() => 'wrong') as unknown as CustomFunction<'async'>, + }, + }) + ).toThrowSassException({line: 0}); + }); + + it('in a calculation', async () => { + await expectAsync(() => + compileStringAsync('a {b: foo()}', { + functions: { + 'foo()': () => + SassCalculation.calc('wrong' as unknown as SassString), + }, + }) + ).toThrowSassException({line: 0}); + }); + }); }); }); diff --git a/js-api-spec/value/argument-list.test.ts b/js-api-spec/value/argument-list.test.ts index b455c460d..ef900126d 100644 --- a/js-api-spec/value/argument-list.test.ts +++ b/js-api-spec/value/argument-list.test.ts @@ -97,6 +97,7 @@ describe('SassArgumentList', () => { it("isn't any other type", () => { expect(() => list.assertBoolean()).toThrow(); + expect(() => list.assertCalculation()).toThrow(); expect(() => list.assertColor()).toThrow(); expect(() => list.assertFunction()).toThrow(); expect(() => list.assertMap()).toThrow(); diff --git a/js-api-spec/value/boolean.test.ts b/js-api-spec/value/boolean.test.ts index 62123e7a4..c830e8f25 100644 --- a/js-api-spec/value/boolean.test.ts +++ b/js-api-spec/value/boolean.test.ts @@ -25,6 +25,7 @@ describe('Sass boolean', () => { }); it("isn't any other type", () => { + expect(value.assertCalculation).toThrow(); expect(value.assertColor).toThrow(); expect(value.assertFunction).toThrow(); expect(value.assertMap).toThrow(); @@ -54,6 +55,7 @@ describe('Sass boolean', () => { }); it("isn't any other type", () => { + expect(value.assertCalculation).toThrow(); expect(value.assertColor).toThrow(); expect(value.assertFunction).toThrow(); expect(value.assertMap).toThrow(); diff --git a/js-api-spec/value/calculation.test.ts b/js-api-spec/value/calculation.test.ts new file mode 100644 index 000000000..a2b6c0ae3 --- /dev/null +++ b/js-api-spec/value/calculation.test.ts @@ -0,0 +1,424 @@ +// Copyright 2023 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import { + Value, + SassCalculation, + SassNumber, + SassString, + CalculationOperation, + CalculationOperator, + CalculationInterpolation, + compileString, + compileStringAsync, +} from 'sass'; +import {List} from 'immutable'; + +import '../utils'; + +const validCalculationValues = [ + new SassNumber(1), + new SassString('1', {quotes: false}), + SassCalculation.calc(new SassNumber(1)), + new CalculationOperation('+', new SassNumber(1), new SassNumber(1)), + new CalculationInterpolation(''), +]; +const invalidCalculationValues = [new SassString('1', {quotes: true})]; + +describe('SassCalculation', () => { + describe('construction', () => { + let calculation: SassCalculation; + beforeEach(() => { + calculation = SassCalculation.calc(new SassNumber(1)); + }); + + it('is a value', () => { + expect(calculation).toBeInstanceOf(Value); + }); + + it('is a calculation', () => { + expect(calculation).toBeInstanceOf(SassCalculation); + expect(calculation.assertCalculation()).toBe(calculation); + }); + + it("isn't any other type", () => { + expect(() => calculation.assertBoolean()).toThrow(); + expect(() => calculation.assertColor()).toThrow(); + expect(() => calculation.assertFunction()).toThrow(); + expect(() => calculation.assertMap()).toThrow(); + expect(calculation.tryMap()).toBe(null); + expect(() => calculation.assertNumber()).toThrow(); + expect(() => calculation.assertString()).toThrow(); + }); + }); + + describe('calc', () => { + it('correctly stores name and arguments', () => { + const result = SassCalculation.calc(new SassNumber(1)); + expect(result.name).toBe('calc'); + expect(result.arguments).toEqualWithHash(List([new SassNumber(1)])); + }); + + it('rejects invalid arguments', () => { + for (const value of invalidCalculationValues) { + expect(() => SassCalculation.calc(value)).toThrow(); + } + }); + + it('accepts valid arguments', () => { + for (const value of validCalculationValues) { + expect(() => SassCalculation.calc(value)).not.toThrow(); + } + }); + }); + + describe('min', () => { + it('correctly stores name and arguments', () => { + const result = SassCalculation.min([ + new SassNumber(1), + new SassNumber(2), + ]); + expect(result.name).toBe('min'); + expect(result.arguments).toEqualWithHash( + List([new SassNumber(1), new SassNumber(2)]) + ); + }); + + it('rejects invalid arguments', () => { + for (const value of invalidCalculationValues) { + expect(() => SassCalculation.min([value, new SassNumber(2)])).toThrow(); + expect(() => SassCalculation.min([new SassNumber(1), value])).toThrow(); + } + }); + + it('accepts valid arguments', () => { + for (const value of validCalculationValues) { + expect(() => + SassCalculation.min([value, new SassNumber(2)]) + ).not.toThrow(); + expect(() => + SassCalculation.min([new SassNumber(1), value]) + ).not.toThrow(); + } + }); + }); + + describe('max', () => { + it('correctly stores name and arguments', () => { + const result = SassCalculation.max([ + new SassNumber(1), + new SassNumber(2), + ]); + expect(result.name).toBe('max'); + expect(result.arguments).toEqualWithHash( + List([new SassNumber(1), new SassNumber(2)]) + ); + }); + + it('rejects invalid arguments', () => { + for (const value of invalidCalculationValues) { + expect(() => SassCalculation.max([value, new SassNumber(2)])).toThrow(); + expect(() => SassCalculation.max([new SassNumber(1), value])).toThrow(); + } + }); + + it('accepts valid arguments', () => { + for (const value of validCalculationValues) { + expect(() => + SassCalculation.max([value, new SassNumber(2)]) + ).not.toThrow(); + expect(() => + SassCalculation.max([new SassNumber(1), value]) + ).not.toThrow(); + } + }); + }); + + describe('clamp', () => { + it('correctly stores name and arguments', () => { + const result = SassCalculation.clamp( + new SassNumber(1), + new SassNumber(2), + new SassNumber(3) + ); + expect(result.name).toBe('clamp'); + expect(result.arguments).toEqualWithHash( + List([new SassNumber(1), new SassNumber(2), new SassNumber(3)]) + ); + }); + + it('rejects invalid arguments', () => { + for (const value of invalidCalculationValues) { + expect(() => + SassCalculation.clamp(value, new SassNumber(2), new SassNumber(3)) + ).toThrow(); + expect(() => + SassCalculation.clamp(new SassNumber(1), value, new SassNumber(3)) + ).toThrow(); + expect(() => + SassCalculation.clamp(new SassNumber(1), new SassNumber(2), value) + ).toThrow(); + } + }); + + it('accepts valid arguments', () => { + for (const value of validCalculationValues) { + expect(() => + SassCalculation.clamp(value, new SassNumber(2), new SassNumber(3)) + ).not.toThrow(); + expect(() => + SassCalculation.clamp(new SassNumber(1), value, new SassNumber(3)) + ).not.toThrow(); + expect(() => + SassCalculation.clamp(new SassNumber(1), new SassNumber(2), value) + ).not.toThrow(); + } + }); + + // When `clamp()` is called with less than three arguments, the list of + // accepted values is much narrower + const validClampValues = [ + new SassString('1', {quotes: false}), + new CalculationInterpolation('1'), + ]; + const invalidClampValues = [ + new SassNumber(1), + new SassString('1', {quotes: true}), + ]; + + it('rejects invalid values for one argument', () => { + for (const value of invalidClampValues) { + expect(() => SassCalculation.clamp(value)).toThrow(); + } + }); + + it('accepts valid values for one argument', () => { + for (const value of validClampValues) { + expect(() => SassCalculation.clamp(value)).not.toThrow(); + } + }); + + it('rejects invalid values for two arguments', () => { + for (const value of invalidClampValues) { + expect(() => SassCalculation.clamp(value, value)).toThrow(); + } + }); + + it('accepts valid values for two arguments', () => { + for (const value of validClampValues) { + expect(() => SassCalculation.clamp(value, value)).not.toThrow(); + } + }); + }); + + describe('simplifies', () => { + it('calc()', () => { + const fn = () => + SassCalculation.calc( + new CalculationOperation('+', new SassNumber(1), new SassNumber(2)) + ); + + expect( + compileString('a {b: foo()}', { + functions: {'foo()': fn}, + }).css + ).toBe('a {\n b: 3;\n}'); + }); + + it('clamp()', () => { + const fn = () => + SassCalculation.clamp( + new SassNumber(1), + new SassNumber(2), + new SassNumber(3) + ); + + expect( + compileString('a {b: foo()}', { + functions: {'foo()': fn}, + }).css + ).toBe('a {\n b: 2;\n}'); + }); + + it('min()', () => { + const fn = () => + SassCalculation.min([new SassNumber(1), new SassNumber(2)]); + + expect( + compileString('a {b: foo()}', { + functions: {'foo()': fn}, + }).css + ).toBe('a {\n b: 1;\n}'); + }); + + it('max()', () => { + const fn = () => + SassCalculation.max([new SassNumber(1), new SassNumber(2)]); + + expect( + compileString('a {b: foo()}', { + functions: {'foo()': fn}, + }).css + ).toBe('a {\n b: 2;\n}'); + }); + + it('operations', () => { + const fn = () => + SassCalculation.calc( + new CalculationOperation( + '+', + SassCalculation.min([new SassNumber(3), new SassNumber(4)]), + new CalculationOperation( + '*', + SassCalculation.max([new SassNumber(5), new SassNumber(6)]), + new CalculationOperation( + '-', + new SassNumber(3), + new CalculationOperation( + '/', + new SassNumber(4), + new SassNumber(5) + ) + ) + ) + ) + ); + + expect( + compileString('a {b: foo()}', { + functions: {'foo()': fn}, + }).css + ).toBe('a {\n b: 16.2;\n}'); + }); + + it('asynchronously', async () => { + const fn = async () => + SassCalculation.calc( + new CalculationOperation('+', new SassNumber(1), new SassNumber(2)) + ); + + const result = await compileStringAsync('a {b: foo()}', { + functions: {'foo()': fn}, + }); + expect(result.css).toBe('a {\n b: 3;\n}'); + }); + }); + + describe('throws when simplifying', () => { + it('calc() with more than one argument', () => { + const fn = () => + // @ts-expect-error: Call `calc` with the wrong number of arguments + new SassCalculation('calc', new SassNumber(1), new SassNumber(2)); + expect(() => + compileString('a {b: foo()}', { + functions: {'foo()': fn}, + }) + ).toThrow(); + }); + + it('clamp() with the wrong number of arguments', () => { + const fn = () => SassCalculation.clamp(new CalculationInterpolation('1')); + expect(() => + compileString('a {b: foo()}', { + functions: {'foo()': fn}, + }) + ).toThrowError(/exactly 3 arguments/); + }); + + it('an unknown calculation function', () => { + const foo = SassCalculation.calc(new SassNumber(1)); + // @ts-expect-error: Assign to read-only property + foo.name = 'foo'; + expect(() => + compileString('a {b: foo()}', { + functions: {'foo()': () => foo}, + }) + ).toThrowError(/"foo" is not a recognized calculation type/); + }); + }); +}); + +describe('CalculationOperation', () => { + const validOperators = ['+', '-', '*', '/']; + const invalidOperators = ['||', '&&', 'plus', 'minus', '']; + + describe('construction', () => { + it('rejects invalid operators', () => { + for (const operator of invalidOperators) { + expect( + () => + new CalculationOperation( + operator as CalculationOperator, + new SassNumber(1), + new SassNumber(2) + ) + ).toThrow(); + } + }); + + it('accepts valid operators', () => { + for (const operator of validOperators) { + expect( + () => + new CalculationOperation( + operator as CalculationOperator, + new SassNumber(1), + new SassNumber(2) + ) + ).not.toThrow(); + } + }); + + it('rejects invalid operands', () => { + for (const operand of invalidCalculationValues) { + expect( + () => new CalculationOperation('+', operand, new SassNumber(1)) + ).toThrow(); + expect( + () => new CalculationOperation('+', new SassNumber(1), operand) + ).toThrow(); + } + }); + + it('accepts valid operands', () => { + for (const operand of validCalculationValues) { + expect( + () => new CalculationOperation('+', operand, new SassNumber(1)) + ).not.toThrow(); + expect( + () => new CalculationOperation('+', new SassNumber(1), operand) + ).not.toThrow(); + } + }); + }); + + describe('stores', () => { + let operation: CalculationOperation; + beforeEach(() => { + operation = new CalculationOperation( + '+', + new SassNumber(1), + new SassNumber(2) + ); + }); + + it('operator', () => { + expect(operation.operator).toBe('+'); + }); + + it('left', () => { + expect(operation.left).toEqual(new SassNumber(1)); + }); + + it('right', () => { + expect(operation.right).toEqual(new SassNumber(2)); + }); + }); +}); + +describe('CalculationInterpolation', () => { + it('stores value', () => { + expect(new CalculationInterpolation('1').value).toEqual('1'); + }); +}); diff --git a/js-api-spec/value/color.test.ts b/js-api-spec/value/color.test.ts index 5133666c8..1a6c6bfa2 100644 --- a/js-api-spec/value/color.test.ts +++ b/js-api-spec/value/color.test.ts @@ -53,6 +53,7 @@ describe('SassColor', () => { it("isn't any other type", () => { expect(() => color.assertBoolean()).toThrow(); + expect(() => color.assertCalculation()).toThrow(); expect(() => color.assertFunction()).toThrow(); expect(() => color.assertMap()).toThrow(); expect(color.tryMap()).toBe(null); diff --git a/js-api-spec/value/list.test.ts b/js-api-spec/value/list.test.ts index 1fc25ea96..e7d784fca 100644 --- a/js-api-spec/value/list.test.ts +++ b/js-api-spec/value/list.test.ts @@ -25,6 +25,7 @@ describe('SassList', () => { it("isn't any other type", () => { expect(() => list.assertBoolean()).toThrow(); + expect(() => list.assertCalculation()).toThrow(); expect(() => list.assertColor()).toThrow(); expect(() => list.assertFunction()).toThrow(); expect(() => list.assertMap()).toThrow(); @@ -354,6 +355,7 @@ describe('SassList', () => { it("isn't any other type", () => { expect(() => list.assertBoolean()).toThrow(); + expect(() => list.assertCalculation()).toThrow(); expect(() => list.assertColor()).toThrow(); expect(() => list.assertFunction()).toThrow(); expect(() => list.assertNumber()).toThrow(); diff --git a/js-api-spec/value/map.test.ts b/js-api-spec/value/map.test.ts index a4d3af735..36a3716a3 100644 --- a/js-api-spec/value/map.test.ts +++ b/js-api-spec/value/map.test.ts @@ -29,6 +29,7 @@ describe('SassMap', () => { it("isn't any other type", () => { expect(() => map.assertBoolean()).toThrow(); + expect(() => map.assertCalculation()).toThrow(); expect(() => map.assertColor()).toThrow(); expect(() => map.assertFunction()).toThrow(); expect(() => map.assertNumber()).toThrow(); diff --git a/js-api-spec/value/null.test.ts b/js-api-spec/value/null.test.ts index 47f2a45ea..2ed3d37df 100644 --- a/js-api-spec/value/null.test.ts +++ b/js-api-spec/value/null.test.ts @@ -25,6 +25,7 @@ describe('Sass null', () => { it("isn't any type", () => { expect(value.assertBoolean).toThrow(); + expect(value.assertCalculation).toThrow(); expect(value.assertColor).toThrow(); expect(value.assertFunction).toThrow(); expect(value.assertMap).toThrow(); diff --git a/js-api-spec/value/number.test.ts b/js-api-spec/value/number.test.ts index e180d4273..f2493821b 100644 --- a/js-api-spec/value/number.test.ts +++ b/js-api-spec/value/number.test.ts @@ -37,6 +37,7 @@ describe('Sass number', () => { it("isn't any other type", () => { expect(() => number.assertBoolean()).toThrow(); + expect(() => number.assertCalculation()).toThrow(); expect(() => number.assertColor()).toThrow(); expect(() => number.assertFunction()).toThrow(); expect(() => number.assertMap()).toThrow(); diff --git a/js-api-spec/value/string.test.ts b/js-api-spec/value/string.test.ts index a8985e252..abe4fc213 100644 --- a/js-api-spec/value/string.test.ts +++ b/js-api-spec/value/string.test.ts @@ -54,6 +54,7 @@ describe('Sass string', () => { it("isn't any other type", () => { const value: Value = new SassString('nb'); expect(value.assertBoolean).toThrow(); + expect(value.assertCalculation).toThrow(); expect(value.assertColor).toThrow(); expect(value.assertFunction).toThrow(); expect(value.assertMap).toThrow();