Browse Source

moving apps into right trunk

Herbert Vojčík 13 years ago
parent
commit
613c573df0

+ 15 - 0
boot.js

@@ -0,0 +1,15 @@
+// (c) 2009, Herbert Vojčík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+(function() {
+  var _an = appjet._native;
+  var tableid = _an.storage_get("obj-root", "bootTable").value;
+  var appBoot = _an.storage_get(tableid, request.headers.Host).value;
+  var name, queue = appjet._internal.queue = appBoot ? appBoot.split(",") : ["0", "", "lib-0"];
+  appjet.appName = queue.shift();
+  appjet.mainDomain = queue.shift() || _an.storage_get("obj-root", "defaultDomain").value;
+  while((queue = appjet._internal.queue) && (name = queue.shift())) {
+    var agent = import({}, name);
+    if (typeof agent.runAgent === "function") { agent.runAgent(queue); }
+  }
+})();

+ 30 - 0
lib-0.js

@@ -0,0 +1,30 @@
+/* appjet:version 0.1 */
+/* appjet:library */
+// (c) 2009, Herbert Vojčík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+import("lib-app/liveui", "lib-app/mvp");
+
+page.setView(box(
+	H1("Default page"),
+	sub(data("missingDomain"), box(P("Cannot find handler for domain: ", data())))	
+));
+
+function get_main() {
+	page.setViewModel({missingDomain:request.headers.Host});
+}
+
+import("storage");
+
+function get_localhostsetup() {
+  if (!storage.bootTable) { storage.bootTable = {}; }
+	storage.bootTable["localhost:8080"] = request.query;
+	page.setView(P("Localhost setup successful"));
+}
+
+import("lib-app/../../config");
+
+import("lib-utils/patches");
+dispatchShouldSearchHere();
+mvp();
+response.setStatusCode(404);

+ 74 - 0
lib-app/appstart.js

@@ -0,0 +1,74 @@
+/* appjet:version 0.1 */
+/* appjet:library */
+// (c) 2010, Herbert Vojčík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+// Fake 'storage' to point to 'storage[appjet.appName]'
+// Must be imported before storage (the best is to import it first).
+var old = appjet._native.runLibrary;
+appjet._native.runLibrary = function () {
+	var lib = Function.prototype.apply.call(old, this, arguments);
+	lib.storage = lib.storage[appjet.appName];
+	return lib;
+};
+try { import({}, "storage"); } finally { appjet._native.runLibrary = old; }
+
+import("lib-app/globals");
+
+var _hdrWrite = page.body.write,
+  _dir;
+
+function appRun (dir, callback) {
+  setupAppDir(dir);
+  setupAppPresenter();
+  setupAppView();
+  setupAppModel();
+  if (callback) callback();
+  appGo();
+}
+
+function setupAppDir (dir) { _dir = dir; }
+
+function setupAppPresenter () {
+  if (request.isGet && request.path !== '/') {
+    var _oldFbInit = fb.init;
+    fb.init = function() {
+      request.method = "POST";
+      try {
+        _oldFbInit.apply(this, arguments);
+      } finally {
+        request.method = "GET";
+      }
+    };
+  }
+  fb.init();
+  import(_dir + "presenter");
+  import("lib-app/dispatch-plus");
+  page.setPresenter(dispatchPlus);
+}
+
+function setupAppView () {
+  dispatchInject(function post_callback_ui_ () {
+    import({}, _dir + "view-gallery").show(true);
+  });
+
+  dispatchInject(function get_callback_ui_ () {
+    page.setMode("html");
+    _hdrWrite = page.head.write;
+    request.method = "POST";
+    import({}, _dir + "view-gallery").show();
+  });
+
+  import(_dir + "view");
+  page.setView(getView());
+}
+
+function setupAppModel () {
+  import(_dir + "model");
+  page.setModel(getModel());
+}
+
+function appGo () {
+  mvp();
+  _hdrWrite(["<style type='text/css'>", getCss(), "</style>", ""].join("\r\n"));
+}

+ 154 - 0
lib-app/dispatch-plus.js

@@ -0,0 +1,154 @@
+/* appjet:version 0.1 */
+// (c) 2009, Herbert Vojcík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+/* appjet:library */
+
+/** Decoded segments of full path */
+request.fullPathSegments = request.path.split('/').map(decodeURIComponent);
+/** Decoded full path */
+request.fullPath = request.fullPathSegments.join('/');
+
+/**@ignore*/
+request.fullPathSegments.of = request.fullPath;
+
+/**
+  * <p>Enhanced dispatcher function. Dispatches according to request.fullPath. Allows for:</p>
+  * <ul><li>nonalphanumeric characters in path. Serve /robots.txt with <code>get_robots$txt</code></li>
+  * <li>subpath matching: <ul>
+  *   <li>Serve /foo/, /foo/bar and /foo/bar/baz from <code>get_foo_</code></li>
+  *   <li><code>get_foo_main</code> serves only the /foo/ path, but not the /foo/bar, nor /foo</li>
+  *   <li>If there are more functions matching, the most specific (the longest one) wins</li>
+  *   <li><code>request.pagePath</code> is set to path of your handler (/foo), <code>request.pathTail</code> is set to the rest(/bar).</li>
+  *   <li><code>request.fullPath</code> contains the full path in decoded form</li>
+  *   <li>request.fullPathSegments, request.pagePathSegments and request.pathTailSegments contain segments of these paths</li>
+  * </li></ul>
+  * <li>404 handling is different: assign your 404 handler to dispatchPlus.f404</li>
+  * <li>If you pass arguments to dispatchPlus call, they are passed to handler function. No more need for globals to pass initial information to handler functions.</li>
+  * </ul>
+  */
+function dispatchPlus(arg1, arg2, etc) {
+    _rejuvenateFullPathSegments();
+    var selectorSegments = request.fullPathSegments.map(function(x) {
+        return [x.replace(/[\W]/g,'$$'), "_"];
+    });
+    selectorSegments.push([selectorSegments.pop()[0] || "main"]);
+
+    var handler, handlerSegments, selector = arguments.callee[request.method] || request.method.toLowerCase();
+    selectorSegments.forEach(function(segment, index) {
+        selector += segment.join('');
+        var candidate = appjet._internal.global[selector];
+        if (typeof candidate === "function") {
+        	handler = candidate;
+        	handlerSegments = index + 1;
+        }
+    });
+
+    if (handler) {
+        request.pagePathSegments = request.fullPathSegments.slice(0, handlerSegments);
+        request.pathTailSegments = request.fullPathSegments.slice(handlerSegments);
+        request.pagePath = request.pagePathSegments.join('/');
+        request.pathTail = request.fullPath.substring(request.pagePath.length);
+    }
+    (handler || arguments.callee.f404).apply(null, arguments);
+}
+
+/** The 404 (page not found) handler. You can replace it with your own. */
+dispatchPlus.f404 = function() {
+    response.setStatusCode(404);
+    printp("Path not found: ", request.fullPath);
+    print(request);
+    response.stop();
+};
+
+/** The prefix for GET method dispatcher functions. Default is "get", you can change it. */
+dispatchPlus.GET = "get";
+
+/** The prefix for HEAD method dispatcher functions. Default is "get", you can change it. */
+dispatchPlus.HEAD = "get";
+
+/** The prefix for POST method dispatcher functions. Default is "post", you can change it. */
+dispatchPlus.POST = "post";
+
+/** The prefix for CRON method dispatcher functions. Default is "cron", you can change it. */
+dispatchPlus.CRON = "cron";
+
+function _rejuvenateFullPathSegments() {
+    if (request.fullPathSegments.of === request.fullPath) return;
+    request.fullPathSegments = request.fullPath.split('/');
+    request.fullPathSegments.of = request.fullPath;
+}
+
+/* appjet:server */
+
+import("lib-app/dispatch-plus");
+
+/**@ignore*/
+function printRequest() {
+    printp({
+        pagePath: request.pagePath,
+        pagePathSegments: request.pagePathSegments,
+        fullPath: request.fullPath,
+        fullPathSegments: request.fullPathSegments,
+        pathTail: request.pathTail,
+        pathTailSegments: request.pathTailSegments
+    });
+};
+
+/**@ignore*/
+function get_(arg) {
+    printp("Serviced by ", CODE(arguments.callee));
+    printp("Try these links:");
+    print(UL(
+        LI(A({href:"/long/path/here.2"}, "/long/path/here.2")),
+        LI(A({href:"/papľuh/path-ing/"}, "/papľuh/path-ing/"))
+    ));
+    printp("Hello, ", arg, "!");
+    printRequest();
+}
+
+/**@ignore*/
+function get_papľuh_(arg) {
+    printp("Serviced by ", CODE(arguments.callee));
+    printp("Hello, ", arg, "!");
+    printRequest();
+}
+
+/**@ignore*/
+var get_papľuh_path$ing = "This is not a function";
+
+/**@ignore*/
+function get_long_path_() {
+    throw new Error("This should not be called.");
+}
+
+/**@ignore*/
+function get_long_path_here$2(arg) {
+    printp("Serviced by ", CODE(arguments.callee));
+    printp("Hello, ", arg, "!");
+    printRequest();
+}
+
+/**@ignore*/
+function get_long_path_here$2_() {
+    throw new Error("This should not be called.");
+}
+
+/**@ignore*/
+function get_long_path_here$2_main() {
+    throw new Error("This should not be called.");
+}
+
+printp("Features of extended dispatcher:");
+print(UL(
+    LI("Any nonalphanumeric characters (except the slash, of course) are replaced by '$' (dollar sign). Now you can serve /robots.txt by get_robots$txt function."),
+    LI("You can put a handler only to a beginning part of the path, if you use name like get_beginning_. This serves all paths in the form /beginning/... The path is broken to request.pagePath (the first part) and request.pathTail (the rest)."),
+    LI("If you end function with _main, it will not serve subpaths (it only matches to full path ending with / (a slash)."),
+    LI("If there are more subpath handlers and full path handlers, the most specific (the one that matches the longest part of the path) is chosen. If case of get_path_ and get_path_main, the latter will be chosen to service /path/, but the former to service /path/some/more."),
+    LI("404 is handled other way: you specify your 404 handler by assigning it to dispatchPlus.f404 before placing a call."),
+    LI("Any arguments you put in dispatchPlus call, gets copied into your handler function.")
+));
+printp("The statments issued in this minitest is ", CODE('dispatchPlus("world")'));
+
+
+dispatchPlus("world");

+ 53 - 0
lib-app/facebook-ext.js

@@ -0,0 +1,53 @@
+/* appjet:version 0.1 */
+// (c) 2009, 2010, Herbert Vojčík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+/* appjet:library */
+import("facebook", "storage");
+
+/**
+ * Contains uid usable in not-logged canvas scenarios as well.
+ * Either it contains fb.uid, canvas user or
+ * "loggedinuser"
+ */
+fb.any_uid = fb.uid;
+if (fb.any_uid == -1) {
+    fb.any_uid = request.params.fb_sig_canvas_user;
+    if (!fb.any_uid) fb.any_uid = "loggedinuser";
+}
+
+fb.requireLogin = function (perms) {
+    if (request.params.fb_sig_added != '1') {
+        fb.redirect("http://www.facebook.com/login.php?"
+            +"v=1.0&"
+            +"canvas=1&"
+            +(perms?"req_perms="+perms.join()+"&":"")
+            +"api_key="+storage.facebooklib.apiKey+"&"
+            +"next="+encodeURIComponent(fb.fullCanvasUrl));
+    }
+};
+
+/** Place for convenience extension functions for UI */
+fb.ui = {
+/**
+ * Creates tabs according to items parameter.
+ * If actual url matches one of them, it is selected.
+ * Next example renders tabs for .../callback/profile, .../callback/invite and .../callback/play.
+ @example
+ print(fb.ui.tabs("profile_Profile", "invite_Invite friends", "play_Play!"));
+ */
+    tabs: function (items) {
+        var menu = FB_TABS();
+        var lastSegment = request.path.split("/").pop();
+        items.forEach (function (item) {
+            var split = item.split("_");
+            menu.push(FB_TAB_ITEM({
+                href: split[0],
+                title: split[1],
+                selected: lastSegment === split[0]
+            }));
+        });
+        return menu;
+    }
+};
+

+ 12 - 0
lib-app/globals.js

@@ -0,0 +1,12 @@
+/* appjet:version 0.1 */
+/* appjet:library */
+// (c) 2010, Herbert Vojèík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+// Globally imported libraries and maybe some definitions
+// that are commonly awaited by all fb apps and their parts
+// both while in production and while tested.
+
+import("lib-app/mvp");
+import("lib-app/facebook-ext");
+import("lib-utils/patches");

+ 180 - 0
lib-app/liveui.js

@@ -0,0 +1,180 @@
+/* appjet:version 0.1 */
+// (c) 2009, Herbert Vojčík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+/* appjet:library */
+
+// ====== Rendering context ======
+
+var _stack = [];
+
+// ====== API ======
+
+/**
+ * A seq is like DIV() or SPAN() without having attributes and without
+ * any tags rendered.
+ */
+function seq(element1, element2, etc) {
+    return new _seq(arguments);
+}
+
+/**
+ * A box is seq, which can have model set via given(model).
+ * By default, model is false.
+ * If a box's model evaluates to false, it renders to ""; otherwise,
+ * it renders normally (eg. renders its contents).
+ */
+function box(element1, element2, etc) {
+    return new _box(arguments);
+}
+
+/**
+ * A lazy is an object which calls appropriate function when rendered
+ * to obtain actual object to be rendered.
+ * This function gets actually rendered boxes' models as its arguments,
+ * the innermost model being the first argument, parent being the second, etc.
+ @param f the rendering function. Returns the structure (eg. DIV(), TABLE()
+   or whatever else which you would put inside print()). It should not print() directly.
+ */
+function lazy(f) {
+    return new _lazy(f);
+}
+
+// ====== Convenience functions ======
+
+/** Convenience lazy that returns data from model, arguments (if any) denote property chain. */
+function data(prop1, prop2, etc) {
+    var args = arguments;
+    return lazy(function(model) {
+        Array.forEach(args, function(p) { model = model[p]; });
+        return model;
+    });
+}
+
+/** Convenience lazy that returns data from parent's model, arguments (if any) denote property chain. */
+function outerdata(prop1, prop2, etc) {
+    var args = arguments;
+    return lazy(function(model, parent) {
+        Array.forEach(args, function(p) { parent = parent[p]; });
+        return parent;
+    });
+}
+
+/**
+ * Convenience proxy lazily representing the box's model.
+ * Use dot notation to lazily represent particular contents of the model, to any depth.
+ */
+//var data = _subdata([]);
+
+/**
+ * Convenience proxy lazily representing the box's parent's model.
+ * Use dot notation to lazily represent particular contents of the parent's model, to any depth.
+ */
+//var outerdata = _subdata([], 1);
+
+/**
+ * Convenience lazy that wraps a box and sets its model from supplied value model
+ * (either lazy or plain value).
+ */
+function sub(valueModel, box) {
+    return lazy(function() { return box.given(valueModel.valueOf()); });
+}
+
+/**
+ * Convenience lazy that iteratively repeats a sub-box with elements
+ * of supplied valueModel (either lazy or plain value) set as a sub-boxes' models.
+ */
+function iter(valueModel, box) {
+    return lazy(function() {
+        var result = seq();
+        valueModel.valueOf().forEach(function(element) {
+            result.push(lazy(function() { return box.given(element); }));
+        });
+        return result;
+    });
+}
+
+// ====== Workers ======
+
+function _seq(content) {
+    this.push.apply(this, content);
+}
+
+_seq.prototype = object(Array.prototype);
+
+_seq.prototype.toHTML = function() { return this.map(toHTML).join(""); };
+
+_seq.prototype.toString = function() { return this.toHTML(); };
+
+function _box(content) {
+    _seq.apply(this, arguments);
+}
+
+_box.prototype = object(_seq.prototype);
+
+_box.prototype.model = false;
+
+_box.prototype.toHTML = function() {
+    if (!this.model) { return ""; }
+    try { _stack.unshift(this.model); return this.map(toHTML).join(""); }
+    finally { _stack.shift(); }
+};
+
+/** Sets model for the box. Returns the box. */
+_box.prototype.given = function(model) {
+    /** Model of the box */
+    this.model = model;
+    return this;
+};
+
+function _lazy(f) {
+    this.f = f;
+}
+
+_lazy.prototype.valueOf = function() {
+    return this.f.apply(null, _stack);
+};
+
+_lazy.prototype.toHTML = function() { return toHTML(this.valueOf()); };
+
+_lazy.prototype.toString = function() { return this.valueOf().toString(); };
+
+/*function _subdata(props, i) {
+    if (!i) { i = 0; }
+    var valueFun = props.length ?
+        function() {
+            var inner = _stack[i];
+            props.forEach(function(p) { inner = inner[p]; });
+            return inner;
+        } : function() { return _stack[i]; };
+    return appjet._native.createProxyObject(
+        function(p) {
+            switch(p) {
+            case "valueOf": return valueFun;
+            default: return _lazy.prototype[p] || _subdata(props.concat([p]), i);
+            }
+        },
+        function() {}, function() {}, function() {return [];}, _lazy.prototype
+    );
+}*/
+
+/* appjet:server */
+
+import("lib-app/liveui");
+
+print(
+    box(
+        "<div>", data("hello"), "</div>  <-- div tags should be written",
+        HR(),
+        sub(data("innerwld"), box(
+            "<div>", outerdata("hello"), ", ", data("foo"), "</div>  <-- here as well"
+        )),
+        UL(iter(data("squares"), box(
+            LI(data("n")," squared is ", data("n2"))
+        )))
+    ).given({
+        hello: "hello",
+        innerwld: {foo: "world"},
+        squares: [1,2,3,4,5].map(function(x) { return { n:x, n2:x*x }; })
+    })
+);

+ 126 - 0
lib-app/mvp.js

@@ -0,0 +1,126 @@
+// (c) 2009, Herbert Vojčík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+/* appjet:library */
+
+/**
+  * Sets model for the page.
+  * Optional. There is no default model.
+  */
+page.setModel = function(model) {
+    this.model = model;
+};
+
+/**
+ * Sets view-model for the page.
+ * This view-model is set for the page view as well.
+ * Optional. Default view-model is true.
+ */
+page.setViewModel = function(viewModel) {
+   this.viewModel = viewModel;
+   if ("view" in this) { this.view.model = viewModel; }
+};
+
+/**
+  * Sets the view for the page. Sets the view model from the page view-model.
+  * Optional. Default view is empty DIV().
+  */
+page.setView = function(view) {
+    this.view = view;
+    if ("viewModel" in this) { view.model = this.viewModel; }
+};
+
+/**
+  * Sets the presenter for the page. It must be a function. It will be called with page model as a parameter.
+  * Optional. Default presenter is legacy dispatch().
+  */
+page.setPresenter = function(presenter) {
+    if (typeof presenter !== "function") {
+        throw new Error("A presenter must be a function");
+    }
+    this.presenter = presenter;
+};
+
+page.setViewModel(true);
+page.setView(DIV());
+page.setPresenter(dispatch);
+
+// Repository for redirected printx functions.
+var _rawprint = DIV();
+
+function _addRawPrintSectionTo(result) {
+    if (_rawprint.length) {
+        result.push(
+            DIV({style:"clear:both"}),
+            DIV({style:"border-top: 1px solid #ccc; margin-top: 1.2em; margin-bottom: 1.2em"},
+                SMALL(STRONG("Raw print:"))),
+            _rawprint
+        );
+    }
+}
+
+/**
+ * Sets up MVP printing mode, then runs the page presenter with the page model as an argument.
+ * You should call it in place you normally call dispatch().
+ */
+function mvp() {
+    var oldBodyWrite = page.body.write;
+    var oldRender = page.render;
+    page.render = function () {
+        this.render = oldRender;
+        this.body.write = oldBodyWrite;
+        return this.render();
+    };
+
+    page.body.write({ toString: function() {
+        var result = [page.view];
+        _addRawPrintSectionTo(result);
+        return result.map(toHTML).join('');
+    }});
+
+    page.body.write = function(rawText) {
+        _rawprint.push(html(rawText));
+    };
+
+    page.presenter(page.model);
+}
+
+/* appjet:server */
+
+import("lib-app/mvp");
+
+/**@ignore*/
+var view = DIV();
+view.push(P("Ahoy!"));
+
+page.setView(P("This is fake view that will be replaced"));
+page.setViewModel("<model>");
+
+print("Raw text before mvp call<>\n*", BR());
+printp("A paragraph");
+
+page.setView(view);
+
+/**@ignore*/
+function get_main() {
+    var list = OL();
+    view.push(list);
+
+    print(CODE(appjet._native.codeSection("server")[1].code));
+
+    list.push(LI("This is the real contents of the page."));
+    list.push(LI("Print-style functions are redirected to raw print section (useful as ad-hoc debug output). ",
+        "The raw print section is only shown when there actually was some raw print."),
+        LI("The contents of the page is created by setView with view object. ",
+        "You can manipulate this view object after setView."));
+    list.push(LI("This way, object/functional style is not mixed with print-style, ",
+        "what just caused bad style and confusion."));
+    list.push(LI("You can setViewModel for the page, which is propagated to view, ",
+        "as well as setPresenter if default dispatch() style is not enough."));
+    list.push(LI("The view model is: ", view.model));
+
+    //page.setView("<div>&ndash;</div>"); // This should appear as visible text, not as html.
+}
+
+mvp();
+print("Raw text after mvp call<>\n*", BR());

+ 40 - 0
lib-utils/footer-links.js

@@ -0,0 +1,40 @@
+/* appjet:version 0.1 */
+// (c) 2009, Herbert Vojčík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+/* appjet:library */
+
+var _links = null;
+
+function _setup() {
+    _links = {};
+    page.head.write("<style type='text/css'>\n"+
+    "	#appjetfooterlinks a { text-decoration:none; color:#666; }\n"+
+    "</style>\n");
+    import({}, "lib-utils/footer").extendTheFooter(function() {
+        var result = SPAN({id:"appjetfooterlinks"}), delim = "";
+        eachProperty(_links, function(key, value) {
+            result.push(delim, link(key, value.replace(" ", "\xa0", "g"))); // non-breaking spaces
+            delim = " - ";
+        });
+        return result;
+    });
+}
+
+/**
+ * Puts a link into the footer.
+ * Beyond its direct use, this function is very convenient
+ * for libraries that integrate into application and reserve
+ * some path to include link to themselves in the footer.
+ */
+function setFooterLink(url, name) {
+    if (!_links) { _setup(); }
+    _links[url] = name;
+}
+
+/* appjet:server */
+import(appjet.appName);
+
+setFooterLink("/testrunner/results", "Test Runner");
+setFooterLink("/lib-files/", "Files");
+setFooterLink("/lib-files/", "File repository");

+ 63 - 0
lib-utils/footer.js

@@ -0,0 +1,63 @@
+/* appjet:version 0.1 */
+// (c) 2009, Herbert Vojčík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+/* appjet:library */
+
+var _re = /<div[^>]*id=['"]appjetfooter['"][^>]*>/;
+
+/**@ignore*/
+function replaceTheFooter(f) {
+    var _oldrender = page.render;
+
+    /**@ignore*/
+    page.render = function() {
+        page.render = _oldrender;
+        var result = page.render();
+        var index = _findFooterIndex(result);
+        if (index !== -1) { result[index] = f(result[index]); }
+        return result;
+    };
+
+    function _findFooterIndex(array) {
+        for (var i = array.length - 1; i >= 0; --i) {
+            if (array[i].match(_re)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+}
+/**
+ * At render time, inserts the content provided
+ * by the supplied function into the footer.
+ */
+function extendTheFooter(f) {
+    replaceTheFooter(function(footer) {
+        return footer.replace(_re, function(a) {
+            return a+toHTML(DIV(f()));
+        });
+    });
+}
+
+/**
+ * Hides the footer at render time.
+ */
+function hideTheFooter(f) {
+    replaceTheFooter(function() { return ""; });
+}
+
+
+/* appjet:server */
+import(appjet.appName);
+
+extendTheFooter(function() {
+    return SPAN(
+        "Server time is: ",
+        new Date(),
+        " ",
+        SPAN({style:"color:silver"}, "(this line extends the footer)")
+    );
+});
+
+//hideTheFooter();

+ 712 - 0
lib-utils/jsunit.js

@@ -0,0 +1,712 @@
+/* appjet:version 0.1 */
+
+/* appjet:library */
+
+function assert() {
+    var booleanValue = _validateArguments(1, arguments)[0];
+
+    if (typeof(booleanValue) != 'boolean')
+        error('Bad argument to assert(boolean)');
+
+    _assert(commentArg(1, arguments), booleanValue === true, 'Call to assert(boolean) with false');
+}
+
+function assertTrue() {
+    var booleanValue = _validateArguments(1, arguments)[0];
+
+    if (typeof(booleanValue) != 'boolean')
+        error('Bad argument to assertTrue(boolean)');
+
+    _assert(commentArg(1, arguments), booleanValue === true, 'Call to assertTrue(boolean) with false');
+}
+
+function assertFalse() {
+    var booleanValue = _validateArguments(1, arguments)[0];
+
+    if (typeof(booleanValue) != 'boolean')
+        error('Bad argument to assertFalse(boolean)');
+
+    _assert(commentArg(1, arguments), booleanValue === false, 'Call to assertFalse(boolean) with true');
+}
+
+function assertEquals() {
+    var args = _validateArguments(2, arguments),
+    	exp = args[0], act = args[1];
+    _assert(commentArg(2, arguments), exp === act, 'Expected ' + _displayStringForValue(exp) + ' but was ' + _displayStringForValue(act));
+}
+
+function assertNotEquals() {
+    var args = _validateArguments(2, arguments),
+	exp = args[0], act = args[1];
+    _assert(commentArg(2, arguments), exp !== act, 'Expected not to be ' + _displayStringForValue(act));
+}
+
+function assertNull() {
+    var aVar = _validateArguments(1, arguments)[0];
+    _assert(commentArg(1, arguments), aVar === null, 'Expected ' + _displayStringForValue(null) + ' but was ' + _displayStringForValue(aVar));
+}
+
+function assertNotNull() {
+    var aVar = _validateArguments(1, arguments)[0];
+    _assert(commentArg(1, arguments), aVar !== null, 'Expected not to be ' + _displayStringForValue(null));
+}
+
+function assertUndefined() {
+    var aVar = _validateArguments(1, arguments)[0];
+    _assert(commentArg(1, arguments), aVar === undefined, 'Expected ' + _displayStringForValue(undefined) + ' but was ' + _displayStringForValue(aVar));
+}
+
+function assertNotUndefined() {
+    var aVar = _validateArguments(1, arguments)[0];
+    _assert(commentArg(1, arguments), aVar !== undefined, 'Expected not to be ' + _displayStringForValue(undefined));
+}
+
+function assertNaN() {
+    var aVar = _validateArguments(1, arguments)[0];
+    _assert(commentArg(1, arguments), isNaN(aVar), 'Expected NaN');
+}
+
+function assertNotNaN() {
+    var aVar = _validateArguments(1, arguments)[0];
+    _assert(commentArg(1, arguments), !isNaN(aVar), 'Expected not NaN');
+}
+
+function assertHTMLEquals() {
+    var args = _validateArguments(2, arguments),
+		exp = args[0], act = args[1];
+    	expStd = _standardizeHTML(exp),
+    	actStd = _standardizeHTML(act);
+
+    assertEquals(commentArg(2, arguments)||"", expStd, actStd);
+}
+
+var _helper = null;//import({}, "lib-simple-htmlparser", "lib-xmldom");
+
+function _standardizeHTML(html) {
+    result = _helper.HTMLtoXML("<scrap>"+html+"</scrap>");
+    xmldoc = new _helper.XMLDoc(result);
+    result = xmldoc. getUnderlyingXMLText();
+    return result;
+}
+
+function assertObjectEquals() {
+    var args = _validateArguments(2, arguments),
+		exp = args[0], act = args[1];
+    if (exp === act) return;
+    var comment = commentArg(2,arguments);
+    var msg = comment ? comment : '';
+    var type = jsuTrueTypeOf(exp);
+    var isEqual = type == jsuTrueTypeOf(act);
+    if (isEqual) {
+        switch (type) {
+            case 'String':
+            case 'Number':
+                isEqual = exp == act;
+                break;
+            case 'Boolean':
+            case 'Date':
+                isEqual = exp === act;
+                break;
+            case 'RegExp':
+            case 'Function':
+                isEqual = exp.toString() === act.toString();
+                break;
+            default: //Object | Array
+                isEqual = exp.length === act.length;
+                if (isEqual)
+                    if (comment == null)
+                        _assertRecursiveEquals(exp, act, assertObjectEquals);
+                    else
+                        _assertRecursiveEquals("Nested: "+comment, exp, act, assertObjectEquals);
+        }
+    }
+    _assert(msg, isEqual, 'Expected ' + _displayStringForValue(exp) + ' but was ' + _displayStringForValue(act));
+}
+
+assertArrayEquals = assertObjectEquals;
+
+function assertHashEquals() {
+    var args = _validateArguments(2, arguments),
+		exp = args[0], act = args[1];
+    var comment = commentArg(2, arguments);
+    if (comment == null)
+        _assertRecursiveEquals(exp, act, assertEquals);
+    else
+        _assertRecursiveEquals(comment, exp, act, assertEquals);
+}
+
+function _assertRecursiveEquals() {
+    var args = _validateArguments(3, arguments),
+		exp = args[0], act = args[1], compare = args[2];
+    var comment = commentArg(3, arguments);
+    var comment = comment ? comment + "\n" : "";
+    for (var key in exp) {
+        assertTrue(comment + "Expected had key " + key + " that was not found", key in act);
+        compare(
+                comment + "Value for key " + key + " mismatch - expected = " + exp[key] + ", actual = " + act[key],
+                exp[key], act[key]
+                );
+    }
+    for (var key in act) {
+        assertTrue(comment + "Actual had key " + key + " that was not expected", key in exp);
+    }
+}
+
+function assertRoughlyEquals() {
+    var args = _validateArguments(3, arguments),
+    	exp = args[0], act = args[1], tolerance = args[2];
+    assertTrue(
+            "Expected " + exp + ", but got " + act + " which was more than " + tolerance + " away",
+            Math.abs(exp - act) < tolerance
+    );
+}
+
+function assertContains() {
+    var args = _validateArguments(2, arguments),
+		contained = args[0], container = args[1];
+    assertTrue(
+            "Expected '" + container + "' to contain '" + contained + "'",
+            container.indexOf(contained) != -1
+    );
+}
+
+function assertEvaluatesToTrue() {
+    var value = _validateArguments(1, arguments)[0];
+    if (!value)
+        fail(commentArg(1, arguments));
+}
+
+function assertEvaluatesToFalse() {
+    var value = _validateArguments(1, arguments)[0];
+    if (value)
+        fail(commentArg(1, arguments));
+}
+
+function _assert(comment, booleanValue, failureMessage) {
+    if (!booleanValue)
+        throw new JsUnitException(comment, failureMessage);
+}
+
+/**@ignore*/
+function argumentsIncludeComments(expectedNumberOfNonCommentArgs, args) {
+    return args.length == expectedNumberOfNonCommentArgs + 1;
+}
+
+/**@ignore*/
+function commentArg(expectedNumberOfNonCommentArgs, args) {
+    if (argumentsIncludeComments(expectedNumberOfNonCommentArgs, args))
+        return args[0];
+
+    return null;
+}
+
+/**@ignore*/
+/*function nonCommentArg(desiredNonCommentArgIndex, expectedNumberOfNonCommentArgs, args) {
+    return argumentsIncludeComments(expectedNumberOfNonCommentArgs, args) ?
+           args[desiredNonCommentArgIndex] :
+           args[desiredNonCommentArgIndex - 1];
+}*/
+
+function _validateArguments(expectedNumberOfNonCommentArgs, args) {
+    if (!( args.length == expectedNumberOfNonCommentArgs ||
+           (args.length == expectedNumberOfNonCommentArgs + 1 && typeof(args[0]) == 'string') ))
+        error('Incorrect arguments passed to assert function');
+    return Array.prototype.slice.call(args, -expectedNumberOfNonCommentArgs);
+}
+
+/**
+ * A more functional typeof
+ * @param Object o
+ * @return String
+ */
+function jsuTrueTypeOf(something) {
+    var result = typeof something;
+    try {
+        switch (result) {
+            case 'string':
+            case 'boolean':
+            case 'number':
+                break;
+            case 'object':
+            case 'function':
+                switch (something.constructor)
+                        {
+                    case String:
+                        result = 'String';
+                        break;
+                    case Boolean:
+                        result = 'Boolean';
+                        break;
+                    case Number:
+                        result = 'Number';
+                        break;
+                    case Array:
+                        result = 'Array';
+                        break;
+                    case RegExp:
+                        result = 'RegExp';
+                        break;
+                    case Function:
+                        result = 'Function';
+                        break;
+                    default:
+                        var m = something.constructor.toString().match(/function\s*([^( ]+)\(/);
+                        if (m)
+                            result = m[1];
+                        else
+                            break;
+                }
+                break;
+        }
+    }
+    finally {
+        result = result.substr(0, 1).toUpperCase() + result.substr(1);
+        return result;
+    }
+}
+
+function _displayStringForValue(aVar) {
+    var result = '<' + aVar + '>';
+    if (aVar != null) {
+        result += ' (' + jsuTrueTypeOf(aVar) + ')';
+    }
+    return result;
+}
+
+/**Call from within test to end it with failure*/
+function fail(failureMessage) {
+    throw new JsUnitException("Call to fail()", failureMessage);
+}
+
+/**Call from within test to end it with error*/
+function error(errorMessage) {
+    var errorObject = new Object();
+    errorObject.description = errorMessage;
+    throw errorObject;
+}
+
+/**This exception is used by jsunit for failures.*/
+function JsUnitException(comment, message) {
+    this.isTestException = true;
+    this.comment = comment;
+    this.testMessage = message;
+}
+
+var counter = 0;
+
+/* appjet:server */
+
+import("lib-utils/jsunit");
+
+function assertJsUnitException(comment, allegedJsUnitException) {
+    assertNotNull(comment, allegedJsUnitException);
+    assert(comment, allegedJsUnitException.isTestException);
+    assertNotUndefined(comment, allegedJsUnitException.comment);
+}
+
+function assertNonJsUnitException(comment, allegedNonJsUnitException) {
+    assertNotNull(comment, allegedNonJsUnitException);
+    assertUndefined(comment, allegedNonJsUnitException.isTestUnitException);
+    assertNotUndefined(comment, allegedNonJsUnitException.description);
+}
+
+page.testSuite = [
+    function assertWorks() {
+        assert("true should be true", true);
+        assert(true);
+    },
+
+    function assertTrueWorks() {
+        assertTrue("true should be true", true);
+        assertTrue(true);
+    },
+
+    function assertFalseWorks() {
+        assertFalse("false should be false", false);
+        assertFalse(false);
+    },
+
+    function assertEqualsWorks() {
+        assertEquals("1 should equal 1", 1, 1);
+        assertEquals(1, 1);
+    },
+
+    function assertNotEqualsWorks() {
+        assertNotEquals("1 should not equal 2", 1, 2);
+        assertNotEquals(1, 2);
+    },
+
+    function assertNullWorks() {
+        assertNull("null should be null", null);
+        assertNull(null);
+    },
+
+    function assertNotNullWorks() {
+        assertNotNull("1 should not be null", 1);
+        assertNotNull(1);
+    },
+
+    function assertUndefinedWorks() {
+        var myVar;
+        assertUndefined("A declared but unassigned variable should have the undefined value", myVar);
+        assertUndefined(myVar);
+    },
+
+    function assertNotUndefinedWorks() {
+        assertNotUndefined("1 should not be undefined", 1);
+        assertNotUndefined(1);
+    },
+
+    function assertNaNWorks() {
+        assertNaN("a string should not be a number", "string");
+        assertNaN("string");
+    },
+
+    function assertNotNaNWorks() {
+        assertNotNaN("1 should not be not a number", 1);
+        assertNotNaN(1);
+    },
+
+    function failWorks() {
+        var excep = null;
+        try {
+            fail("Failure message");
+        } catch (e) {
+            excep = e;
+        }
+        assertJsUnitException("fail(string) should throw a JsUnitException", excep);
+    },
+
+    function tooFewArguments() {
+        var excep = null;
+        try {
+            assert();
+        } catch (e1) {
+            excep = e1;
+        }
+        assertNonJsUnitException("Calling an assertion function with too few arguments should throw an exception", excep);
+    },
+
+    function tooManyArguments() {
+        var excep = null;
+        try {
+            assertEquals("A comment", true, true, true);
+        } catch (e2) {
+            excep = e2;
+        }
+        assertNonJsUnitException("Calling an assertion function with too many arguments should throw an exception", excep);
+    },
+
+    function invalidCommentArgumentType() {
+        var excep = null;
+        try {
+            assertNull(1, true);
+        } catch (e3) {
+            excep = e3;
+        }
+        assertNonJsUnitException("Calling an assertion function with a non-string comment should throw an exception", excep);
+    },
+
+    function invalidArgumentType() {
+        var excep = null;
+        try {
+            assert("string");
+        } catch (e) {
+            excep = e;
+        }
+        assertNonJsUnitException("Calling an assertion function with an invalid argument should throw an exception", excep);
+    },
+
+    function assertArrayEqualsWorks() {
+        var array1 = ["foo", "bar", "foobar"];
+        var array2 = ["foo", "bar", "foobar"];
+        var array3 = ["foo", "bar"];
+        var array4 = ["bar", "foo", "foobar"];
+
+        assertArrayEquals([undefined],[undefined]);
+        assertArrayEquals(array1, array1);
+        assertArrayEquals(array1, array2);
+        try {
+            assertArrayEquals(array1, undefined);
+            fail("Should not be equal to undefined");
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+            if (e.comment == "Call to fail()")
+                fail(e.comment + e.testMessage); //tried fail is also caught
+        }
+        try {
+            assertArrayEquals(array1, array3);
+            fail("Should not be equal");
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+            if (e.comment == "Call to fail()")
+                fail(e.comment + e.testMessage); //tried fail is also caught
+        }
+        try {
+            assertArrayEquals(array1, array4);
+            fail("Should not be equal");
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+            if (e.comment == "Call to fail()")
+                fail(e.comment + e.testMessage); //tried fail is also caught
+        }
+        var array5 = ['foo', 'bar', ['nested', 'bar'], 'foobar'];
+        var array6 = ['foo', 'bar', ['nested', 'bar'], 'foobar'];
+        var array7 = ['foo', 'bar', ['nested', 'foo'], 'foobar'];
+        assertArrayEquals('Equal nested arrays', array5, array6);
+        try {
+            assertArrayEquals(array5, array7);
+            var failure = 'Differing nested arrays found to be equal';
+            fail(failure);
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+            if (e.testMessage == failure)
+                fail(e.testMessage);
+        }
+    },
+
+    function assertObjectEqualsWorksOnObjects() {
+        var failure;
+        assertObjectEquals(undefined, undefined);
+        var o1 = {foo:'bar'};
+        var o2 = {foo:'bar'};
+        assertObjectEquals('Single object', o1, o1);
+        assertObjectEquals('Same objects', o1, o2);
+        var o3 = {foo:'foo'};
+        var o4 = {foo:'foo',
+            bar: function () {
+                this.foo = 'bar';
+                delete this.bar;
+            }};
+        var o5 = {foo:'foo',
+            bar: function () {
+                this.foo = 'foo';
+                delete this.bar;
+            }};
+        try {
+            assertObjectEquals(o1, o3);
+            fail(failure = 'Simple differing objects found to be the same');
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+            if (e.testMessage == failure)
+                fail(e.testtMessage);
+        }
+        try {
+            assertObjectEquals(o3, o4);
+            fail(failure = 'Object with additional fields found to be the same with non-grown one.');
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+            if (e.testMessage == failure)
+                fail(e.testMessage);
+        }
+        try {
+            assertObjectEquals(o4, o5);
+            fail(failure = 'Objects with different methods found to be the same');
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+            if (e.testMessage == failure)
+                fail(e.testMessage);
+        }
+
+        o4.bar();
+        assertObjectEquals('Different objects, made to be the same', o1, o4);
+        try {
+            assertObjectEquals({ts:new Date()}, {ts:new Date()});
+            fail(failure = 'Objects with different Date attributes found to be the same');
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+            if (e.testMessage == failure)
+                fail(e.testMessage);
+        }
+        try {
+            assertObjectEquals(new Date(), new Date());
+            fail(failure = 'Different Date objects found to be the same');
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+            if (e.testMessage == failure)
+                fail(e.testMessage);
+        }
+        assertObjectEquals(/a/, new RegExp('a'));
+        assertObjectEquals(/a/i, new RegExp('a', 'i'));
+        assertObjectEquals(function(a,b){return a+b;}, function(a,b){return a+b;});
+        //XXX challenge for the future
+        //assertObjectEquals(function(a,b){return a+b}, new Function("a","b","return a+b"));
+
+        try {
+            assertObjectEquals(/a/i, new RegExp('a', 'g'));
+            fail(failure = 'RegExp with different flags found to be the same');
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+            if (e.testMessage == failure)
+                fail(e.testMessage);
+        }
+        try {
+            assertObjectEquals(/a/, new RegExp('b'));
+            fail(failure = 'RegExp with different patterns found to be the same');
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+            if (e.testMessage == failure)
+                fail(e.testMessage);
+        }
+    },
+
+    function assertObjectEqualsWorksOnStrings() {
+        var s1 = 'string1';
+        var s2 = 'string1';
+        var newS1 = new String('string1');
+        assertObjectEquals('Same Strings', s1, s2);
+        assertObjectEquals('Same Strings 1 with new', s1, newS1);
+    },
+
+    function assertObjectEqualsWorksOnNumbers() {
+        var failure;
+        var n1 = 1;
+        var n2 = 1;
+        var newN1 = new Number(1);
+        assertObjectEquals('Same Numbers', n1, n2);
+        assertObjectEquals('Same Numbers 1 with new', n1, newN1);
+        var n3 = 2;
+        var newN3 = new Number(2);
+        try {
+            assertObjectEquals(n1, n3);
+            fail(failure = 'Different Numbers');
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+            if (e.testMessage == failure)
+                fail(e.testMessage);
+        }
+        try {
+            assertObjectEquals(newN1, newN3);
+            fail(failure = 'Different New Numbers');
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+            if (e.testMessage == failure)
+                fail(e.testMessage);
+        }
+
+    },
+
+    function assertEvaluatesToTrueWorks() {
+        assertEvaluatesToTrue("foo");
+        assertEvaluatesToTrue(true);
+        assertEvaluatesToTrue(1);
+        try {
+            assertEvaluatesToTrue(null);
+            fail("Should have thrown a JsUnitException");
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+        }
+    },
+
+    function assertEvaluatesToFalseWorks() {
+        assertEvaluatesToFalse("");
+        assertEvaluatesToFalse(null);
+        assertEvaluatesToFalse(false);
+        assertEvaluatesToFalse(0);
+        try {
+            assertEvaluatesToFalse("foo");
+            fail("Should have thrown a JsUnitException");
+        } catch (e) {
+            assertJsUnitException("Should be a JsUnitException", e);
+        }
+    },
+
+    function assertHtmlEqualsWorks() {
+        assertHTMLEquals("<div id=mydiv>foobar</div>", "<div id='mydiv'>foobar</div>");
+        assertHTMLEquals("<p/>", "<p></p>");
+        assertHTMLEquals("foo bar", "foo bar");
+        assertHTMLEquals("a comment", "<p id='foo'>foo bar</p>", "<p id=foo>foo bar</p>");
+    },
+
+    function assertHashEqualsWorks() {
+        var hash1 = new Array();
+        hash1["key1"] = "value1";
+        hash1["key2"] = "value2";
+
+        var hash2 = new Array();
+        try {
+            assertHashEquals(hash1, hash2);
+            fail();
+        } catch (e) {
+            assertJsUnitException("hash2 is empty", e);
+        }
+        hash2["key1"] = "value1";
+        try {
+            assertHashEquals(hash1, hash2);
+            fail();
+        } catch (e) {
+            assertJsUnitException("hash2 is a of a different size", e);
+        }
+        hash2["key2"] = "foo";
+        try {
+            assertHashEquals(hash1, hash2);
+            fail();
+        } catch (e) {
+            assertJsUnitException("hash2 has different values", e);
+        }
+        hash2["key2"] = "value2";
+        assertHashEquals(hash1, hash2);
+    },
+
+    function assertRoughlyEqualsWorks() {
+        assertRoughlyEquals(1, 1.1, 0.5);
+        assertRoughlyEquals(1, 5, 6);
+        assertRoughlyEquals(-4, -5, 2);
+        assertRoughlyEquals(-0.5, 0.1, 0.7);
+        try {
+            assertRoughlyEquals(1, 2, 0.5);
+        } catch (e) {
+            assertJsUnitException("1 and 2 are more than 0.5 apart", e);
+        }
+    },
+
+    function assertContainsWorks() {
+        assertContains("foo", "foobar");
+        assertContains("ooba", "foobar");
+        assertContains("bar", "foobar");
+    },
+];
+
+/**@ignore*/
+function FooBarThingy() {
+    this.foo = 'bar';
+}
+
+/**@ignore*/
+FooBarThingy.prototype.bar = function() {
+    return this.foo;
+};
+
+page.testSuite.push(function testTrueTypeOf() {
+    assertEquals('Boolean', jsuTrueTypeOf(true));
+    assertEquals('Using new', 'Boolean', jsuTrueTypeOf(new Boolean('1')));
+
+    assertEquals('Number', jsuTrueTypeOf(1));
+    var GI = new Number(1);
+    assertEquals('Using new', 'Number', jsuTrueTypeOf(GI));
+    assertEquals('Number', jsuTrueTypeOf(1.5));
+
+    assertEquals('String', jsuTrueTypeOf('foo'));
+    assertEquals('Using new', 'String', jsuTrueTypeOf(new String('foo')));
+
+    assertEquals('Using new', 'Function', jsuTrueTypeOf(new Function()));
+    assertEquals('Function', jsuTrueTypeOf(function foo() {}));
+    assertEquals('Function', jsuTrueTypeOf(testTrueTypeOf));
+
+    assertEquals('RegExp', jsuTrueTypeOf(/foo/));
+    assertEquals('Using new', 'RegExp', jsuTrueTypeOf(new RegExp('foo')));
+
+    var o = {foo: 'bar'};
+    assertEquals('Object', jsuTrueTypeOf(o));
+    var o = new FooBarThingy();
+    assertEquals('FooBarThingy', jsuTrueTypeOf(o));
+    assertEquals('String', jsuTrueTypeOf(o.foo));
+    assertEquals('String', jsuTrueTypeOf(o.bar()));
+    assertEquals('Function', jsuTrueTypeOf(o.bar));
+
+    assertEquals('Object without constructor', 'Object', jsuTrueTypeOf(appjet));
+});
+
+import("lib-utils/runner");
+
+runTestsByDefault();

+ 13 - 0
lib-utils/patches.js

@@ -0,0 +1,13 @@
+/* appjet:version 0.1 */
+// (c) 2010, Herbert Vojčík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+/* appjet:library */
+
+function dispatchShouldSearchHere() {
+	appjet._internal.global = this;
+}
+
+function dispatchInject(f) {
+	appjet._internal.global[f.name] = f;
+}

+ 292 - 0
lib-utils/runner.js

@@ -0,0 +1,292 @@
+/* appjet:version 0.1 */
+// (c) 2009, Herbert Vojčík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+/* appjet:library */
+
+/**
+ * @overview This is a library that runs unit tests
+ * and prints the results. It is stripped-down port
+ * of existing jsunit 2.2 test runner. So far, it only has
+ * html rendering of test results in a table.
+ * It honours setUp and tearDown functions and treats
+ * any function that begins with "test" as test function.
+ * You can write setUp, tearDown and testXxx function
+ * with prepended underscore (to ignore them in docs).
+ * The runner works with both alternatives, though,
+ * it's behaviour is undefined if you mix both forms
+ * for the same function name.
+ *
+ * @example
+im port("lib-jsunit22");
+
+function _testXxx() { ... }
+...
+
+import("lib-testrunner");
+
+if (you want auto redirection) { redirectTestRunnerResults(); }
+if (you want footer link) { setTestRunnerFooterLink(); }
+ */
+
+_runner = {
+    start: function () {
+        this._clear();
+        var timeRunStarted = new Date();
+        this._runAllTests();
+        this.timeTaken = (new Date() - timeRunStarted) / 1000;
+    },
+
+    _clear: function() {
+        this.totalCount = 0;
+        this.errorCount = 0;
+        this.failureCount = 0;
+        this.results = [];
+    },
+
+    _runAllTests: function () {
+        this.addResults(new _suite().runAllTests());
+    },
+
+    addResults: function (suiteRawResults) {
+        var self = this;
+        suiteRawResults.forEach(function(rawResult) {
+            var result = {
+              name: rawResult.test.name,
+              time: rawResult.time,
+              status: "S",
+              message: "passed",
+              altName: rawResult.test.testName
+            };
+            self.totalCount++;
+            if (rawResult.exception != null) {
+                if (_isTestException(rawResult.exception)) {
+                    result.status = "F";
+                    self.failureCount++;
+                } else {
+                    result.status = "E";
+                    self.errorCount++;
+                }
+                result.message = _problemDetailMessageFor(rawResult.exception);
+            }
+            self.results.push(result);
+        });
+    }
+
+};
+
+function _problemDetailMessageFor(excep) {
+    var result;
+    if (_isTestException(excep)) {
+        result = '';
+        if (excep.comment != null)
+            result += ('"' + excep.comment + '"\n');
+
+        result += excep.testMessage;
+
+        if (excep.stackTrace)
+            result += '\n\nStack trace follows:\n' + excep.stackTrace;
+    } else {
+        result = 'Error message is:\n"';
+        result +=
+            (typeof(excep.description) == 'undefined') ?
+            excep : excep.description;
+        result += '"';
+        if (typeof(excep.stack) != 'undefined') // Mozilla only
+            result += '\n\nStack trace follows:\n' + excep.stack;
+    }
+    return result;
+}
+
+function _suite() {
+    this.tests = [].concat(page.testSuite);
+    this.setup = _find(this.tests, "setUp") || function() {};
+    this.teardown = _find(this.tests, "tearDown") || function() {};
+}
+
+function _find(array, name) {
+    var index = -1;
+//    array.every(function(value, i) { if (value.name === name) { index = i; return false; } return true; });
+    array.some(function(value, i) { if (value.name === name) { index = i; return true; } return false; });
+    if (index !== -1) { return array.splice(index, 1)[0]; }
+}
+
+_suite.prototype = {
+    runAllTests: function() {
+        var self = this;
+        return this.tests.map(function(test) { return self.executeTestFunction(test); });
+    },
+
+    executeTestFunction: function (test) {
+        var excep;
+        var timeBefore = new Date();
+        try {
+            this.setup();
+            test();
+        }
+        catch (e1) {
+            excep = e1;
+        }
+        try {
+            this.teardown();
+        }
+        catch (e2) {
+            //Unlike JUnit, only assign a tearDown exception to excep if there is not already an exception from the test body
+            if (!excep) { excep = e2; }
+        }
+        var time = (new Date() - timeBefore) / 1000;
+        return {test:test, time:time, exception: excep};
+    },
+
+    executeUnsafeByName: function (name) {
+        var run = false;
+        var self = this;
+        this.tests.forEach(function (test) {
+            if (test.name === name) {
+                run = true;
+                self.setup();
+                test();
+                self.teardown();
+            }
+        });
+        if (!run) { throw new Error("Test function <" + name + "> not found."); }
+    }
+};
+
+function _isTestException(exception) {
+    return exception.isTestException === true;
+}
+
+/**
+ * Runs the tests and fills the results.
+ * You don not need to call it directly,
+ * it is called when path is /testrunner/results.
+ */
+function runTheTests() {
+    _runner.start();
+}
+
+/**
+ * This function renders test results, in normal html.
+ * If argument is true, failed tests are rendered with
+ * links to /testrunner/test?RestOfTheName in status field.
+ * It is called automatically when path is /testrunner/results.
+ * @see get_testrunner_test
+ */
+function renderTestsAsHtml(links) {
+    page.setTitle(appjet.appName+" Test Runner");
+    page.head.write('<style type="text/css">');
+    page.head.write(_testRunnerCss);
+    page.head.write('</style>');
+    print(P(
+        {className:"testresult"+(
+            _runner.errorCount?"E":_runner.failureCount?"F":"S")},
+        _runner.totalCount, " test(s) run, ",
+        _runner.errorCount, " error(s), ",
+        _runner.failureCount, " failure(s)."
+    ));
+    var table = UL({className:"testresults"});
+    for (var ri in _runner.results) {
+    	var result = _runner.results[ri],
+        	testStatus = result.status;
+        	testMessage = "";
+        if (links && testStatus != "S") {
+            testStatus = A({href:"/testrunner/test?"+result.name}, testStatus);
+            testMessage = UL(LI(result.message));
+        }
+        table.push(LI(
+            SPAN({className:"testresult"+result.status},"[",testStatus,"]"),
+            " ",
+            result.altName || _nameToTestCase(result.name),
+            testMessage
+        ));
+    }
+    print(table);
+}
+
+function _nameToTestCase(name) {
+    var n = name.replace(/([A-Z])/g, " $1").replace("_", ", ","g").replace(/  /g, " ");
+    return n.charAt(0).toUpperCase()+n.substring(1);
+}
+
+var _testRunnerCss = "" +
+"table.testresults td, table.testresults th { border: 1px solid silver; padding-left: 1ex; padding-right: 1ex }\n" +
+".testresultS { background-color: lightgreen }\n" +
+".testresultF { background-color: yellow }\n" +
+".testresultE { background-color: red }\n" +
+"td.testcase, td.testmessage { text-align: left }\n" +
+"td.teststatus { text-align: center }\n";
+
+/** Overwrites default CSS used when rendering tests. */
+function setTestRunnerCss(css) {
+    _testRunnerCss = css;
+}
+
+if (appjet.isPreview) { setTestRunnerFooterLink(); }
+
+switch (request.path) {
+    case '/testrunner/test':
+        new _suite().executeUnsafeByName(request.query);
+        response.stop();
+        break;
+    case '/testrunner/results':
+        runTheTests();
+        renderTestsAsHtml(true);
+        response.stop();
+        break;
+}
+
+/**
+ * Redirects from / to /testrunner/results or returns if path is different than /.
+ */
+function runTestsByDefault() {
+    if (request.path === "/") { response.redirect("/testrunner/results"); }
+}
+
+/**
+ * Puts footer link to testrunner results.
+ * Called automatically in preview. It is safe to call it more times
+ * (you can call it again without checking isPreview).
+ */
+function setTestRunnerFooterLink() {
+    import({}, "lib-utils/footer-links").setFooterLink("/testrunner/results", "Test Runner");
+}
+
+/* appjet:server */
+
+var _globalEx = new Error("setUp probably wasn't started");
+
+function thr(x) { if (x) throw x; }
+
+page.testSuite = [
+    function setUp() { _globalEx = null; },
+
+    function tearDown() { thr(_globalEx); },
+
+    function so_NothingBadHappens_ThatsIt()
+    { var ex = _globalEx; _globalEx = null; thr(ex); },
+
+    function errorIsRaised_InsideTest()
+    { throw new Error("Intentional error"); },
+
+    function failureIsRaised_InsideTest() {
+        error = new Error();
+        error.isTestException = true;
+        error.testMessage = "Intentional failure";
+        throw error;
+    },
+
+    function errorIsRaised_InTearDown()
+    { _globalEx = new Error("Intentional error in tearDown"); },
+
+    function failureIsRaised_InTearDown() {
+        _globalEx = new Error();
+        _globalEx.isTestException = true;
+        _globalEx.testMessage = "Intentional failure in tearDown";
+    },
+];
+
+import("lib-utils/runner");
+
+runTestsByDefault();
+setTestRunnerFooterLink();

+ 284 - 0
lib-utils/ui-harness.js

@@ -0,0 +1,284 @@
+/* appjet:version 0.1 */
+// (c) 2009, Herbert Vojčík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+/* appjet:library */
+
+var _ = import({}, "lib-utils/useful");
+
+var _testView, _showNoCode, _tabs, _code;
+
+/**
+  * Call this to say you will use the harness.
+  * Enables autodispatch with dispatchPlus, showNoCode and testView,
+  * as well as prepares harness components.
+  * You should call harness(path) afterwards to actually fill it with contents.
+  @param fb if evaluates to true, uses facebook tags to build harness
+   for facebook views.
+  */
+function prepareHarness(fb) {
+     import(_, "lib-app/mvp", "lib-app/dispatch-plus", "lib-app/liveui");
+     if (fb) import(_, "lib-app/facebook-ext");
+
+     var _canvas, _prints = DIV();
+
+     page.setView(fb ? _.box(
+         _tabs = _.box(_.lazy(_renderFbTabs)),
+         _canvas = _.box(DIV({style:"margin-top: 3em"},
+             _.data("view")
+         )),
+         _code = _.box(
+             DIV(HR(), P("Code of scenario: "), CODE(_.data()))
+         )
+     ) : _.box(TABLE({cellspacing:0, cellpadding:0, style:"width: 100%"},
+         TR(
+             _tabs = _.box(
+                 TD({rowspan:2,style:"width: 150px; vertical-align: top"}, _.lazy(_renderTabs))
+             ),
+             _canvas = _.box(TD({style:_.seq(
+                 "padding:20px;",
+                 "vertical-align:top;",
+                 "background-color:", _.outerdata("col")
+             )}, _.data("view")))
+         ),
+         TR(
+             _code = _.box(
+                 TD({style:"padding:20px"}, HR(), P("Code of scenario: "), CODE(_.data()))
+             )
+         )
+     )));
+     _canvas.given({view:_prints});
+     page.setPresenter(_.dispatchPlus);
+     page.setViewModel({col:"inherit"});
+
+ //    var _oldPageBodyWrite = page.body.write;
+
+     /**@ignore*/
+/*     page.body.write = function(text) {
+         _prints.push(html(text));
+     };*/
+
+     _testView = function(v) {
+         _canvas.model.view = v;
+//         page.body.write = _oldPageBodyWrite;
+     };
+
+     _showNoCode = function() { _code[0].length = 0; };
+}
+
+function _renderTabs(model) {
+     var list = DIV();
+     model.forEach(function(x, i) {
+         list.push(DIV({style: "padding: 10px; background-color: "+x.col},
+             link(x.link, x.text)
+         ));
+     });
+     return list;
+}
+
+function _renderFbTabs(model) {
+     var inputs = model.map(function(x){
+         return x.link+"_"+x.text;
+     });
+     return fb.ui.tabs(inputs);
+}
+
+function _toLink(fname) {
+     return fname.split("_")[1].split("$").join("-");
+}
+
+function _toText(fname) {
+     return fname.split("_")[1].split("$").join(" ");
+}
+
+/**
+  * When you use view/content object that you manipulate,
+  * not the print functions, call this to show it.
+  */
+function testView(v) {
+     if (!_testView) throw "prepareHarness not called.";
+     _testView(v);
+}
+
+/**
+  * Suppresses showing code of the scenario. Effectively turns
+  * the UI harness to simple tabbed-page-view application.
+  */
+function showNoCode() {
+     if (!_showNoCode) throw "prepareHarness not called.";
+     _showNoCode();
+}
+
+/**
+  * This function creates the harness.
+  * In non-fb case, it is called automatically.
+  * In fb case, you should call it manually.
+  @param uiPath "path" to ui scenario (/name-of-scenario).
+   When using lib-dispatch-plus, it is request.pathTail.
+  */
+function harness(uiPath) {
+     page.setPresenter(function() {});
+     var prefix = request.method.toLowerCase()+"ui_";
+     var getuis = keys(appjet._internal.global).filter(function(x) {
+         return x !== "getui_main" && x.substring(0, prefix.length) === prefix;
+     });
+     var step = Math.floor(getuis.length / 2);
+     var oddNumber = 2*step+1;
+     function nthColour(i) {
+         function _rem(x) { return x - Math.floor(x); }
+         var c = 1;
+         _.hsv2rgb(_rem(i*step/oddNumber), 0.05, 0.95).
+             forEach(function(x) { c = (c<<8)+x; });
+         return "#" + c.toString(16).substring(1);
+     }
+     var tabsInfo = getuis.map(function(x, i) {
+         var result = {
+             link:_toLink(x),
+             text: _toText(x),
+             col: nthColour(i)
+         };
+         if ("/"+result.link === uiPath) {
+             _.extend(page.viewModel, result);
+         }
+         return result;
+     });
+     _tabs.given(tabsInfo);
+     request.fullPath = "ui" + uiPath;
+     var prefix = _.dispatchPlus[request.method]+"ui_";
+     var fname = prefix+uiPath.substring(1).replace(/-/g, '$$');
+     _code.given((fname !== prefix && appjet._internal.global[fname]) || "");
+     _.dispatchPlus();
+}
+
+/**@ignore*/
+function getui_() {
+     print(
+         H1("Scenario '", request.pathTail, "' not found!"),
+         P("Select from scenarios in the tabs, or create some ",
+             "by creating functions with names of the form ",
+             "getui_name$of$scenario"
+         )
+     );
+}
+
+/**@ignore*/
+function postui_() {
+     print(
+         H1("Scenario '", request.pathTail, "' not found!"),
+         P("Select from scenarios in the tabs, or create some ",
+             "by creating functions with names of the form ",
+             "postui_name$of$scenario"
+         )
+     );
+}
+
+/* appjet:server */
+
+import("lib-utils/ui-harness");
+
+// ====== Simulated import(appjet.appName) ======
+
+import("lib-app/liveui");
+
+var _lorem = "Lorem ipsum dolor amet. ";
+
+/**@ignore*/
+function renderUsingPrint(text) {
+     print(H1("Using print functions"));
+     printp("<",text,">; entity: &amp;");
+}
+
+/**@ignore*/
+function getView() {
+     return box(
+         H1("Using testView function"),
+         P("<",data(),">; entity: &amp;")
+     );
+}
+
+// ====== End of simulated import(appjet.appName) ======
+
+/**@ignore*/
+function getui_main() {
+     printp(
+         "This is harness for UIs. It is useful in scenario, ",
+         "where you put UI of your application in a separate library, ",
+         "because it's right to separate the domain (data and computation) ",
+         "from the presentation (UI)."
+     );
+     printp(
+         "You use this harness by importing it in /*appjet:server*/ part ",
+         "of the UI library, then importing the library itself. ",
+         "Then, you create scenarios of using the parts of your UI, ",
+         "each in a function of form getui_name$of$scenario. ",
+         "The harness automatically finds them and presents tabs ",
+         "for selecting them. This way, you can develop UI ",
+         "without resorting to real application running ",
+         "and see how UI use cases look immediately without clicking through the app itself."
+     );
+     printp(
+         "Though primarily aimed at testing UIs, you can use it ",
+         "as simple tabbing interface, if your app only shows pages. ",
+         "In this case, call showNoCode() to suppress showing code."
+     );
+     printp(
+         "Create getui_main function to show the initial screen ",
+         "(like this one). It is not included in tabs, ",
+         "it is only used for the main screen."
+     );
+}
+
+/**@ignore*/
+function getui_using$print() {
+     renderUsingPrint(_lorem);
+}
+
+/**@ignore*/
+function getui_using$print$longer() {
+     renderUsingPrint(_lorem+_lorem);
+}
+
+/**@ignore*/
+function getui_setting$test$view() {
+     var v = getView();
+     testView(v);
+     v.given(_lorem+" && "+_lorem);
+}
+
+/* appjet:disabled */
+
+// In case you want to use harness only in some cases,
+// inspire by this code:
+
+import("lib-dispatch-plus");
+
+// for path /no-harness
+/**@ignore*/
+function get_no$harness() {
+     print(H1("This is normal output"));
+     printp("No harness code is intervening.");
+}
+
+// for other paths, including /
+/**@ignore*/
+function get_() {
+     prepareHarness();
+     harness(request.pathTail);
+}
+
+dispatchPlus();
+
+/* appjet:server */
+
+import("lib-utils/patches");
+dispatchShouldSearchHere();
+prepareHarness();
+
+// prepareHarness includes mvp with autodispatch and dispatchPlus
+// so only creating get_ function is enough.
+/**@ignore*/
+function get_() {
+     harness(request.pathTail);
+}
+
+import({}, "lib-app/mvp").mvp();

+ 323 - 0
lib-utils/useful.js

@@ -0,0 +1,323 @@
+/* appjet:version 0.1 */
+// (c) 2009, Herbert Vojčík
+// Licensed by MIT license (http://www.opensource.org/licenses/mit-license.php)
+
+/* appjet:library */
+
+/**@overview This library contains various utility functions
+ * that appeared during developing other apps but are general enough.
+ */
+
+/**
+ * Returns global object.
+ */
+function global() { return {}.valueOf.call(); }
+
+/**
+ * Returns stub function that logs arguments called
+ * into its property called log and returns retval.
+ * Useful primarily for testing.
+ */
+function stub(retval) {
+    var result = function() {
+        result.log.push(Array.slice(arguments));
+        return retval;
+    };
+    result.log = [];
+    return result;
+}
+
+/**
+ * Read-only wrapper on an object.
+ */
+function readonly(o) {
+    return appjet._native.createProxyObject(
+        function(name) { return o[name]; },
+        function() {},
+        function() {},
+        function() { return []; },
+        o
+    );
+}
+
+/**
+ * Extends an object by copying all other object's own properties.
+ * Returns the extended object. This implies you can create clone
+ * of simple object by extend({}, x).
+ */
+function extend(dst, src) {
+    eachProperty(src, function(p) { dst[p] = src[p]; });
+    return dst;
+}
+
+/**
+ * Akin to <code>Array >> streamContents:</code> in Smalltalk.
+ * Calls f and returns array of every item that was printed using print.
+ * Useful for collecting any item in some loop(s) for which
+ * filter or map can not be straightforwardly used. Both examples get keys
+ * of own properties
+ @example
+ collectPrint(function() { eachProperty(object, print); });
+ collectPrint(function() {for (var p in object) { if (object.hasOwnProperty(p) print(p); }});
+ */
+function collectPrint(f) {
+    var result = [];
+    var _g = global();
+    var savedPrint = _g.print;
+    _g.print = function() { result.push.apply(result, arguments); }; // cannot inline, or f***ing try/finally bug which nobody is willing to fix appears
+    try { f(); } finally { _g.print = savedPrint; }
+    return result;
+}
+
+function _collector(result) { return function() { result.push.apply(result, arguments); }; }
+
+/**
+ * Akin to <code>Array >> streamContents:</code> in Smalltalk.
+ * Calls f and returns array of every item that was yielded using yield.
+ * Useful for collecting any item in some loop(s) for which
+ * filter or map can not be straightforwardly used. An example gets keys
+ * of own properties
+ @example
+ collectYield(function () {for (var p in object) { if (object.hasOwnProperty(p) yield p; }});
+ */
+function collectYield(f) {
+    var result = [];
+    var gen = f();
+    try {
+        for(;;) { result.push(gen.next()); }
+    } catch (ex if ex instanceof StopIteration) {
+        //gen.close(); //strange, not needed
+    }
+    return result;
+}
+
+//from web; then edited
+/** Converts h,s,v (0-1) to rgb (0-255). Returns array [r, g, b]. */
+function hsv2rgb(h,s,v) {
+    // Adapted from http://www.easyrgb.com/math.html
+    // hsv values = 0 - 1, rgb values = 0 - 255
+    var r, g, b;
+    if (s == 0) {
+    	r = g = b = v;
+    } else {
+        var vh = 6*h;
+        var vi = Math.floor(vh);
+        var v1 = v*(1-s);
+        var v2 = v*(1-s*(vh-vi));
+        var v3 = v*(1-s*(1-(vh-vi)));
+        switch(vi) {
+            case 0: r = v; g = v3; b = v1; break;
+            case 1: r = v2; g = v; b = v1; break;
+            case 2: r = v1; g = v; b = v3; break;
+            case 3: r = v1; g = v2; b = v; break;
+            case 4: r = v3; g = v1; b = v; break;
+            case 5: r = v; g = v1; b = v2; break;
+        }
+    }
+    return [ Math.round(255*r), Math.round(255*g), Math.round(255*b) ];
+}
+
+/**
+ * Call this function with etag of actual requested resource.
+ * Includes caching headers in the response,
+ * and if the ETag matches, it immediately stops the response
+ * with "304 Not Modified"; in other case it returns
+ * and lets you continue with populating the response data.
+ * Call after you KNOW the resource exists! It is illegal
+ * to change response status to 404 or similar after this call.
+ */
+function etagCaching(etag) {
+    response.setCacheable(null);
+    response.setHeader("Cache-Control", "public, must-revalidate");
+    response.setHeader("ETag", etag);
+    var cond = request.headers["If-None-Match"];
+    if (cond && (cond === "*" || ([].concat(cond).indexOf(etag) !== -1))) {
+        response.setStatusCode(304);
+        response.stop();
+    }
+}
+
+/* appjet:server */
+import("lib-utils/jsunit");
+var _l = import({}, "lib-utils/useful");
+
+// ==== Test cases ====
+
+page.testSuite = [
+
+	function collectPrintCollectsPrintedArguments() {
+	    var collected = _l.collectPrint(function() {
+	        print("Hello", "world", "!");
+	        print();
+	        print(3,1,4);
+	    });
+	    assertArrayEquals("Incorrect items collected.", ["Hello", "world", "!", 3, 1, 4], collected);
+	},
+	
+	function collectPrintRetainsLegacyPrint() {
+	    var savedPrint = this.print;
+	    _l.collectPrint(function() {
+	        print("Hello", "world", "!");
+	        print();
+	        print(3,1,4);
+	    });
+	    assertEquals("Print not retained.", savedPrint, this.print);
+	},
+	
+	function collectPrintRetainsLegacyPrintInCaseOfException() {
+	    var savedPrint = this.print;
+	    var f = function() { throw "exception"; };
+	    try {
+	        _l.collectPrint(f);
+	        fail("Exception not thrown");
+	    } catch(ex if ex === "exception") {
+	        // pass
+	    }
+	    assertEquals("Print not retained.", savedPrint, this.print);
+	},
+	
+	function collectPrintRetainsLegacyPrintInCaseOfUndefinedException() {
+	    var savedPrint = this.print;
+	    var f = function() { throw undefined; };
+	    try {
+	        _l.collectPrint(f);
+	        fail("Exception not thrown");
+	    } catch(ex if ex === undefined) {
+	        // pass
+	    }
+	    assertEquals("Print not retained.", savedPrint, this.print);
+	},
+	
+	function collectYieldCollectsYieldedArguments() {
+	    var collected = _l.collectYield(function() {
+	        yield "Hello";
+	        yield 3;
+	    });
+	    assertArrayEquals("Incorrect items collected.", ["Hello", 3], collected);
+	},
+	
+	function collectYieldCollectsYieldeesFromTryBlockAndFinallyRuns() {
+	    var frun = false;
+	    var collected = _l.collectYield(function() {
+	        try {
+	            yield "Hello";
+	            yield 3;
+	        } finally { frun = true; }
+	    });
+	    assertArrayEquals("Incorrect items collected.", ["Hello", 3], collected);
+	    assertTrue("Finally did not run", frun);
+	},
+	
+	function collectYieldThrowsExceptionIfThrownInF() {
+	    try {
+	        var collected = _l.collectYield(function() {
+	            yield "Hello";
+	            throw "exception";
+	            yield "world";
+	        });
+	        fail("Exception not thrown.");
+	    } catch(ex if ex === "exception") {
+	        // pass
+	    }
+	},
+	
+	function collectYieldThrowsUndefinedIfThrownInF() {
+	    try {
+	        var collected = _l.collectYield(function() {
+	            yield "Hello";
+	            throw undefined;
+	            yield "world";
+	        });
+	        fail("Exception not thrown.");
+	    } catch(ex if ex === undefined) {
+	        // pass
+	    }
+	},
+	
+	function collectYieldThrowsExceptionIfExceptionThrownInF_AndFinallyInFRuns() {
+	    var finallyRun = false;
+	    try {
+	        var collected = _l.collectYield(function() {
+	            try {
+	                yield "Hello";
+	                throw "exception";
+	                yield "world";
+	            } finally { finallyRun = true; }
+	        });
+	        fail("Exception not thrown.");
+	    } catch(ex if ex === "exception") {
+	        // pass
+	    }
+	    assertTrue("Finally in f did not run.", finallyRun);
+	},
+	
+	function readonlyReadsCorrectlyAndIsDynamic() {
+	    var x = {hello: "world"};
+	    var rx = _l.readonly(x);
+	    assertEquals("Hello read incorrectly", "world", rx.hello);
+	    assertUndefined("Nonexistent property is not undefined", rx.thisIsNot);
+	    x.hello = "again";
+	    x.thisIsNot = "this is";
+	    assertEquals("Hello read incorrectly", "again", rx.hello);
+	    assertEquals("ThisIsNot read incorrectly", "this is", rx.thisIsNot);
+	},
+	
+	function readonlyIsReadOnly() {
+	    var x = {hello: "world"};
+	    var rx = _l.readonly(x);
+	    rx.thisIsNot = "this is";
+	    delete rx.hello;
+	    assertEquals("Hello read incorrectly", "world", rx.hello);
+	    assertUndefined("Nonexistent property is not undefined", rx.thisIsNot);
+	},
+	
+	function extendExtendsCorrectlyForAllCombinations() {
+	    var srcProto = { pro: "to" }
+	    var src = object(srcProto);
+	    src.a = "b"; src.c = "d";
+	    var dst = { pro: "fi", a: "bc", yes: "true" };
+	    var result = _l.extend(dst, src);
+	    assertEquals("extend did not return extended object", dst, result);
+	    assertEquals("a wasn't copied", "b", dst.a);
+	    assertEquals("c wasn't copied", "d", dst.c);
+	    assertEquals("pro was changed", "fi", dst.pro);
+	    assertEquals("yes was changed", "true", dst.yes);
+	},
+	
+	function logIsEmptyForNewStub() {
+	    var f = _l.stub();
+	    assertArrayEquals("Log is not empty array", [], f.log);
+	},
+	
+	function stubIsAFunction() {
+	    var f = _l.stub();
+	    assertEquals("Stub is not a function", "function", typeof f);
+	},
+	
+	function stubWithoutRetvalReturnsUndefined() {
+	    var f = _l.stub();
+	    assertEquals("Stub doesn't return undefined", undefined, f(3, "hello"));
+	},
+	
+	function stubWithRetvalReturnsRetval() {
+	    var f = _l.stub("world");
+	    assertEquals("Stub returns incorrect value", "world", f(3, "hello"));
+	},
+	
+	function stub_CalledMultipleTimes_ReturnsAlwaysRetval_AndLogsArguments() {
+	    var f = _l.stub("hi");
+	    var r1 = f("hello", 14, "world");
+	    assertArrayEquals("Log is incorrect 1", [["hello", 14, "world"]], f.log);
+	    var r2 = f("!", f);
+	    assertArrayEquals("Log is incorrect 2", [["hello", 14, "world"], ["!", f]], f.log);
+	    f();
+	    assertArrayEquals("Log is incorrect 3", [["hello", 14, "world"], ["!", f], []], f.log);
+	    assertEquals("Stub returns incorrect value 1", "hi", r1);
+	    assertEquals("Stub returns incorrect value 2", "hi", r2);
+	},
+
+];
+
+// ==== Test runner ====
+import("lib-utils/runner");
+runTestsByDefault();