sass-references/angular-material/material/core/theming/tests/theming-mixin-api.spec.ts

291 lines
9.4 KiB
TypeScript

import {parse, Root, Rule} from 'postcss';
import {compileString} from 'sass';
import {runfiles} from '@bazel/runfiles';
import * as path from 'path';
import {compareNodes} from '../../../../../tools/postcss/compare-nodes';
import {createLocalAngularPackageImporter} from '../../../../../tools/sass/local-sass-importer';
// Note: For Windows compatibility, we need to resolve the directory paths through runfiles
// which are guaranteed to reside in the source tree.
const testDir = path.join(runfiles.resolvePackageRelative('../_all-theme.scss'), '../tests');
const packagesDir = path.join(runfiles.resolveWorkspaceRelative('src/cdk/_index.scss'), '../..');
const localPackageSassImporter = createLocalAngularPackageImporter(packagesDir);
describe('theming api', () => {
/** Map of known selectors for density styles and their corresponding AST rule. */
let knownDensitySelectors: Map<string, Rule>;
// Before all tests, we collect all nodes specific to density styles. We can then
// use this check how density styles are generated. i.e. if they are properly scoped to a
// given selector.
beforeAll(() => {
knownDensitySelectors = new Map();
parse(transpile(`@include mat.all-component-densities(0);`)).each(node => {
if (node.type === 'rule') {
node.selectors.forEach(s => knownDensitySelectors.set(s, node));
}
});
});
it('should warn if color styles are duplicated', () => {
spyOn(process.stderr, 'write');
transpile(`
$theme: mat.m2-define-light-theme((
color: (
primary: mat.m2-define-palette(mat.$m2-red-palette),
accent: mat.m2-define-palette(mat.$m2-red-palette),
)
));
@include mat.all-component-themes($theme);
.dark-theme {
@include mat.all-component-themes($theme);
}
`);
expectWarning(/The same color styles are generated multiple times/);
});
it('should not warn if color styles and density are not duplicated', () => {
const parsed = parse(
transpile(`
$theme: mat.m2-define-light-theme((
color: (
primary: mat.m2-define-palette(mat.$m2-red-palette),
accent: mat.m2-define-palette(mat.$m2-red-palette),
)
));
$theme2: mat.m2-define-light-theme((
color: (
primary: mat.m2-define-palette(mat.$m2-red-palette),
accent: mat.m2-define-palette(mat.$m2-blue-palette),
)
));
@include mat.all-component-themes($theme);
.dark-theme {
@include mat.all-component-colors($theme2);
}
`),
);
expect(hasDensityStyles(parsed, null)).toBe('all');
expect(hasDensityStyles(parsed, '.dark-theme')).toBe('none');
expectNoWarning(/The same color styles are generated multiple times/);
});
it('should be possible to modify color configuration directly', () => {
const result = transpile(`
$theme: mat.m2-define-light-theme((
color: (
primary: mat.m2-define-palette(mat.$m2-red-palette),
accent: mat.m2-define-palette(mat.$m2-blue-palette),
)
));
// Updates the "icon" foreground color to hotpink.
$color: map-get($theme, color);
$theme: map-merge($color,
(foreground: map-merge(map-get($color, foreground), (icon: hotpink))));
@include mat.all-component-themes($theme);
`);
expect(result).toContain(': hotpink');
});
it('should warn if default density styles are duplicated', () => {
spyOn(process.stderr, 'write');
const parsed = parse(
transpile(`
@include mat.all-component-themes((color: null));
.dark-theme {
@include mat.all-component-themes((color: null));
}
`),
);
expect(hasDensityStyles(parsed, null)).toBe('all');
// TODO(mmalerba): Re-enable - disabled because this test does not account
// for the fact that:
// ```scss
// @include mat.button-theme($theme);
// @include mat.checkbox-theme($theme);
// ```
// produces different results than:
// ```scss
// html {
// @include mat.button-theme($theme);
// @include mat.checkbox-theme($theme);
// }
// ```
// expect(hasDensityStyles(parsed, '.dark-theme')).toBe('all');
expectWarning(/The same density styles are generated multiple times/);
});
it('should warn if density styles are duplicated', () => {
spyOn(process.stderr, 'write');
transpile(`
@include mat.all-component-themes((density: -1));
.dark-theme {
@include mat.all-component-themes((density: -1));
}
`);
expectWarning(/The same density styles are generated multiple times/);
});
it('should not warn if density styles are not duplicated', () => {
spyOn(process.stderr, 'write');
transpile(`
@include mat.all-component-themes((density: -1));
.dark-theme {
@include mat.all-component-themes((density: -2));
}
`);
expect(process.stderr.write).toHaveBeenCalledTimes(0);
});
it('should warn if typography styles are duplicated', () => {
spyOn(process.stderr, 'write');
transpile(`
$theme: (typography: mat.m2-define-typography-config(), density: null);
@include mat.all-component-themes($theme);
.dark-theme {
@include mat.all-component-themes($theme);
}
`);
expectWarning(/The same typography styles are generated multiple times/);
});
it('should not warn if typography styles are not duplicated', () => {
spyOn(process.stderr, 'write');
transpile(`
@include mat.all-component-themes((
typography: mat.m2-define-typography-config(),
density: null,
));
.dark-theme {
@include mat.all-component-themes((
typography: mat.m2-define-typography-config($font-family: "sans-serif"),
density: null,
));
}
`);
expect(process.stderr.write).toHaveBeenCalledTimes(0);
});
/**
* Checks whether the given parsed stylesheet contains density styles scoped to
* a given selector. If the selector is `null`, then density is expected to be
* generated at top-level.
*/
function hasDensityStyles(parsed: Root, baseSelector: string | null): 'all' | 'partial' | 'none' {
expect(parsed.nodes).withContext('Expected CSS to be not empty.').toBeDefined();
expect(knownDensitySelectors.size).not.toBe(0);
const missingDensitySelectors = new Set(knownDensitySelectors.keys());
const baseSelectorRegex = new RegExp(`^${baseSelector} `, 'g');
// Go through all rules in the stylesheet and check if they match with any
// of the density style selectors. If so, we remove it from the copied set
// of density selectors. If the set is empty at the end, we know that density
// styles have been generated as expected.
parsed.nodes!.forEach(node => {
if (node.type !== 'rule') {
return;
}
node.selectors.forEach(selector => {
if (baseSelector && selector === baseSelector) {
// Styles emitted directly to the baseSelector are emitted to html
// when there is no baseSelector.
selector = 'html';
} else {
// Only check selectors that match the specified base selector.
if (baseSelector && !baseSelectorRegex.test(selector)) {
return;
}
}
selector = selector.replace(baseSelectorRegex, '');
const matchingRule = knownDensitySelectors.get(selector);
if (matchingRule && compareNodes(node, matchingRule)) {
missingDensitySelectors.delete(selector);
}
});
});
// If there are no unmatched density selectors, then it's confirmed that
// all density styles have been generated (scoped to the given selector).
if (missingDensitySelectors.size === 0) {
return 'all';
}
// If no density selector has been matched at all, then no density
// styles have been generated.
if (missingDensitySelectors.size === knownDensitySelectors.size) {
return 'none';
}
console.error('MISSING!!! ', [...missingDensitySelectors].join(','));
return 'partial';
}
/** Transpiles given Sass content into CSS. */
function transpile(content: string) {
return compileString(
`
@use '../../../index' as mat;
${content}
`,
{
loadPaths: [testDir],
importers: [localPackageSassImporter],
},
).css.toString();
}
/** Expects the given warning to be reported in Sass. */
function expectWarning(message: RegExp) {
expect(getMatchingWarning(message))
.withContext('Expected warning to be printed.')
.toBeDefined();
}
/** Expects the given warning not to be reported in Sass. */
function expectNoWarning(message: RegExp) {
expect(getMatchingWarning(message))
.withContext('Expected no warning to be printed.')
.toBeUndefined();
}
/**
* Gets first instance of the given warning reported in Sass. Dart sass directly writes
* to the `process.stderr` stream, so we spy on the `stderr.write` method. We
* cannot expect a specific amount of writes as Sass calls `stderr.write` multiple
* times for a warning (e.g. spacing and stack trace)
*/
function getMatchingWarning(message: RegExp) {
const writeSpy = process.stderr.write as jasmine.Spy;
return (writeSpy.calls?.all() ?? []).find(
(s: jasmine.CallInfo<typeof process.stderr.write>) =>
typeof s.args[0] === 'string' && message.test(s.args[0]),
);
}
});