@expressen/tallahassee
Advanced tools
Comparing version 11.21.0 to 12.0.0
@@ -6,2 +6,13 @@ Changelog | ||
## 12.0.0 | ||
- implement custom element, i.e. `window.customElements.define(name, ` | ||
- fix submit from click emitting `PointerEvent` picked up by form element and submitted | ||
- new option for regexp override of `window.matchMedia` mediaQuery | ||
- add `setProperty` to `CSSStyleDeclaration` | ||
- add `submitter` property to SubmitEvent | ||
- fix `target` property of Event | ||
- fix `currentTarget` property of Event | ||
- `MediaQueryList` as class extending EventTarget | ||
## 11.21.0 | ||
@@ -8,0 +19,0 @@ |
@@ -54,3 +54,3 @@ "use strict"; | ||
return new WebPage(this[kOrigin], this.jar, requestHeaders); | ||
return new WebPage(this[kOrigin], this.jar, requestHeaders, this.options); | ||
}; |
@@ -50,2 +50,3 @@ "use strict"; | ||
const options = webPage.options; | ||
const window = this.window = new Window(resp, { | ||
@@ -56,3 +57,4 @@ fetch: Fetch(webPage.fetch.bind(webPage)), | ||
}, | ||
}, webPage.userAgent); | ||
...(options?.console && {console}) | ||
}, webPage.userAgent, options); | ||
@@ -175,3 +177,3 @@ Object.defineProperty(document, "_window", { | ||
const method = form.getAttribute("method") || "GET"; | ||
const formaction = (event._submitElement && event._submitElement.getAttribute("formaction")) || form.getAttribute("action"); | ||
const formaction = (event.submitter?.getAttribute("formaction")) || form.getAttribute("action"); | ||
const action = formaction || this.window.location.pathname + (this.window.location.search ? this.window.location.search : ""); | ||
@@ -178,0 +180,0 @@ |
@@ -90,2 +90,5 @@ "use strict"; | ||
} | ||
setProperty(name, value) { | ||
this[name] = value; | ||
} | ||
removeProperty(name) { | ||
@@ -92,0 +95,0 @@ delete this[name]; |
@@ -9,3 +9,3 @@ "use strict"; | ||
const DOMImplementation = require("./DOMImplementation"); | ||
const elementFactory = require("./elementFactory"); | ||
const ElementFactory = require("./elementFactory"); | ||
const HTMLCollection = require("./HTMLCollection"); | ||
@@ -22,2 +22,3 @@ const Location = require("./Location"); | ||
const fullscreenElementSymbol = Symbol.for("fullscreenElement"); | ||
const kElementFactory = Symbol.for("element factory"); | ||
@@ -34,2 +35,3 @@ module.exports = class Document extends Node { | ||
this[fullscreenElementSymbol] = null; | ||
this[kElementFactory] = new ElementFactory(this); | ||
} | ||
@@ -96,3 +98,3 @@ get $() { | ||
createElement(elementTagName) { | ||
const element = elementFactory(this, this.$(`<${elementTagName}></${elementTagName}>`)); | ||
const element = this[kElementFactory].create(this.$(`<${elementTagName}></${elementTagName}>`)); | ||
this[loadedSymbol].push(element); | ||
@@ -113,2 +115,3 @@ return element; | ||
dispatchEvent(event) { | ||
event.path.push(this); | ||
if (event?.type === "fullscreenchange") { | ||
@@ -129,6 +132,5 @@ if (!event.target) return; | ||
exitFullscreen() { | ||
const fullscreenchangeEvent = new Event("fullscreenchange"); | ||
fullscreenchangeEvent.target = this[fullscreenElementSymbol]; | ||
this.dispatchEvent(fullscreenchangeEvent); | ||
const fullScreenElm = this[fullscreenElementSymbol]; | ||
fullScreenElm?.dispatchEvent(new Event("fullscreenchange", { bubbles: true })); | ||
this[fullscreenElementSymbol] = null; | ||
} | ||
@@ -177,3 +179,3 @@ getElementById(id) { | ||
mockElement = elementFactory(this, $elm); | ||
mockElement = this[kElementFactory].create($elm); | ||
@@ -180,0 +182,0 @@ loaded.push(mockElement); |
"use strict"; | ||
const { DOCUMENT_FRAGMENT_NODE, TEXT_NODE } = require("./nodeTypes"); | ||
const { Event } = require("./Events"); | ||
const { Event, PointerEvent } = require("./Events"); | ||
const { getElementsByClassName, getElementsByTagName } = require("./getElements"); | ||
@@ -254,3 +254,3 @@ const Attr = require("./Attr"); | ||
this.dispatchEvent(new Event("click", { bubbles: true })); | ||
this.dispatchEvent(new PointerEvent("click", { bubbles: true })); | ||
} | ||
@@ -405,5 +405,3 @@ closest(selector) { | ||
const fullscreenchangeEvent = new Event("fullscreenchange", { bubbles: true }); | ||
fullscreenchangeEvent.target = this; | ||
this.ownerDocument.dispatchEvent(fullscreenchangeEvent); | ||
this.dispatchEvent(fullscreenchangeEvent); | ||
} | ||
@@ -410,0 +408,0 @@ cloneNode(deep) { |
@@ -19,35 +19,46 @@ "use strict"; | ||
module.exports = function elementFactory(document, $elm) { | ||
module.exports = ElementFactory; | ||
function ElementFactory(document) { | ||
this.document = document; | ||
this.definitions = { | ||
a: HTMLAnchorElement, | ||
button: HTMLButtonElement, | ||
dialog: HTMLDialogElement, | ||
form: HTMLFormElement, | ||
input: HTMLInputElement, | ||
video: HTMLVideoElement, | ||
template: HTMLTemplateElement, | ||
textarea: HTMLTextAreaElement, | ||
script: HTMLScriptElement, | ||
select: HTMLSelectElement, | ||
option: HTMLOptionElement, | ||
"!doctype": DocumentType, | ||
}; | ||
this.custom = {}; | ||
} | ||
ElementFactory.prototype.create = function define($elm) { | ||
const nodeType = $elm[0].nodeType; | ||
const document = this.document; | ||
if (nodeType === TEXT_NODE) return new Text(document, $elm); | ||
const tagName = ($elm[0]?.name || "").toLowerCase(); | ||
const definitions = this.definitions; | ||
if (tagName in definitions) return new definitions[tagName](document, $elm); | ||
const custom = this.custom; | ||
if (tagName in custom) { | ||
const elm = new custom[tagName](document, $elm); | ||
if (elm.connectedCallback) elm.connectedCallback(); | ||
return elm; | ||
} | ||
switch (tagName) { | ||
case "a": | ||
return new HTMLAnchorElement(document, $elm); | ||
case "button": | ||
return new HTMLButtonElement(document, $elm); | ||
case "dialog": | ||
return new HTMLDialogElement(document, $elm); | ||
case "form": | ||
return new HTMLFormElement(document, $elm); | ||
case "input": | ||
return new HTMLInputElement(document, $elm); | ||
case "video": | ||
return new HTMLVideoElement(document, $elm); | ||
case "template": | ||
return new HTMLTemplateElement(document, $elm); | ||
case "textarea": | ||
return new HTMLTextAreaElement(document, $elm); | ||
case "script": | ||
return new HTMLScriptElement(document, $elm); | ||
case "select": | ||
return new HTMLSelectElement(document, $elm); | ||
case "option": | ||
return new HTMLOptionElement(document, $elm); | ||
case "!doctype": | ||
return new DocumentType(document, $elm); | ||
default: | ||
return new Element(document, $elm); | ||
} | ||
return new Element(document, $elm); | ||
}; | ||
ElementFactory.prototype.define = function define(tagName, TagClass) { | ||
this.custom[tagName] = TagClass; | ||
}; | ||
ElementFactory.prototype.get = function get(name) { | ||
return this.custom[name]; | ||
}; |
@@ -6,2 +6,3 @@ "use strict"; | ||
const detailSymbol = Symbol.for("detail"); | ||
const kSubmitter = Symbol.for("submitter"); | ||
@@ -22,2 +23,8 @@ class Event { | ||
} | ||
get currentTarget() { | ||
return this.path[this.path.length - 1]; | ||
} | ||
get target() { | ||
return this.path[0]; | ||
} | ||
preventDefault() { | ||
@@ -54,6 +61,23 @@ this[defaultPreventedSymbol] = true; | ||
class SubmitEvent extends Event { | ||
constructor(type, options = {bubbles: true}) { | ||
super(type, options); | ||
this[kSubmitter] = undefined; | ||
} | ||
get submitter() { | ||
return this[kSubmitter]; | ||
} | ||
} | ||
class PointerEvent extends Event {} | ||
module.exports = { | ||
Event, | ||
CustomEvent, | ||
InputEvent, | ||
CustomEvent | ||
PointerEvent, | ||
SubmitEvent, | ||
symbols: { | ||
submitter: kSubmitter, | ||
}, | ||
}; |
@@ -48,6 +48,2 @@ "use strict"; | ||
event.path.push(this); | ||
if (!event.target) { | ||
event.target = this; | ||
} | ||
const eventName = event.type; | ||
@@ -54,0 +50,0 @@ this[kEmitter].emit(eventName, event); |
"use strict"; | ||
const { Event } = require("./Events"); | ||
const { PointerEvent } = require("./Events"); | ||
const Element = require("./Element"); | ||
@@ -25,19 +25,5 @@ | ||
if (this.disabled) return; | ||
const clickEvent = new Event("click", { bubbles: true }); | ||
const clickEvent = new PointerEvent("click", { bubbles: true }); | ||
this.dispatchEvent(clickEvent); | ||
if (clickEvent.defaultPrevented) return; | ||
if (!this.form) return; | ||
if (!this.type || this.type === "submit") { | ||
const submitEvent = new Event("submit", { bubbles: true }); | ||
submitEvent._submitElement = this; | ||
if (this.form.reportValidity()) { | ||
this.form.dispatchEvent(submitEvent); | ||
} | ||
} else if (this.type === "reset") { | ||
this.form.reset(); | ||
} | ||
} | ||
}; |
"use strict"; | ||
const { Event } = require("./Events"); | ||
const { Event, SubmitEvent, symbols } = require("./Events"); | ||
const Element = require("./Element"); | ||
@@ -63,2 +63,16 @@ const HTMLFormControlsCollection = require("./HTMLFormControlsCollection"); | ||
} | ||
dispatchEvent(event) { | ||
super.dispatchEvent(event); | ||
const target = event.target; | ||
if (!target || target === this || event.type !== "click") return; | ||
if (target.type === "submit" || (target.tagName === "BUTTON" && !target.type)) { | ||
if (!this.reportValidity()) return; | ||
const submitEvent = new SubmitEvent("submit"); | ||
submitEvent[symbols.submitter] = event.target; | ||
super.dispatchEvent(submitEvent); | ||
} else if (target.type === "reset") { | ||
this.reset(); | ||
} | ||
} | ||
submit() { | ||
@@ -65,0 +79,0 @@ this.dispatchEvent(new Event("_form_submit", { bubbles: true })); |
"use strict"; | ||
const { Event, InputEvent } = require("./Events"); | ||
const { Event, InputEvent, PointerEvent } = require("./Events"); | ||
const Element = require("./Element"); | ||
@@ -60,3 +60,3 @@ | ||
if (this.disabled) return; | ||
const clickEvent = new Event("click", { bubbles: true }); | ||
const clickEvent = new PointerEvent("click", { bubbles: true }); | ||
const type = this.type; | ||
@@ -63,0 +63,0 @@ |
"use strict"; | ||
const {EventEmitter} = require("events"); | ||
const EventTarget = require("./EventTarget"); | ||
module.exports = function MediaQueryList(mediaQuery) { | ||
if (mediaQuery === undefined) throw new TypeError("Failed to execute 'matchMedia' on 'Window': 1 argument required, but only 0 present."); | ||
const kWindow = Symbol.for("window"); | ||
const kMedia = Symbol.for("media"); | ||
const kMatches = Symbol.for("matches"); | ||
const kOverride = Symbol.for("match media override"); | ||
const window = this; | ||
module.exports = class MediaQueryList extends EventTarget { | ||
constructor(window, mediaQuery, overrideMatchMedia) { | ||
if (mediaQuery === undefined) throw new TypeError("Failed to execute 'matchMedia' on 'Window': 1 argument required, but only 0 present."); | ||
super(); | ||
this[kWindow] = window; | ||
this[kMedia] = mediaQuery; | ||
this[kOverride] = overrideMatchMedia; | ||
this[kMatches] = this._evaluate(); | ||
window.styleMedia = window.styleMedia || { type: "screen" }; | ||
let matches = evaluate(); | ||
const emitter = new EventEmitter(); | ||
window.addEventListener("resize", reEvaluate); | ||
return { | ||
media: mediaQuery, | ||
get matches() { | ||
return matches; | ||
}, | ||
addListener(callback) { | ||
emitter.on("change", callback); | ||
}, | ||
removeListener(callback) { | ||
emitter.off("change", callback); | ||
window.addEventListener("resize", this._reEvaluate.bind(this)); | ||
} | ||
get media() { | ||
return this[kMedia]; | ||
} | ||
get matches() { | ||
return this[kMatches]; | ||
} | ||
_evaluate() { | ||
const mediaQuery = this.media; | ||
if (this[kOverride] instanceof RegExp && this[kOverride].test(mediaQuery)) { | ||
return true; | ||
} | ||
}; | ||
function reEvaluate() { | ||
const newMatches = evaluate(); | ||
if (matches === newMatches) return; | ||
matches = newMatches; | ||
const mediaQueryListEvent = new window.Event("change"); | ||
mediaQueryListEvent.matches = matches; | ||
emitter.emit("change", mediaQueryListEvent); | ||
} | ||
function evaluate() { | ||
const mediaTypes = /^(only\s|any\s|not\s)?(all|screen|print)/.exec(mediaQuery); | ||
@@ -45,3 +37,3 @@ let match = false; | ||
if (mediaTypes) { | ||
match = evaluateMediaTypes(mediaTypes); | ||
match = this._evaluateMediaTypes(mediaTypes); | ||
} | ||
@@ -51,24 +43,32 @@ | ||
if (mediaConditions) { | ||
match = evaluateMediaConditions(mediaConditions); | ||
match = this._evaluateMediaConditions(mediaConditions); | ||
} | ||
return match; | ||
} | ||
_evaluateMediaConditions(conditions) { | ||
const window = this[kWindow]; | ||
for (let i = 1; i < conditions.length; i++) { | ||
const condition = conditions[i]; | ||
const [prop, value] = condition.split(":"); | ||
function evaluateMediaConditions(conditions) { | ||
for (let i = 1; i < conditions.length; i++) { | ||
const condition = conditions[i]; | ||
const [prop, value] = condition.split(":"); | ||
if (prop.startsWith("min")) { | ||
return window.innerWidth >= parseInt(value); | ||
} else if (prop.startsWith("max")) { | ||
return window.innerWidth <= parseInt(value); | ||
} | ||
if (prop.startsWith("min")) { | ||
return window.innerWidth >= parseInt(value); | ||
} else if (prop.startsWith("max")) { | ||
return window.innerWidth <= parseInt(value); | ||
} | ||
} | ||
} | ||
_evaluateMediaTypes(types) { | ||
return types[0] === (this[kWindow].styleMedia?.type || "screen"); | ||
} | ||
_reEvaluate() { | ||
const newMatches = this._evaluate(); | ||
if (this[kMatches] === newMatches) return; | ||
function evaluateMediaTypes(types) { | ||
return types[0] === window.styleMedia.type; | ||
} | ||
this[kMatches] = newMatches; | ||
const mediaQueryListEvent = new this[kWindow].Event("change"); | ||
mediaQueryListEvent.matches = newMatches; | ||
this.dispatchEvent(mediaQueryListEvent); | ||
} | ||
}; |
@@ -16,3 +16,3 @@ "use strict"; | ||
module.exports = class WebPage { | ||
constructor(origin, jar, originRequestHeaders) { | ||
constructor(origin, jar, originRequestHeaders, options) { | ||
this[kOrigin] = origin; | ||
@@ -25,2 +25,3 @@ this.jar = jar; | ||
this.referrer = originRequestHeaders.referer; | ||
this.options = options; | ||
} | ||
@@ -27,0 +28,0 @@ async navigateTo(uri, headers, statusCode = 200) { |
@@ -7,3 +7,5 @@ "use strict"; | ||
const {performance} = require("perf_hooks"); | ||
const CustomElementRegistry = require("./CustomElementRegistry"); | ||
const Element = require("./Element"); | ||
const HTMLElement = require("./HTMLElement"); | ||
const FormData = require("./FormData"); | ||
@@ -28,6 +30,10 @@ const History = require("./History"); | ||
const windowSizeSymbol = Symbol.for("windowSize"); | ||
const kOptions = Symbol.for("browser options"); | ||
const kCustomElements = Symbol.for("custom elements"); | ||
module.exports = class Window { | ||
constructor(resp, windowObjects = {console}, userAgent) { | ||
constructor(resp, windowObjects = {console}, userAgent, options) { | ||
this[kResponse] = resp; | ||
this[kOptions] = options; | ||
const webPageUrl = windowObjects.location ? url.format(windowObjects.location) : resp.url; | ||
@@ -45,2 +51,3 @@ const location = this[locationSymbol] = new Location(this, webPageUrl); | ||
}; | ||
this[kCustomElements] = new CustomElementRegistry(this); | ||
this.atob = atob; | ||
@@ -92,5 +99,11 @@ this.btoa = btoa; | ||
} | ||
get customElements() { | ||
return this[kCustomElements]; | ||
} | ||
get Element() { | ||
return Element; | ||
} | ||
get HTMLElement() { | ||
return HTMLElement; | ||
} | ||
get FormData() { | ||
@@ -129,4 +142,4 @@ return FormData; | ||
} | ||
matchMedia(...args) { | ||
return MediaQueryList.call(this, ...args); | ||
matchMedia(mediaQueryString) { | ||
return new MediaQueryList(this, mediaQueryString, this[kOptions]?.matchMedia); | ||
} | ||
@@ -133,0 +146,0 @@ scroll(xCoord, yCoord) { |
{ | ||
"name": "@expressen/tallahassee", | ||
"version": "11.21.0", | ||
"version": "12.0.0", | ||
"description": "Lightweight client testing framework", | ||
@@ -39,6 +39,6 @@ "main": "index.js", | ||
"chai": "^4.3.6", | ||
"eslint": "^8.24.0", | ||
"express": "^4.18.1", | ||
"eslint": "^8.25.0", | ||
"express": "^4.18.2", | ||
"markdown-toc": "^1.2.0", | ||
"mocha": "^10.0.0", | ||
"mocha": "^10.1.0", | ||
"nock": "^13.2.9" | ||
@@ -45,0 +45,0 @@ }, |
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
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
115689
59
3190