Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/theme/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

- `ThemeProvider`: Strictly enforce the documented seed-color input contract for `color.primary` / `color.background`. Inputs outside sRGB (e.g. `oklch()`), previously accepted incidentally, now throw a clear error ([#79148](https://github.com/WordPress/gutenberg/pull/79148)).

### Internal

- Add unit tests for `ThemeProvider` and `useThemeProviderStyles` ([#79126](https://github.com/WordPress/gutenberg/pull/79126)).

## 0.15.0 (2026-06-10)

### New Features
Expand Down
2 changes: 2 additions & 0 deletions packages/theme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@
"@terrazzo/parser": "^2.0.3",
"@terrazzo/plugin-css": "^2.0.3",
"@terrazzo/token-tools": "^2.0.3",
"@testing-library/dom": "^10.4.1",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this dependency? I don't see that we're using it? Or is it used indirectly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need it because it's a mandatory peer dependency of @testing-library/react@16, every other Gutenberg package using react-testing-library declares it

"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/jest": "^29.5.14",
"@types/node": "^20.19.39",
"esbuild": "^0.27.2",
Expand Down
176 changes: 176 additions & 0 deletions packages/theme/src/test/theme-provider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// These tests read the resolved CSS custom properties produced by the provider.
// jsdom does not cascade custom properties to descendant elements, so the value
// is asserted on the provider's own scoping element (where the property is
// defined and from which real children would inherit) and on the document root.

import { render, screen } from '@testing-library/react';
import { ThemeProvider } from '../theme-provider';

// Mock the CSS module so the provider's scoping class is a real, stable class
// name. Without this the global Jest CSS mock leaves the class `undefined`, and
// jsdom cannot match the generated rules to compute the custom properties.
jest.mock( '../style.module.css', () => ( {
root: 'theme-provider-root',
} ) );

// The "strong" brand background resolves to the `color.primary` seed itself, and
// the neutral surface resolves to the `color.background` seed itself, which makes
// the expected values predictable. (Extreme seeds get snapped into the accessible
// ramp, so the seeds below are deliberately mid-range/light hex values that
// round-trip. Ramp generation itself is covered by the color-ramps tests.)
const BRAND_BG = '--wpds-color-background-interactive-brand-strong';
const SURFACE_BG = '--wpds-color-background-surface-neutral';
const CURSOR_CONTROL = '--wpds-cursor-control';
const PRIMARY = '#1e90ff';
const OTHER_PRIMARY = '#8e44ad';
const BACKGROUND = '#f8f8f8';

function readProp( element: Element, property: string ) {
return getComputedStyle( element ).getPropertyValue( property ).trim();
}

// The `ThemeProvider` wrapper element that scopes the given descendant.
function getScopingProvider( element: Element ) {
return element.closest< HTMLElement >( '[data-wpds-theme-provider-id]' )!;
}

describe( 'ThemeProvider', () => {
it( 'renders its children', () => {
render( <ThemeProvider>content</ThemeProvider> );

expect( screen.getByText( 'content' ) ).toBeInTheDocument();
} );

it( 'defines the color tokens from the seeds within its subtree', () => {
render(
<ThemeProvider
color={ { primary: PRIMARY, background: BACKGROUND } }
>
<div data-testid="child">x</div>
</ThemeProvider>
);

const provider = getScopingProvider( screen.getByTestId( 'child' ) );
expect( readProp( provider, BRAND_BG ) ).toBe( PRIMARY );
expect( readProp( provider, SURFACE_BG ) ).toBe( BACKGROUND );
} );

it( 'does not define the custom property outside of the provider', () => {
render(
<ThemeProvider color={ { primary: PRIMARY } }>
<div data-testid="child">x</div>
</ThemeProvider>
);

const outside = document.createElement( 'div' );
document.body.appendChild( outside );

expect( readProp( outside, BRAND_BG ) ).toBe( '' );
} );

it( 'applies the cursor custom property when set', () => {
render(
<ThemeProvider cursor={ { control: 'pointer' } }>
<div data-testid="child">x</div>
</ThemeProvider>
);

const provider = getScopingProvider( screen.getByTestId( 'child' ) );
expect( readProp( provider, CURSOR_CONTROL ) ).toBe( 'pointer' );
} );

describe( 'cornerRadius', () => {
it( 'reflects the preset as a data attribute', () => {
render(
<ThemeProvider cornerRadius="pronounced">
<div data-testid="child">x</div>
</ThemeProvider>
);

expect(
getScopingProvider( screen.getByTestId( 'child' ) )
).toHaveAttribute( 'data-wpds-corner-radius', 'pronounced' );
} );

it( 'defaults to the subtle preset', () => {
render(
<ThemeProvider>
<div data-testid="child">x</div>
</ThemeProvider>
);

expect(
getScopingProvider( screen.getByTestId( 'child' ) )
).toHaveAttribute( 'data-wpds-corner-radius', 'subtle' );
} );
} );

describe( 'isRoot', () => {
it( 'defines the token on the document root', () => {
render(
<ThemeProvider isRoot color={ { primary: PRIMARY } }>
<div>x</div>
</ThemeProvider>
);

expect( readProp( document.documentElement, BRAND_BG ) ).toBe(
PRIMARY
);
} );

it( 'does not affect the document root by default', () => {
render(
<ThemeProvider color={ { primary: PRIMARY } }>
<div>x</div>
</ThemeProvider>
);

expect( readProp( document.documentElement, BRAND_BG ) ).toBe( '' );
} );
} );

describe( 'nested providers', () => {
Comment thread
ciampo marked this conversation as resolved.
it( 'overrides the settings it defines and inherits the rest', () => {
render(
<ThemeProvider
color={ { primary: PRIMARY, background: BACKGROUND } }
cursor={ { control: 'pointer' } }
cornerRadius="pronounced"
>
<ThemeProvider>
<div data-testid="inheriting">a</div>
</ThemeProvider>
<ThemeProvider color={ { primary: OTHER_PRIMARY } }>
<div data-testid="overriding">b</div>
</ThemeProvider>
</ThemeProvider>
);

const inheriting = getScopingProvider(
screen.getByTestId( 'inheriting' )
);
const overriding = getScopingProvider(
screen.getByTestId( 'overriding' )
);

// A nested provider with no settings of its own inherits everything.
expect( readProp( inheriting, BRAND_BG ) ).toBe( PRIMARY );
expect( readProp( inheriting, SURFACE_BG ) ).toBe( BACKGROUND );
expect( readProp( inheriting, CURSOR_CONTROL ) ).toBe( 'pointer' );
expect( inheriting ).toHaveAttribute(
'data-wpds-corner-radius',
'pronounced'
);

// A nested provider overrides only what it defines (`primary`),
// inheriting the rest (`background`, `cursor`, `cornerRadius`).
expect( readProp( overriding, BRAND_BG ) ).toBe( OTHER_PRIMARY );
expect( readProp( overriding, SURFACE_BG ) ).toBe( BACKGROUND );
expect( readProp( overriding, CURSOR_CONTROL ) ).toBe( 'pointer' );
expect( overriding ).toHaveAttribute(
'data-wpds-corner-radius',
'pronounced'
);
} );
} );
} );
Loading
Loading