291 lines
9.4 KiB
TypeScript
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]),
|
|
);
|
|
}
|
|
});
|