Skip to content

Commit

Permalink
chore: adds tests to design-system-tailwind-preset for colors and ind…
Browse files Browse the repository at this point in the history
…ex (#51)

## **Description**

This PR adds unit tests for the Tailwind preset package to ensure that
all CSS color variables used in the preset align with those defined in
the `@metamask/design-tokens` package. Specifically, it verifies:

- CSS variables defined in the preset exist in the design tokens
stylesheet
- CSS variables that aren't defined in the preset but exist in the
stylesheet
- Filtering using starts with prefix e.g. `--color` for color object
- Ensuring that no missing or incorrect CSS variables exist, preventing
breakage due to mismatched or non-existent variables.
- Tailwind shortened class names work for text, background and border.
- Tailwind preset configuration is correct

This change improves the reliability of the Tailwind preset by ensuring
consistency with the design tokens.

## **Related issues**

Fixes: #48

## **Manual testing steps**

1. Pull this branch.
2. Run `yarn` in the root of the `metamask-design-system`.
3. Run `yarn workspace @metamask/design-system-tailwind-preset jest` to
confirm that tests pass.
4. In `colors.ts`, change a CSS variable to one that doesn't exist in
the `design-tokens` package.
5. Run `yarn workspace @metamask/design-system-tailwind-preset jest` to
confirm the tests fail.
6. In `node_modules/@metamask/design-tokens/dist/styles.css`, add a
color token `--color-random: #000000`.
7. Save the file.
8. Run `yarn workspace @metamask/design-system-tailwind-preset jest` to
confirm the tests fail.

## **Screenshots/Recordings**

### **After**

The recording demonstrates tests validating the stylesheet CSS against a
preset color object and the Tailwind preset configuration.

`colors.test.ts`


https://github.com/user-attachments/assets/9aa006b0-4a39-4e00-8174-64acdd88bcf9


`index.test.ts`


https://github.com/user-attachments/assets/321f274d-03e9-47c3-9cc1-85ad7ab189b0



100% test coverage

<img width="756" alt="Screenshot 2024-10-17 at 4 30 57 PM"
src="https://github.com/user-attachments/assets/94bd9fee-674d-46e1-bd49-8e915aa94363">


## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs)
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests to verify the changes
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the correct labels on the PR

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR and validated the changes.
- [ ] All acceptance criteria described in the related issue have been
met.
- [ ] Appropriate testing evidence (screenshots, recordings) has been
provided.
  • Loading branch information
georgewrmarshall authored Oct 18, 2024
1 parent 9263aa0 commit d86723d
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 12 deletions.
12 changes: 9 additions & 3 deletions packages/design-system-tailwind-preset/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
* https://jestjs.io/docs/configuration
*/

const merge = require('deepmerge');
const path = require('path');

const baseConfig = require('../../jest.config.packages');

const displayName = path.basename(__dirname);

module.exports = merge(baseConfig, {
module.exports = {
// Spread the base configuration to inherit all default settings
...baseConfig,

// The display name when running multiple projects
displayName,

Expand All @@ -23,4 +25,8 @@ module.exports = merge(baseConfig, {
statements: 100,
},
},
});

// In jest.config.packages.js we are ignoring all index.ts files e.g. coveragePathIgnorePatterns: ['./src/index.ts'],
// We want to include index.ts in coverage so we override the coveragePathIgnorePatterns
coveragePathIgnorePatterns: [],
};
3 changes: 2 additions & 1 deletion packages/design-system-tailwind-preset/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,14 @@
"@types/jest": "^27.4.1",
"deepmerge": "^4.2.2",
"jest": "^27.5.1",
"tailwindcss": "^3.4.13",
"postcss": "^8.4.47",
"ts-jest": "^27.1.4",
"typedoc": "^0.24.8",
"typedoc-plugin-missing-exports": "^2.0.0",
"typescript": "~5.2.2"
},
"peerDependencies": {
"@metamask/design-tokens": "^4.0.0",
"tailwindcss": "^3.0.0"
},
"engines": {
Expand Down
78 changes: 78 additions & 0 deletions packages/design-system-tailwind-preset/scripts/testUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import fs from 'fs';
import path from 'path';
import postcss from 'postcss';

/**
* Parses the design-tokens stylesheet and extracts CSS variable names based on provided prefixes.
*
* This utility function reads the `styles.css` file from the `@metamask/design-tokens` package,
* parses its content using PostCSS, and collects all CSS variables that match any of the specified prefixes.
*
* @param prefixes - (Optional) An array of prefixes to filter CSS variables. Defaults to ['--'].
* For example, ['--color'] will only collect variables that start with '--color'.
* @returns A Promise that resolves to a Set containing the filtered CSS variable names.
*/
export const getDesignTokenVariables = async (
prefixes: string[] = ['--'],
): Promise<Set<string>> => {
// Resolve the absolute path to the styles.css file in the design-tokens package
const stylesheetPath = path.resolve(
__dirname,
'../../../node_modules/@metamask/design-tokens/dist/styles.css',
);

// Asynchronously read the content of the stylesheet
const cssContent = await fs.promises.readFile(stylesheetPath, 'utf-8');

// Parse the CSS content using PostCSS to create an Abstract Syntax Tree (AST)
const parsedRoot = postcss.parse(cssContent);

// Initialize a Set to store the filtered CSS variable names
const variables = new Set<string>();

// Traverse all CSS declarations in the AST
parsedRoot.walkDecls((decl) => {
const { prop } = decl;
// Check if the property name starts with any of the specified prefixes
if (prefixes.some((prefix) => prop.startsWith(prefix))) {
variables.add(prop); // Add the matching variable to the Set
}
});

return variables; // Return the Set of filtered CSS variable names
};

/**
* Recursively traverses an object(e.g. colors, shadows, typography) to collect all CSS variable values.
*
* This utility function traverses a given object, searches for strings that reference CSS variables
* using the `var(--variable-name)` syntax, and collects the names of these variables.
*
* @param obj - The object to traverse. Typically, this would be the preset object containing the CSS variables.
* @returns An array of extracted CSS variable names.
*/
export const collectCssVariables = (obj: Record<string, unknown>): string[] => {
const variables: string[] = []; // Initialize an array to store the found CSS variable names

/**
* Helper function to recursively traverse the object.
*
* @param current - The current value being traversed. Can be of any type.
*/
const traverse = (current: unknown) => {
if (typeof current === 'string') {
// Use a regular expression to match the CSS variable pattern
const match = current.match(/var\((--[^)]+)\)/u);
if (match) {
variables.push(match[1]); // Extract and store the variable name
}
} else if (typeof current === 'object' && current !== null) {
// If the current value is an object, recursively traverse its properties
Object.values(current).forEach((value) => traverse(value));
}
// Non-string and non-object values are ignored
};

traverse(obj); // Start the traversal with the root object
return variables; // Return the array of collected CSS variable names
};
58 changes: 54 additions & 4 deletions packages/design-system-tailwind-preset/src/colors.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,58 @@
import {
getDesignTokenVariables,
collectCssVariables,
} from '../scripts/testUtils';
import { colors } from './colors';

describe('Colors', () => {
// TODO: Implement tests for colors https://github.com/MetaMask/metamask-design-system/issues/48
it('should have a dummy test', () => {
expect(colors).toBeDefined();
describe('Color Preset', () => {
// Collect all CSS variables used in the 'colors' object
const usedVariables = collectCssVariables(colors);

// Define CSS variables to ignore in the unused variables check
const ignoreList: string[] = [
'--color-shadow-default', // used in shadow.ts
'--color-shadow-primary', // used in shadow.ts
'--color-shadow-error', // used in shadow.ts
'--color-flask-default', // not needed for tailwind
'--color-flask-inverse', // not needed for tailwind
];

/**
* Test to ensure that all CSS variables used in the 'colors' object
* are defined in the @metamask/design-tokens package.
*/
it('should use only CSS variables that exist in @metamask/design-tokens', async () => {
// Retrieve all design token variables that start with '--color'
const designTokens = await getDesignTokenVariables(['--color']);

// Identify any used variables that are missing from the design tokens
const missingVariables = usedVariables.filter(
(varName) => !designTokens.has(varName),
);

// Expect no missing variables
expect(missingVariables).toHaveLength(0);
});

/**
* Test to ensure that there are no unused CSS variables in the
* design-tokens package that are not used in the 'colors' object,
* excluding those in the ignore list.
*/
it('should not have unused CSS variables in @metamask/design-tokens', async () => {
// Retrieve all design token variables that start with '--color'
const designTokens = await getDesignTokenVariables(['--color']);

// Create sets for used variables and ignored variables for efficient lookup
const usedSet = new Set(usedVariables);
const ignoredSet = new Set(ignoreList);

// Identify design token variables that are neither used nor ignored
const unusedVariables = Array.from(designTokens).filter(
(varName) => !usedSet.has(varName) && !ignoredSet.has(varName),
);

// Expect no unused variables
expect(unusedVariables).toHaveLength(0);
});
});
71 changes: 69 additions & 2 deletions packages/design-system-tailwind-preset/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,75 @@
import tailwindConfig from '.';
import { colors } from './colors';

describe('Tailwind Preset', () => {
// TODO: Implement tests for Tailwind preset https://github.com/MetaMask/metamask-design-system/issues/48
it('should have a dummy test', () => {
/**
* Structure
*/

it('default export is tailwindConfig', () => {
expect(tailwindConfig).toBeDefined();
});

it('configuration has correct structure', () => {
expect(tailwindConfig).toHaveProperty('content');
expect(tailwindConfig).toHaveProperty('theme.extend');
expect(tailwindConfig).toHaveProperty('plugins');
});

/**
* Colors
*/

it('colors are correctly imported and spread', () => {
expect(tailwindConfig.theme?.extend?.colors).toStrictEqual(
expect.objectContaining(colors),
);
});

it('textColor function incorporates theme colors(e.g. text-primary-default, text-error-default) and text colors(e.g. text-default, text-muted)', () => {
const textColorFn = tailwindConfig.theme?.extend?.textColor as (options: {
theme: (path: string) => unknown;
}) => Record<string, string>;
const result = textColorFn({
theme: (path: string) => (path === 'colors' ? colors : {}),
});
expect(result).toStrictEqual(
expect.objectContaining({
...colors,
...colors.text,
}),
);
});

it('backgroundColor function incorporates theme colors(e.g. bg-primary-default, bg-error-default) and background colors(e.g. bg-default, bg-muted)', () => {
const bgColorFn = tailwindConfig.theme?.extend
?.backgroundColor as (options: {
theme: (path: string) => unknown;
}) => Record<string, string>;
const result = bgColorFn({
theme: (path: string) => (path === 'colors' ? colors : {}),
});
expect(result).toStrictEqual(
expect.objectContaining({
...colors,
...colors.background,
}),
);
});

it('borderColor function incorporates theme colors(e.g. border-primary-default, border-error-default) and border colors(e.g. border-default, border-muted)', () => {
const borderColorFn = tailwindConfig.theme?.extend
?.borderColor as (options: {
theme: (path: string) => unknown;
}) => Record<string, string>;
const result = borderColorFn({
theme: (path: string) => (path === 'colors' ? colors : {}),
});
expect(result).toStrictEqual(
expect.objectContaining({
...colors,
...colors.border,
}),
);
});
});
2 changes: 1 addition & 1 deletion packages/design-system-tailwind-preset/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"baseUrl": "./"
},
"references": [],
"include": ["../../types", "./src"]
"include": ["src/**/*", "scripts/**/*"]
}
3 changes: 2 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1471,12 +1471,13 @@ __metadata:
"@types/jest": "npm:^27.4.1"
deepmerge: "npm:^4.2.2"
jest: "npm:^27.5.1"
tailwindcss: "npm:^3.4.13"
postcss: "npm:^8.4.47"
ts-jest: "npm:^27.1.4"
typedoc: "npm:^0.24.8"
typedoc-plugin-missing-exports: "npm:^2.0.0"
typescript: "npm:~5.2.2"
peerDependencies:
"@metamask/design-tokens": ^4.0.0
tailwindcss: ^3.0.0
languageName: unknown
linkType: soft
Expand Down

0 comments on commit d86723d

Please sign in to comment.