@expressen/tallahassee
Advanced tools
Comparing version 0.2.0 to 0.3.0
"use strict"; | ||
const Document = require("./lib/Document"); | ||
const Fetch = require("./lib/Fetch"); | ||
const supertest = require("supertest"); | ||
const Window = require("./lib/Window"); | ||
const {compile} = require("./lib/Compiler"); | ||
const {Document, Fetch, Window, Compiler} = require("./lib"); | ||
const {compile} = Compiler; | ||
@@ -9,0 +7,0 @@ module.exports = Tallahassee; |
@@ -32,3 +32,3 @@ "use strict"; | ||
function Compiler(assetPatterns) { | ||
function Compiler(assetPatterns, plugins = [], presets = ["node8"] ) { | ||
if (registered) return; | ||
@@ -40,4 +40,4 @@ | ||
ignore, | ||
plugins: [], | ||
presets: ["node8"], | ||
plugins, | ||
presets, | ||
}); | ||
@@ -44,0 +44,0 @@ |
@@ -6,4 +6,4 @@ "use strict"; | ||
const getLocation = require("./getLocation"); | ||
const Url = require("url"); | ||
const vm = require("vm"); | ||
const Element = require("./Element"); | ||
const {EventEmitter} = require("events"); | ||
@@ -16,19 +16,34 @@ | ||
let cookieHeader = getHeaders(resp).cookie || ""; | ||
const setCookies = {}; | ||
const $ = cheerio.load(resp.text, {decodeEntities: false}); | ||
const loaded = []; | ||
const doc = MockElement($); | ||
doc._getElement = getElement; | ||
doc.$ = $; | ||
doc.getElementById = getElementById; | ||
const document = Object.assign(new EventEmitter(), { | ||
createElement, | ||
getElementById, | ||
getElementsByTagName, | ||
getElementsByClassName, | ||
location, | ||
textContent: null, | ||
$, | ||
_getElement: getElement, | ||
}); | ||
Object.defineProperty(doc, "head", { | ||
Object.defineProperty(document, "head", { | ||
get: getHead | ||
}); | ||
Object.defineProperty(doc, "body", { | ||
Object.defineProperty(document, "body", { | ||
get: getBody | ||
}); | ||
Object.defineProperty(doc, "cookie", { | ||
Object.defineProperty(document, "firstElementChild", { | ||
get: () => getElement($("html")) | ||
}); | ||
Object.defineProperty(document, "firstChild", { | ||
get: () => getElement($("> :first-child")) | ||
}); | ||
Object.defineProperty(document, "cookie", { | ||
get: () => cookieHeader, | ||
@@ -43,4 +58,11 @@ set: (value) => { | ||
return doc; | ||
Object.defineProperty(document, "title", { | ||
get: () => getElement($("head > title")).textContent, | ||
set: (value) => { | ||
getElement($("head > title")).textContent = value; | ||
} | ||
}); | ||
return document; | ||
function getHead() { | ||
@@ -55,3 +77,3 @@ return getElement($("head")); | ||
function getElement($elm) { | ||
if ($elm === $) return doc; | ||
if ($elm === $) return document; | ||
@@ -63,3 +85,3 @@ let mockElement = loaded.find((mockedElm) => mockedElm.$elm[0] === $elm[0]); | ||
mockElement = MockElement($elm); | ||
mockElement = Element(document, $elm); | ||
loaded.push(mockElement); | ||
@@ -76,321 +98,35 @@ return mockElement; | ||
function MockElement($elm) { | ||
const isDocument = $elm === $; | ||
const tagName = (($elm[0] && $elm[0].name) || "").toLowerCase(); | ||
let top = 761, bottom = top + 760 * 2, height = bottom - top; | ||
const emitter = new EventEmitter(); | ||
const listenEvents = []; | ||
function getElementsByTagName(query) { | ||
return $(`${query}`).map((idx, elm) => getElement(document.$(elm))).toArray(); | ||
} | ||
const classList = getClassList(); | ||
const element = { | ||
$elm, | ||
appendChild, | ||
classList, | ||
click, | ||
dispatchEvent, | ||
getAttribute, | ||
getElementsByClassName, | ||
getElementsByTagName, | ||
insertAdjacentHTML, | ||
getBoundingClientRect, | ||
setBoundingClientRect, | ||
addEventListener, | ||
style: {}, | ||
createElement, | ||
removeChild, | ||
runScripts, | ||
setAttribute, | ||
removeAttribute, | ||
_emitter: emitter, | ||
_listenEvents: listenEvents | ||
}; | ||
function getElementsByClassName(className) { | ||
return $(`.${className}`).map((idx, elm) => getElement(document.$(elm))).toArray(); | ||
} | ||
function addEventListener(eventName, fn) { | ||
listenEvents.push(eventName); | ||
emitter.on(eventName, fn.bind(this)); | ||
} | ||
function createElement(elementTagName) { | ||
const elementDOM = Document({ text: `<${elementTagName}></${elementTagName}>` }); | ||
return elementDOM.getElementsByTagName(elementTagName)[0]; | ||
} | ||
Object.defineProperty(element, "firstElementChild", { | ||
get: getElementFirstChild | ||
}); | ||
function parseSetCookie(cookie) { | ||
const setCookiePattern = /^(\w+)=(.*?)(;(.*?))?$/g; | ||
const parsed = {}; | ||
if (!/;$/.test(cookie)) cookie = `${cookie};`; | ||
Object.defineProperty(element, "lastElementChild", { | ||
get: getElementLastChild | ||
cookie.trim().replace(setCookiePattern, (match, name, value, settings) => { | ||
parsed.cookie = `${name}=${value};`; | ||
parsed.name = name; | ||
parsed.value = decodeURIComponent(value); | ||
Object.assign(parsed, parseSettings(settings)); | ||
}); | ||
Object.defineProperty(element, "dataset", { | ||
get: getDataset | ||
}); | ||
Object.defineProperty(element, "tagName", { | ||
get: () => tagName ? tagName.toUpperCase() : undefined | ||
}); | ||
Object.defineProperty(element, "children", { | ||
get: getChildren | ||
}); | ||
Object.defineProperty(element, "parentElement", { | ||
get: () => getElement($elm.parent()) | ||
}); | ||
Object.defineProperty(element, "innerText", { | ||
get: () => $elm.html() | ||
}); | ||
Object.defineProperty(element, "href", { | ||
get: () => { | ||
const rel = getAttribute("href"); | ||
if (!rel) return; | ||
if (!rel.startsWith("/")) return rel; | ||
return Url.format(Object.assign({}, location, {path: rel})); | ||
} | ||
}); | ||
Object.defineProperty(element, "value", { | ||
get: () => { | ||
return getAttribute("value"); | ||
}, | ||
set: (value) => { | ||
return setAttribute("value", value); | ||
} | ||
}); | ||
Object.defineProperty(element, "textContent", { | ||
get: () => { | ||
if (isDocument) return null; | ||
return tagName === "script" ? $elm.html() : $elm.text(); | ||
}, | ||
set: (value) => { | ||
if (isDocument) return; | ||
return tagName === "script" ? $elm.html(value) : $elm.text(value); | ||
} | ||
}); | ||
Object.defineProperty(element, "checked", { | ||
get: () => { | ||
return getAttribute("checked") === "checked"; | ||
}, | ||
set: (value) => { | ||
const oldValue = (getAttribute("checked") === "checked"); | ||
uncheckRadioButtons(); | ||
if (value) { | ||
setAttribute("checked", "checked"); | ||
if (!oldValue) { | ||
emitter.emit("change", emptyEvent()); | ||
} | ||
} | ||
} | ||
}); | ||
return element; | ||
function getElementFirstChild() { | ||
return getElement(find("> :first-child")); | ||
if (parsed.name) { | ||
setCookies[parsed.name] = parsed; | ||
} | ||
function getElementLastChild() { | ||
return getElement(find("> :last-child")); | ||
} | ||
function toMockElement(idx, elm) { | ||
return getElement($(elm)); | ||
} | ||
function getElementsByClassName(query) { | ||
return find(`.${query}`).map(toMockElement).toArray(); | ||
} | ||
function getElementsByTagName(query) { | ||
return find(`${query}`).map((idx, elm) => getElement($(elm))).toArray(); | ||
} | ||
function appendChild(childElement) { | ||
$elm.append(childElement.$elm.parent().html()); | ||
if (childElement.$elm[0].tagName === "script") { | ||
vm.runInThisContext(childElement.innerText); | ||
} | ||
} | ||
function click() { | ||
emitter.emit("click", { | ||
preventDefault: () => {}, | ||
stopPropagation: () => {} | ||
}); | ||
} | ||
function dispatchEvent(...args) { | ||
emitter.emit(...args); | ||
} | ||
function getAttribute(name) { | ||
return $elm.attr(name); | ||
} | ||
function removeChild(childElement) { | ||
childElement.$elm.remove(); | ||
} | ||
function find(selector) { | ||
if (isDocument) { | ||
return $(selector); | ||
} | ||
return $elm.find(selector); | ||
} | ||
function getDataset() { | ||
if (!$elm || !$elm[0] || !$elm[0].attribs) return {}; | ||
const {attribs} = $elm[0]; | ||
return Object.keys(attribs).reduce((acc, key) => { | ||
if (key.startsWith("data-")) { | ||
acc[key.replace(/^data-/, "").replace(/-(\w)/g, (a, b) => b.toUpperCase())] = attribs[key]; | ||
} | ||
return acc; | ||
}, {}); | ||
} | ||
function getChildren() { | ||
if (!$elm) return []; | ||
return $elm.children().map(toMockElement).toArray(); | ||
} | ||
function insertAdjacentHTML(position, markup) { | ||
$elm.append(markup); | ||
} | ||
function setBoundingClientRect(setTop, setBottom) { | ||
top = setTop; | ||
if (setBottom === undefined) { | ||
bottom = top + height; | ||
} else { | ||
bottom = setBottom; | ||
height = bottom - top; | ||
} | ||
} | ||
function getBoundingClientRect() { | ||
return { | ||
top, | ||
bottom, | ||
height | ||
}; | ||
} | ||
function createElement(elementTagName) { | ||
const elementDOM = Document({ text: `<${elementTagName}></${elementTagName}>` }); | ||
return elementDOM.getElementsByTagName(elementTagName)[0]; | ||
} | ||
function runScripts() { | ||
$("script").each((idx, elm) => { | ||
const scriptBody = $(elm).html(); | ||
if (scriptBody) vm.runInThisContext(scriptBody); | ||
}); | ||
} | ||
function setAttribute(name, val) { | ||
$elm.attr(name, val); | ||
} | ||
function removeAttribute(name) { | ||
$elm.removeAttr(name); | ||
} | ||
function uncheckRadioButtons() { | ||
if ($elm.attr("type") !== "radio") return; | ||
const name = $elm.attr("name"); | ||
const $form = $elm.closest("form"); | ||
if ($form && $form.length) { | ||
return $form.find(`input[type="radio"][name="${name}"]`).removeAttr("checked"); | ||
} | ||
$(`input[type="radio"][name="${name}"]`).removeAttr("checked"); | ||
} | ||
function emptyEvent() { | ||
let defaultPrevented; | ||
const event = { | ||
preventDefault, | ||
stopPropagation: () => {} | ||
}; | ||
Object.defineProperty(event, "defaultPrevented", { | ||
get: () => defaultPrevented | ||
}); | ||
return event; | ||
function preventDefault() { | ||
defaultPrevented = true; | ||
} | ||
} | ||
function getClassList() { | ||
if (!$elm.attr) return; | ||
const classListApi = { | ||
contains(className) { | ||
return $elm.hasClass(className); | ||
}, | ||
add(...classNames) { | ||
$elm.addClass(classNames.join(" ")); | ||
}, | ||
remove(...classNames) { | ||
$elm.removeClass(classNames.join(" ")); | ||
}, | ||
toggle(className, force) { | ||
const hasClass = $elm.hasClass(className); | ||
if (force === undefined) { | ||
$elm.toggleClass(className); | ||
return !hasClass; | ||
} | ||
if (force) { | ||
$elm.addClass(className); | ||
} else { | ||
$elm.removeClass(className); | ||
} | ||
return !hasClass; | ||
} | ||
}; | ||
Object.defineProperty(classListApi, "_classes", { | ||
get: getClassArray | ||
}); | ||
return classListApi; | ||
function getClassArray() { | ||
return ($elm.attr("class") || "").split(" "); | ||
} | ||
} | ||
return parsed; | ||
} | ||
} | ||
function parseSetCookie(cookie) { | ||
const setCookiePattern = /^(\w+)=(.*?)(;(.*?))?$/g; | ||
const parsed = {}; | ||
if (!/;$/.test(cookie)) cookie = `${cookie};`; | ||
cookie.trim().replace(setCookiePattern, (match, name, value, settings) => { | ||
parsed.cookie = `${name}=${value};`; | ||
parsed.name = name; | ||
parsed.value = decodeURIComponent(value); | ||
Object.assign(parsed, parseSettings(settings)); | ||
}); | ||
if (parsed.name) { | ||
this.setCookies[parsed.name] = parsed; | ||
} | ||
return parsed; | ||
} | ||
function parseSettings(settings) { | ||
@@ -407,2 +143,1 @@ const settingsPattern = /(\w+)=(.*?)(;|$)/g; | ||
} | ||
@@ -6,4 +6,2 @@ "use strict"; | ||
function ElementScroller(browser, stackedElementsFn) { | ||
const innerHeight = browser.window.innerHeight; | ||
stackedElementsFn = stackedElementsFn || getArticleElements; | ||
@@ -18,13 +16,12 @@ | ||
const elements = stackedElementsFn(); | ||
const index = elements.indexOf(element); | ||
if (!element) return; | ||
const {top: previousTop} = element.getBoundingClientRect(); | ||
const diff = offset - previousTop; | ||
for (let i = 0; i < elements.length; i++) { | ||
const elm = elements[i]; | ||
if (i < index) { | ||
elm.setBoundingClientRect(-99999); | ||
} else if (i === index) { | ||
elm.setBoundingClientRect(0 + offset); | ||
} else { | ||
elm.setBoundingClientRect(1000); | ||
} | ||
const {top} = elm.getBoundingClientRect(); | ||
elm.setBoundingClientRect(top + diff); | ||
} | ||
@@ -36,23 +33,10 @@ | ||
function scrollToBottomOfElement(element, offset = 0) { | ||
const elements = stackedElementsFn(); | ||
const index = elements.indexOf(element); | ||
for (let i = 0; i < elements.length; i++) { | ||
const elm = elements[i]; | ||
if (i < index) { | ||
elm.setBoundingClientRect(-99999); | ||
} else if (i === index) { | ||
const {height} = elm.getBoundingClientRect(); | ||
elm.setBoundingClientRect(innerHeight - height + offset); | ||
} else { | ||
elm.setBoundingClientRect(innerHeight + 1000); | ||
} | ||
} | ||
browser.window.scroll(); | ||
const {height} = element.getBoundingClientRect(); | ||
const offsetFromBottom = browser.window.innerHeight - height; | ||
return scrollToTopOfElement(element, offsetFromBottom + offset); | ||
} | ||
function getArticleElements() { | ||
return browser.document.getElementsByClassName("article"); | ||
return browser.document.body.getElementsByTagName("article"); | ||
} | ||
} |
@@ -10,3 +10,6 @@ "use strict"; | ||
for (const name in reqHeader) { | ||
headers[name.toLowerCase()] = reqHeader[name]; | ||
const lowerName = name.toLowerCase(); | ||
let val = reqHeader[name]; | ||
if (lowerName === "cookie") val = cleanCookie(val); | ||
headers[lowerName] = val; | ||
} | ||
@@ -16,1 +19,7 @@ | ||
}; | ||
function cleanCookie(cookie) { | ||
if (!cookie) return ""; | ||
if (!/;$/.test(cookie)) return `${cookie};`; | ||
return cookie; | ||
} |
@@ -6,3 +6,3 @@ "use strict"; | ||
module.exports = function Window(resp, windowObjects, innerHeight = 760) { | ||
module.exports = function Window(resp, windowObjects, innerWidth = 760, innerHeight = 760) { | ||
const location = getLocation(resp.request); | ||
@@ -12,12 +12,15 @@ | ||
const window = Object.assign({ | ||
innerHeight, | ||
location, | ||
_resize: resizeWindow, | ||
addEventListener, | ||
clearTimeout: () => {}, | ||
dispatchEvent, | ||
scroll: dispatchEvent.bind(null, "scroll"), | ||
setTimeout: (fn, ms, ...args) => fn(...args), | ||
removeEventListener, | ||
history: { | ||
replaceState | ||
}, | ||
innerHeight, | ||
innerWidth, | ||
location, | ||
removeEventListener, | ||
scroll: dispatchEvent.bind(null, "scroll"), | ||
setTimeout: (fn, ms, ...args) => fn(...args), | ||
}, windowObjects); | ||
@@ -42,2 +45,12 @@ | ||
} | ||
function resizeWindow(newInnerWidth, newInnerHeight) { | ||
if (newInnerWidth !== undefined) { | ||
window.innerWidth = newInnerWidth; | ||
} | ||
if (newInnerHeight !== undefined) { | ||
window.innerHeight = newInnerHeight; | ||
} | ||
window.dispatchEvent("resize"); | ||
} | ||
}; |
{ | ||
"name": "@expressen/tallahassee", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "Expressen client testing framework", | ||
@@ -15,3 +15,6 @@ "main": "index.js", | ||
"headless", | ||
"browser" | ||
"browser", | ||
"fake", | ||
"mock", | ||
"IntersectionObserver" | ||
], | ||
@@ -33,6 +36,6 @@ "author": "AB Kvällstidningen Expressen", | ||
"chai-as-promised": "^7.1.1", | ||
"eslint": "^4.12.1", | ||
"eslint": "^4.16.0", | ||
"express": "^4.16.2", | ||
"mocha": "^4.0.1", | ||
"nock": "^9.1.4" | ||
"mocha": "^5.0.0", | ||
"nock": "^9.1.6" | ||
}, | ||
@@ -39,0 +42,0 @@ "files": [ |
@@ -6,8 +6,14 @@ Tallahassee | ||
[![Build Status](https://travis-ci.org/ExpressenAB/tallahassee.svg?branch=master)](https://travis-ci.org/ExpressenAB/tallahassee) | ||
[![Build Status](https://travis-ci.org/ExpressenAB/tallahassee.svg?branch=master)](https://travis-ci.org/ExpressenAB/tallahassee)[![dependencies Status](https://david-dm.org/ExpressenAB/tallahassee/status.svg)](https://david-dm.org/ExpressenAB/tallahassee) | ||
Test your client scripts in a headless browser. | ||
Example: | ||
# Introduction | ||
Supports just about everything except `querySelectorAll()` which we don´t want developers to use. | ||
- IntersectionObserver? Yes, check [here](#intersectionobserver) | ||
# Example: | ||
```javascript | ||
@@ -48,5 +54,4 @@ "use strict"; | ||
expect(browser.document.cookie).to.equal("_ga=1"); | ||
expect(browser.document.cookie).to.equal("_ga=1;"); | ||
expect(browser.document.getElementsByClassName("set-by-js")).to.have.length(1); | ||
}); | ||
@@ -65,1 +70,49 @@ | ||
``` | ||
# Api | ||
## IntersectionObserver | ||
```javascript | ||
"use strict"; | ||
const app = require("../app/express-js-app"); | ||
const Browser = require("@expressen/tallahassee"); | ||
const {Compiler, IntersectionObserver, ElementScroller} = require("@expressen/tallahassee/lib"); | ||
describe("IntersectionObserver", () => { | ||
before(() => { | ||
Compiler.Compiler([/assets\/scripts/]); | ||
}); | ||
it("observes elements", async () => { | ||
const browser = await Browser(app).navigateTo("/", { | ||
Cookie: "_ga=1" | ||
}); | ||
const intersectionObserver = browser.window.IntersectionObserver = IntersectionObserver(browser); | ||
require("../app/assets/scripts/main"); | ||
expect(intersectionObserver._getObserved()).to.have.length(1); | ||
}); | ||
it("listens to window scroll", async () => { | ||
const browser = await Browser(app).navigateTo("/", { | ||
Cookie: "_ga=1" | ||
}); | ||
browser.window.IntersectionObserver = IntersectionObserver(browser); | ||
require("../app/assets/scripts/main"); | ||
const scroller = ElementScroller(browser, () => { | ||
return browser.document.getElementsByClassName("lazy-load"); | ||
}); | ||
scroller.scrollToTopOfElement(browser.document.getElementsByClassName("lazy-load")[0]); | ||
expect(browser.document.getElementsByClassName("lazy-load").length).to.equal(0); | ||
expect(browser.document.getElementsByTagName("img")[1].src).to.be.ok; | ||
}); | ||
}); | ||
``` |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
26993
14
738
116
2