@zombieland/tallahassee
v0.1.1
Published
A browser module around JSDOM for testing a web application as opposed to a document. Navigation with headers, cookies, clicks and form submits.
Readme
Tallahassee
A browser module around JSDOM for testing a web application as opposed to a document. Navigation with headers, cookies, clicks and form submits.
I really want the name Tallahassee to remain, although Columbus sounds more browsery.
Table of Contents
Basic usage
import assert from 'node:assert/strict';
import { Browser } from '@zombieland/tallahassee';
let browser;
before('a browser with a default origin', () => {
browser = new Browser('http://localhost:7411');
});
test('simple navigation', () => {
const dom = await browser.navigateTo('/');
assert.equal(dom.window.document.title, 'Zombieland');
});
test('detailed navigation', () => {
const response = await browser.fetch('/', {
headers: { 'Cookie': 'signed-in=1' },
});
assert.equal(response.status, 200);
const dom = await browser.load(response, { runScripts: 'dangerously' });
assert.equal(dom.window.document.title, 'Zombieland');
});API
Browser
A module for testing navigation within an origin
import { Browser } from '@zombieland/tallahassee';new Browser(origin[, cookieJar])
Creates a new browser instance
origin<string>Base URL used bybrowser.fetchcookieJar<CookieJar>A jar of cookies to be used bybrowser.fetchmethod. Defaultnew CookieJar()
browser.navigateTo(resource[, fetchOptions, loadOptions])
Fetches a document and loads a DOM
resourcePassed on tobrowser.fetchfetchOptionsPassed on tobrowser.fetchloadOptionsPassed on tobrowser.load.- Returns:
<Promise>Fulfills with aJSDOMon success
const dom = await browser.navigateTo(
'/',
{ headers: { 'Cookie'; 'some-cookie=value' } },
{ runScripts: 'dangerously' }
);browser.fetch(resource[, options])
Fetches a document. Useful for inspecting response details before loading DOM with browser.load().
resource<string>|<URL>|<Request>path (relative to the browser origin) / URL to a document or a request objectoptions<Object>ARequestInitdictionary- Returns:
<Promise>Fulfills with aResponseon success
const pendingResponse = browser.fetch('/', {
headers: { 'Cookie'; 'some-cookie=value' }
});Any request Cookie header will be applied to browser.cookieJar before actual fetch().
Any response Set-Cookie will be applied to browser.cookieJar before creating new JSDOM().
Redirects are followed manually with recursive calls to browser.fetch in order to properly set/get cookies from browser.cookieJar.
browser.load(resource[, options])
Loads a DOM from a document string or response
resource<string>|<Response>|<Promise>A document string or response string to load into JSDOMoptions<Object>{[painter, resources, ...jsdomOptions]}painter:<Painter>Little RockPainterinstanceresources:<Resources>WichitaResourcesinstancejsdomOptions:<Object>Options to be passed onto JSDOM- Default:
runScripts:'outside-only'ifoptions.painterpretendToBeVisual:trueifoptions.painter
- Fixed values:
url:urlfromresourceif instance ofResponsecontentType:Content-Typeresponse header fromresourceif instance ofResponsecookieJar:cookieJarfrombrowserinstancebeforeParse: A function that will run:options.painter?.beforeParse: From a Little RockPainterinstanceoptions.resources?.beforeParse: From a WichitaResourcesinstanceoptions.beforeParse
- Default:
- Returns:
<Promise>Fulfills with aJSDOMon success
const dom = await browser.load(pendingResponse, {
runScripts: 'dangerously'
});If using Little Rock and/or Wichita their beforeParse methods will be run automatically if passed into options:
import { Browser } from "@zombieland/tallahassee";
import { Painter } from "@zombieland/little-rock";
import { ResourceLoader } from "@zombieland/wichita";
const browser = new Browser(…);
const pendingResponse = browser.fetch('/');
const dom = await browser.load(pendingResponse, {
painter: new Painter(…),
resources: new ResourceLoader(…),
});browser.captureNavigation(dom[, follow])
Captures navigation from link clicks and form submits.
dom<JSDOM>A DOM to observefollow<Boolean>To follow request or not. Defaultfalse- Returns:
<Promise>Resolves with a<Request>or<Response>fromfetchiffollow: true. Rejects with anEventwhich blocked the navigation.
const linkOrFormSubmit = dom.window.querySelector('a, button[type=submit]');
const pendingNavigation = browser.captureNavigation(dom, false);
linkOrFormSubmit.click();
const request = await pendingNavigation;
assert(request instanceof Request);Or with follow: true to perform a call to browser.fetch()
const pendingNavigation = browser.captureNavigation(dom, true);
linkOrFormSubmit.click();
const response = await pendingNavigation;
assert(request instanceof Response);Navigation will fail if stopped by:
- Prevented default action of link
click/ formsubmitevent usingpreventDefault() - Form element
invalidevent
await assert.reject(pendingNavigation, (event) => {
assert.equal(event.type, 'invalid');
assert.equal(event.target, form.elements[1]);
return true;
})Navigation is intercepted at the window level using event listeners. Promise will not settle if event propagation is stopped or if form submit is triggered without event, e.g. with the submit method.
ReverseProxy
A module for emulating a CDN like reverse proxy. Basically a wrapper around nock.
import { ReverseProxy } from '@zombieland/tallahassee';new ReverseProxy(proxyOrigin, upstreamOrigin[, headers])
Creates HTTP interceptor for a public proxy origin and proxies request to a local upstream origin.
proxyOrigin<string>Public URL originupstreamOrigin<string>Server URL originheaders<Object>|<Headers>Headers to pass along to server. Default Standard forwarding headers (Forwarded,X-Forwarded-Proto,X-Forwarded-Host) derived fromproxyOrigin- Returns:
<ReverseProxy>
import http from 'node:http';
import { Browser, ReverseProxy } from '@zombieland/tallahassee';
http.createServer(…).listen(7411);
const reverseProxy = new ReverseProxy('https://tallahassee.zl', 'http://localhost:7411')
const browser = new Browser('https://tallahassee.zl');
const dom = await browser.navigateTo('/safe-house');
assert.equal(dom.window.location, 'https://tallahassee.zl/safe-house');reverseProxy.modifyUpstreamRequest(req)
Modifies the upstream request before it is sent to the upstream origin. This method can be overridden to customize request headers or other properties.
The default implementation applies the headers supplied to the constructor.
req<Request>The request object to be sent to the upstream origin- Returns:
<Request>The modified request object
class CustomReverseProxy extends ReverseProxy {
modifyUpstreamRequest(req) {
req = super.modifyUpstreamRequest(req);
const forwarded = req.headers.get('forwarded');
req.headers.set('forwarded', `for=192.168.0.1;${forwarded}`);
req.headers.set('Via', '1.1 MyProxy');
return req;
}
}reverseProxy.clear()
Clears nocked responses for proxyOrigin
- Returns:
undefined
reverseProxy.clear();Todo
- [ ] Reloading page
- [ ] Unload browser and all its active jsdom instances
- [ ] Expose network requests
- [x] Use node
fetch/Response/Request- [x] Stable version of Nock
- [x] In-page navigation (clicking links etc.)
- [x] Containing requests to the app is currently done by setting up a
nockscope around app origin which intercepts all reqs and proxies them throughsupertest. Not ideal for a bunch of reasons:- [x] There is no built in way to clear a specific scope - creative workaround
- [x] Scrap use of SuperTest. It's incorrectly used as an HTTP lib because of its ability to make requests to a server. Not having a listening server makes handling of client side requests messy. Calls to
XMLHttpRequestneeds to be intercepted and cookies will need to be handled manually. Also having the consumer starting / stopping their server once per test process would be more performant than doing it adhoc for each request.
