UNPKG

inlineresources

Version:

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

661 lines (541 loc) 23.1 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), { baseUrl: "some_url/" } ); 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(); }); }); }); }); });