history testing and rework

This commit is contained in:
carson 2020-05-13 06:45:43 -07:00
parent fe7b878952
commit 00e2249f0b
10 changed files with 252 additions and 127 deletions

View File

@ -7,19 +7,19 @@
## TODOS
* Testing
* history
* polling
* kt-boost
* interval parsing
* table elements in responses
* scrolling/'revealed' event
* checkbox inputs
* history
* kt-boost
* kt-swap-oob (verify, chrome coverage tool bad?)
* X-KT-Trigger response header
* SSE stuff
* kt-trigger delay
* class operation parsing
* class toggling
* polling
* transition model for content swaps

69
dist/kutty.js vendored
View File

@ -8,6 +8,21 @@ var kutty = kutty || (function () {
// Utilities
//====================================================================
function makeBrowserHistorySupport() {
return {
getFullPath: function () {
return location.pathname + location.search;
},
getLocalStorage: function () {
return localStorage;
},
getHistory: function () {
return history;
}
};
}
var browserHistorySupport = makeBrowserHistorySupport();
function parseInterval(str) {
if (str === "null" || str === "false" || str === "") {
return null;
@ -36,6 +51,10 @@ var kutty = kutty || (function () {
function getDocument() {
return document;
}
function getBody() {
return getDocument().body;
}
function getClosestMatch(elt, condition) {
if (condition(elt)) {
@ -147,7 +166,7 @@ var kutty = kutty || (function () {
}
function bodyContains(elt) {
return getDocument().body.contains(elt);
return getBody().contains(elt);
}
function concat(arr1, arr2) {
@ -179,7 +198,7 @@ var kutty = kutty || (function () {
} else {
var data = getInternalData(elt);
if (data.boosted) {
return getDocument().body;
return getBody();
} else {
return elt;
}
@ -208,7 +227,7 @@ var kutty = kutty || (function () {
settleTasks = settleTasks.concat(swapOuterHTML(target, fragment));
} else {
child.parentNode.removeChild(child);
triggerEvent(getDocument().body, "oobErrorNoTarget.kutty", {content: child})
triggerEvent(getBody(), "oobErrorNoTarget.kutty", {content: child})
}
}
});
@ -665,14 +684,14 @@ var kutty = kutty || (function () {
var target, event, listener;
if (isFunction(arg1)) {
ready(function(){
target = getDocument().body;
target = getBody();
event = "all.kutty";
listener = arg1;
target.addEventListener(event, listener);
})
} else if (isFunction(arg2)) {
ready(function () {
target = getDocument().body;
target = getBody();
event = arg1;
listener = arg2;
target.addEventListener(event, listener);
@ -690,7 +709,7 @@ var kutty = kutty || (function () {
//====================================================================
function getHistoryElement() {
var historyElt = getDocument().querySelector('[kt-history-elt]');
return historyElt || getDocument().body;
return historyElt || getBody();
}
function purgeOldestPaths(paths, historyTimestamps) {
@ -702,32 +721,32 @@ var kutty = kutty || (function () {
slot++;
if (slot > 20) {
delete historyTimestamps[path];
localStorage.removeItem(path);
browserHistorySupport.getLocalStorage().removeItem(path);
}
});
}
function bumpHistoryAccessDate(pathAndSearch) {
var historyTimestamps = JSON.parse(localStorage.getItem("kt-history-timestamps")) || {};
var historyTimestamps = JSON.parse(browserHistorySupport.getLocalStorage().getItem("kt-history-timestamps")) || {};
historyTimestamps[pathAndSearch] = Date.now();
var paths = Object.keys(historyTimestamps);
if (paths.length > 20) {
purgeOldestPaths(paths, historyTimestamps);
}
localStorage.setItem("kt-history-timestamps", JSON.stringify(historyTimestamps));
browserHistorySupport.getLocalStorage().setItem("kt-history-timestamps", JSON.stringify(historyTimestamps));
}
function saveHistory() {
var elt = getHistoryElement();
var pathAndSearch = location.pathname+location.search;
triggerEvent(getDocument().body, "historyUpdate.kutty", {path:pathAndSearch, historyElt:elt});
history.replaceState({}, getDocument().title, window.location.href);
localStorage.setItem('kt-history:' + pathAndSearch, elt.innerHTML);
var pathAndSearch = browserHistorySupport.getFullPath();
triggerEvent(getBody(), "historyUpdate.kutty", {path:pathAndSearch, historyElt:elt});
browserHistorySupport.getHistory().replaceState({}, getDocument().title);
browserHistorySupport.getLocalStorage().setItem('kt-history:' + pathAndSearch, elt.innerHTML);
bumpHistoryAccessDate(pathAndSearch);
}
function pushUrlIntoHistory(url) {
history.pushState({}, "", url );
browserHistorySupport.getHistory().pushState({}, "", url );
}
function settleImmediately(settleTasks) {
@ -739,25 +758,25 @@ var kutty = kutty || (function () {
function loadHistoryFromServer(pathAndSearch) {
var request = new XMLHttpRequest();
var details = {path: pathAndSearch, xhr:request};
triggerEvent(getDocument().body, "historyCacheMiss.kutty", details);
triggerEvent(getBody(), "historyCacheMiss.kutty", details);
request.open('GET', pathAndSearch, true);
request.onload = function () {
if (this.status >= 200 && this.status < 400) {
triggerEvent(getDocument().body, "historyCacheMissLoad.kutty", details);
triggerEvent(getBody(), "historyCacheMissLoad.kutty", details);
var fragment = makeFragment(this.response);
fragment = fragment.querySelector('[kt-history-elt]') || fragment;
settleImmediately(swapInnerHTML(getHistoryElement(), fragment));
} else {
triggerEvent(getDocument().body, "historyCacheMissLoadError.kutty", details);
triggerEvent(getBody(), "historyCacheMissLoadError.kutty", details);
}
};
request.send();
}
function restoreHistory() {
var pathAndSearch = location.pathname+location.search;
triggerEvent(getDocument().body, "historyRestore.kutty", {path:pathAndSearch});
var content = localStorage.getItem('kt-history:' + pathAndSearch);
var pathAndSearch = browserHistorySupport.getFullPath();
triggerEvent(getBody(), "historyRestore.kutty", {path:pathAndSearch});
var content = browserHistorySupport.getLocalStorage().getItem('kt-history:' + pathAndSearch);
if (content) {
bumpHistoryAccessDate(pathAndSearch);
settleImmediately(swapInnerHTML(getHistoryElement(), makeFragment(content)));
@ -1138,7 +1157,7 @@ var kutty = kutty || (function () {
// initialize the document
ready(function () {
var body = getDocument().body;
var body = getBody();
processNode(body);
triggerEvent(body, 'load.kutty', {});
window.onpopstate = function () {
@ -1164,6 +1183,14 @@ var kutty = kutty || (function () {
}
}
function setBrowserHistorySupport(mock) {
browserHistorySupport = mock;
}
function restoreBrowserHistorySupport() {
browserHistorySupport = browserHistorySupport();
}
// Public API
return {
processElement: processNode,

2
dist/kutty.min.js vendored

File diff suppressed because one or more lines are too long

BIN
dist/kutty.min.js.gz vendored

Binary file not shown.

View File

@ -1,5 +1,5 @@
// noinspection JSUnusedAssignment
var kutty = kutty || (function () {
var kutty = (function () {
'use strict';
var VERBS = ['get', 'post', 'put', 'delete', 'patch']
@ -60,7 +60,7 @@ var kutty = kutty || (function () {
var matchesFunction = elt.matches ||
elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector
|| elt.webkitMatchesSelector || elt.oMatchesSelector;
return (elt != null) && matchesFunction != null && matchesFunction.call(elt, selector);
return matchesFunction && matchesFunction.call(elt, selector);
}
function closest(elt, selector) {
@ -395,7 +395,7 @@ var kutty = kutty || (function () {
}
}
function processClassList(elt, classList, operation) {
function processClassList(elt, classList) {
forEach(classList.split("&"), function (run) {
var currentRunTime = 0;
forEach(run.split(","), function(value){
@ -688,46 +688,49 @@ var kutty = kutty || (function () {
//====================================================================
// History Support
//====================================================================
var currentPathForHistory = null;
function getHistoryElement() {
var historyElt = getDocument().querySelector('[kt-history-elt]');
return historyElt || getDocument().body;
}
function purgeOldestPaths(paths, historyTimestamps) {
paths = paths.sort(function (path1, path2) {
return historyTimestamps[path2] - historyTimestamps[path1]
});
var slot = 0;
forEach(paths, function (path) {
slot++;
if (slot > 20) {
delete historyTimestamps[path];
localStorage.removeItem(path);
function saveToHistoryCache(url, content, title, scroll) {
var historyCache = JSON.parse(localStorage.getItem("kutty-history-cache")) || [];
for (var i = 0; i < historyCache.length; i++) {
if (historyCache[i].url === url) {
historyCache = historyCache.slice(i, 1);
break;
}
});
}
historyCache.push({url:url, content: content, title:title, scroll:scroll})
while (historyCache.length > kutty.config.historyCacheSize) {
historyCache.shift();
}
localStorage.setItem("kutty-history-cache", JSON.stringify(historyCache));
}
function bumpHistoryAccessDate(pathAndSearch) {
var historyTimestamps = JSON.parse(localStorage.getItem("kt-history-timestamps")) || {};
historyTimestamps[pathAndSearch] = Date.now();
var paths = Object.keys(historyTimestamps);
if (paths.length > 20) {
purgeOldestPaths(paths, historyTimestamps);
function getCachedHistory(url) {
var historyCache = JSON.parse(localStorage.getItem("kutty-history-cache")) || [];
for (var i = 0; i < historyCache.length; i++) {
if (historyCache[i].url === url) {
return historyCache[i];
}
}
localStorage.setItem("kt-history-timestamps", JSON.stringify(historyTimestamps));
return null;
}
function saveHistory() {
var elt = getHistoryElement();
var pathAndSearch = location.pathname+location.search;
triggerEvent(getDocument().body, "historyUpdate.kutty", {path:pathAndSearch, historyElt:elt});
history.replaceState({}, getDocument().title, window.location.href);
localStorage.setItem('kt-history:' + pathAndSearch, elt.innerHTML);
bumpHistoryAccessDate(pathAndSearch);
var path = currentPathForHistory || location.pathname+location.search;
triggerEvent(getDocument().body, "beforeHistorySave.kutty", {path:path, historyElt:elt});
if(kutty.config.historyEnabled) history.replaceState({}, getDocument().title, window.location.href);
saveToHistoryCache(path, elt.innerHTML, getDocument().title, window.scrollY);
}
function pushUrlIntoHistory(url) {
history.pushState({}, "", url );
function pushUrlIntoHistory(path) {
if(kutty.config.historyEnabled) history.pushState({}, "", path);
currentPathForHistory = path;
}
function settleImmediately(settleTasks) {
@ -736,17 +739,18 @@ var kutty = kutty || (function () {
});
}
function loadHistoryFromServer(pathAndSearch) {
function loadHistoryFromServer(path) {
var request = new XMLHttpRequest();
var details = {path: pathAndSearch, xhr:request};
var details = {path: path, xhr:request};
triggerEvent(getDocument().body, "historyCacheMiss.kutty", details);
request.open('GET', pathAndSearch, true);
request.open('GET', path, true);
request.onload = function () {
if (this.status >= 200 && this.status < 400) {
triggerEvent(getDocument().body, "historyCacheMissLoad.kutty", details);
var fragment = makeFragment(this.response);
fragment = fragment.querySelector('[kt-history-elt]') || fragment;
settleImmediately(swapInnerHTML(getHistoryElement(), fragment));
currentPathForHistory = path;
} else {
triggerEvent(getDocument().body, "historyCacheMissLoadError.kutty", details);
}
@ -754,15 +758,18 @@ var kutty = kutty || (function () {
request.send();
}
function restoreHistory() {
var pathAndSearch = location.pathname+location.search;
triggerEvent(getDocument().body, "historyRestore.kutty", {path:pathAndSearch});
var content = localStorage.getItem('kt-history:' + pathAndSearch);
if (content) {
bumpHistoryAccessDate(pathAndSearch);
settleImmediately(swapInnerHTML(getHistoryElement(), makeFragment(content)));
function restoreHistory(path) {
saveHistory(currentPathForHistory);
path = path || location.pathname+location.search;
triggerEvent(getDocument().body, "historyRestore.kutty", {path:path});
var cached = getCachedHistory(path);
if (cached) {
settleImmediately(swapInnerHTML(getHistoryElement(), makeFragment(cached.content)));
document.title = cached.title;
window.scrollTo(0, cached.scroll);
currentPathForHistory = path;
} else {
loadHistoryFromServer(pathAndSearch);
loadHistoryFromServer(path);
}
}
@ -958,9 +965,9 @@ var kutty = kutty || (function () {
function getSwapSpecification(elt) {
var swapInfo = getClosestAttributeValue(elt, "kt-swap");
var swapSpec = {
"swapStyle" : "innerHTML",
"swapDelay" : 0,
"settleDelay" : 100
"swapStyle" : kutty.config.defaultSwapStyle,
"swapDelay" : kutty.config.defaultSwapDelay,
"settleDelay" : kutty.config.defaultSettleDelay
}
if (swapInfo) {
var split = splitOnWhitespace(swapInfo);
@ -1075,7 +1082,6 @@ var kutty = kutty || (function () {
// push URL and save new page
if (shouldSaveHistory) {
pushUrlIntoHistory(pushedUrl || requestURL );
saveHistory();
}
triggerEvent(elt, 'afterSettle.kutty', eventDetail);
}
@ -1136,8 +1142,19 @@ var kutty = kutty || (function () {
addRule(".kutty-request .kutty-indicator{opacity:1}");
addRule(".kutty-request.kutty-indicator{opacity:1}");
function getConfig() {
return Object.assign({
historyEnabled:true,
historyCacheSize:10,
defaultSwapStyle:'innerHTML',
defaultSwapDelay:0,
defaultSettleDelay:100
}, JSON.parse(getDocument().querySelector('meta[name="kutty-config"]').content))
}
// initialize the document
ready(function () {
kutty.config = getConfig();
var body = getDocument().body;
processNode(body);
triggerEvent(body, 'load.kutty', {});
@ -1170,8 +1187,10 @@ var kutty = kutty || (function () {
on: addKuttyEventListener,
onLoad: onLoadHelper,
logAll : logAll,
logger : null,
config : null,
version: "0.0.1",
_:internalEval
}
}
)();
)() || kutty;

View File

@ -18,8 +18,85 @@
</script>
<script src="util.js"></script>
<script src="kt-push-url.js"></script>
<script src="kt-boost.js"></script>
<script>
describe("Browser Only Tests", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it("should handle a basic back button click", function (done) {
this.server.respondWith("GET", "/test", "second");
getWorkArea().innerHTML.should.be.equal("");
var div = make('<div kt-push-url="true" kt-get="/test">first</div>');
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("second")
history.back();
setTimeout(function(){
getWorkArea().textContent.should.equal("first");
done();
}, 20);
});
it("should handle two forward clicks then back twice", function (done) {
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, "" + i);
});
getWorkArea().innerHTML.should.equal("");
var div = make('<div kt-push-url="true" kt-get="/test" class="">0</div>');
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("1")
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("2")
history.back();
setTimeout(function(){
getWorkArea().textContent.should.equal("1");
history.back();
setTimeout(function(){
getWorkArea().textContent.should.equal("0");
done();
}, 20);
}, 20);
})
it("should handle a back, forward, back button click", function (done) {
this.server.respondWith("GET", "/test", "second");
getWorkArea().innerHTML.should.equal("");
var div = make('<div kt-push-url="true" kt-get="/test" class="">first</div>');
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("second")
history.back();
setTimeout(function(){
getWorkArea().textContent.should.equal("first");
history.forward();
setTimeout(function() {
getWorkArea().textContent.should.equal("second");
history.back();
setTimeout(function() {
getWorkArea().textContent.should.equal("first");
done();
}, 20);
}, 20);
}, 20);
})
});
</script>
<script class="mocha-exec">

View File

@ -9,6 +9,7 @@
<meta http-equiv="expires" content="0" />
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
<meta http-equiv="pragma" content="no-cache" />
<meta name="kutty-config" content='{"historyEnabled":false}'>
</head>
<body>
<div id="mocha"></div>
@ -39,6 +40,7 @@
<script src="kt-params.js"></script>
<script src="kt-patch.js"></script>
<script src="kt-post.js"></script>
<script src="kt-push-url.js"></script>
<script src="kt-put.js"></script>
<script src="kt-swap-oob.js"></script>
<script src="kt-swap.js"></script>

View File

@ -1,77 +1,77 @@
describe("kt-push-url attribute", function() {
var KUTTY_HISTORY_CACHE = "kutty-history-cache";
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
localStorage.removeItem(KUTTY_HISTORY_CACHE);
});
afterEach(function () {
this.server.restore();
clearWorkArea();
localStorage.removeItem(KUTTY_HISTORY_CACHE);
});
it("should handle a basic back button click", function (done) {
it("navigation should push an element into the cache ", function () {
this.server.respondWith("GET", "/test", "second");
getWorkArea().innerHTML.should.be.equal("");
var div = make('<div kt-push-url="true" kt-get="/test">first</div>');
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("second")
history.back();
setTimeout(function(){
getWorkArea().textContent.should.equal("first");
done();
}, 20);
var cache = JSON.parse(localStorage.getItem(KUTTY_HISTORY_CACHE));
cache.length.should.equal(1);
});
it("should handle two forward clicks then back twice", function (done) {
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, "" + i);
});
getWorkArea().innerHTML.should.equal("");
var div = make('<div kt-push-url="true" kt-get="/test" class="">0</div>');
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("1")
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("2")
history.back();
setTimeout(function(){
getWorkArea().textContent.should.equal("1");
history.back();
setTimeout(function(){
getWorkArea().textContent.should.equal("0");
done();
}, 20);
}, 20);
})
it("should handle a back, forward, back button click", function (done) {
it("restore should return old value", function () {
this.server.respondWith("GET", "/test", "second");
getWorkArea().innerHTML.should.equal("");
var div = make('<div kt-push-url="true" kt-get="/test" class="">first</div>');
getWorkArea().innerHTML.should.be.equal("");
var div = make('<div kt-push-url="true" kt-get="/test">first</div>');
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("second")
history.back();
setTimeout(function(){
getWorkArea().textContent.should.equal("first");
history.forward();
setTimeout(function() {
getWorkArea().textContent.should.equal("second");
history.back();
setTimeout(function() {
getWorkArea().textContent.should.equal("first");
done();
}, 20);
}, 20);
}, 20);
})
});
kutty._('restoreHistory')(location.pathname+location.search)
getWorkArea().textContent.should.equal("first")
});
it("cache should only store 10 entries", function () {
var x = 0;
this.server.respondWith("GET", /test.*/, function(xhr){
x++;
xhr.respond(200, {}, '<div id="d1" kt-push-url="true" kt-get="/test' + x + '" kt-swap="outerHTML settle:0"></div>')
});
getWorkArea().innerHTML.should.be.equal("");
make('<div id="d1" kt-push-url="true" kt-get="/test" kt-swap="outerHTML settle:0"></div>');
for (var i = 0; i < 20; i++) { // issue 20 requests
byId("d1").click();
this.server.respond();
}
var cache = JSON.parse(localStorage.getItem(KUTTY_HISTORY_CACHE));
cache.length.should.equal(10); // should only be 10 elements
});
it("cache miss should issue another GET", function () {
this.server.respondWith("GET", "/test1", '<div id="d2" kt-push-url="true" kt-get="/test2" kt-swap="outerHTML settle:0">test1</div>');
this.server.respondWith("GET", "/test2", '<div id="d3" kt-push-url="true" kt-get="/test3" kt-swap="outerHTML settle:0">test2</div>');
make('<div id="d1" kt-push-url="true" kt-get="/test1" kt-swap="outerHTML settle:0">init</div>');
byId("d1").click();
this.server.respond();
var workArea = getWorkArea();
workArea.textContent.should.equal("test1")
byId("d2").click();
this.server.respond();
workArea.textContent.should.equal("test2")
var cache = JSON.parse(localStorage.getItem(KUTTY_HISTORY_CACHE));
cache.length.should.equal(2);
localStorage.removeItem(KUTTY_HISTORY_CACHE); // clear cache
kutty._('restoreHistory')("/test1")
this.server.respond();
getWorkArea().textContent.should.equal("test1")
});
});

View File

@ -97,7 +97,7 @@ This event is triggered when kutty handles a history restoration action
* `detail.path` - the path and query of the page being restored
### <a name="historyUpdate.kutty"></a> Event - [`historyUpdate.kutty`](#historyUpdate.kutty)
### <a name="beforeHistorySave.kutty"></a> Event - [`beforeHistorySave.kutty`](#beforeHistorySave.kutty)
This event is triggered when kutty handles a history restoration action

View File

@ -76,7 +76,7 @@ title: </> kutty - Attributes
| [`historyCacheMissError.kutty`](/events#historyCacheMissError.kutty) | triggered on a unsuccessful remote retrieval
| [`historyCacheMissLoad.kutty`](/events#historyCacheMissLoad.kutty) | triggered on a succesful remote retrieval
| [`historyRestore.kutty`](/events#historyRestore.kutty) | triggered when kutty handles a history restoration action
| [`historyUpdate.kutty`](/events#historyUpdate.kutty) | triggered when a new history element is added to the local cache
| [`beforeHistorySave.kutty`](/events#beforeHistorySave.kutty) | triggered before content is saved to the history cache
| [`initSSE.kutty`](/events#initSSE.kutty) | triggered when a new Server Sent Event source is created
| [`load.kutty`](/events#load.kutty) | triggered when new content is added to the DOM
| [`noSSESourceError.kutty`](/events#noSSESourceError.kutty) | triggered when an element refers to a SSE event in its trigger, but no parent SSE source has been defined