Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
32 changes: 19 additions & 13 deletions src/BeaconPreloadFonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,20 +258,26 @@ class BeaconPreloadFonts {
}

// --- Main logic for fetching Google Fonts ---
const externalFontsProviders = [
'fonts.googleapis.com',
'fonts.gstatic.com',
'use.typekit.net',
'fonts.adobe.com',
'cdn.fonts.net',
// Add more known external font domains as needed
];

// Process ALL external CSS links, filter by exclusions instead of allowlist
const links = [
...document.querySelectorAll('link[rel="stylesheet"]'),
].filter((link) =>
externalFontsProviders.some((domain) => link.href.includes(domain))
);
...document.querySelectorAll('link[rel="stylesheet"]')
].filter(link => {
try {
const linkUrl = new URL(link.href);
const currentUrl = new URL(window.location.href);

// Only process external domains (different origin)
if (linkUrl.origin === currentUrl.origin) {
return false;
Comment thread
jeawhanlee marked this conversation as resolved.
Outdated
Comment thread
jeawhanlee marked this conversation as resolved.
Outdated
}
Comment thread
jeawhanlee marked this conversation as resolved.
Outdated

// Check exclusions instead of allowlist
const exclusions = this.config.external_font_exclusions || [];
return !exclusions.some(exclusion => link.href.includes(exclusion));
} catch (e) {
return false;
}
});

if (links.length === 0) {
this.logger.logMessage('No external CSS links found to process.');
Expand Down
80 changes: 79 additions & 1 deletion test/BeaconPreloadFonts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ describe('BeaconPreloadFonts', () => {
const config = {
system_fonts: ['Arial', 'Helvetica'],
font_data: {},
preload_fonts_exclusions: []
preload_fonts_exclusions: [],
external_font_exclusions: []
};

// Initialize the class with mock config and logger
Expand Down Expand Up @@ -1123,9 +1124,44 @@ describe('BeaconPreloadFonts', () => {
let originalQuerySelectorAll;
let originalFetch;
let originalCSSStyleSheet;
let originalLocation;
let originalURL;
beforeEach(() => {
// Mock CSSRule.FONT_FACE_RULE
global.CSSRule = { FONT_FACE_RULE: 5 };

// Mock window.location.href to have a different origin than the Google Fonts link
originalLocation = global.window?.location;
global.window = global.window || {};
global.window.location = {
href: 'https://example.com/test-page'
};

// Mock the URL constructor to handle the origin comparison properly
originalURL = global.URL;
global.URL = function(url, base) {
if (url === 'https://example.com/test-page') {
return {
href: 'https://example.com/test-page',
origin: 'https://example.com'
};
}
if (url === 'https://fonts.googleapis.com/css?family=Roboto') {
return {
href: 'https://fonts.googleapis.com/css?family=Roboto',
origin: 'https://fonts.googleapis.com'
};
}
if (url === 'https://example.com/same-origin.css') {
return {
href: 'https://example.com/same-origin.css',
origin: 'https://example.com'
};
}
// Fallback for any other URL
return originalURL ? new originalURL(url, base) : { href: url, origin: url.split('/').slice(0, 3).join('/') };
};

// Mock document.querySelectorAll to return fake link elements
originalQuerySelectorAll = global.document.querySelectorAll;
global.document.querySelectorAll = sinon.stub().returns([
Expand Down Expand Up @@ -1165,6 +1201,12 @@ describe('BeaconPreloadFonts', () => {
global.document.querySelectorAll = originalQuerySelectorAll;
global.fetch = originalFetch;
global.CSSStyleSheet = originalCSSStyleSheet;
global.URL = originalURL;
if (originalLocation) {
global.window.location = originalLocation;
} else {
delete global.window.location;
}
});

it('should fetch, parse, and return external font CSS as styleSheets and fontPairs', async () => {
Expand All @@ -1187,5 +1229,41 @@ describe('BeaconPreloadFonts', () => {
assert.deepStrictEqual(result.styleSheets, []);
assert.deepStrictEqual(result.fontPairs, {});
});

it('should exclude links based on external_font_exclusions config', async () => {
// Update the config to exclude fonts.googleapis.com
beaconPreloadFonts.config.external_font_exclusions = ['fonts.googleapis.com'];

const result = await beaconPreloadFonts.externalStylesheetsDoc();

// Should return empty arrays since the Google Fonts link should be excluded
assert.strictEqual(result.styleSheets.length, 0);
assert.deepStrictEqual(result.fontPairs, {});
});

it('should process links from different origins only', async () => {
// Set up links with same and different origins
global.document.querySelectorAll = sinon.stub().returns([
{ href: 'https://example.com/same-origin.css', rel: 'stylesheet' }, // Same origin as window.location
{ href: 'https://fonts.googleapis.com/css?family=Roboto', rel: 'stylesheet' }, // Different origin
]);

// Mock fetch to handle multiple calls
global.fetch = sinon.stub();
global.fetch.onFirstCall().resolves({
ok: true,
text: () => Promise.resolve('@font-face { font-family: "Roboto"; src: url("https://fonts.gstatic.com/s/roboto.woff2"); font-weight: 400; font-style: normal; }')
});

const result = await beaconPreloadFonts.externalStylesheetsDoc();

// Should only process the external origin link, not the same-origin one
assert.strictEqual(result.styleSheets.length, 1);
assert.ok(result.fontPairs['https://fonts.gstatic.com/s/roboto.woff2']);

// Verify fetch was called only once (for the external link)
assert.strictEqual(global.fetch.callCount, 1);
assert.ok(global.fetch.calledWith('https://fonts.googleapis.com/css?family=Roboto', { mode: 'cors' }));
});
});
});