feat(theme): new CSS cascade layers plugin + built-in v4.useCssCascadeLayers future flag (#11142)

Co-authored-by: slorber <749374+slorber@users.noreply.github.com>
This commit is contained in:
Sébastien Lorber 2025-05-22 19:55:02 +02:00 committed by GitHub
parent a301b24d64
commit abd04a2b71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 894 additions and 0 deletions

View file

@ -0,0 +1,96 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
generateLayersDeclaration,
findLayer,
isValidLayerName,
} from '../layers';
import type {PluginOptions} from '../options';
describe('isValidLayerName', () => {
it('accepts valid names', () => {
expect(isValidLayerName('layer1')).toBe(true);
expect(isValidLayerName('layer1.layer2')).toBe(true);
expect(isValidLayerName('layer-1.layer_2.layer3')).toBe(true);
});
it('rejects layer with coma', () => {
expect(isValidLayerName('lay,er1')).toBe(false);
});
it('rejects layer with space', () => {
expect(isValidLayerName('lay er1')).toBe(false);
});
});
describe('generateLayersDeclaration', () => {
it('for list of layers', () => {
expect(generateLayersDeclaration(['layer1', 'layer2'])).toBe(
'@layer layer1, layer2;',
);
});
it('for empty list of layers', () => {
// Not useful to generate it, but still valid CSS anyway
expect(generateLayersDeclaration([])).toBe('@layer ;');
});
});
describe('findLayer', () => {
const inputFilePath = 'filePath';
function testFor(layers: PluginOptions['layers']) {
return findLayer(inputFilePath, Object.entries(layers));
}
it('for empty layers', () => {
expect(testFor({})).toBeUndefined();
});
it('for single matching layer', () => {
expect(testFor({layer: (filePath) => filePath === inputFilePath})).toBe(
'layer',
);
});
it('for single non-matching layer', () => {
expect(
testFor({layer: (filePath) => filePath !== inputFilePath}),
).toBeUndefined();
});
it('for multiple matching layers', () => {
expect(
testFor({
layer1: (filePath) => filePath === inputFilePath,
layer2: (filePath) => filePath === inputFilePath,
layer3: (filePath) => filePath === inputFilePath,
}),
).toBe('layer1');
});
it('for multiple non-matching layers', () => {
expect(
testFor({
layer1: (filePath) => filePath !== inputFilePath,
layer2: (filePath) => filePath !== inputFilePath,
layer3: (filePath) => filePath !== inputFilePath,
}),
).toBeUndefined();
});
it('for multiple mixed matching layers', () => {
expect(
testFor({
layer1: (filePath) => filePath !== inputFilePath,
layer2: (filePath) => filePath === inputFilePath,
layer3: (filePath) => filePath !== inputFilePath,
layer4: (filePath) => filePath === inputFilePath,
}),
).toBe('layer2');
});
});

View file

@ -0,0 +1,105 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import {
validateOptions,
type PluginOptions,
type Options,
DEFAULT_OPTIONS,
} from '../options';
import type {Validate} from '@docusaurus/types';
function testValidateOptions(options: Options) {
return validateOptions({
validate: normalizePluginOptions as Validate<Options, PluginOptions>,
options,
});
}
describe('validateOptions', () => {
it('accepts undefined options', () => {
// @ts-expect-error: should error
expect(testValidateOptions(undefined)).toEqual(DEFAULT_OPTIONS);
});
it('accepts empty options', () => {
expect(testValidateOptions({})).toEqual(DEFAULT_OPTIONS);
});
describe('layers', () => {
it('accepts empty layers', () => {
expect(testValidateOptions({layers: {}})).toEqual({
...DEFAULT_OPTIONS,
layers: {},
});
});
it('accepts undefined layers', () => {
const config: Options = {
layers: undefined,
};
expect(testValidateOptions(config)).toEqual(DEFAULT_OPTIONS);
});
it('accepts custom layers', () => {
const config: Options = {
layers: {
layer1: (filePath: string) => {
return !!filePath;
},
layer2: (filePath: string) => {
return !!filePath;
},
},
};
expect(testValidateOptions(config)).toEqual({
...DEFAULT_OPTIONS,
layers: config.layers,
});
});
it('rejects layer with bad name', () => {
const config: Options = {
layers: {
'layer 1': (filePath) => !!filePath,
},
};
expect(() =>
testValidateOptions(config),
).toThrowErrorMatchingInlineSnapshot(`""layers.layer 1" is not allowed"`);
});
it('rejects layer with bad value', () => {
const config: Options = {
layers: {
// @ts-expect-error: should error
layer1: 'bad value',
},
};
expect(() =>
testValidateOptions(config),
).toThrowErrorMatchingInlineSnapshot(
`""layers.layer1" must be of type function"`,
);
});
it('rejects layer with bad function arity', () => {
const config: Options = {
layers: {
// @ts-expect-error: should error
layer1: () => {},
},
};
expect(() =>
testValidateOptions(config),
).toThrowErrorMatchingInlineSnapshot(
`""layers.layer1" must have an arity of 1"`,
);
});
});
});