Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
8e811f3
feat: fix Checkbox + Radio color tokens
dreamwasp Sep 3, 2025
f4ac4e3
feat: Percipio theme
dreamwasp Sep 3, 2025
4e9d261
first stab at theme
dreamwasp Sep 3, 2025
0e7e452
removed duplicated values + added story
dreamwasp Sep 3, 2025
df076c2
theme switcher
dreamwasp Sep 3, 2025
a7f842d
add better docs for theme switcher
dreamwasp Sep 3, 2025
a4fb2a8
start fixing fonts
dreamwasp Sep 3, 2025
cd33610
fix font issues
dreamwasp Sep 3, 2025
8e357f7
sussy webfont changes - needd to fix build
dreamwasp Sep 5, 2025
cf21d73
Merge branch 'main' into cass-gm-1212
dreamwasp Sep 17, 2025
b124b22
build fixes + utilities added
dreamwasp Sep 17, 2025
7fac438
add defaults for createFontLink
dreamwasp Sep 17, 2025
28fc28e
better types
dreamwasp Sep 17, 2025
9b9cf19
new snapshots
dreamwasp Sep 17, 2025
30f9583
refactor + clean up
dreamwasp Sep 18, 2025
461382d
first draft of docs
dreamwasp Sep 18, 2025
f97fd12
more docs
dreamwasp Sep 18, 2025
a797647
reset to gamut themeing
dreamwasp Sep 18, 2025
10a8149
AssetProvider theme not required
dreamwasp Sep 18, 2025
6520af3
still working through theming;
dreamwasp Sep 19, 2025
c58246c
fix types in tests + types
dreamwasp Sep 22, 2025
bd0e212
tweak tests
dreamwasp Sep 22, 2025
ee215d1
lint fixed
dreamwasp Sep 22, 2025
0fa6f09
move AssetProvider
dreamwasp Sep 22, 2025
516502e
remove assetprovider
dreamwasp Sep 22, 2025
358ace2
Merge branch 'main' into cass-gm-1212
dreamwasp Sep 22, 2025
724337c
fix DocsContainer
dreamwasp Sep 22, 2025
4fbb623
Merge branch 'cass-gm-1212' of github.com:Codecademy/gamut into cass-…
dreamwasp Sep 22, 2025
a8ed0c9
remove unneeded tests
dreamwasp Sep 22, 2025
af285b3
Merge branch 'main' into cass-gm-1212
dreamwasp Sep 23, 2025
7c5ef40
fix theme stories
dreamwasp Sep 23, 2025
96049e9
fix type errors of DocsContainer
dreamwasp Sep 23, 2025
ab05306
better docs
dreamwasp Sep 23, 2025
370b9ab
yarn change
dreamwasp Sep 23, 2025
4404757
swap to core default
dreamwasp Sep 23, 2025
5363c1f
test memo
dreamwasp Sep 24, 2025
91f97fc
debug logging
dreamwasp Sep 24, 2025
ef2a07e
test story in prenv
dreamwasp Sep 24, 2025
46024d3
fix stories
dreamwasp Sep 24, 2025
5d3db5d
remove comments
dreamwasp Sep 24, 2025
5a6dc38
dinger fixed
dreamwasp Sep 24, 2025
37246e9
danger fix
dreamwasp Sep 24, 2025
35244f5
kenny comments
dreamwasp Sep 25, 2025
f1251ec
fix link
dreamwasp Sep 25, 2025
1b9927a
Merge branch 'main' into cass-gm-1212
dreamwasp Oct 6, 2025
f7712b8
asset provider
dreamwasp Oct 6, 2025
91d12d4
Apply suggestion from @aresnik11
dreamwasp Oct 13, 2025
e405a74
amy feedback
dreamwasp Oct 13, 2025
98d415d
docs tweaks
dreamwasp Oct 14, 2025
c733d74
Merge branch 'main' into cass-gm-1212
dreamwasp Oct 14, 2025
82d13d5
Merge branch 'main' into cass-gm-1212
dreamwasp Oct 14, 2025
4c116b9
variables for stories
dreamwasp Oct 14, 2025
0970504
Merge branch 'main' into cass-gm-1212
dreamwasp Oct 15, 2025
b682ab1
plat added
dreamwasp Oct 15, 2025
fdea32d
as any
dreamwasp Oct 15, 2025
eba1087
formatted
dreamwasp Oct 15, 2025
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
40 changes: 35 additions & 5 deletions packages/gamut-styles/src/AssetProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import { Theme } from '@emotion/react';
import * as React from 'react';

import { webFonts } from './remoteAssets/fonts';
import { coreTheme } from './themes';
import { FontConfig, getFonts } from './utils/fontUtils';

/*
* Only preload woff2 fonts, since woff1 are only included as fallbacks.
*/
export const createFontLinks = () =>
webFonts
.filter((f) => f.extensions.includes('woff2'))
export const createFontLinks = (fonts?: readonly FontConfig[]) => {
const currentFonts = fonts ?? webFonts.core;

if (!Array.isArray(currentFonts)) {
return [];
}

return currentFonts
.filter(
(f) =>
f?.extensions &&
Array.isArray(f.extensions) &&
f.extensions.includes('woff2') &&
f.filePath &&
f.filePath.trim() !== ''
)
.map(({ filePath }) => (
<link
as="font"
Expand All @@ -16,7 +34,19 @@ export const createFontLinks = () =>
type="font/woff2"
/>
));
};

export const AssetProvider: React.FC<{ theme?: Theme }> = ({ theme }) => {
try {
const defaultTheme = theme ?? coreTheme;
const themeName = defaultTheme?.name || 'core';
const fonts = getFonts(themeName);

export const AssetProvider = () => {
return <>{createFontLinks()}</>;
return <>{createFontLinks(fonts)}</>;
} catch (error) {
// Handle font loading errors gracefully
// eslint-disable-next-line no-console
console.warn('Font loading failed:', error);
return null;
}
};
4 changes: 2 additions & 2 deletions packages/gamut-styles/src/GamutProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export const GamutProvider: React.FC<GamutProviderProps> = ({

const globals = shouldInsertGlobals && (
<>
<Typography />
<Reboot />
<Typography theme={theme} />
<Reboot theme={theme} />
<Variables variables={theme._variables} />
{variables && <Variables variables={variables} />}
</>
Expand Down
333 changes: 333 additions & 0 deletions packages/gamut-styles/src/__tests__/AssetProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
import '@testing-library/jest-dom';

import { setupRtl } from '@codecademy/gamut-tests';
import { render } from '@testing-library/react';

import { AssetProvider, createFontLinks } from '../AssetProvider';
import { coreTheme, percipioTheme } from '../themes';

const renderView = setupRtl(AssetProvider, {});

jest.mock('../utils/fontUtils', () => ({
getFonts: jest.fn(),
}));

jest.mock('../remoteAssets/fonts', () => ({
webFonts: {
core: [
{
filePath: 'https://www.codecademy.com/gamut/apercu-regular-pro',
extensions: ['woff2', 'woff'],
name: 'Apercu',
},
{
filePath: 'https://www.codecademy.com/gamut/apercu-bold-pro',
extensions: ['woff2', 'woff'],
name: 'Apercu',
weight: 'bold',
},
],
percipio: [
{
filePath: 'https://www.codecademy.com/gamut/roboto-regular',
extensions: ['woff2', 'woff'],
name: 'Roboto',
},
{
filePath: 'https://www.codecademy.com/gamut/roboto-bold',
extensions: ['woff2', 'woff'],
name: 'Roboto',
weight: 'bold',
},
],
},
}));

const mockGetFonts = require('../utils/fontUtils').getFonts;

describe('AssetProvider', () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe('createFontLinks', () => {
it('should create font links for woff2 fonts only', () => {
const fonts = [
{
filePath: 'https://www.codecademy.com/gamut/test-font',
extensions: ['woff2', 'woff'],
name: 'Test Font',
},
{
filePath: 'https://www.codecademy.com/gamut/test-font-woff1-only',
extensions: ['woff'],
name: 'Test Font Woff1',
},
];

const { container } = render(<>{createFontLinks(fonts)}</>);
const links = container.querySelectorAll('link[rel="preload"]');

expect(links).toHaveLength(1);
expect(links[0]).toHaveAttribute(
'href',
'https://www.codecademy.com/gamut/test-font.woff2'
);
expect(links[0]).toHaveAttribute('type', 'font/woff2');
expect(links[0]).toHaveAttribute('as', 'font');
expect(links[0]).toHaveAttribute('crossOrigin', 'anonymous');
});

it('should handle empty fonts array', () => {
const { container } = render(<>{createFontLinks([])}</>);
const links = container.querySelectorAll('link[rel="preload"]');
expect(links).toHaveLength(0);
});

it('should handle undefined fonts parameter', () => {
const { container } = render(<>{createFontLinks(undefined)}</>);
const links = container.querySelectorAll('link[rel="preload"]');
expect(links).toHaveLength(2);
});

it('should filter out fonts without woff2 extension', () => {
const fonts = [
{
filePath: 'https://www.codecademy.com/gamut/font-with-woff2',
extensions: ['woff2', 'woff'],
name: 'Font With Woff2',
},
{
filePath: 'https://www.codecademy.com/gamut/font-woff1-only',
extensions: ['woff'],
name: 'Font Woff1 Only',
},
{
filePath: 'https://www.codecademy.com/gamut/font-no-extensions',
extensions: [],
name: 'Font No Extensions',
},
];

const { container } = render(<>{createFontLinks(fonts)}</>);
const links = container.querySelectorAll('link[rel="preload"]');
expect(links).toHaveLength(1);
expect(links[0]).toHaveAttribute(
'href',
'https://www.codecademy.com/gamut/font-with-woff2.woff2'
);
});

it('should handle malformed font configurations gracefully', () => {
const fonts = [
{
filePath: 'https://www.codecademy.com/gamut/valid-font',
extensions: ['woff2'],
name: 'Valid Font',
},
{
// Missing required properties
filePath: '',
extensions: ['woff2'],
name: '',
} as any,
{
filePath: 'https://www.codecademy.com/gamut/another-valid-font',
extensions: ['woff2'],
name: 'Another Valid Font',
},
];

const { container } = render(<>{createFontLinks(fonts)}</>);
const links = container.querySelectorAll('link[rel="preload"]');
expect(links).toHaveLength(2);
});
});

describe('AssetProvider component', () => {
it('should render font links for core theme by default', () => {
mockGetFonts.mockReturnValue([
{
filePath: 'https://www.codecademy.com/gamut/apercu-regular-pro',
extensions: ['woff2', 'woff'],
name: 'Apercu',
},
]);

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');

expect(links).toHaveLength(1);
expect(links[0]).toHaveAttribute(
'href',
'https://www.codecademy.com/gamut/apercu-regular-pro.woff2'
);
expect(mockGetFonts).toHaveBeenCalledWith('core');
});

it('should render font links for percipio theme', () => {
mockGetFonts.mockReturnValue([
{
filePath: 'https://www.codecademy.com/gamut/roboto-regular',
extensions: ['woff2', 'woff'],
name: 'Roboto',
},
]);

const { view } = renderView({ theme: percipioTheme as any });
const links = view.container.querySelectorAll('link[rel="preload"]');

expect(links).toHaveLength(1);
expect(links[0]).toHaveAttribute(
'href',
'https://www.codecademy.com/gamut/roboto-regular.woff2'
);
expect(mockGetFonts).toHaveBeenCalledWith('percipio');
});

it('should handle theme without name property', () => {
const themeWithoutName = { ...coreTheme, name: undefined };
mockGetFonts.mockReturnValue([]);

renderView({ theme: themeWithoutName });
expect(mockGetFonts).toHaveBeenCalledWith('core');
});

it('should handle theme with invalid name', () => {
const themeWithInvalidName = { ...coreTheme, name: 'invalid-theme' };
mockGetFonts.mockReturnValue([]);

renderView({ theme: themeWithInvalidName });
expect(mockGetFonts).toHaveBeenCalledWith('invalid-theme');
});

it('should handle getFonts throwing an error', () => {
mockGetFonts.mockImplementation(() => {
throw new Error('Font loading failed');
});

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');
expect(links).toHaveLength(0);
});

it('should fallback to core fonts when getFonts returns undefined', () => {
mockGetFonts.mockReturnValue(undefined);

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');
expect(links).toHaveLength(2);
});

it('should fallback to core fonts when getFonts returns null', () => {
mockGetFonts.mockReturnValue(null);

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');
expect(links).toHaveLength(2);
});

it('should fallback to core fonts when getFonts returns non-array', () => {
mockGetFonts.mockReturnValue('not-an-array');

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');
expect(links).toHaveLength(0);
});

it('should render multiple font links when multiple fonts are provided', () => {
mockGetFonts.mockReturnValue([
{
filePath: 'https://www.codecademy.com/gamut/font1',
extensions: ['woff2'],
name: 'Font 1',
},
{
filePath: 'https://www.codecademy.com/gamut/font2',
extensions: ['woff2'],
name: 'Font 2',
},
{
filePath: 'https://www.codecademy.com/gamut/font3',
extensions: ['woff2'],
name: 'Font 3',
},
]);

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');

expect(links).toHaveLength(3);
expect(links[0]).toHaveAttribute(
'href',
'https://www.codecademy.com/gamut/font1.woff2'
);
expect(links[1]).toHaveAttribute(
'href',
'https://www.codecademy.com/gamut/font2.woff2'
);
expect(links[2]).toHaveAttribute(
'href',
'https://www.codecademy.com/gamut/font3.woff2'
);
});

it('should only rnder valid font configurations ', () => {
mockGetFonts.mockReturnValue([
{
filePath: 'https://www.codecademy.com/gamut/valid-font',
extensions: ['woff2'],
name: 'Valid Font',
},
{
filePath: '', // Empty filePath
extensions: ['woff2'],
name: 'Empty Path Font',
},
{
// Missing filePath entirely
extensions: ['woff2'],
name: 'Missing Path Font',
} as any,
]);

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');

expect(links).toHaveLength(1);
expect(links[0]).toHaveAttribute(
'href',
'https://www.codecademy.com/gamut/valid-font.woff2'
);
});

it('should handle font configurations with missing extensions', () => {
mockGetFonts.mockReturnValue([
{
filePath: 'https://www.codecademy.com/gamut/font-with-extensions',
extensions: ['woff2'],
name: 'Font With Extensions',
},
{
filePath: 'https://www.codecademy.com/gamut/font-no-extensions',
extensions: undefined,
name: 'Font No Extensions',
} as any,
{
filePath: 'https://www.codecademy.com/gamut/font-empty-extensions',
extensions: [],
name: 'Font Empty Extensions',
},
]);

const { view } = renderView();
const links = view.container.querySelectorAll('link[rel="preload"]');

expect(links).toHaveLength(1);
expect(links[0]).toHaveAttribute(
'href',
'https://www.codecademy.com/gamut/font-with-extensions.woff2'
);
});
});
});
Loading
Loading