diff --git a/.mocharc.yml b/.mocharc.yml index a8bacb01..75eca7be 100644 --- a/.mocharc.yml +++ b/.mocharc.yml @@ -1 +1 @@ -timeout: 30000 +timeout: 300000 diff --git a/lib/subsetFonts.js b/lib/subsetFonts.js index 0792f895..f2f28cc2 100644 --- a/lib/subsetFonts.js +++ b/lib/subsetFonts.js @@ -581,7 +581,7 @@ async function subsetFonts( fontDisplay, onlyInfo, dynamic, - console, + console = global.console, } = {} ) { if (!validFontDisplayValues.includes(fontDisplay)) { @@ -909,7 +909,7 @@ async function subsetFonts( When your primary webfont doesn't contain the glyphs you use, browsers that don't support unicode-range will load your fallback fonts, which will be a potential waste of bandwidth. These glyphs are used on your site, but they don't exist in the font you applied to them:`; - assetGraph.warn(new Error(`${message}\n${errorLog.join('\n')}`)); + assetGraph.info(new Error(`${message}\n${errorLog.join('\n')}`)); } // Insert subsets: diff --git a/package.json b/package.json index 90e1138d..ced68d78 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ }, "devDependencies": { "coveralls": "^3.0.9", + "css-generators": "^0.2.0", "eslint": "^7.4.0", "eslint-config-prettier": "^6.7.0", "eslint-config-standard": "^14.0.0", @@ -76,7 +77,9 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.0.1", "eslint-plugin-standard": "^4.0.0", + "html-generators": "^1.0.3", "httpception": "^3.0.0", + "magicpen-prism": "^3.0.2", "mocha": "^8.0.1", "nyc": "^15.1.0", "offline-github-changelog": "^1.6.1", @@ -85,6 +88,7 @@ "puppeteer": "^3.1.0", "sinon": "^9.0.2", "unexpected": "^11.8.1", + "unexpected-check": "^2.3.1", "unexpected-resemble": "^4.3.0", "unexpected-set": "^2.0.1", "unexpected-sinon": "^10.11.2" diff --git a/test/expect.js b/test/expect.js new file mode 100644 index 00000000..a337e0f3 --- /dev/null +++ b/test/expect.js @@ -0,0 +1,114 @@ +const expect = require('unexpected') + .clone() + .use(require('unexpected-resemble')) + .use(require('unexpected-check')) + .use(require('magicpen-prism')); +const subsetFonts = require('../lib/subsetFonts'); +const pathModule = require('path'); +const AssetGraph = require('assetgraph'); + +let browser; +async function getBrowser() { + if (!browser) { + browser = await require('puppeteer').launch(); + + after(async function () { + await browser.close(); + }); + } + return browser; +} + +async function screenshot(browser, assetGraph, bannedUrls) { + const page = await browser.newPage(); + await page.setRequestInterception(true); + const loadedUrls = []; + page.on('request', (request) => { + const url = request.url(); + loadedUrls.push(url); + if (url.startsWith('https://example.com/')) { + let agUrl = url.replace('https://example.com/', assetGraph.root); + if (/\/$/.test(agUrl)) { + agUrl += 'index.html'; + } + const asset = assetGraph.findAssets({ + isLoaded: true, + url: agUrl, + })[0]; + if (asset) { + request.respond({ + status: 200, + contentType: asset.contentType, + body: asset.rawSrc, + }); + return; + } + } + request.continue(); + }); + await page.goto('https://example.com/'); + if (bannedUrls) { + const loadedBannedUrls = loadedUrls.filter((url) => + bannedUrls.includes(url) + ); + if (loadedBannedUrls.length > 0) { + throw new Error( + `One or more of the original fonts were loaded:\n ${loadedBannedUrls.join( + '\n ' + )}` + ); + } + } + const screenshot = await page.screenshot(); + await page.close(); + return screenshot; +} + +expect.addAssertion( + ' to render the same after subsetting ', + (expect, path, ...rest) => { + const assetGraph = new AssetGraph({ + root: pathModule.resolve( + __dirname, + '..', + 'testdata', + 'referenceImages', + path + ), + }); + return expect(assetGraph, 'to render the same after subsetting', ...rest); + } +); + +expect.addAssertion( + ' to render the same after subsetting ', + async (expect, assetGraph, options = {}) => { + const [htmlAsset] = await assetGraph.loadAssets('index.html'); + const originalText = htmlAsset.text; + expect.subjectOutput = (output) => { + output.code(originalText, 'html'); + }; + + await assetGraph.populate(); + const browser = await getBrowser(); + const fontsBefore = assetGraph + .findAssets({ type: { $in: ['Ttf', 'Woff', 'Woff2', 'Eot'] } }) + .map((asset) => + asset.url.replace(assetGraph.root, 'https://example.com/') + ); + const screenshotBefore = await screenshot(browser, assetGraph); + const { fontInfo } = await subsetFonts(assetGraph, options); + if (fontInfo.length > 0) { + const screenshotAfter = await screenshot( + browser, + assetGraph, + fontsBefore + ); + await expect(screenshotAfter, 'to resemble', screenshotBefore, { + mismatchPercentage: 0, + }); + } + } +); + +module.exports = expect; diff --git a/test/generatedHtml.js b/test/generatedHtml.js new file mode 100644 index 00000000..03d71e19 --- /dev/null +++ b/test/generatedHtml.js @@ -0,0 +1,115 @@ +const fs = require('fs'); +const { html } = require('html-generators'); +const { stylesheet } = require('css-generators'); +const AssetGraph = require('assetgraph'); +const pathModule = require('path'); +const stringify = require('html-generators/src/stringify'); +const expect = require('./expect'); + +function fixupUnsupportedHtmlConstructs(obj) { + if (obj.type === 'tag') { + if (obj.tag === 'object') { + delete obj.attributes.data; + } + if ('src' in obj.attributes) { + delete obj.attributes.src; + } + for (const child of obj.children) { + fixupUnsupportedHtmlConstructs(child); + } + } +} + +describe('generated html', function () { + let smileySvgBase64; + + before(async function () { + smileySvgBase64 = `data:image/svg+xml;base64,${fs + .readFileSync( + pathModule.resolve(__dirname, '..', 'testdata', 'smiley.svg') + ) + .toString('base64')}`; + }); + + it('should render the same before and after subsetting', async function () { + return expect( + async (htmlObjectTree, stylesheet) => { + fixupUnsupportedHtmlConstructs(htmlObjectTree); + + stylesheet = stylesheet + .replace(/url\([^)]*\)/g, `url(${smileySvgBase64})`) + .replace(/all: (?:initial|unset);/g, '') // Makes the contents of stylesheets visible + .replace(/font-variant-caps: [^;]+;/, '') // See build #260.3 failure + .replace(/oblique [0-9.]+\w+/, 'oblique'); // oblique with an angle is not yet fully standardized or implemented in font-snapper + const head = htmlObjectTree.children[0]; + head.children.push({ + type: 'tag', + tag: 'style', + attributes: [], + children: [ + { + type: 'text', + value: ` + @font-face { + font-family: 'IBM Plex Sans'; + font-style: normal; + font-weight: 400; + src: url('IBMPlexSans-Regular.woff') format('woff'); + } + + @font-face { + font-family: 'IBM Plex Sans'; + font-style: italic; + font-weight: 400; + src: url('IBMPlexSans-Italic.woff') format('woff'); + } + body { + font-family: 'IBM Plex Sans', sans-serif; + font-style: normal; + font-weight: 400; + } + + ${stylesheet} + `, + }, + ], + }); + + const assetGraph = new AssetGraph({ + root: pathModule.resolve( + __dirname, + '..', + 'testdata', + 'subsetFonts', + 'unused-variant-on-one-page' + ), + }); + const text = stringify(htmlObjectTree); + assetGraph.addAsset({ + url: `${assetGraph.root}index.html`, + type: 'Html', + text, + }); + await assetGraph.populate(); + return expect(assetGraph, 'to render the same after subsetting', { + omitFallbacks: true, + }); + }, + 'to be valid for all', + { + maxIterations: 1, + generators: [ + html({ + excludedDescendants: new Set([ + 'svg', + 'script', + 'style', + 'progress', + ]), + }), + stylesheet, + ], + } + ); + }); +}); diff --git a/test/referenceImages.js b/test/referenceImages.js index 88599b73..3dcd8017 100644 --- a/test/referenceImages.js +++ b/test/referenceImages.js @@ -1,97 +1,6 @@ -const expect = require('unexpected') - .clone() - .use(require('unexpected-resemble')); -const subsetFonts = require('../lib/subsetFonts'); -const AssetGraph = require('assetgraph'); -const pathModule = require('path'); - -async function screenshot(browser, assetGraph, bannedUrls) { - const page = await browser.newPage(); - await page.setRequestInterception(true); - const loadedUrls = []; - page.on('request', (request) => { - const url = request.url(); - loadedUrls.push(url); - if (url.startsWith('https://example.com/')) { - let agUrl = url.replace('https://example.com/', assetGraph.root); - if (/\/$/.test(agUrl)) { - agUrl += 'index.html'; - } - const asset = assetGraph.findAssets({ - isLoaded: true, - url: agUrl, - })[0]; - if (asset) { - request.respond({ - status: 200, - contentType: asset.contentType, - body: asset.rawSrc, - }); - return; - } - } - request.continue(); - }); - await page.goto('https://example.com/'); - if (bannedUrls) { - const loadedBannedUrls = loadedUrls.filter((url) => - bannedUrls.includes(url) - ); - if (loadedBannedUrls.length > 0) { - throw new Error( - `One or more of the original fonts were loaded:\n ${loadedBannedUrls.join( - '\n ' - )}` - ); - } - } - const screenshot = await page.screenshot(); - await page.close(); - return screenshot; -} +const expect = require('./expect'); describe('reference images', function () { - let browser; - before(async function () { - browser = await require('puppeteer').launch(); - }); - - after(async function () { - await browser.close(); - }); - - expect.addAssertion( - ' to render the same after subsetting ', - async (expect, path, options = {}) => { - const assetGraph = new AssetGraph({ - root: pathModule.resolve( - __dirname, - '..', - 'testdata', - 'referenceImages', - path - ), - }); - await assetGraph.loadAssets('index.html'); - await assetGraph.populate(); - const fontsBefore = assetGraph - .findAssets({ type: { $in: ['Ttf', 'Woff', 'Woff2', 'Eot'] } }) - .map((asset) => - asset.url.replace(assetGraph.root, 'https://example.com/') - ); - const screenshotBefore = await screenshot(browser, assetGraph); - await subsetFonts(assetGraph, options); - const screenshotAfter = await screenshot( - browser, - assetGraph, - fontsBefore - ); - await expect(screenshotAfter, 'to resemble', screenshotBefore, { - mismatchPercentage: 0, - }); - } - ); - for (const inlineCss of [true, false]) { describe(`with inlineCss:${inlineCss}`, function () { for (const inlineFonts of [true, false]) { @@ -121,6 +30,36 @@ describe('reference images', function () { } ); }); + + for (const dynamic of [true, false]) { + describe(`with dynamic:${dynamic}`, function () { + it('should render missing glyphs', async function () { + await expect( + 'missingGlyphs', + 'to render the same after subsetting', + { + inlineCss, + inlineFonts, + omitFallbacks, + dynamic, + } + ); + }); + + it('should render unused variants', async function () { + await expect( + 'unusedVariants', + 'to render the same after subsetting', + { + inlineCss, + inlineFonts, + omitFallbacks, + dynamic, + } + ); + }); + }); + } }); } }); diff --git a/test/subsetFonts.js b/test/subsetFonts.js index fbb47113..ee3450ab 100644 --- a/test/subsetFonts.js +++ b/test/subsetFonts.js @@ -3172,17 +3172,17 @@ describe('subsetFonts', function () { }); describe('when the highest prioritized font-family is missing glyphs', function () { - it('should emit a warning', async function () { + it('should emit an info event', async function () { httpception(); - const warnSpy = sinon.spy().named('warn'); + const infoSpy = sinon.spy().named('warn'); const assetGraph = new AssetGraph({ root: pathModule.resolve( __dirname, '../testdata/subsetFonts/missing-glyphs/' ), }); - assetGraph.on('warn', warnSpy); + assetGraph.on('info', infoSpy); await assetGraph.loadAssets('index.html'); await assetGraph.populate({ followRelations: { @@ -3193,8 +3193,8 @@ describe('subsetFonts', function () { inlineFonts: false, }); - expect(warnSpy, 'to have calls satisfying', function () { - warnSpy({ + expect(infoSpy, 'to have calls satisfying', function () { + infoSpy({ message: expect .it('to contain', 'Missing glyph fallback detected') .and('to contain', '\\u{4e2d} (中)') @@ -3334,14 +3334,14 @@ describe('subsetFonts', function () { it('should check for missing glyphs in any subset format', async function () { httpception(); - const warnSpy = sinon.spy().named('warn'); + const infoSpy = sinon.spy().named('info'); const assetGraph = new AssetGraph({ root: pathModule.resolve( __dirname, '../testdata/subsetFonts/missing-glyphs/' ), }); - assetGraph.on('warn', warnSpy); + assetGraph.on('info', infoSpy); await assetGraph.loadAssets('index.html'); await assetGraph.populate({ followRelations: { @@ -3353,8 +3353,8 @@ describe('subsetFonts', function () { formats: [`woff2`], }); - expect(warnSpy, 'to have calls satisfying', function () { - warnSpy({ + expect(infoSpy, 'to have calls satisfying', function () { + infoSpy({ message: expect .it('to contain', 'Missing glyph fallback detected') .and('to contain', '\\u{4e2d} (中)') @@ -3363,18 +3363,18 @@ describe('subsetFonts', function () { }); }); - // Some fonts don't contain these, but browsers don't seem to mind, so the warnings would just be noise + // Some fonts don't contain these, but browsers don't seem to mind, so the messages would just be noise it('should not warn about tab and newline missing from the font being subset', async function () { httpception(); - const warnSpy = sinon.spy().named('warn'); + const infoSpy = sinon.spy().named('info'); const assetGraph = new AssetGraph({ root: pathModule.resolve( __dirname, '../testdata/subsetFonts/missing-tab-and-newline-glyphs/' ), }); - assetGraph.on('warn', warnSpy); + assetGraph.on('warn', infoSpy); await assetGraph.loadAssets('index.html'); await assetGraph.populate({ followRelations: { @@ -3385,7 +3385,7 @@ describe('subsetFonts', function () { inlineFonts: false, }); - expect(warnSpy, 'was not called'); + expect(infoSpy, 'was not called'); }); }); diff --git a/testdata/referenceImages/missingGlyphs/InputMono-Medium.woff2 b/testdata/referenceImages/missingGlyphs/InputMono-Medium.woff2 new file mode 100644 index 00000000..8f416f16 Binary files /dev/null and b/testdata/referenceImages/missingGlyphs/InputMono-Medium.woff2 differ diff --git a/testdata/referenceImages/missingGlyphs/InputMono-Regular.woff2 b/testdata/referenceImages/missingGlyphs/InputMono-Regular.woff2 new file mode 100644 index 00000000..b81f0039 Binary files /dev/null and b/testdata/referenceImages/missingGlyphs/InputMono-Regular.woff2 differ diff --git a/testdata/referenceImages/missingGlyphs/OutputSans-Bold.woff2 b/testdata/referenceImages/missingGlyphs/OutputSans-Bold.woff2 new file mode 100644 index 00000000..d260c945 Binary files /dev/null and b/testdata/referenceImages/missingGlyphs/OutputSans-Bold.woff2 differ diff --git a/testdata/referenceImages/missingGlyphs/OutputSans-Regular.woff2 b/testdata/referenceImages/missingGlyphs/OutputSans-Regular.woff2 new file mode 100644 index 00000000..c9d89ee5 Binary files /dev/null and b/testdata/referenceImages/missingGlyphs/OutputSans-Regular.woff2 differ diff --git a/testdata/referenceImages/missingGlyphs/index.html b/testdata/referenceImages/missingGlyphs/index.html new file mode 100644 index 00000000..4f436531 --- /dev/null +++ b/testdata/referenceImages/missingGlyphs/index.html @@ -0,0 +1,38 @@ + + + + + + + +
load('中国').then(function
+ +
Hello!
+ + diff --git a/testdata/referenceImages/unusedVariants/InputMono-Medium.woff2 b/testdata/referenceImages/unusedVariants/InputMono-Medium.woff2 new file mode 100644 index 00000000..8f416f16 Binary files /dev/null and b/testdata/referenceImages/unusedVariants/InputMono-Medium.woff2 differ diff --git a/testdata/referenceImages/unusedVariants/InputMono-Regular.woff2 b/testdata/referenceImages/unusedVariants/InputMono-Regular.woff2 new file mode 100644 index 00000000..b81f0039 Binary files /dev/null and b/testdata/referenceImages/unusedVariants/InputMono-Regular.woff2 differ diff --git a/testdata/referenceImages/unusedVariants/index.html b/testdata/referenceImages/unusedVariants/index.html new file mode 100644 index 00000000..b0451297 --- /dev/null +++ b/testdata/referenceImages/unusedVariants/index.html @@ -0,0 +1,24 @@ + + + + + + + foo + + diff --git a/testdata/smiley.svg b/testdata/smiley.svg new file mode 100644 index 00000000..8b7ff71f --- /dev/null +++ b/testdata/smiley.svg @@ -0,0 +1,13 @@ + + + + + + + + +