apostrophe
Version:
The Apostrophe Content Management System.
1,797 lines (1,733 loc) • 95 kB
JavaScript
const t = require('../test-lib/test.js');
const assert = require('assert/strict');
const universal = import(
'../modules/@apostrophecms/styles/ui/apos/universal/render.mjs'
);
describe('Styles', function () {
this.timeout(t.timeout);
describe('Universal Renderer', function () {
let apos;
before(async function () {
apos = await t.create({
root: module,
modules: {
'@apostrophecms/styles': {}
}
});
});
after(async function () {
return t.destroy(apos);
});
it('should export render styles functions', async function () {
const {
default: renderStyles, renderGlobalStyles, renderScopedStyles
} = await universal;
assert.equal(
typeof renderStyles,
'function',
'Default export should be a function'
);
assert.equal(
typeof renderGlobalStyles,
'function',
'renderGlobalStyles should be a function'
);
assert.equal(
typeof renderScopedStyles,
'function',
'renderScopedStyles should be a function'
);
});
it('should normalize field', async function () {
const { NORMALIZERS } = await universal;
// Basic field
{
const field = {
name: 'testField',
type: 'string',
selector: '.test-class',
property: 'color',
unit: 'px'
};
const actual = NORMALIZERS._(
field,
{
testField: 'red'
}
);
assert.deepEqual(
actual,
{
raw: field,
selectors: [ '.test-class' ],
properties: [ 'color' ],
value: 'red',
unit: 'px'
},
'Basic field should be normalized correctly'
);
}
// Basic field with rootSelector
{
const field = {
name: 'testField',
type: 'string',
selector: '.test-class',
property: 'color',
unit: 'px'
};
const actual = NORMALIZERS._(
field,
{
testField: 'red'
},
{
rootSelector: '#root'
}
);
assert.deepEqual(
actual,
{
raw: field,
selectors: [ '#root .test-class' ],
properties: [ 'color' ],
value: 'red',
unit: 'px'
},
'Basic field with rootSelector should be normalized correctly'
);
}
// Basic field with array selector
{
const field = {
name: 'testField',
type: 'string',
selector: [ '.class1', '.class2' ],
property: 'color',
unit: 'px'
};
const actual = NORMALIZERS._(
field,
{
testField: 'red'
}
);
assert.deepEqual(
actual,
{
raw: field,
selectors: [ '.class1', '.class2' ],
properties: [ 'color' ],
value: 'red',
unit: 'px'
},
'Basic field with array selector should be normalized correctly'
);
}
// Basic field with array property
{
const field = {
name: 'testField',
type: 'string',
selector: '.test-class',
property: [ 'color', 'background-color' ],
unit: 'px'
};
const actual = NORMALIZERS._(
field,
{
testField: 'red'
}
);
assert.deepEqual(
actual,
{
raw: field,
selectors: [ '.test-class' ],
properties: [ 'color', 'background-color' ],
value: 'red',
unit: 'px'
},
'Basic field with array property should be normalized correctly'
);
}
// Basic field with rootSelector and without field selector
{
const field = {
name: 'testField',
type: 'string',
property: 'color',
unit: 'px'
};
const actual = NORMALIZERS._(
field,
{
testField: 'red'
},
{
rootSelector: '#root'
}
);
assert.deepEqual(
actual,
{
raw: field,
selectors: [ '#root' ],
properties: [ 'color' ],
value: 'red',
unit: 'px'
},
'Basic field with rootSelector and without field selector should be normalized correctly'
);
}
// Empty, not relevant to styles field, that will be skipped later
{
const field = {
name: 'hasBorder',
type: 'boolean'
};
const actual = NORMALIZERS._(
field,
{
hasBorder: true
}
);
assert.deepEqual(
actual,
{
raw: field,
selectors: [],
properties: [],
value: true,
unit: ''
},
'Empty field should be normalized correctly'
);
}
});
it('should normalize object field (UI container)', async function () {
const { NORMALIZERS } = await universal;
const subField1 = {
name: 'subField1',
type: 'string',
selector: '.sub-class1',
property: 'color',
unit: 'px'
};
const subField2 = {
name: 'subField2',
type: 'string',
selector: '.sub-class2',
property: 'background-color',
unit: 'px'
};
const field = {
name: 'testObjectField',
type: 'object',
schema: [ subField1, subField2 ]
};
const { subfields: actualSubfields, ...actualObject } = NORMALIZERS.object(
field,
{
testObjectField: {
subField1: 'blue',
subField2: 'green'
}
}
);
assert.deepEqual(
actualObject,
{
raw: field,
selectors: [],
properties: [],
value: {
subField1: 'blue',
subField2: 'green'
}
},
'Object field container should be normalized correctly'
);
assert.deepEqual(
actualSubfields.length,
2,
'Object field should have two subfields'
);
assert.deepEqual(
actualSubfields[0],
{
raw: subField1,
selectors: [ '.sub-class1' ],
properties: [ 'color' ],
value: 'blue',
unit: 'px'
},
'First subfield should be normalized correctly'
);
assert.deepEqual(
actualSubfields[1],
{
raw: subField2,
selectors: [ '.sub-class2' ],
properties: [ 'background-color' ],
value: 'green',
unit: 'px'
},
'Second subfield should be normalized correctly'
);
});
it('should normalize object field with deep selectors', async function () {
const { NORMALIZERS } = await universal;
const isEnabled = {
name: 'isEnabled',
type: 'boolean'
};
const subField1 = {
name: 'subField1',
type: 'string',
selector: '.sub-class1',
property: 'color',
unit: 'px'
};
const subField2 = {
name: 'subField2',
type: 'string',
property: 'background-color',
unit: 'px'
};
const field = {
name: 'testObjectField',
type: 'object',
selector: '.object-class',
valueTemplate: '%subField1% %subField2%',
schema: [ isEnabled, subField1, subField2 ]
};
const actual = NORMALIZERS.object(
field,
{
testObjectField: {
isEnabled: true,
subField1: 'blue',
subField2: 'green'
}
},
{
rootSelector: '#root'
}
);
const { subfields: actualSubfieds, ...actualObject } = actual;
assert.deepEqual(
actualObject,
{
raw: field,
selectors: [ '#root .object-class' ],
properties: [],
value: {
isEnabled: true,
subField1: 'blue',
subField2: 'green'
},
valueTemplate: '%subField1% %subField2%'
},
'Object field root field should be normalized correctly'
);
assert.equal(
actualSubfieds.length,
2,
'Object field should have two subfields'
);
assert.deepEqual(
actualSubfieds[0],
{
raw: subField1,
selectors: [ '#root .object-class .sub-class1' ],
properties: [ 'color' ],
value: 'blue',
unit: 'px'
},
'First subfield should be normalized correctly'
);
assert.deepEqual(
actualSubfieds[1],
{
raw: subField2,
selectors: [ '#root .object-class' ],
properties: [ 'background-color' ],
value: 'green',
unit: 'px'
},
'Second subfield should be normalized correctly'
);
});
it('should normalize object field with only root selector', async function () {
const { NORMALIZERS } = await universal;
const subField1 = {
name: 'subField1',
type: 'string',
property: '--primary-color'
};
const field = {
name: 'testObjectField',
type: 'object',
schema: [ subField1 ]
};
const actual = NORMALIZERS.object(
field,
{
testObjectField: {
subField1: 'blue'
}
},
{
rootSelector: ':root'
}
);
const { subfields: actualSubfieds, ...actualObject } = actual;
assert.deepEqual(
actualObject,
{
raw: field,
selectors: [ ':root' ],
properties: [],
value: {
subField1: 'blue'
}
},
'Object field root field should be normalized correctly'
);
assert.equal(
actualSubfieds.length,
1,
'Object field should have one subfield'
);
assert.deepEqual(
actualSubfieds[0],
{
raw: subField1,
selectors: [ ':root' ],
properties: [ '--primary-color' ],
value: 'blue',
unit: ''
},
'First subfield should be normalized correctly'
);
});
});
describe('Setup', function () {
let apos;
before(async function () {
apos = await t.create({
root: module,
modules: {
'@apostrophecms/styles': {
styles(self, options) {
return {
add: {
border: {
preset: 'border',
selector: '.border-style'
},
width: {
preset: 'width',
label: 'Custom Width',
selector: '.custom-width'
},
borderCard: {
preset: 'border',
label: 'Card Border',
selector: '.card'
}
},
group: {
typography: {
label: 'Styles',
group: {
border: {
label: 'Border',
inline: true,
fields: [ 'border' ]
},
width: {
label: 'Width',
fields: [ 'width' ]
},
borderCard: {
label: 'Border Card',
fields: [ 'borderCard' ]
}
}
}
}
};
},
extendMethods(self) {
return {
registerPresets(_super) {
_super();
// Extend an existing one:
const widthPreset = self.getPreset('width');
// Ensure we really change the default
assert.equal(
widthPreset.step,
10,
'Width preset should have step 10 by default'
);
widthPreset.step = 1;
self.setPreset('width', widthPreset);
// Add a custom one:
self.setPreset('customPreset', {
label: 'Custom Preset',
type: 'string',
def: 'custom',
property: 'customProperty'
});
// Test validation
assert.throws(
() => {
self.setPreset('invalidPreset', {
label: 'Invalid Preset'
});
},
{
message: /Preset must be an object with a "type" property/i
},
'Should validate preset type while registering'
);
assert.throws(
() => {
self.setPreset('anotherInvalidPreset', null);
},
{
message: /Preset must be an object with a "type" property/i
},
'Should validate null preset while registering'
);
},
// Test that presets getter fails before registration.
composeSchema(_super, ...args) {
assert.throws(
() => {
self.getPreset('width');
},
{
message: /Presets have not been initialzed yet/i
},
'Should not be able to get presets before they are registered'
);
return _super(...args);
}
};
}
},
'test-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Test Widget'
},
styles: {
add: {
border: 'border',
width: {
preset: 'width',
label: 'Custom Width',
selector: '.custom-width'
},
backgroundColor: {
label: 'Background Color',
type: 'color',
required: true
}
}
},
fields: {
add: {
title: {
type: 'string',
label: 'Title'
},
aStyle: {
type: 'string',
label: 'A Style'
}
},
// Explicit existing groups
group: {
basics: {
label: 'Basics',
fields: [ 'title' ]
},
styles: {
label: 'Styles',
fields: [ 'aStyle' ]
}
}
}
},
'test-empty-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Test Groups Widget'
}
},
'test-empty-styles-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Test Groups Widget'
},
fields: {
add: {
myField: {
type: 'string',
label: 'My Field'
}
}
}
},
'test-empty-fields-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Test Empty Fields Widget'
},
styles: {
add: {
width: {
preset: 'width',
label: 'Custom Width',
selector: '.card'
}
}
}
},
'test-nogroups-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Test No Groups Widget'
},
fields: {
add: {
myField: {
type: 'string',
label: 'A Style'
}
}
},
styles: {
add: {
width: {
preset: 'width',
label: 'Custom Width',
selector: '.card'
}
}
}
}
}
});
});
after(async function () {
return t.destroy(apos);
});
it('@apostrophecms/styles should exist', async function () {
assert(
apos.modules['@apostrophecms/styles'],
'@apostrophecms/styles module should exist'
);
assert(apos.styles, 'Alias `apos.styles` should exist');
});
it('should cascade styles (@apostrophecms/styles)', async function () {
assert(apos.styles.styles, '@apostrophecms/styles should have a styles property');
const schema = apos.styles.schema;
const borderField = schema.find(field => field.name === 'border');
const widthField = schema.find(field => field.name === 'width');
const borderCardField = schema.find(field => field.name === 'borderCard');
assert.equal(borderField?.type, 'object', 'Border field not expanded correctly');
assert.equal(borderField.group.name, 'ungrouped', 'Styles groups should be ungrouped');
assert.equal(widthField?.type, 'range', 'Width field not expanded correctly');
assert.equal(widthField.group.name, 'ungrouped', 'Styles groups should be ungrouped');
assert.equal(
borderCardField?.type,
'object',
'Border Card field not expanded correctly'
);
assert.equal(borderCardField.group.name, 'ungrouped', 'Styles groups should be ungrouped');
});
it('should cascade styles (widgets)', async function () {
assert(apos.modules['test-widget'].styles, 'test-widget should have a styles property');
const schema = apos.modules['test-widget'].schema;
const borderField = schema.find(field => field.name === 'border');
const widthField = schema.find(field => field.name === 'width');
const backgroundColorField = schema.find(field => field.name === 'backgroundColor');
const titleField = schema.find(field => field.name === 'title');
const aStyleField = schema.find(field => field.name === 'aStyle');
assert.equal(borderField?.type, 'object', 'Border field not expanded correctly');
assert.equal(borderField.group.name, 'styles', 'Border field not in styles group');
assert.equal(widthField?.type, 'range', 'Width field not expanded correctly');
assert.equal(widthField.group.name, 'styles', 'Width field not in styles group');
assert.equal(
backgroundColorField?.type,
'color',
'Background Color field not expanded correctly'
);
assert.equal(
backgroundColorField.group.name,
'styles',
'Background Color field not in styles group'
);
assert.deepEqual(
apos.modules['test-widget'].fieldsGroups.styles.fields,
[ 'border', 'width', 'backgroundColor', 'aStyle' ],
'Styles group should contain all style fields in the right order'
);
assert.equal(titleField?.type, 'string', 'Title field should remain unchanged');
assert.equal(titleField.group.name, 'basics', 'Title field should remain in basics group');
assert.equal(aStyleField?.type, 'string', 'aStyle field should remain unchanged');
assert.equal(aStyleField.group.name, 'styles', 'sStyle field should be in styles group');
});
it('should handle empty styles and fields groups (widgets)', async function () {
assert.deepEqual(
apos.modules['test-empty-widget'].fieldsGroups,
{},
'Fields groups should be empty'
);
});
it('should handle empty styles groups (widgets)', async function () {
assert.deepEqual(
apos.modules['test-empty-styles-widget'].fieldsGroups,
{},
'Fields groups should be empty'
);
});
it('should handle empty fields groups (widgets)', async function () {
assert.deepEqual(
apos.modules['test-empty-fields-widget'].fieldsGroups,
{
styles: {
label: 'apostrophe:styles',
fields: [ 'width' ]
}
},
'Fields groups should contain basics and styles groups'
);
});
it('should handle fields and styles groups (widgets)', async function () {
assert.deepEqual(
apos.modules['test-nogroups-widget'].fieldsGroups,
{
basics: {
label: 'apostrophe:basics',
fields: [ 'myField' ]
},
styles: {
label: 'apostrophe:styles',
fields: [ 'width' ]
}
},
'Fields groups should contain basics and styles groups'
);
});
it('should register and retrieve presets', async function () {
const borderPreset = apos.styles.getPreset('border');
const widthPreset = apos.styles.getPreset('width');
const customPreset = apos.styles.getPreset('customPreset');
assert(borderPreset, 'Border preset should be registered');
assert(widthPreset, 'Width preset should be registered');
assert.equal(
widthPreset.step,
1,
'Width preset should be modified correctly'
);
assert(customPreset, 'Custom preset should be registered');
assert.equal(
customPreset.type,
'string',
'Custom preset should be registered correctly'
);
});
it('should fail to set presets after initialization', async function () {
assert.throws(
() => {
apos.styles.setPreset('anotherPreset', {
label: 'Another Preset',
type: 'string',
property: 'anotherProperty'
});
},
{
message: /Attempt to set preset anotherPreset after initialization/i
},
'Should throw an error when trying to set a preset after initialization'
);
});
});
describe('Object styles', function () {
let apos;
// A multi-field as selector
const styleSelectorConfig = (options) => ({
border: {
label: 'apostrophe:styleBorder',
type: 'object',
selector: '.border-style',
fields: {
add: {
active: {
label: 'apostrophe:styleBorder',
type: 'boolean',
def: false
},
width: {
label: 'apostrophe:styleWidth',
type: 'box',
def: {
top: 1,
right: 1,
bottom: 1,
left: 1
},
if: {
active: true
},
unit: 'px',
property: 'border-%key%-width'
},
radius: {
label: 'apostrophe:styleRadius',
type: 'range',
min: 0,
max: 32,
def: 0,
if: {
active: true
},
property: 'border-radius',
unit: 'px'
},
color: {
label: 'apostrophe:styleColor',
type: 'color',
def: options.borderColor,
if: {
active: true
},
property: 'border-color'
},
style: {
label: 'apostrophe:styleStyle',
type: 'select',
def: 'solid',
if: {
active: true
},
choices: [
{
label: 'apostrophe:styleSolid',
value: 'solid'
},
{
label: 'apostrophe:styleDotted',
value: 'dotted'
},
{
label: 'apostrophe:styleDashed',
value: 'dashed'
}
],
property: 'border-style'
}
}
}
}
});
const inlineStyleConfig = (options) => ({
border: {
label: 'apostrophe:styleBorder',
type: 'object',
fields: {
add: {
width: {
label: 'apostrophe:styleWidth',
type: 'box',
def: {
top: 1,
right: 1,
bottom: 1,
left: 1
},
unit: 'px',
property: 'border-%key%-width'
},
radius: {
label: 'apostrophe:styleRadius',
type: 'range',
min: 0,
max: 32,
def: 0,
property: 'border-radius',
unit: 'px'
},
color: {
label: 'apostrophe:styleColor',
type: 'color',
def: options.borderColor,
property: 'border-color'
},
style: {
label: 'apostrophe:styleStyle',
type: 'select',
def: 'solid',
choices: [
{
label: 'apostrophe:styleSolid',
value: 'solid'
},
{
label: 'apostrophe:styleDotted',
value: 'dotted'
},
{
label: 'apostrophe:styleDashed',
value: 'dashed'
}
],
property: 'border-style'
}
}
}
}
});
const classesStyleConfig = () => ({
alignSelect: {
type: 'select',
class: true,
choices: [
{
label: 'None',
value: ''
},
{
label: 'Left',
value: 'apos-left'
},
{
label: 'Center',
value: 'apos-center'
},
{
label: 'Right',
value: 'apos-right'
}
],
def: ''
},
leftBoolean: {
type: 'boolean',
class: 'apos-left',
def: false
},
checkboxes: {
type: 'checkboxes',
class: true,
choices: [
{
label: 'Rounded Corners',
value: 'rounded-corners'
},
{
label: 'Shadow',
value: 'shadow'
},
{
label: 'Border',
value: 'border'
}
]
}
});
const mediaQueryStyleConfig = () => ({
responsivePadding: {
type: 'object',
mediaQuery: '(width > 1200px)',
selector: '.responsive-padding',
fields: {
add: {
desktop: {
type: 'range',
min: 0,
max: 32,
def: 0,
property: 'padding',
unit: 'px'
},
tablet: {
type: 'range',
min: 0,
max: 32,
def: 0,
property: 'padding',
unit: 'px',
mediaQuery: '(560px < width <= 1200px)'
},
mobile: {
type: 'range',
min: 0,
max: 32,
def: 0,
property: 'padding',
unit: 'px',
mediaQuery: '(width <= 560px)'
}
}
}
}
});
const valueTemplateStyleConfig = () => ({
boxShadow: {
type: 'object',
valueTemplate: '%x% %y% %blur% %color%',
property: 'box-shadow',
selector: '.box-shadow',
fields: {
add: {
active: {
type: 'boolean'
},
// Assert no output when one of the fields is missing
x: {
type: 'range',
min: -32,
max: 32,
def: 4,
unit: 'px',
if: {
active: true
}
},
y: {
type: 'range',
min: -32,
max: 32,
def: 4,
unit: 'px'
},
blur: {
type: 'range',
min: 0,
max: 32,
def: 2,
unit: 'px'
},
color: {
type: 'color'
},
standalone: {
type: 'integer',
property: 'width',
// No unit to test interpolation in valueTemplate
valueTemplate: '%VALUE%px'
}
}
}
}
});
const boxValueTemplateConfig = (options) => ({
position: {
type: 'object',
valueTemplate: '%box.top% %box.right% %box.bottom% %box.left%',
property: 'inset',
selector: '.box-position',
fields: {
add: {
active: {
type: 'boolean'
},
box: {
type: 'box',
unit: 'px',
if: {
active: true
}
}
}
}
}
});
before(async function () {
apos = await t.create({
root: module,
modules: {
'@apostrophecms/styles': {
styles(self, options) {
return {
add: {
border: styleSelectorConfig(options).border,
...classesStyleConfig(),
...mediaQueryStyleConfig(),
...valueTemplateStyleConfig(),
...boxValueTemplateConfig()
}
};
}
},
'test-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Test Widget'
},
styles: {
add: {
border: styleSelectorConfig({
borderColor: 'black',
shadowColor: 'gray'
}).border
}
}
},
'test-inline-style-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Test Inline Style Widget'
},
styles: {
add: inlineStyleConfig({
borderColor: 'black',
shadowColor: 'gray'
})
}
},
'test-classes-style-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Test Classes Style Widget'
},
styles: {
add: classesStyleConfig()
}
},
'test-media-query-style-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Test Media Query Style Widget'
},
styles: {
add: mediaQueryStyleConfig()
}
},
'test-value-template-style-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Test Value Template Style Widget'
},
styles: {
add: {
...valueTemplateStyleConfig(),
...boxValueTemplateConfig()
}
}
}
}
});
});
after(async function () {
return t.destroy(apos);
});
it('should render object styles correctly (@apostrophecms/styles)', async function () {
const actual = await apos.styles.getStylesheet(
{
border: {
active: true,
width: {
top: 2,
right: 4,
bottom: 2,
left: 4
},
radius: 8,
color: 'red',
style: 'dashed'
},
boxShadow: {
active: true,
x: 5,
y: 5,
blur: 10,
color: 'rgba(0,0,0,0.5)'
}
}
);
const styles = actual.css;
const actualChecks = {
selector: styles.startsWith('.border-style{'),
selectorEnd: styles.endsWith('}'),
borderTopWidth: styles.includes('border-top-width: 2px'),
borderRightWidth: styles.includes('border-right-width: 4px'),
borderBottomWidth: styles.includes('border-bottom-width: 2px'),
borderLeftWidth: styles.includes('border-left-width: 4px'),
borderRadius: styles.includes('border-radius: 8px;'),
borderColor: styles.includes('border-color: red;'),
borderStyle: styles.includes('border-style: dashed;')
};
assert.deepEqual(actualChecks, {
selector: true,
selectorEnd: true,
borderTopWidth: true,
borderRightWidth: true,
borderBottomWidth: true,
borderLeftWidth: true,
borderRadius: true,
borderColor: true,
borderStyle: true
});
});
it('should render object with selector correctly (widgets)', async function () {
const actual = await apos.modules['test-widget'].getStylesheet(
{
border: {
active: true,
width: {
top: 3,
right: 3,
bottom: 3,
left: 3
},
radius: 12,
color: 'blue',
style: 'dotted'
},
boxShadow: {
active: true,
x: 2,
y: 2,
blur: 5,
color: 'rgba(0,0,0,0.3)'
}
},
'randomStyleId'
);
const styles = actual.css;
const actualChecks = {
selector: styles.startsWith('#randomStyleId .border-style{'),
selectorEnd: styles.endsWith('}'),
borderWidth: styles.includes('border-width: 3px'),
borderTopWidth: styles.includes('border-top-width: 3px'),
borderRightWidth: styles.includes('border-right-width: 3px'),
borderBottomWidth: styles.includes('border-bottom-width: 3px'),
borderLeftWidth: styles.includes('border-left-width: 3px'),
borderRadius: styles.includes('border-radius: 12px;'),
borderColor: styles.includes('border-color: blue;'),
borderStyle: styles.includes('border-style: dotted;')
};
assert.deepEqual(actualChecks, {
selector: true,
selectorEnd: true,
borderWidth: true,
borderTopWidth: false,
borderRightWidth: false,
borderBottomWidth: false,
borderLeftWidth: false,
borderRadius: true,
borderColor: true,
borderStyle: true
});
});
it('should render object as inline style correctly (widgets)', async function () {
const actual = await apos.modules['test-inline-style-widget'].getStylesheet(
{
border: {
active: true,
width: {
top: 3,
right: 3,
bottom: 3,
left: 3
},
radius: 12,
color: 'blue',
style: 'dotted'
},
boxShadow: {
active: true,
x: 2,
y: 2,
blur: 5,
color: 'rgba(0,0,0,0.3)'
}
},
'randomStyleId'
);
assert.equal(actual.css, '');
const styles = actual.inline;
const actualChecks = {
selector: !styles.includes('#randomStyleId{'),
isInline: !styles.includes('{') && !styles.includes('}'),
borderWidth: styles.includes('border-width: 3px'),
borderRadius: styles.includes('border-radius: 12px;'),
borderColor: styles.includes('border-color: blue;'),
borderStyle: styles.includes('border-style: dotted;')
};
assert.deepEqual(actualChecks, {
selector: true,
isInline: true,
borderWidth: true,
borderRadius: true,
borderColor: true,
borderStyle: true
});
});
it('should extract classes from the styles schema (@apostrophecms/styles)', async function () {
const actual = apos.styles.getStylesheet(
{
alignSelect: 'apos-center',
leftBoolean: true,
checkboxes: [ 'rounded-corners', 'shadow' ]
}
);
const classes = actual.classes;
assert.deepEqual(classes.sort(), [
'apos-center',
'apos-left',
'rounded-corners',
'shadow'
].sort()
);
});
it('should extract classes from the styles schema (widget)', async function () {
const actual = apos.modules['test-classes-style-widget'].getStylesheet(
{
alignSelect: 'apos-center',
leftBoolean: true,
checkboxes: [ 'rounded-corners', 'shadow' ]
},
'randomStyleId'
);
assert.equal(actual.css, '');
assert.equal(actual.inline, '');
const classes = actual.classes;
assert.deepEqual(classes.sort(), [
'apos-center',
'apos-left',
'rounded-corners',
'shadow'
].sort()
);
});
it('should render media query styles correctly (@apostrophecms/styles)', async function () {
const actual = apos.styles.getStylesheet(
{
responsivePadding: {
mobile: 4,
tablet: 8,
desktop: 12
}
}
);
assert.equal(actual.inline, undefined);
assert.deepEqual(actual.classes, []);
const styles = actual.css;
assert.equal(
styles,
'@media (width > 1200px){.responsive-padding{padding: 12px;}}' +
'@media (560px < width <= 1200px){.responsive-padding{padding: 8px;}}' +
'@media (width <= 560px){.responsive-padding{padding: 4px;}}'
);
});
it('should render media query styles correctly (widget)', async function () {
const actual = apos.modules['test-media-query-style-widget'].getStylesheet(
{
responsivePadding: {
mobile: 2,
tablet: 6,
desktop: 10
}
},
'randomStyleId'
);
assert.equal(actual.inline, '');
assert.deepEqual(actual.classes, []);
const styles = actual.css;
assert.equal(
styles,
'@media (width > 1200px){#randomStyleId .responsive-padding{padding: 10px;}}' +
'@media (560px < width <= 1200px){#randomStyleId .responsive-padding{padding: 6px;}}' +
'@media (width <= 560px){#randomStyleId .responsive-padding{padding: 2px;}}'
);
});
it('should render value template styles correctly (@apostrophecms/styles)', async function () {
{
const actual = apos.styles.getStylesheet(
{
boxShadow: {
active: true,
x: 3,
y: 3,
blur: 6,
color: 'rgba(0,0,0,0.4)',
standalone: 10
}
}
);
const styles = actual.css;
assert.deepEqual(actual.classes, []);
assert.equal(
styles,
'.box-shadow{box-shadow: 3px 3px 6px rgba(0,0,0,0.4);width: 10px;}',
'Output CSS does not match expected value template output when active'
);
}
{
const actual = apos.styles.getStylesheet(
{
boxShadow: {
active: false,
x: 5,
y: 5,
blur: 10,
color: 'rgba(0,0,0,0.5)',
standalone: 15
}
}
);
const styles = actual.css;
assert.deepEqual(actual.classes, []);
assert.equal(
styles,
'.box-shadow{width: 15px;}',
'Output CSS does not match expected value template output when inactive'
);
}
// Test `%box.value%` interpolation support
{
const actual = apos.styles.getStylesheet(
{
position: {
active: true,
box: {
top: 10,
right: 15,
bottom: 20,
left: 25
}
}
}
);
const styles = actual.css;
assert.deepEqual(actual.classes, []);
assert.equal(
styles,
'.box-position{inset: 10px 15px 20px 25px;}',
'Output CSS does not match expected %box.value% template output'
);
}
// Test `%box.value%` interpolation when inactive
{
const actual = apos.styles.getStylesheet(
{
position: {
active: false,
box: {
top: 10,
right: 15,
bottom: 20,
left: 25
}
}
}
);
const styles = actual.css;
assert.deepEqual(actual.classes, []);
assert.equal(
styles,
'',
'Output CSS does not match expected %box.value% template output'
);
}
});
it('should render value template styles correctly (widget)', async function () {
{
const actual = apos.modules['test-value-template-style-widget'].getStylesheet(
{
boxShadow: {
active: true,
x: 4,
y: 5,
blur: 8,
color: 'rgba(0,0,0,0.6)',
standalone: 12
}
},
'randomStyleId'
);
const styles = actual.css;
assert.deepEqual(actual.classes, []);
assert.equal(
styles,
'#randomStyleId .box-shadow{box-shadow: 4px 5px 8px rgba(0,0,0,0.6);width: 12px;}',
'Output CSS does not match expected value template output when active'
);
}
{
const actual = apos.modules['test-value-template-style-widget'].getStylesheet(
{
boxShadow: {
active: false,
x: 6,
y: 7,
blur: 12,
color: 'rgba(0,0,0,0.7)',
standalone: 18
}
},
'randomStyleId'
);
const styles = actual.css;
assert.deepEqual(actual.classes, []);
assert.equal(
styles,
'#randomStyleId .box-shadow{width: 18px;}',
'Output CSS does not match expected value template output when inactive'
);
}
// Test `%box.value%` interpolation support
{
const actual = apos.modules['test-value-template-style-widget'].getStylesheet(
{
position: {
active: true,
box: {
top: 10,
right: 15,
bottom: 20,
left: 25
}
}
},
'randomStyleId'
);
const styles = actual.css;
assert.deepEqual(actual.classes, []);
assert.equal(
styles,
'#randomStyleId .box-position{inset: 10px 15px 20px 25px;}',
'Output CSS does not match expected %box.value% template output'
);
}
// Test `%box.value%` interpolation disabled when inactive
{
const actual = apos.modules['test-value-template-style-widget'].getStylesheet(
{
position: {
active: false,
box: {
top: 10,
right: 15,
bottom: 20,
left: 25
}
}
},
'randomStyleId'
);
const styles = actual.css;
assert.deepEqual(actual.classes, []);
assert.equal(
styles,
'',
'Output CSS does not match expected %box.value% template output when inactive'
);
}
});
});
describe('Conditional styles', function () {
let apos;
const conditionalStylesSchema = () => ({
// This is first to show that order does not matter
anotherBorder: {
type: 'object',
selector: '.another-border-style',
fields: {
add: {
color: {
type: 'color',
property: 'border-color',
if: {
'<border.active': true
}
}
}
}
},
textColor: {
label: 'Text Color',
type: 'color',
property: '--color',
selector: ':root'
},
backgroundColor: {
label: 'Background Color',
type: 'color',
property: '--background-color',
selector: ':root',
if: {
parentActive: true
}
},
// When background is not visible, highlight is not either,
// no matter the condition value of backgroundColor
// matches.
highlightColor: {
label: 'Highlight Color',
type: 'color',
property: '--highlight-color',
selector: ':root',
if: {
backgroundColor: '#000000'
}
},
border: {
type: 'object',
selector: '.border-style',
fields: {
add: {
width: {
type: 'box',
def: {
top: 1,
right: 1,
bottom: 1,
left: 1
},
if: {
active: true,
'<parentActive': true
},
unit: 'px',
property: 'border-%key%-width'
},
color: {
type: 'color',
if: {
active: true
},
property: 'border-color'
},
// Last to show order does not matter
active: {
type: 'boolean',
def: false
}
}
}
},
// Last to show order does not matter
parentActive: {
type: 'boolean',
def: false
}
});
before(async function () {
apos = await t.create({
root: module,
modules: {
'@apostrophecms/styles': {
styles: {
add: conditionalStylesSchema()
}
},
'test-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Test Widget'
},
styles: {
add: conditionalStylesSchema()
}
}
}
});
});
after(async function () {
return t.destroy(apos);
});
it('should support conditional style fields (@apostrophecms/styles)', async function () {
{
const actual = await apos.styles.getStylesheet(
{
parentActive: false,
border: {
active: false,
width: {
top: 2,
right: 4,
bottom: 2,
left: 4
},
color: 'red'
},
anotherBorder: {
color: 'green'
},
textColor: 'blue',
backgroundColor: '#000000',
highlightColor: 'yellow'
}
);
assert.equal(
actual.css,
':root{--color: blue;}',
'Output CSS does not match expected with `parentActive: false` and `border.active: false`'
);
}
{
const actual = await apos.styles.getStylesheet(
{
parentActive: true,
border: {
active: false,
width: {
top: 2,
right: 2,
bottom: 2,
left: 2
},
color: 'red'
},
anotherBorder: {
color: 'green'
},
textColor: 'blue',
backgroundColor: '#ffffff',
highlightColor: 'yellow'
}
);
assert.equal(
actual.css,
':root{--color: blue;--background-color: #ffffff;}',
'Output CSS does not match expected with `parentActive: true` and `border.active: false`'
);
}
{
const actual = await apos.styles.getStylesheet(
{
parentActive: false,
border: {
active: true,
width: {
top: 3,
right: 3,
bottom: 3,
left: 3
},
color: 'red'
},
anotherBorder: {
color: 'green'
},
textColor: 'blue',
backgroundColor: '#000000',
highlightColor: 'yellow'
}
);
// Key point:
// - `highlightColor` should be omitted because backgroundColor is not visible.
assert.equal(
actual.css,
'.another-border-style{border-color: green;}:root{--color: blue;}.border-style{border-color: red;}',
'Output CSS does n