all repos — h3 @ e5eab54053c1beabef632902f0a587251185f76c

A tiny, extremely minimalist JavaScript microframework.

Added unit tests; badges.
h3rald h3rald@h3rald.com
Tue, 21 Apr 2020 17:22:54 +0200
commit

e5eab54053c1beabef632902f0a587251185f76c

parent

2e81b061e9bb015d24cb861db817ca1c82f6cae1

5 files changed, 158 insertions(+), 12 deletions(-)

jump to
M __tests__/h3.js__tests__/h3.js

@@ -423,4 +423,30 @@ await h3.init(c);

expect(appendChild).toHaveBeenCalled(); expect(body.childNodes[0].childNodes[0].data).toEqual("Hello, World!"); }); + + it("should provide some validation at initialization time", async () => { + try { + await h3.init({ element: "INVALID", routes: {} }); + } catch (e) { + expect(e.message).toMatch(/Invalid element/); + } + try { + await h3.init({ element: document.body }); + } catch (e) { + expect(e.message).toMatch(/not a valid configuration object/); + } + try { + await h3.init({ element: document.body, routes: {} }); + } catch (e) { + expect(e.message).toMatch(/No routes/); + } + }); + + it("should expose a redraw method", async () => { + const vnode = h3("div"); + await h3.init(() => vnode); + jest.spyOn(vnode, "redraw"); + h3.redraw(); + expect(vnode.redraw).toHaveBeenCalled(); + }); });
A __tests__/router.js

@@ -0,0 +1,76 @@

+const h3 = require("../h3.js").default; + +let preStartCalled = false; +let postStartCalled = false; + +let hash = "#/c2"; +const mockLocation = { + get hash() { + return hash; + }, + set hash(value) { + hash = value; + window.dispatchEvent(new HashChangeEvent("hashchange")); + }, +}; + +describe("h3 (Router)", () => { + beforeEach(async () => { + const preStart = () => (preStartCalled = true); + const postStart = () => (postStartCalled = true); + const C1 = () => { + const parts = h3.route.parts; + const content = Object.keys(parts).map((key) => + h3("li", `${key}: ${parts[key]}`) + ); + return h3("ul.c1", content); + }; + const C2 = () => { + const params = h3.route.params; + const content = Object.keys(params).map((key) => + h3("li", `${key}: ${params[key]}`) + ); + return h3("ul.c2", content); + }; + return await h3.init({ + routes: { + "/c1/:a/:b/:c": C1, + "/c2": C2, + }, + location: mockLocation, + preStart: preStart, + postStart: postStart, + }); + }); + + it("should support routing configuration at startup", () => { + expect(h3.route.def).toEqual("/c2"); + }); + + it("should support pre/post start hooks", () => { + expect(preStartCalled).toEqual(true); + expect(postStartCalled).toEqual(true); + }); + + it("should support the capturing of parts within the current route", () => { + mockLocation.hash = "#/c1/1/2/3"; + expect(document.body.childNodes[0].childNodes[1].textContent).toEqual( + "b: 2" + ); + }); + + it("should expose a navigateTo method to navigate to another path", () => { + h3.navigateTo("/c2", {test1: 1, test2: 2}); + expect(document.body.childNodes[0].childNodes[1].textContent).toEqual( + "test2: 2" + ); + }); + + it("should fail if no route matches at startup", async () => { + try { + await h3.init({element: document.body, routes: {"/gasdgasdg": () => false}}); + } catch(e) { + expect(e.message).toMatch(/No route matches/); + } + }); +});
A __tests__/store.js

@@ -0,0 +1,37 @@

+const h3 = require("../h3.js").default; + +describe("h3 (Store)", () => { + beforeEach(async () => { + const test = () => { + h3.on("$init", () => ({ online: true })); + h3.on("$stop", () => ({ online: false })); + h3.on("online/set", (state, data) => ({ online: data })); + }; + return await h3.init({ modules: [test], routes: { "/": () => h3("div") } }); + }); + + afterEach(() => { + h3.dispatch("$stop"); + }); + + it("should expose a method to retrieve the application state", () => { + expect(h3.state.online).toEqual(true); + }); + + it("should expose a method to dispatch messages", () => { + expect(h3.state.online).toEqual(true); + h3.dispatch("online/set", "YEAH!"); + expect(h3.state.online).toEqual("YEAH!"); + }); + + it("should expose a method to subscribe to messages (and also cancel subscriptions)", () => { + const sub = h3.on("online/clear", () => ({ online: undefined })); + h3.dispatch("online/clear"); + expect(h3.state.online).toEqual(undefined); + h3.dispatch("online/set", "reset"); + expect(h3.state.online).toEqual("reset"); + sub(); + h3.dispatch("online/clear"); + expect(h3.state.online).toEqual("reset"); + }); +});
M docs/md/overview.mddocs/md/overview.md

@@ -1,3 +1,9 @@

+[![NPM](https://img.shields.io/npm/v/h3js)](https://www.npmjs.com/package/h3js) +[![License](https://img.shields.io/github/license/h3rald/h3)](https://github.com/h3rald/h3/blob/master/LICENSE) +[![Build](https://img.shields.io/travis/h3rald/h3)](https://travis-ci.org/github/h3rald/h3) +[![Coverage](https://img.shields.io/coveralls/github/h3rald/h3)](https://coveralls.io/github/h3rald/h3?branch=master) + + ## Overview **H3** is a microframework to build client-side single-page applications (SPAs) in modern JavaScript.
M h3.jsh3.js

@@ -525,13 +525,11 @@ }

} class Router { - constructor({ element, routes, store }) { + constructor({ element, routes, store, location }) { this.element = element; this.redraw = null; this.store = store; - if (!this.element) { - throw new Error(`[Router] No view element specified.`); - } + this.location = location || window.location; if (!routes || Object.keys(routes).length === 0) { throw new Error("[Router] No routes defined."); }

@@ -549,14 +547,14 @@ }

start() { const processPath = (data) => { - const hash = + const fragment = (data && data.newURL && data.newURL.match(/(#.+)$/) && data.newURL.match(/(#.+)$/)[1]) || - window.location.hash; - const path = hash.replace(/\?.+$/, "").slice(1); - const rawQuery = hash.match(/\?(.+)$/); + this.location.hash; + const path = fragment.replace(/\?.+$/, "").slice(1); + const rawQuery = fragment.match(/\?(.+)$/); const query = rawQuery && rawQuery[1] ? rawQuery[1] : ""; const pathParts = path.split("/").slice(1); let parts = {};

@@ -581,7 +579,7 @@ break;

} } if (!this.route) { - this.route = new Route({ query, path, def, parts }); + throw new Error(`[Router] No route matches '${fragment}'`); } // Display View while (this.element.firstChild) {

@@ -602,7 +600,7 @@ let query = Object.keys(params || {})

.map((p) => `${encodeURIComponent(p)}=${encodeURIComponent(params[p])}`) .join("&"); query = query ? `?${query}` : ""; - window.location.hash = `#${path}${query}`; + this.location.hash = `#${path}${query}`; } }

@@ -615,9 +613,12 @@ let store = null;

let router = null; h3.init = (config) => { - let { element, routes, modules, preStart, postStart } = config; + let { element, routes, modules, preStart, postStart, location } = config; if (!routes) { // Assume config is a component object, define default route + if (typeof config !== "function") { + throw new Error("[h3.init] The specified argument is not a valid configuration object or component function"); + } routes = { "/": config }; } element = element || document.body;

@@ -631,7 +632,7 @@ if (i) i(store);

}); store.dispatch("$init"); // Initialize router - router = new Router({ element, routes, store }); + router = new Router({ element, routes, store, location }); return Promise.resolve(preStart && preStart()) .then(() => router.start()) .then(() => postStart && postStart());