inlineresources
Version:
Inlines style sheets, images, fonts and scripts in HTML documents. Works in the browser.
661 lines (541 loc) • 23.1 kB
JavaScript
;
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();
});
});
});
});
});