UNPKG

inlineresources

Version:

Inlines style sheets, images, fonts and scripts in HTML documents. Works in the browser.

496 lines (374 loc) 19.4 kB
"use strict"; var inline = require('../../src/inline'), inlineCss = require('../../src/inlineCss'), util = require('../../src/util'), testHelper = require('../testHelper'); describe("Inline CSS links", function () { var doc, joinUrlSpy, ajaxSpy, adjustPathsOfCssResourcesSpy, loadCSSImportsForRulesSpy, loadAndInlineCSSResourcesForRulesSpy, ajaxUrlMocks = {}; var setupAjaxMock = function () { ajaxSpy = spyOn(util, "ajax").and.callFake(function (url, options) { if (ajaxUrlMocks[url + ' ' + options.baseUrl] !== undefined) { return Promise.resolve(ajaxUrlMocks[url + ' ' + options.baseUrl]); // try matching without base url } else if (ajaxUrlMocks[url] !== undefined) { return Promise.resolve(ajaxUrlMocks[url]); } else { return Promise.reject({ url: 'THEURL' + url }); } }); }; var mockAjaxUrl = function () { var url = arguments[0], baseUrl = arguments.length > 2 ? arguments[1] : null, content = arguments.length > 2 ? arguments[2] : arguments[1], urlKey = baseUrl === null ? url : url + ' ' + baseUrl; ajaxUrlMocks[urlKey] = content; }; var aCssLinkWith = function (url, content) { var cssLink = window.document.createElement("link"); cssLink.href = url; cssLink.rel = "stylesheet"; cssLink.type = "text/css"; mockAjaxUrl(cssLink.href, content); // href will return absolute path, attributes.href.value relative one in Chrome mockAjaxUrl(cssLink.attributes.href.value, content); return cssLink; }; var aCssLink = function () { return aCssLinkWith("url/some.css", "p { font-size: 14px; }"); }; var fulfilled = function (value) { return Promise.resolve(value); }; beforeEach(function () { doc = document.implementation.createHTMLDocument(""); joinUrlSpy = spyOn(util, "joinUrl"); adjustPathsOfCssResourcesSpy = spyOn(inlineCss, 'adjustPathsOfCssResources'); loadCSSImportsForRulesSpy = spyOn(inlineCss, 'loadCSSImportsForRules').and.returnValue(fulfilled({ hasChanges: false, errors: [] })); loadAndInlineCSSResourcesForRulesSpy = spyOn(inlineCss, 'loadAndInlineCSSResourcesForRules').and.returnValue(fulfilled({ hasChanges: false, errors: [] })); setupAjaxMock(); }); it("should do nothing if no linked CSS is found", function (done) { inline.loadAndInlineCssLinks(doc, {}).then(function () { expect(doc.head.getElementsByTagName("style").length).toEqual(0); done(); }); }); it("should not touch non-CSS links", function (done) { var faviconLink = window.document.createElement("link"); faviconLink.href = "favicon.ico"; faviconLink.type = "image/x-icon"; doc.head.appendChild(faviconLink); inline.loadAndInlineCssLinks(doc, {}).then(function () { expect(doc.head.getElementsByTagName("style").length).toEqual(0); expect(doc.head.getElementsByTagName("link").length).toEqual(1); done(); }); }); it("should inline linked CSS", function (done) { doc.head.appendChild(aCssLink()); inline.loadAndInlineCssLinks(doc, {}).then(function () { expect(doc.head.getElementsByTagName("style").length).toEqual(1); expect(doc.head.getElementsByTagName("style")[0].textContent).toEqual("p { font-size: 14px; }"); expect(doc.head.getElementsByTagName("link").length).toEqual(0); done(); }); }); it("should inline linked CSS without a type", function (done) { var noTypeCssLink = window.document.createElement("link"); noTypeCssLink.href = 'yet_another.css'; noTypeCssLink.rel = "stylesheet"; doc.head.appendChild(noTypeCssLink); mockAjaxUrl(noTypeCssLink.href, "p { font-size: 14px; }"); // href will return absolute path, attributes.href.value relative one in Chrome mockAjaxUrl(noTypeCssLink.attributes.href.value, "p { font-size: 14px; }"); inline.loadAndInlineCssLinks(doc, {}).then(function () { expect(doc.head.getElementsByTagName("style").length).toEqual(1); expect(doc.head.getElementsByTagName("style")[0].textContent).toEqual("p { font-size: 14px; }"); expect(doc.head.getElementsByTagName("link").length).toEqual(0); done(); }); }); it("should inline multiple linked CSS and keep order", function (done) { var anotherCssLink = aCssLinkWith("url/another.css", "a { text-decoration: none; }"), inlineCss = window.document.createElement("style"); inlineCss.type = "text/css"; inlineCss.textContent = "span { margin: 0; }"; doc.head.appendChild(aCssLink()); doc.head.appendChild(inlineCss); doc.head.appendChild(anotherCssLink); inline.loadAndInlineCssLinks(doc, {}).then(function () { expect(doc.head.getElementsByTagName("style").length).toEqual(3); expect(doc.head.getElementsByTagName("style")[0].textContent.trim()).toEqual("p { font-size: 14px; }"); expect(doc.head.getElementsByTagName("style")[1].textContent.trim()).toEqual("span { margin: 0; }"); expect(doc.head.getElementsByTagName("style")[2].textContent.trim()).toEqual("a { text-decoration: none; }"); expect(doc.head.getElementsByTagName("link").length).toEqual(0); done(); }); }); it("should not add inline CSS if no content given", function (done) { var emptyCssLink = aCssLinkWith("url/empty.css", ""); doc.head.appendChild(emptyCssLink); inline.loadAndInlineCssLinks(doc, {}).then(function () { expect(doc.head.getElementsByTagName("style").length).toEqual(0); expect(doc.head.getElementsByTagName("link").length).toEqual(0); done(); }); }); it("should inline CSS imports", function (done) { doc.head.appendChild(aCssLink()); inline.loadAndInlineCssLinks(doc, {}).then(function () { expect(loadCSSImportsForRulesSpy).toHaveBeenCalled(); expect(loadCSSImportsForRulesSpy.calls.mostRecent().args[0][0].cssText).toMatch(/p \{\s*font-size: 14px;\s*\}/); done(); }); }); it("should inline CSS resources", function (done) { doc.head.appendChild(aCssLink()); inline.loadAndInlineCssLinks(doc, {}).then(function () { expect(loadAndInlineCSSResourcesForRulesSpy).toHaveBeenCalled(); expect(loadAndInlineCSSResourcesForRulesSpy.calls.mostRecent().args[0][0].cssText).toMatch(/p \{\s*font-size: 14px;\s*\}/); done(); }); }); it("should respect the document's baseURI when loading linked CSS", function (done) { var getDocumentBaseUrlSpy = spyOn(util, 'getDocumentBaseUrl').and.callThrough(); testHelper.loadHTMLDocumentFixture("externalCSS.html").then(function (doc) { mockAjaxUrl("some.css", "p { font-size: 14px; }"); inline.loadAndInlineCssLinks(doc, {}).then(function () { expect(doc.getElementsByTagName("style").length).toEqual(1); expect(doc.getElementsByTagName("style")[0].textContent).toEqual("p { font-size: 14px; }"); expect(doc.getElementsByTagName("link").length).toEqual(0); expect(ajaxSpy.calls.mostRecent().args[1].baseUrl).toEqual(doc.baseURI); expect(loadCSSImportsForRulesSpy.calls.mostRecent().args[2].baseUrl).toEqual(doc.baseURI); expect(loadAndInlineCSSResourcesForRulesSpy.calls.mostRecent().args[1].baseUrl).toEqual(doc.baseURI); expect(getDocumentBaseUrlSpy).toHaveBeenCalledWith(doc); done(); }); }); }); it("should respect optional baseUrl when loading linked CSS", function (done) { mockAjaxUrl("some.css", "p { font-size: 14px; }"); testHelper.loadHTMLDocumentFixtureWithoutBaseURI("externalCSS.html").then(function (doc) { inline.loadAndInlineCssLinks(doc, {baseUrl: testHelper.fixturesPath}).then(function () { expect(ajaxSpy.calls.mostRecent().args[1].baseUrl).toEqual(testHelper.fixturesPath); expect(loadCSSImportsForRulesSpy.calls.mostRecent().args[2].baseUrl).toEqual(testHelper.fixturesPath); expect(loadAndInlineCSSResourcesForRulesSpy.calls.mostRecent().args[1].baseUrl).toEqual(testHelper.fixturesPath); done(); }); }); }); it("should favour explicit baseUrl over document.baseURI when loading linked CSS", function (done) { var baseUrl = testHelper.fixturesPath; testHelper.loadHTMLDocumentFixture("externalCSS.html").then(function (doc) { expect(doc.baseURI).not.toBeNull(); expect(doc.baseURI).not.toEqual("about:blank"); expect(doc.baseURI).not.toEqual(baseUrl); mockAjaxUrl("some.css", "p { font-size: 14px; }"); inline.loadAndInlineCssLinks(doc, {baseUrl: testHelper.fixturesPath}).then(function () { expect(ajaxSpy.calls.mostRecent().args[1].baseUrl).toEqual(testHelper.fixturesPath); expect(loadCSSImportsForRulesSpy.calls.mostRecent().args[2].baseUrl).toEqual(testHelper.fixturesPath); expect(loadAndInlineCSSResourcesForRulesSpy.calls.mostRecent().args[1].baseUrl).toEqual(testHelper.fixturesPath); done(); }); }); }); it("should map resource paths relative to the stylesheet", function (done) { var cssWithRelativeResource; cssWithRelativeResource = window.document.createElement("link"); cssWithRelativeResource.href = "below/some.css"; cssWithRelativeResource.rel = "stylesheet"; cssWithRelativeResource.type = "text/css"; doc.head.appendChild(cssWithRelativeResource); mockAjaxUrl("below/some.css", "some_url/", 'div { background-image: url("../green.png"); }\n' + '@font-face { font-family: "test font"; src: url("fake.woff"); }'); inline.loadAndInlineCssLinks(doc, {baseUrl: "some_url/"}).then(function () { expect(adjustPathsOfCssResourcesSpy).toHaveBeenCalledWith("below/some.css", jasmine.any(Object)); done(); }); }); it("should circumvent caching if requested", function (done) { var cssLink = aCssLink(); doc.head.appendChild(cssLink); inline.loadAndInlineCssLinks(doc, {cache: 'none'}).then(function () { expect(ajaxSpy).toHaveBeenCalledWith(cssLink.attributes.href.value, jasmine.objectContaining({ cache: 'none' })); expect(loadCSSImportsForRulesSpy.calls.mostRecent().args[2].cache).toEqual('none'); expect(loadAndInlineCSSResourcesForRulesSpy.calls.mostRecent().args[1].cache).toEqual('none'); done(); }); }); it("should not circumvent caching by default", function (done) { var cssLink = aCssLink(); doc.head.appendChild(cssLink); inline.loadAndInlineCssLinks(doc, {}).then(function () { expect(ajaxSpy).toHaveBeenCalledWith(cssLink.attributes.href.value, jasmine.any(Object)); expect(ajaxSpy).not.toHaveBeenCalledWith(jasmine.any(String), jasmine.objectContaining({ cache: 'none' })); expect(loadCSSImportsForRulesSpy.calls.mostRecent().args[2].cache).not.toBe(false); expect(loadAndInlineCSSResourcesForRulesSpy.calls.mostRecent().args[1].cache).not.toBe(false); done(); }); }); it("should cache inlined content if a cache bucket is given", function (done) { var cacheBucket = {}; // first call doc = document.implementation.createHTMLDocument(""); doc.head.appendChild(aCssLink()); inline.loadAndInlineCssLinks(doc, {cacheBucket: cacheBucket}).then(function () { expect(ajaxSpy).toHaveBeenCalled(); ajaxSpy.calls.reset(); loadCSSImportsForRulesSpy.calls.reset(); loadAndInlineCSSResourcesForRulesSpy.calls.reset(); // second call doc = document.implementation.createHTMLDocument(""); doc.head.appendChild(aCssLink()); inline.loadAndInlineCssLinks(doc, {cacheBucket: cacheBucket}).then(function () { expect(ajaxSpy).not.toHaveBeenCalled(); expect(loadCSSImportsForRulesSpy).not.toHaveBeenCalled(); expect(loadAndInlineCSSResourcesForRulesSpy).not.toHaveBeenCalled(); expect(doc.getElementsByTagName("style")[0].textContent).toEqual("p { font-size: 14px; }"); done(); }); }); }); it("should cache inlined content for different pages if baseUrl is the same", function (done) { var cacheBucket = {}; joinUrlSpy.and.callThrough(); // first call doc = testHelper.readDocumentFixture("empty1.html"); doc.getElementsByTagName("head")[0].appendChild(aCssLink()); inline.loadAndInlineCssLinks(doc, {cacheBucket: cacheBucket}).then(function () { ajaxSpy.calls.reset(); loadCSSImportsForRulesSpy.calls.reset(); loadAndInlineCSSResourcesForRulesSpy.calls.reset(); // second call doc = testHelper.readDocumentFixture("empty2.html"); // use a document with different url, but same baseUrl doc.getElementsByTagName("head")[0].appendChild(aCssLink()); inline.loadAndInlineCssLinks(doc, {cacheBucket: cacheBucket}).then(function () { expect(ajaxSpy).not.toHaveBeenCalled(); expect(loadCSSImportsForRulesSpy).not.toHaveBeenCalled(); expect(loadAndInlineCSSResourcesForRulesSpy).not.toHaveBeenCalled(); expect(doc.getElementsByTagName("style")[0].textContent).toEqual("p { font-size: 14px; }"); done(); }); }); }); it("should not cache inlined content if caching turned off", function (done) { var cacheBucket = {}; // first call doc = document.implementation.createHTMLDocument(""); doc.head.appendChild(aCssLink()); inline.loadAndInlineCssLinks(doc, {cacheBucket: cacheBucket, cache: 'none'}).then(function () { expect(ajaxSpy).toHaveBeenCalled(); ajaxSpy.calls.reset(); // second call doc = document.implementation.createHTMLDocument(""); doc.head.appendChild(aCssLink()); inline.loadAndInlineCssLinks(doc, {cacheBucket: cacheBucket, cache: 'none'}).then(function () { expect(ajaxSpy).toHaveBeenCalled(); done(); }); }); }); describe("error handling", function () { var brokenCssLink, anotherBrokenCssLink; beforeEach(function () { brokenCssLink = window.document.createElement("link"); brokenCssLink.href = "a_document_that_doesnt_exist.css"; brokenCssLink.rel = "stylesheet"; brokenCssLink.type = "text/css"; anotherBrokenCssLink = window.document.createElement("link"); anotherBrokenCssLink.href = "another_document_that_doesnt_exist.css"; anotherBrokenCssLink.rel = "stylesheet"; anotherBrokenCssLink.type = "text/css"; joinUrlSpy.and.callThrough(); }); it("should report an error if a stylesheet could not be loaded", function (done) { doc.head.appendChild(brokenCssLink); inline.loadAndInlineCssLinks(doc, {}).then(function (errors) { expect(errors).toEqual([{ resourceType: "stylesheet", url: "THEURL" + "a_document_that_doesnt_exist.css", msg: "Unable to load stylesheet " + "THEURL" + "a_document_that_doesnt_exist.css" }]); done(); }); }); it("should only report a failing stylesheet as error", function (done) { doc.head.appendChild(brokenCssLink); doc.head.appendChild(aCssLink()); inline.loadAndInlineCssLinks(doc, {}).then(function (errors) { expect(errors).toEqual([{ resourceType: "stylesheet", url: "THEURL" + "a_document_that_doesnt_exist.css", msg: jasmine.any(String) }]); done(); }); }); it("should report multiple failing stylesheets as error", function (done) { doc.head.appendChild(brokenCssLink); doc.head.appendChild(anotherBrokenCssLink); inline.loadAndInlineCssLinks(doc, {}).then(function (errors) { expect(errors).toEqual([jasmine.any(Object), jasmine.any(Object)]); expect(errors[0]).not.toEqual(errors[1]); done(); }); }); it("should report errors from inlining resources", function (done) { doc.head.appendChild(aCssLink()); loadCSSImportsForRulesSpy.and.returnValue(fulfilled({ hasChanges: false, errors: ["import inline error"] })); loadAndInlineCSSResourcesForRulesSpy.and.returnValue(fulfilled({ hasChanges: false, errors: ["resource inline error"] })); inline.loadAndInlineCssLinks(doc, {}).then(function (errors) { expect(errors).toEqual(["import inline error", "resource inline error"]); done(); }); }); it("should report an empty list for a successful stylesheet", function (done) { doc.head.appendChild(aCssLink()); inline.loadAndInlineCssLinks(doc, {}).then(function (errors) { expect(errors).toEqual([]); done(); }); }); it("should cache errors alongside if a cache bucket is given", function (done) { var cacheBucket = {}; loadCSSImportsForRulesSpy.and.returnValue(fulfilled({ hasChanges: false, errors: ["import inline error"] })); // first call doc = document.implementation.createHTMLDocument(""); doc.head.appendChild(aCssLink()); inline.loadAndInlineCssLinks(doc, {cacheBucket: cacheBucket}).then(function () { // second call doc = document.implementation.createHTMLDocument(""); doc.head.appendChild(aCssLink()); inline.loadAndInlineCssLinks(doc, {cacheBucket: cacheBucket}).then(function (errors) { expect(errors).toEqual(["import inline error"]); done(); }); }); }); }); });