sass-references/angular-material/material/core/theming/tests/m3-theme.spec.ts

245 lines
8.4 KiB
TypeScript

import {runfiles} from '@bazel/runfiles';
import * as path from 'path';
import {parse} from 'postcss';
import {compileString} from 'sass';
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);
/** Transpiles given Sass content into CSS. */
function transpile(content: string) {
return compileString(
`
@use '../../../index' as mat;
$internals: _mat-theming-internals-do-not-access;
$theme: mat.define-theme();
${content}
`,
{
loadPaths: [testDir],
importers: [localPackageSassImporter],
},
).css.toString();
}
function union<T>(...sets: Set<T>[]): Set<T> {
return new Set(sets.flatMap(s => [...s]));
}
function intersection<T>(set: Set<T>, ...sets: Set<T>[]): Set<T> {
return new Set([...set].filter(i => sets.every(s => s.has(i))));
}
/** 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]),
);
}
describe('M3 theme', () => {
it('should emit all styles under the given selector', () => {
const root = parse(
transpile(`
html {
@include mat.all-component-themes($theme);
@include mat.all-component-bases($theme);
@include mat.all-component-colors($theme);
@include mat.all-component-typographies($theme);
@include mat.all-component-densities($theme);
}
`),
);
const selectors = new Set<string>();
root.walkRules(rule => {
selectors.add(rule.selector);
});
expect(Array.from(selectors)).toEqual(['html']);
});
it('should only emit CSS variables', () => {
const root = parse(transpile(`html { @include mat.all-component-themes($theme); }`));
const nonVarProps: string[] = [];
root.walkDecls(decl => {
if (!decl.prop.startsWith('--')) {
nonVarProps.push(decl.prop);
}
});
expect(nonVarProps).toEqual([]);
});
it('should not have overlapping tokens between theme dimensions', () => {
const css = transpile(`
$theme: mat.define-theme();
base {
@include mat.all-component-bases($theme);
}
color {
@include mat.all-component-colors($theme);
}
typography {
@include mat.all-component-typographies($theme);
}
density {
@include mat.all-component-densities($theme);
}
`);
const root = parse(css);
const propSets: {[key: string]: Set<string>} = {};
root.walkRules(rule => {
rule.walkDecls(decl => {
propSets[rule.selector] = propSets[rule.selector] || new Set();
propSets[rule.selector].add(decl.prop);
});
});
let overlap = new Set();
for (const [dimension1, props1] of Object.entries(propSets)) {
for (const [dimension2, props2] of Object.entries(propSets)) {
if (dimension1 !== dimension2) {
overlap = union(overlap, intersection(props1, props2));
}
}
}
expect([...overlap])
.withContext('Did you forget to wrap these in `_hardcode()`?')
.toEqual([]);
});
it('should throw if theme included at root', () => {
expect(() => transpile(`@include mat.all-component-themes($theme)`)).toThrowError(
/Calls to Angular Material theme mixins with an M3 theme must be wrapped in a selector/,
);
});
describe('theme override API', () => {
beforeEach(() => {
spyOn(process.stderr, 'write').and.callThrough();
});
it('should allow overriding non-ambiguous token value', () => {
const css = transpile(`
div {
@include mat.checkbox-overrides((selected-checkmark-color: magenta));
}
`);
expect(css).toContain('--mdc-checkbox-selected-checkmark-color: magenta');
expectNoWarning(/`selected-checkmark-color` is deprecated/);
});
it('should allow overriding ambiguous token value using prefix', () => {
const css = transpile(`
div {
@include mat.form-field-overrides((filled-caret-color: magenta));
}
`);
expect(css).toContain('--mdc-filled-text-field-caret-color: magenta');
expect(css).not.toContain('--mdc-outline-text-field-caret-color: magenta');
expectNoWarning(/`filled-caret-color` is deprecated/);
});
it('should allow overriding ambiguous token value without using prefix, but warn', () => {
const css = transpile(`
div {
@include mat.form-field-overrides((caret-color: magenta));
}
`);
expect(css).toContain('--mdc-filled-text-field-caret-color: magenta');
expect(css).toContain('--mdc-outlined-text-field-caret-color: magenta');
expectWarning(
/Token `caret-color` is deprecated. Please use one of the following alternatives: filled-caret-color, outlined-caret-color/,
);
});
it('should error on invalid token name', () => {
expect(() =>
transpile(`
div {
@include mat.form-field-overrides((fake: magenta));
}
`),
).toThrowError(/Invalid token name `fake`./);
});
it('should not error when calling theme override functions', () => {
// Ensures that no components have issues with ambiguous token names.
expect(() =>
transpile(`
html {
@include mat.core-overrides(());
@include mat.ripple-overrides(());
@include mat.option-overrides(());
@include mat.optgroup-overrides(());
@include mat.pseudo-checkbox-overrides(());
@include mat.autocomplete-overrides(());
@include mat.badge-overrides(());
@include mat.bottom-sheet-overrides(());
@include mat.button-overrides(());
@include mat.fab-overrides(());
@include mat.icon-button-overrides(());
@include mat.button-toggle-overrides(());
@include mat.card-overrides(());
@include mat.checkbox-overrides(());
@include mat.chips-overrides(());
@include mat.datepicker-overrides(());
@include mat.dialog-overrides(());
@include mat.divider-overrides(());
@include mat.expansion-overrides(());
@include mat.form-field-overrides(());
@include mat.grid-list-overrides(());
@include mat.icon-overrides(());
@include mat.list-overrides(());
@include mat.menu-overrides(());
@include mat.paginator-overrides(());
@include mat.progress-bar-overrides(());
@include mat.progress-spinner-overrides(());
@include mat.radio-overrides(());
@include mat.select-overrides(());
@include mat.sidenav-overrides(());
@include mat.slide-toggle-overrides(());
@include mat.slider-overrides(());
@include mat.snack-bar-overrides(());
@include mat.sort-overrides(());
@include mat.stepper-overrides(());
@include mat.table-overrides(());
@include mat.tabs-overrides(());
@include mat.toolbar-overrides(());
@include mat.tooltip-overrides(());
@include mat.tree-overrides(());
}
`),
).not.toThrow();
});
});
});