Merge branch 'devel' into bundle-android

Conflicts:
	tools/utils.js
This commit is contained in:
Sashko Stubailo
2014-08-07 11:32:07 -07:00
60 changed files with 1355 additions and 699 deletions

View File

@@ -27,6 +27,7 @@ GITHUB: codeinthehole <david.winterbottom@gmail.com>
GITHUB: dandv <ddascalescu+github@gmail.com>
GITHUB: davegonzalez <gonzalez.dalex@gmail.com>
GITHUB: ducdigital <duc@ducdigital.com>
GITHUB: duckspeaker <gallo.j@gmail.com>
GITHUB: emgee3 <hello@gravitronic.com>
GITHUB: felixrabe <felix@rabe.io>
GITHUB: FredericoC <frederico.carvalho@3stack.com.au>
@@ -75,3 +76,4 @@ METEOR: sixolet <naomi@meteor.com>
METEOR: Slava <slava@meteor.com>
METEOR: stubailo <sashko@mit.edu>
METEOR: ekatek <ekate@meteor.com>
METEOR: mariapacana <maria.pacana@gmail.com>

View File

@@ -1,4 +1,4 @@
## v.NEXT.NEXT
## v.NEXT
* The `appcache` package now defaults to functioning on all browsers that
support the AppCache API, rather than a whitelist of browsers. You can still
@@ -6,16 +6,90 @@
change is that `appcache` is now enabled by default on Firefox, because
Firefox no longer makes a confusing popup. #2241
* When a call to `match` fails in a method or subscription, log the
failure on the server. (This matches the behavior described in our docs)
## v.NEXT
## v0.8.3
#### Blaze
* Refactor Blaze to simplify internals while preserving the public
API. `UI.Component` has been replaced with `Blaze.View.`
* Fix performance issues and memory leaks concerning event handlers.
* Add `UI.remove`, which removes a template after `UI.render`/`UI.insert`.
* Add `this.autorun` to the template instance, which is like `Deps.autorun`
but is automatically stopped when the template is destroyed.
* Create `<a>` tags as SVG elements when they have `xlink:href`
attributes. (Previously, `<a>` tags inside SVGs were never created as
SVG elements.) #2178
* Throw an error in `{{foo bar}}` if `foo` is missing or not a function.
* Cursors returned from template helpers for #each should implement
the `observeChanges` method and don't have to be Minimongo cursors
(allowing new custom data stores for Blaze like Miniredis).
* Remove warnings when {{#each}} iterates over a list of strings,
numbers, or other items that contains duplicates. #1980
#### Meteor Accounts
* Fix regression in 0.8.2 where an exception would be thrown if
`Meteor.loginWithPassword` didn't have a callback. Callbacks to
`Meteor.loginWithPassword` are now optional again. #2255
* Fix OAuth popup flow in mobile apps that don't support
`window.opener`. #2302
* Fix "Email already exists" error with MongoDB 2.6. #2238
#### mongo-livedata and minimongo
* Fix performance issue where a large batch of oplog updates could block
the node event loop for long periods. #2299.
* Fix oplog bug resulting in error message "Buffer inexplicably empty". #2274
* Fix regression from 0.8.2 that caused collections to appear empty in
reactive `findOne()` or `fetch` queries that run before a mutator
returns. #2275
#### Miscellaneous
* Stop including code by default that automatically refreshes the page
if JavaScript and CSS don't load correctly. While this code is useful
in some multi-server deployments, it can cause infinite refresh loops
if there are errors on the page. Add the `reload-safetybelt` package
to your app if you want to include this code.
* On the server, `Meteor.startup(c)` now calls `c` immediately if the
server has already started up, matching the client behavior. #2239
* Add support for server-side source maps when debugging with
`node-inspector`.
* Add `WebAppInternals.addStaticJs()` for adding static JavaScript code
to be served in the app, inline if allowed by `browser-policy`.
* Make the `tinytest/run` method return immediately, so that `wait`
method calls from client tests don't block on server tests completing.
* Log errors from method invocations on the client if there is no
callback provided.
* Upgraded dependencies:
- node: 0.10.29 (from 0.10.28)
- less: 1.7.1 (from 1.6.1)
Patches contributed by GitHub users Cangit, cmather, duckspeaker, zol.
## v0.8.2

View File

@@ -1 +1 @@
0.8.2
0.8.3

View File

@@ -2189,6 +2189,12 @@ This property provides access to the data context at the top level of
the template. It is updated each time the template is re-rendered.
Access is read-only and non-reactive.
{{> api_box template_autorun}}
You can use `this.autorun` from a [`created`](#template_created) or
[`rendered`](#template_rendered) callback to reactively update the DOM
or the template instance. The Computation is automatically stopped
when the template is destroyed.
<h2 id="ui"><span>Template utilities</span></h2>
@@ -2205,6 +2211,15 @@ any part of the DOM for finer control than just using template inclusions.
You can define helpers and event maps on `UI.body` just like on any
`Template.myTemplate` object.
Helpers on `UI.body` are only available in the `<body>` tags of your
app. To register a global helper, use
[UI.registerHelper](#ui_registerhelper).
Event maps on `UI.body` don't apply to elements added to the body via
`UI.insert`, jQuery, or the DOM API, or to the body element itself.
To handle events on the body, window, or document, use jQuery or the
DOM API.
{{> api_box ui_render}}
This returns an "rendered template" object, which can be passed to
@@ -2238,7 +2253,15 @@ changes.
{{> api_box ui_getelementdata}}
{{> api_box ui_dynamic}}
`UI.dynamic` allows you to include a template by name, where the name
may be calculated by a helper and may change reactively. The `data`
argument is optional, and if it is omitted, the current data context
is used.
For example, if there is a template named "foo", `{{dstache}}> UI.dynamic
template="foo"}}` is equivalent to `{{dstache}}> foo}}`.
{{#api_box eventmaps}}
@@ -2281,8 +2304,8 @@ Example:
// Fires when any element with the 'accept' class is clicked
'click .accept': function (event) { ... },
// Fires when 'accept' is clicked, or a key is pressed
'keydown, click .accept': function (event) { ... }
// Fires when 'accept' is clicked or focused, or a key is pressed
'click .accept, focus .accept, keypress': function (event) { ... }
}
Most events bubble up the document tree from their originating

View File

@@ -1186,6 +1186,11 @@ Template.api.accounts_ui_config = {
type: "Object",
descr: "To ask the user for permission to act on their behalf when offline, map the relevant external service to `true`. Currently only supported with Google. See [Meteor.loginWithExternalService](#meteor_loginwithexternalservice) for more details."
},
{
name: "forceApprovalPrompt",
type: "Boolean",
descr: "If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google."
},
{
name: "passwordSignupFields",
type: "String",
@@ -1845,6 +1850,17 @@ Template.api.template_data = {
descr: ["The data context of this instance's latest invocation."]
};
Template.api.template_autorun = {
id: "template_autorun",
name: "<em>this</em>.autorun(runFunc)",
locus: "Client",
descr: ["A version of [Deps.autorun](#deps_autorun) that is stopped when the template is destroyed."],
args: [
{name: "runFunc",
type: "Function",
descr: "The function to run. It receives one argument: a Deps.Computation object."}
]
};
Template.api.ui_registerhelper = {
id: "ui_registerhelper",
@@ -1862,6 +1878,22 @@ Template.api.ui_registerhelper = {
}]
};
Template.api.ui_dynamic = {
id: "ui_dynamic",
name: "{{> UI.dynamic template=templateName [data=dataContext]}}",
locus: "Client",
descr: ["Choose a template to include dynamically, by name."],
args: [
{name: "templateName",
type: "String",
descr: "The name of the template to include."
},
{name: "dataContext",
type: "Object",
descr: "Optional. The data context in which to include the template."
}]
};
Template.api.ui_body = {
id: "ui_body",
name: "UI.body",

View File

@@ -33,7 +33,7 @@ packages that most any app will use (for example `webapp`, which
handles incoming HTTP connections, and `templating`, which lets you
make HTML templates that automatically update live as data changes).
Then there are optional packages like `email`, which lets your app
send emails, or the Meteor Accounts series (`account-password`,
send emails, or the Meteor Accounts series (`accounts-password`,
`accounts-facebook`, `accounts-ui`, and others) which provide a
full-featured user account system that you can drop right into your
app. And beyond these "official" packages, there are hundreds of

View File

@@ -253,7 +253,8 @@ var toc = [
{instance: "this", name: "find", id: "template_find"},
{instance: "this", name: "firstNode", id: "template_firstNode"},
{instance: "this", name: "lastNode", id: "template_lastNode"},
{instance: "this", name: "data", id: "template_data"}
{instance: "this", name: "data", id: "template_data"},
{instance: "this", name: "autorun", id: "template_autorun"}
],
"UI", [
"UI.registerHelper",
@@ -262,7 +263,8 @@ var toc = [
"UI.renderWithData",
"UI.insert",
"UI.remove",
"UI.getElementData"
"UI.getElementData",
{name: "{{> UI.dynamic}}", id: "ui_dynamic"}
],
{type: "spacer"},
{name: "Event maps", style: "noncode"}

View File

@@ -1,5 +1,5 @@
// While galaxy apps are on their own special meteor releases, override
// Meteor.release here.
if (Meteor.isClient) {
Meteor.release = Meteor.release ? "0.8.2" : undefined;
Meteor.release = Meteor.release ? "0.8.3" : undefined;
}

View File

@@ -1 +1 @@
0.8.2
0.8.3

View File

@@ -1 +1 @@
0.8.2
0.8.3

View File

@@ -1 +1 @@
0.8.2
0.8.3

View File

@@ -1 +1 @@
0.8.2
0.8.3

View File

@@ -35,6 +35,16 @@ var randomString = function (length) {
return ret;
};
var preCall = function (name) {
console.log('> ' + name);
};
var postCall = function (name) {
return function (err, callback) {
console.log('< ' + name + ' ' + (err ? 'ERR' : 'OK'));
};
};
var pickCollection = function () {
return Random.choice(Collections);
};
@@ -96,7 +106,8 @@ if (Meteor.isServer) {
Meteor.setInterval(function () {
var when = +(new Date) - PARAMS.maxAgeSeconds*1000;
_.each(Collections, function (C) {
C.remove({when: {$lt: when}});
preCall('removeMaxAge');
C.remove({when: {$lt: when}}, postCall('removeMaxAge'));
});
// Clear out 5% of the DB each time, steady state. XXX parameterize?
}, 1000*PARAMS.maxAgeSeconds / 20);
@@ -121,7 +132,8 @@ if (Meteor.isServer) {
doc.when = +(new Date);
var C = pickCollection();
C.insert(doc);
preCall('insert');
C.insert(doc, postCall('insert'));
},
update: function (processId, field, value) {
check([processId, field, value], [String]);
@@ -130,15 +142,18 @@ if (Meteor.isServer) {
var C = pickCollection();
// update one message.
C.update({fromProcess: processId}, {$set: modifer}, {multi: false});
preCall('update');
C.update({fromProcess: processId}, {$set: modifer}, {multi: false}, postCall('update'));
},
remove: function (processId) {
check(processId, String);
var C = pickCollection();
// remove one message.
var obj = C.findOne({fromProcess: processId});
if (obj)
C.remove(obj._id);
if (obj) {
preCall('remove');
C.remove(obj._id, postCall('remove'));
}
}
});

View File

@@ -1,8 +1,12 @@
#!/bin/bash
PORT=9000
NUM_CLIENTS=10
DURATION=120
if [ -z "$NUM_CLIENTS" ]; then
NUM_CLIENTS=10
fi
if [ -z "$DURATION" ]; then
DURATION=120
fi
REPORT_INTERVAL=10
set -e
@@ -20,7 +24,7 @@ pkill -f "$PROJDIR/.meteor/local/db" || true
../../../meteor reset || true
# start the benchmark app
../../../meteor --production --settings "scenarios/${SCENARIO}.json" --port 9000 &
../../../meteor --production --settings "scenarios/${SCENARIO}.json" --port ${PORT} &
OUTER_PID=$!
echo "Waiting for server to come up"
@@ -30,12 +34,13 @@ function wait_for_port {
sleep 1
N=$(($N+1))
if [ $N -ge $2 ] ; then
curl -v "$1" || true
echo "Timed out waiting for port $1"
exit 2
fi
done
}
wait_for_port "http://localhost:9001" 60
wait_for_port "http://localhost:${PORT}" 60
echo "Starting phantoms"

View File

@@ -0,0 +1,11 @@
{
"params": {
"numCollections": 1,
"maxAgeSeconds": 60,
"insertsPerSecond": 10,
"updatesPerSecond": 10,
"removesPerSecond": 1,
"documentSize": 128,
"documentNumFields": 2
}
}

View File

@@ -0,0 +1,11 @@
{
"params": {
"numCollections": 1,
"maxAgeSeconds": 60,
"insertsPerSecond": 100,
"updatesPerSecond": 100,
"removesPerSecond": 10,
"documentSize": 128,
"documentNumFields": 2
}
}

View File

@@ -0,0 +1,11 @@
{
"params": {
"numCollections": 1,
"maxAgeSeconds": 60,
"insertsPerSecond": 20,
"updatesPerSecond": 20,
"removesPerSecond": 2,
"documentSize": 128,
"documentNumFields": 2
}
}

View File

@@ -0,0 +1,11 @@
{
"params": {
"numCollections": 1,
"maxAgeSeconds": 60,
"insertsPerSecond": 40,
"updatesPerSecond": 40,
"removesPerSecond": 4,
"documentSize": 128,
"documentNumFields": 2
}
}

View File

@@ -0,0 +1,11 @@
{
"params": {
"numCollections": 1,
"maxAgeSeconds": 60,
"insertsPerSecond": 50,
"updatesPerSecond": 50,
"removesPerSecond": 5,
"documentSize": 128,
"documentNumFields": 2
}
}

View File

@@ -1 +1 @@
0.8.2
0.8.3

View File

@@ -2,12 +2,14 @@ Accounts.ui = {};
Accounts.ui._options = {
requestPermissions: {},
requestOfflineToken: {}
requestOfflineToken: {},
forceApprovalPrompt: {}
};
// XXX refactor duplicated code in this function
Accounts.ui.config = function(options) {
// validate options keys
var VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken'];
var VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken', 'forceApprovalPrompt'];
_.each(_.keys(options), function (key) {
if (!_.contains(VALID_KEYS, key))
throw new Error("Accounts.ui.config: Invalid key: " + key);
@@ -56,6 +58,20 @@ Accounts.ui.config = function(options) {
}
});
}
// deal with `forceApprovalPrompt`
if (options.forceApprovalPrompt) {
_.each(options.forceApprovalPrompt, function (value, service) {
if (service !== 'google')
throw new Error("Accounts.ui.config: `forceApprovalPrompt` only supported for Google login at the moment.");
if (Accounts.ui._options.forceApprovalPrompt[service]) {
throw new Error("Accounts.ui.config: Can't set `forceApprovalPrompt` more than once for " + service);
} else {
Accounts.ui._options.forceApprovalPrompt[service] = value;
}
});
}
};
passwordSignupFields = function () {

View File

@@ -5,7 +5,7 @@
// XXX it'd be cool to also test that the right thing happens if options
// *are* validated, but Accouns.ui._options is global state which makes this hard
// *are* validated, but Accounts.ui._options is global state which makes this hard
// (impossible?)
Tinytest.add('accounts-ui - config validates keys', function (test) {
test.throws(function () {
@@ -19,4 +19,8 @@ Tinytest.add('accounts-ui - config validates keys', function (test) {
test.throws(function () {
Accounts.ui.config({requestPermissions: {facebook: "not an array"}});
});
test.throws(function () {
Accounts.ui.config({forceApprovalPrompt: {facebook: "only google"}});
});
});

View File

@@ -29,6 +29,8 @@ Template._loginButtonsLoggedOutSingleLoginButton.events({
options.requestPermissions = Accounts.ui._options.requestPermissions[serviceName];
if (Accounts.ui._options.requestOfflineToken[serviceName])
options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName];
if (Accounts.ui._options.forceApprovalPrompt[serviceName])
options.forceApprovalPrompt = Accounts.ui._options.forceApprovalPrompt[serviceName];
loginWithService(options, callback);
}

View File

@@ -169,6 +169,7 @@ Blaze.InOuterTemplateScope = function (templateView, contentFunc) {
parentView = parentView.parentView;
view.onCreated(function () {
this.originalParentView = this.parentView;
this.parentView = parentView;
});
return view;

View File

@@ -29,6 +29,28 @@ Blaze.DOMRange = function (nodeAndRangeArray) {
};
var DOMRange = Blaze.DOMRange;
// In IE 8, don't use empty text nodes as placeholders
// in empty DOMRanges, use comment nodes instead. Using
// empty text nodes in modern browsers is great because
// it doesn't clutter the web inspector. In IE 8, however,
// it seems to lead in some roundabout way to the OAuth
// pop-up crashing the browser completely. In the past,
// we didn't use empty text nodes on IE 8 because they
// don't accept JS properties, so just use the same logic
// even though we don't need to set properties on the
// placeholder anymore.
DOMRange._USE_COMMENT_PLACEHOLDERS = (function () {
var result = false;
var textNode = document.createTextNode("");
try {
textNode.someProp = true;
} catch (e) {
// IE 8
result = true;
}
return result;
})();
// static methods
DOMRange._insert = function (rangeOrNode, parentElement, nextNode, _isMove) {
var m = rangeOrNode;
@@ -118,7 +140,10 @@ DOMRange.prototype.attach = function (parentElement, nextNode, _isMove) {
DOMRange._insert(members[i], parentElement, nextNode, _isMove);
}
} else {
var placeholder = document.createTextNode("");
var placeholder = (
DOMRange._USE_COMMENT_PLACEHOLDERS ?
document.createComment("") :
document.createTextNode(""));
this.emptyRangePlaceholder = placeholder;
parentElement.insertBefore(placeholder, nextNode || null);
}

View File

@@ -6,7 +6,9 @@ var bindIfIsFunction = function (x, target) {
};
};
var bindToCurrentDataIfIsFunction = function (x) {
// If `x` is a function, binds the value of `this` for that function
// to the current data context.
var bindDataContext = function (x) {
if (typeof x === 'function') {
return function () {
var data = Blaze.getCurrentData();
@@ -22,6 +24,8 @@ var wrapHelper = function (f) {
return Blaze.wrapCatchingExceptions(f, 'template helper');
};
// !!! FIX THIS COMMENT !!!
//
// Implements {{foo}} where `name` is "foo"
// and `component` is the component the tag is found in
// (the lexical "self," on which to look for methods).
@@ -45,11 +49,11 @@ Blaze.View.prototype.lookup = function (name, _options) {
return Blaze._parentData(name.length - 1, true /*_functionWrapped*/);
} else if (template && (name in template)) {
return wrapHelper(bindToCurrentDataIfIsFunction(template[name]));
return wrapHelper(bindDataContext(template[name]));
} else if (lookupTemplate && Template.__lookup__(name)) {
return Template.__lookup__(name);
} else if (UI._globalHelpers[name]) {
return wrapHelper(bindToCurrentDataIfIsFunction(UI._globalHelpers[name]));
return wrapHelper(bindDataContext(UI._globalHelpers[name]));
} else {
return function () {
var isCalledAsFunction = (arguments.length > 0);

View File

@@ -49,13 +49,19 @@ Blaze.DOMMaterializer.def({
var rawAttrs = tag.attrs;
var children = tag.children;
if (tagName === 'textarea' && ! (rawAttrs && ('value' in rawAttrs))) {
// turn TEXTAREA contents into a value attribute.
// Reactivity in the form of nested Views won't work here
// because the Views have already been instantiated. To
// get Views in a textarea they need to be wrapped in a
// function and provided as the "value" attribute by the
// compiler.
if (tagName === 'textarea' && tag.children.length &&
! (rawAttrs && ('value' in rawAttrs))) {
// Provide very limited support for TEXTAREA tags with children
// rather than a "value" attribute.
// Reactivity in the form of Views nested in the tag's children
// won't work. Compilers should compile textarea contents into
// the "value" attribute of the tag, wrapped in a function if there
// is reactivity.
if (typeof rawAttrs === 'function' ||
HTML.isArray(rawAttrs)) {
throw new Error("Can't have reactive children of TEXTAREA node; " +
"use the 'value' attribute instead.");
}
rawAttrs = _.extend({}, rawAttrs || null);
rawAttrs.value = Blaze._expand(children, self.parentView);
children = [];

View File

@@ -312,14 +312,21 @@ Blaze.HTMLJSExpander.def({
}
});
// Return Blaze.currentView, but only if it is being rendered
// (i.e. we are in its render() method).
var currentViewIfRendering = function () {
var view = Blaze.currentView;
return (view && view.isInRender) ? view : null;
};
Blaze._expand = function (htmljs, parentView) {
parentView = parentView || Blaze.currentView;
parentView = parentView || currentViewIfRendering();
return (new Blaze.HTMLJSExpander(
{parentView: parentView})).visit(htmljs);
};
Blaze._expandAttributes = function (attrs, parentView) {
parentView = parentView || Blaze.currentView;
parentView = parentView || currentViewIfRendering();
return (new Blaze.HTMLJSExpander(
{parentView: parentView})).visitAttributes(attrs);
};
@@ -383,7 +390,7 @@ Blaze.runTemplate = function (t/*, args*/) {
};
Blaze.render = function (content, parentView) {
parentView = parentView || Blaze.currentView;
parentView = parentView || currentViewIfRendering();
var view;
if (typeof content === 'function') {
@@ -401,7 +408,7 @@ Blaze.render = function (content, parentView) {
Blaze.toHTML = function (htmljs, parentView) {
if (typeof htmljs === 'function')
throw new Error("Blaze.toHTML doesn't take a function, just HTMLjs");
parentView = parentView || Blaze.currentView;
parentView = parentView || currentViewIfRendering();
return HTML.toHTML(Blaze._expand(htmljs, parentView));
};
@@ -414,7 +421,7 @@ Blaze.toText = function (htmljs, parentView, textMode) {
textMode = parentView;
parentView = null;
}
parentView = parentView || Blaze.currentView;
parentView = parentView || currentViewIfRendering();
if (! textMode)
throw new Error("textMode required");
@@ -529,7 +536,11 @@ Blaze._addEventMap = function (view, eventMap, thisInHandler) {
function (evt) {
if (! range.containsElement(evt.currentTarget))
return null;
return handler.apply(thisInHandler || this, arguments);
var handlerThis = thisInHandler || this;
var handlerArgs = arguments;
return Blaze.withCurrentView(view, function () {
return handler.apply(handlerThis, handlerArgs);
});
},
range, function (r) {
return r.parentRange;

View File

@@ -1456,6 +1456,16 @@ var wrapInternalException = function (exception, context) {
if (!exception || exception instanceof Meteor.Error)
return exception;
// tests can set the 'expected' flag on an exception so it won't go to the
// server log
if (!exception.expected) {
Meteor._debug("Exception " + context, exception.stack);
if (exception.sanitizedError) {
Meteor._debug("Sanitized and reported to the client as:", exception.sanitizedError.message);
Meteor._debug();
}
}
// Did the error contain more details that could have been useful if caught in
// server code (or if thrown from non-client-originated code), but also
// provided a "sanitized" version with more context than 500 Internal server
@@ -1467,11 +1477,6 @@ var wrapInternalException = function (exception, context) {
"is not a Meteor.Error; ignoring");
}
// tests can set the 'expected' flag on an exception so it won't go to the
// server log
if (!exception.expected)
Meteor._debug("Exception " + context, exception.stack);
return new Meteor.Error(500, "Internal server error");
};

View File

@@ -526,16 +526,18 @@ if (Meteor.isClient) {
]);
testAsyncMulti("livedata - publisher errors", (function () {
// Use a separate connection so that we can safely check to see if
// conn._subscriptions is empty.
var conn = new LivedataTest.Connection('/',
{reloadWithOutstanding: true});
var collName = Random.id();
var coll = new Meteor.Collection(collName, {connection: conn});
var conn, collName, coll;
var errorFromRerun;
var gotErrorFromStopper = false;
return [
function (test, expect) {
// Use a separate connection so that we can safely check to see if
// conn._subscriptions is empty.
conn = new LivedataTest.Connection('/',
{reloadWithOutstanding: true});
collName = Random.id();
coll = new Meteor.Collection(collName, {connection: conn});
var testSubError = function (options) {
conn.subscribe("publisherErrors", collName, options, {
onReady: expect(),

View File

@@ -54,7 +54,7 @@ _.extend(LivedataTest.ClientStream.prototype, {
// But _launchConnection calls _cleanup which closes previous connections.
// It's our belief that this stifles future 'open' events, but maybe
// we are wrong?
throw new Error("Got open from inactive client");
throw new Error("Got open from inactive client " + !!self.client);
}
if (self._forcedToDisconnect) {

View File

@@ -14,6 +14,7 @@ Meteor._SynchronousQueue = function () {
var self = this;
self._tasks = [];
self._running = false;
self._runTimeout = null;
};
_.extend(Meteor._SynchronousQueue.prototype, {
@@ -25,6 +26,15 @@ _.extend(Meteor._SynchronousQueue.prototype, {
var tasks = self._tasks;
self._tasks = [];
self._running = true;
if (self._runTimeout) {
// Since we're going to drain the queue, we can forget about the timeout
// which tries to run it. (But if one of our tasks queues something else,
// the timeout will be correctly re-created.)
clearTimeout(self._runTimeout);
self._runTimeout = null;
}
try {
while (!_.isEmpty(tasks)) {
var t = tasks.shift();
@@ -47,12 +57,12 @@ _.extend(Meteor._SynchronousQueue.prototype, {
queueTask: function (task) {
var self = this;
var wasEmpty = _.isEmpty(self._tasks);
self._tasks.push(task);
// Intentionally not using Meteor.setTimeout, because it doesn't like runing
// in stubs for now.
if (wasEmpty)
setTimeout(_.bind(self.flush, self), 0);
if (!self._runTimeout) {
self._runTimeout = setTimeout(_.bind(self.flush, self), 0);
}
},
flush: function () {

View File

@@ -339,6 +339,21 @@ _.extend(LocalCollection.Cursor.prototype, {
query.movedBefore = wrapCallback(options.movedBefore);
}
if (!options._suppress_initial && !self.collection.paused) {
// XXX unify ordered and unordered interface
var each = ordered
? _.bind(_.each, null, query.results)
: _.bind(query.results.forEach, query.results);
each(function (doc) {
var fields = EJSON.clone(doc);
delete fields._id;
if (ordered)
query.addedBefore(doc._id, fields, null);
query.added(doc._id, fields);
});
}
var handle = new LocalCollection.ObserveHandle;
_.extend(handle, {
collection: self.collection,
@@ -358,30 +373,9 @@ _.extend(LocalCollection.Cursor.prototype, {
handle.stop();
});
}
if (!options._suppress_initial && !self.collection.paused) {
// XXX unify ordered and unordered interface
var each = ordered
? _.bind(_.each, null, query.results)
: _.bind(query.results.forEach, query.results);
each(function (doc) {
var fields = EJSON.clone(doc);
delete fields._id;
if (ordered)
query.addedBefore(doc._id, fields, null);
query.added(doc._id, fields);
});
// run the observe callbacks resulting from the initial contents
// before we leave the observe.
if (self.collection._observeQueue.safeToRunTask()) {
self.collection._observeQueue.drain();
} else if (options.added || options.addedBefore) {
// See #2315.
throw Error("observeChanges called from an observe callback on the same collection cannot differentiate between initial and later adds");
}
}
// run the observe callbacks resulting from the initial contents
// before we leave the observe.
self.collection._observeQueue.drain();
return handle;
}

View File

@@ -3109,28 +3109,3 @@ Tinytest.add("minimongo - fetch in observe", function (test) {
observe.stop();
computation.stop();
});
Tinytest.add("minimongo - observe in observe", function (test) {
var coll = new LocalCollection;
coll.insert({foo: 2});
var observe1AddedCalled = false;
var observe1 = coll.find({foo: 1}).observeChanges({
added: function (id, fields) {
observe1AddedCalled = true;
test.equal(fields, {foo: 1});
// It would be even better if this didn't throw; see #2315.
test.throws(function () {
coll.find({foo: 2}).observeChanges({
added: function () {
}
});
});
}
});
test.isFalse(observe1AddedCalled);
coll.insert({foo: 1});
test.isTrue(observe1AddedCalled);
observe1.stop();
});

View File

@@ -159,7 +159,8 @@ LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks)
var oldDoc = self.docs.get(id);
var doc = EJSON.clone(oldDoc);
LocalCollection._applyChanges(doc, fields);
observeCallbacks.changed(transform(doc), transform(oldDoc));
observeCallbacks.changed(transform(doc),
transform(EJSON.clone(oldDoc)));
}
},
removed: function (id) {
@@ -176,32 +177,5 @@ LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks)
var handle = cursor.observeChanges(changeObserver.applyChange);
suppressed = false;
if (changeObserver.ordered) {
// Fetches the current list of documents, in order, as an array. Can be
// called at any time. Internal API assumed by the `observe-sequence`
// package (used by Meteor UI for `#each` blocks). Only defined on ordered
// observes (those that listen on `addedAt` or similar). Continues to work
// after `stop()` is called on the handle.
//
// Because we already materialize the full OrderedDict of all documents, it
// seems nice to provide access to the view rather than making the data
// consumer reconstitute it. This gives the consumer a shot at doing
// something smart with the feed like proxying it, since firing callbacks
// like `changed` and `movedTo` basically requires omniscience (knowing old
// and new documents, old and new indices, and the correct value for
// `before`).
//
// NOTE: If called from an observe callback for a certain change, the result
// is *not* guaranteed to be a snapshot of the cursor up to that
// change. This is because the callbacks are invoked before updating docs.
handle._fetch = function () {
var docsArray = [];
changeObserver.docs.forEach(function (doc) {
docsArray.push(transform(EJSON.clone(doc)));
});
return docsArray;
};
}
return handle;
};

File diff suppressed because it is too large Load Diff

View File

@@ -4,19 +4,28 @@
if (##SET_CREDENTIAL_TOKEN##) {
var credentialToken = ##TOKEN##;
var credentialSecret = ##SECRET##;
try {
localStorage[##LOCAL_STORAGE_PREFIX## + credentialToken] = credentialSecret;
} catch (err) {
// localStorage didn't work; try window.opener.
window.opener &&
window.opener.Package.oauth.OAuth._handleCredentialSecret(
credentialToken, credentialSecret);
// If window.opener isn't set, we can't do much else, but at least
// close the popup instead of having it hang around on a blank page.
if (window.opener && window.opener.Package &&
window.opener.Package.oauth) {
window.opener.Package.oauth.OAuth._handleCredentialSecret(
credentialToken, credentialSecret);
} else {
try {
localStorage[##LOCAL_STORAGE_PREFIX## + credentialToken] = credentialSecret;
} catch (err) {
// We can't do much else, but at least close the popup instead
// of having it hang around on a blank page.
}
}
}
window.close();
</script>
</head>
<body></body>
<body>
<p>
Login completed. <a href="#" onclick="window.close()">
Click here</a> to close this window.
</p>
<img src="not-a-real-image-for-oauth" style="width:1px;height:1px"
onerror="window.close();" />
</body>
</html>

View File

@@ -92,17 +92,16 @@ OAuth._handleCredentialSecret = function (credentialToken, secret) {
// Used by accounts-oauth, which needs both a credentialToken and the
// corresponding to credential secret to call the `login` method over DDP.
OAuth._retrieveCredentialSecret = function (credentialToken) {
// Check localStorage first, then check the secrets collected by
// OAuth._handleCredentialSecret. This matches what we do in
// First check the secrets collected by OAuth._handleCredentialSecret,
// then check localStorage. This matches what we do in
// end_of_login_response.html.
var localStorageKey = OAuth._localStorageTokenPrefix +
credentialToken;
var secret = Meteor._localStorage.getItem(localStorageKey);
if (secret) {
var secret = credentialSecrets[credentialToken];
if (! secret) {
var localStorageKey = OAuth._localStorageTokenPrefix +
credentialToken;
secret = Meteor._localStorage.getItem(localStorageKey);
Meteor._localStorage.removeItem(localStorageKey);
} else {
secret = credentialSecrets[credentialToken];
delete credentialSecrets[credentialToken];
}
return secret;

View File

@@ -11,7 +11,6 @@ var registeredServices = {};
// Internal: Maps from service version to handler function. The
// 'oauth1' and 'oauth2' packages manipulate this directly to register
// for callbacks.
//
OAuth._requestHandlers = {};

View File

@@ -82,11 +82,10 @@ ObserveSequence = {
Deps.nonreactive(function () {
var seqArray; // same structure as `lastSeqArray` above.
// If we were previously observing a cursor, replace lastSeqArray with
// more up-to-date information (specifically, the state of the observe
// before it was stopped, which may be older than the DB).
if (activeObserveHandle) {
lastSeqArray = _.map(activeObserveHandle._fetch(), function (doc) {
// If we were previously observing a cursor, replace lastSeqArray with
// more up-to-date information. Then stop the old observe.
lastSeqArray = _.map(lastSeq.fetch(), function (doc) {
return {_id: doc._id, item: doc};
});
activeObserveHandle.stop();
@@ -94,78 +93,19 @@ ObserveSequence = {
}
if (!seq) {
seqArray = [];
diffArray(lastSeqArray, seqArray, callbacks);
seqArray = seqChangedToEmpty(lastSeqArray, callbacks);
} else if (seq instanceof Array) {
var idsUsed = {};
seqArray = _.map(seq, function (item, index) {
var id;
if (typeof item === 'string') {
// ensure not empty, since other layers (eg DomRange) assume this as well
id = "-" + item;
} else if (typeof item === 'number' ||
typeof item === 'boolean' ||
item === undefined) {
id = item;
} else if (typeof item === 'object') {
id = (item && item._id) || index;
} else {
throw new Error("{{#each}} doesn't support arrays with " +
"elements of type " + typeof item);
}
var idString = idStringify(id);
if (idsUsed[idString]) {
if (typeof item === 'object' && '_id' in item)
warn("duplicate id " + id + " in", seq);
id = Random.id();
} else {
idsUsed[idString] = true;
}
return { _id: id, item: item };
});
diffArray(lastSeqArray, seqArray, callbacks);
seqArray = seqChangedToArray(lastSeqArray, seq, callbacks);
} else if (isStoreCursor(seq)) {
var cursor = seq;
seqArray = [];
var initial = true; // are we observing initial data from cursor?
activeObserveHandle = cursor.observe({
addedAt: function (document, atIndex, before) {
if (initial) {
// keep track of initial data so that we can diff once
// we exit `observe`.
if (before !== null)
throw new Error("Expected initial data from observe in order");
seqArray.push({ _id: document._id, item: document });
} else {
callbacks.addedAt(document._id, document, atIndex, before);
}
},
changedAt: function (newDocument, oldDocument, atIndex) {
callbacks.changedAt(newDocument._id, newDocument, oldDocument,
atIndex);
},
removedAt: function (oldDocument, atIndex) {
callbacks.removedAt(oldDocument._id, oldDocument, atIndex);
},
movedTo: function (document, fromIndex, toIndex, before) {
callbacks.movedTo(
document._id, document, fromIndex, toIndex, before);
}
});
initial = false;
// diff the old sequnce with initial data in the new cursor. this will
// fire `addedAt` callbacks on the initial data.
diffArray(lastSeqArray, seqArray, callbacks);
var result /* [seqArray, activeObserveHandle] */ =
seqChangedToCursor(lastSeqArray, seq, callbacks);
seqArray = result[0];
activeObserveHandle = result[1];
} else {
throw badSequenceError();
}
diffArray(lastSeqArray, seqArray, callbacks);
lastSeq = seq;
lastSeqArray = seqArray;
});
@@ -306,3 +246,73 @@ var diffArray = function (lastSeqArray, seqArray, callbacks) {
}
});
};
seqChangedToEmpty = function (lastSeqArray, callbacks) {
return [];
};
seqChangedToArray = function (lastSeqArray, array, callbacks) {
var idsUsed = {};
var seqArray = _.map(array, function (item, index) {
var id;
if (typeof item === 'string') {
// ensure not empty, since other layers (eg DomRange) assume this as well
id = "-" + item;
} else if (typeof item === 'number' ||
typeof item === 'boolean' ||
item === undefined) {
id = item;
} else if (typeof item === 'object') {
id = (item && item._id) || index;
} else {
throw new Error("{{#each}} doesn't support arrays with " +
"elements of type " + typeof item);
}
var idString = idStringify(id);
if (idsUsed[idString]) {
if (typeof item === 'object' && '_id' in item)
warn("duplicate id " + id + " in", array);
id = Random.id();
} else {
idsUsed[idString] = true;
}
return { _id: id, item: item };
});
return seqArray;
};
seqChangedToCursor = function (lastSeqArray, cursor, callbacks) {
var initial = true; // are we observing initial data from cursor?
var seqArray = [];
var observeHandle = cursor.observe({
addedAt: function (document, atIndex, before) {
if (initial) {
// keep track of initial data so that we can diff once
// we exit `observe`.
if (before !== null)
throw new Error("Expected initial data from observe in order");
seqArray.push({ _id: document._id, item: document });
} else {
callbacks.addedAt(document._id, document, atIndex, before);
}
},
changedAt: function (newDocument, oldDocument, atIndex) {
callbacks.changedAt(newDocument._id, newDocument, oldDocument,
atIndex);
},
removedAt: function (oldDocument, atIndex) {
callbacks.removedAt(oldDocument._id, oldDocument, atIndex);
},
movedTo: function (document, fromIndex, toIndex, before) {
callbacks.movedTo(
document._id, document, fromIndex, toIndex, before);
}
});
initial = false;
return [seqArray, observeHandle];
};

View File

@@ -439,9 +439,8 @@ Tinytest.add('observe-sequence - cursor to same cursor', function (test) {
}, [
{addedAt: ["13", {_id: "13", rank: 1}, 0, null]},
{addedAt: ["24", {_id: "24", rank: 2}, 1, null]},
// even if the cursor changes to the same cursor, we diff to see if we
// missed anything during the invalidation, which leads to these
// 'changedAt' events.
// even if the cursor changes to the same cursor, we do a diff,
// which leads to these 'changedAt' events.
{changedAt: ["13", {_id: "13", rank: 1}, {_id: "13", rank: 1}, 0]},
{changedAt: ["24", {_id: "24", rank: 2}, {_id: "24", rank: 2}, 1]},
{addedAt: ["78", {_id: "78", rank: 3}, 2, null]}

View File

@@ -13,6 +13,7 @@ Package.on_test(function (api) {
api.use('test-helpers');
api.use('showdown');
api.use('minimongo');
api.use('deps');
api.use('templating', 'client');
api.add_files([

View File

@@ -922,3 +922,41 @@ Hi there!
<template name="spacebars_test_isolated_lookup3">
{{> bar}}--{{> spacebars_test_isolated_lookup_inclusion}}
</template>
<template name="spacebars_test_current_view_in_event">
<span>{{.}}</span>
</template>
<template name="spacebars_test_textarea_attrs">
<textarea {{attrs}}></textarea>
</template>
<template name="spacebars_test_textarea_attrs_contents">
<textarea {{attrs}}>Hello {{name}}</textarea>
</template>
<template name="spacebars_test_textarea_attrs_array_contents">
<textarea {{attrs}} class="bar">Hello {{name}}</textarea>
</template>
<template name="spacebars_test_autorun">
{{#if show}}
{{>spacebars_test_autorun_inner}}
{{/if}}
</template>
<template name="spacebars_test_autorun_inner">
Hello
</template>
<template name="spacebars_test_contentBlock_arg">
{{#spacebars_test_contentBlock_arg_inner}}
{{this.bar}}
{{/spacebars_test_contentBlock_arg_inner}}
</template>
<template name="spacebars_test_contentBlock_arg_inner">
{{#with foo="AAA" bar="BBB"}}
{{this.foo}} {{> UI.contentBlock this}}
{{/with}}
</template>

View File

@@ -2604,3 +2604,163 @@ _.each([1, 2, 3], function (n) {
}
);
});
Tinytest.add('spacebars-tests - template_tests - current view in event handler', function (test) {
var tmpl = Template.spacebars_test_current_view_in_event;
var currentView;
var currentData;
tmpl.events({
'click span': function () {
currentView = Blaze.getCurrentView();
currentData = Blaze.getCurrentData();
}
});
var div = renderToDiv(tmpl, 'blah');
test.equal(canonicalizeHtml(div.innerHTML), '<span>blah</span>');
document.body.appendChild(div);
clickElement(div.querySelector('span'));
$(div).remove();
test.isTrue(currentView);
test.equal(currentData, 'blah');
});
Tinytest.add(
"spacebars-tests - template_tests - textarea attrs", function (test) {
var tmplNoContents = {
tmpl: Template.spacebars_test_textarea_attrs,
hasTextAreaContents: false
};
var tmplWithContents = {
tmpl: Template.spacebars_test_textarea_attrs_contents,
hasTextAreaContents: true
};
var tmplWithContentsAndMoreAttrs = {
tmpl: Template.spacebars_test_textarea_attrs_array_contents,
hasTextAreaContents: true
};
_.each(
[tmplNoContents, tmplWithContents,
tmplWithContentsAndMoreAttrs],
function (tmplInfo) {
var id = new ReactiveVar("textarea-" + Random.id());
var name = new ReactiveVar("one");
var attrs = new ReactiveVar({
id: "textarea-" + Random.id()
});
var div = renderToDiv(tmplInfo.tmpl, {
attrs: function () {
return attrs.get();
},
name: function () {
return name.get();
}
});
// Check that the id and value attribute are as we expect.
// We can't check div.innerHTML because Chrome at least doesn't
// appear to put textarea value attributes in innerHTML.
var textarea = div.querySelector("textarea");
test.equal(textarea.id, attrs.get().id);
test.equal(
textarea.value, tmplInfo.hasTextAreaContents ? "Hello one" : "");
// One of the templates has a separate attribute in addition to
// an attributes dictionary.
if (tmplInfo === tmplWithContentsAndMoreAttrs) {
test.equal($(textarea).attr("class"), "bar");
}
// Change the id, check that the attribute updates reactively.
attrs.set({ id: "textarea-" + Random.id() });
Deps.flush();
test.equal(textarea.id, attrs.get().id);
// Change the name variable, check that the textarea value
// updates reactively.
name.set("two");
Deps.flush();
test.equal(
textarea.value, tmplInfo.hasTextAreaContents ? "Hello two" : "");
if (tmplInfo === tmplWithContentsAndMoreAttrs) {
test.equal($(textarea).attr("class"), "bar");
}
});
});
Tinytest.add(
"spacebars-tests - template_tests - this.autorun",
function (test) {
var tmpl = Template.spacebars_test_autorun;
var tmplInner = Template.spacebars_test_autorun_inner;
// Keep track of the value of `UI._templateInstance()` inside the
// autorun each time it runs.
var autorunTemplateInstances = [];
var actualTemplateInstance;
var returnedComputation;
var computationArg;
var show = new ReactiveVar(true);
var rv = new ReactiveVar("foo");
tmplInner.created = function () {
actualTemplateInstance = this;
returnedComputation = this.autorun(function (c) {
computationArg = c;
rv.get();
autorunTemplateInstances.push(UI._templateInstance());
});
};
tmpl.helpers({
show: function () {
return show.get();
}
});
var div = renderToDiv(tmpl);
test.equal(autorunTemplateInstances.length, 1);
test.equal(autorunTemplateInstances[0], actualTemplateInstance);
// Test that the autorun returned a computation and received a
// computation as an argument.
test.isTrue(returnedComputation instanceof Deps.Computation);
test.equal(returnedComputation, computationArg);
// Make sure the autorun re-runs when `rv` changes, and that it has
// the correct current view.
rv.set("bar");
Deps.flush();
test.equal(autorunTemplateInstances.length, 2);
test.equal(autorunTemplateInstances[1], actualTemplateInstance);
// If the inner template is destroyed, the autorun should be stopped.
show.set(false);
Deps.flush();
rv.set("baz");
Deps.flush();
test.equal(autorunTemplateInstances.length, 2);
test.equal(rv.numListeners(), 0);
}
);
// Test that argument in {{> UI.contentBlock arg}} is evaluated in
// the proper data context.
Tinytest.add(
"spacebars-tests - template_tests - contentBlock argument",
function (test) {
var tmpl = Template.spacebars_test_contentBlock_arg;
var div = renderToDiv(tmpl);
test.equal(canonicalizeHtml(div.innerHTML), 'AAA BBB');
});

View File

@@ -17,10 +17,6 @@ render the template. -->
the template to render) and a `data` property, which can be falsey. -->
<template name="__dynamicWithDataContext">
{{#with chooseTemplate template}}
{{#with ../data}} {{! original 'dataContext' argument to __dynamic}}
{{> ..}} {{! return value from chooseTemplate(template) }}
{{else}} {{! if the 'dataContext' argument was falsey }}
{{> .. ../data}} {{! return value from chooseTemplate(template) }}
{{/with}}
{{> .. ../data}} {{!-- The .. is evaluated inside {{#with ../data}} --}}
{{/with}}
</template>

View File

@@ -205,7 +205,32 @@ Spacebars.dot = function (value, id1/*, id2, ...*/) {
};
Spacebars.TemplateWith = function (argFunc, contentBlock) {
var w = Blaze.With(argFunc, contentBlock);
var w;
// This is a little messy. When we compile `{{> UI.contentBlock}}`, we
// wrap it in Blaze.InOuterTemplateScope in order to skip the intermediate
// parent Views in the current template. However, when there's an argument
// (`{{> UI.contentBlock arg}}`), the argument needs to be evaluated
// in the original scope. There's no good order to nest
// Blaze.InOuterTemplateScope and Spacebars.TemplateWith to achieve this,
// so we wrap argFunc to run it in the "original parentView" of the
// Blaze.InOuterTemplateScope.
//
// To make this better, reconsider InOuterTemplateScope as a primitive.
// Longer term, evaluate expressions in the proper lexical scope.
var wrappedArgFunc = function () {
var viewToEvaluateArg = null;
if (w.parentView && w.parentView.kind === 'InOuterTemplateScope') {
viewToEvaluateArg = w.parentView.originalParentView;
}
if (viewToEvaluateArg) {
return Blaze.withCurrentView(viewToEvaluateArg, argFunc);
} else {
return argFunc();
}
};
w = Blaze.With(wrappedArgFunc, contentBlock);
w.__isTemplateWith = true;
return w;
};

View File

@@ -1,22 +1,26 @@
// 'url' is assigned to in a statement before this.
var page = require('webpage').create();
page.open(url);
setInterval(function() {
var ready = page.evaluate(function () {
if (typeof Meteor !== 'undefined'
&& typeof(Meteor.status) !== 'undefined'
&& Meteor.status().connected) {
Deps.flush();
return DDP._allSubscriptionsReady();
}
return false;
});
if (ready) {
var out = page.content;
out = out.replace(/<script[^>]+>(.|\n|\r)*?<\/script\s*>/ig, '');
out = out.replace('<meta name="fragment" content="!">', '');
console.log(out);
page.open(url, function(status) {
if (status === 'fail')
phantom.exit();
}
}, 100);
setInterval(function() {
var ready = page.evaluate(function () {
if (typeof Meteor !== 'undefined'
&& typeof(Meteor.status) !== 'undefined'
&& Meteor.status().connected) {
Deps.flush();
return DDP._allSubscriptionsReady();
}
return false;
});
if (ready) {
var out = page.content;
out = out.replace(/<script[^>]+>(.|\n|\r)*?<\/script\s*>/ig, '');
out = out.replace('<meta name="fragment" content="!">', '');
console.log(out);
phantom.exit();
}
}, 100);
});

View File

@@ -37,6 +37,9 @@ Template.__updateTemplateInstance = function (view) {
data: null,
firstNode: null,
lastNode: null,
autorun: function (f) {
return view.autorun(f);
},
__view__: view
};
}

View File

@@ -2,6 +2,7 @@ canonicalizeHtml = function(html) {
var h = html;
// kill IE-specific comments inserted by DomRange
h = h.replace(/<!--IE-->/g, '');
h = h.replace(/<!---->/g, '');
// ignore exact text of comments
h = h.replace(/<!--.*?-->/g, '<!---->');
// make all tags lowercase

View File

@@ -8,18 +8,43 @@ TEST_STATUS = {
FAILURES: null
};
// xUnit format uses XML output
var XML_CHAR_MAP = {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;',
"'": '&apos;'
};
// Escapes a string for insertion into XML
var escapeXml = function (s) {
return s.replace(/[<>&"']/g, function (c) {
return XML_CHAR_MAP[c];
});
}
// Returns a human name for a test
var getName = function (result) {
return (result.server ? "S: " : "C: ") +
result.groupPath.join(" - ") + " - " + result.test;
};
// Calls console.log, but returns silently if console.log is not available
var log = function (/*arguments*/) {
if (typeof console !== 'undefined') {
console.log.apply(console, arguments);
}
};
// Logs xUnit output, if xunit output is enabled
// Output is sent to console.log, prefixed with a magic string 'XUNIT '
// By grepping for that prefix, the xUnit output can be extracted
var xunit = function (s) {
if (xunitEnabled) {
log('XUNIT ' + s);
}
};
var passed = 0;
var failed = 0;
@@ -31,6 +56,10 @@ var hrefPath = document.location.href.split("/");
var platform = decodeURIComponent(hrefPath.length && hrefPath[hrefPath.length - 1]);
if (!platform)
platform = "local";
// We enable xUnit output when platform is xunit
var xunitEnabled = (platform == 'xunit');
var doReport = Meteor &&
Meteor.settings &&
Meteor.settings.public &&
@@ -82,10 +111,13 @@ Meteor.startup(function () {
status: "PENDING",
events: [],
server: !!results.server,
testPath: testPath
testPath: testPath,
test: results.test
};
report(name, false);
}
// Loop through events, and record status for each test
// Also log result if test has finished
_.each(results.events, function (event) {
resultSet[name].events.push(event);
switch (event.type) {
@@ -136,6 +168,7 @@ Meteor.startup(function () {
});
},
// After test completion, log a quick summary
function () {
if (failed > 0) {
log("~~~~~~~ THERE ARE FAILURES ~~~~~~~");
@@ -153,6 +186,43 @@ Meteor.startup(function () {
TEST_STATUS.DONE = DONE = true;
}
});
// Also log xUnit output
xunit('<testsuite errors="" failures="" name="meteor" skips="" tests="" time="">');
_.each(resultSet, function (result, name) {
var classname = result.testPath.join('.').replace(/ /g, '-') + (result.server ? "-server" : "-client");
var name = result.test.replace(/ /g, '-') + (result.server ? "-server" : "-client");
var time = "";
var error = "";
_.each(result.events, function (event) {
switch (event.type) {
case "finish":
var timeMs = event.timeMs;
if (timeMs !== undefined) {
time = (timeMs / 1000) + "";
}
break;
case "fail":
var details = event.details || {};
error = (details.message || '?') + " filename=" + (details.filename || '?') + " line=" + (details.line || '?');
}
});
switch (event.status) {
case "FAIL":
error = error || '?';
break;
case "EXPECTED":
error = "Expected failure";
break;
}
xunit('<testcase classname="' + escapeXml(classname) + '" name="' + escapeXml(name) + '" time="' + time + '">');
if (error) {
xunit(' <failure message="test failure">' + escapeXml(error) + '</failure>');
}
xunit('</testcase>');
});
xunit('</testsuite>');
},
["tinytest"]);
});

View File

@@ -2,23 +2,40 @@
// the server. Sets a 'server' flag on test results that came from the
// server.
//
Tinytest._runTestsEverywhere = function (onReport, onComplete, pathPrefix) {
// Options:
// serial if true, will not run tests in parallel. Currently this means
// running the server tests before running the client tests.
// Default is currently true (serial operation), but we will likely
// change this to false in future.
Tinytest._runTestsEverywhere = function (onReport, onComplete, pathPrefix, options) {
var runId = Random.id();
var localComplete = false;
var localStarted = false;
var remoteComplete = false;
var done = false;
options = _.extend({
serial: true
}, options);
var serial = !!options.serial;
var maybeDone = function () {
if (!done && localComplete && remoteComplete) {
done = true;
onComplete && onComplete();
}
if (serial && remoteComplete && !localStarted) {
startLocalTests();
}
};
Tinytest._runTests(onReport, function () {
localComplete = true;
maybeDone();
}, pathPrefix);
var startLocalTests = function() {
localStarted = true;
Tinytest._runTests(onReport, function () {
localComplete = true;
maybeDone();
}, pathPrefix);
};
var handle;
@@ -59,4 +76,8 @@ Tinytest._runTestsEverywhere = function (onReport, onComplete, pathPrefix) {
// XXX better report error
throw new Error("Test server returned an error");
});
if (!serial) {
startLocalTests();
}
};

View File

@@ -1,7 +1,5 @@
=> Meteor 0.8.2: Switch `accounts-password` to use bcrypt on the
server. User accounts will seamlessly transition to bcrypt on the
next login, but this transition is one-way, so you cannot downgrade a
production app once you upgrade to 0.8.2.
=> Meteor 0.8.3: Performance improvements and a big refactoring of the
Blaze internals.
This release is being downloaded in the background. Update your
project to Meteor 0.8.2 by running 'meteor update'.
project to Meteor 0.8.3 by running 'meteor update'.

View File

@@ -141,7 +141,7 @@ Couldn't write the launcher script. Please either:
(1) Run the following as root:
cp ~/.meteor/tools/latest/launch-meteor /usr/bin/meteor
(2) Add ~/.meteor to your path, or
(2) Add "$HOME/.meteor" to your path, or
(3) Rerun this command to try again.
Then to get started, take a look at 'meteor --help' or see the docs at
@@ -153,7 +153,7 @@ else
Now you need to do one of the following:
(1) Add ~/.meteor to your path, or
(1) Add "$HOME/.meteor" to your path, or
(2) Run this command as root:
cp ~/.meteor/tools/latest/launch-meteor /usr/bin/meteor

View File

@@ -149,6 +149,9 @@
]
}
},
{
"release": "0.8.3"
},
{
"release": "NEXT"
}

View File

@@ -59,7 +59,15 @@ var configureS3 = function () {
return {accessKey: accessKey, secretKey: secretKey};
};
var s3Credentials = getS3Credentials();
var s3Credentials;
if (process.env.AWS_ACCESS_KEY_ID) {
s3Credentials = {};
s3Credentials.accessKey = process.env.AWS_ACCESS_KEY_ID;
s3Credentials.secretKey = process.env.AWS_SECRET_ACCESS_KEY;
} else {
s3Credentials = getS3Credentials();
}
var s3 = new S3({
accessKeyId: s3Credentials.accessKey,
secretAccessKey: s3Credentials.secretKey,

View File

@@ -1026,6 +1026,7 @@ _.extend(Run.prototype, {
self._ensureStarted();
var timeout = self.baseTimeout + self.extraTime;
timeout *= utils.timeoutScaleFactor;
self.extraTime = 0;
return self.stdoutMatcher.match(pattern, timeout, _strict);
}),
@@ -1036,6 +1037,7 @@ _.extend(Run.prototype, {
self._ensureStarted();
var timeout = self.baseTimeout + self.extraTime;
timeout *= utils.timeoutScaleFactor;
self.extraTime = 0;
return self.stderrMatcher.match(pattern, timeout, _strict);
}),
@@ -1081,6 +1083,7 @@ _.extend(Run.prototype, {
self._ensureStarted();
var timeout = self.baseTimeout + self.extraTime;
timeout *= utils.timeoutScaleFactor;
self.extraTime = 0;
self.expectExit();
@@ -1098,6 +1101,7 @@ _.extend(Run.prototype, {
if (self.exitStatus === undefined) {
var timeout = self.baseTimeout + self.extraTime;
timeout *= utils.timeoutScaleFactor;
self.extraTime = 0;
var fut = new Future;

View File

@@ -3,11 +3,12 @@
</head>
<body>
<h1>Welcome to Meteor!</h1>
{{> hello}}
</body>
<template name="hello">
<h1>Hello World!</h1>
{{greeting}}
<input type="button" value="Click" />
<button>Click Me</button>
<p>You've pressed the button {{counter}} times.</p>
</template>

View File

@@ -1,13 +1,17 @@
if (Meteor.isClient) {
Template.hello.greeting = function () {
return "Welcome to ~name~.";
};
// counter starts at 0
Session.setDefault("counter", 0);
Template.hello.helpers({
counter: function () {
return Session.get("counter");
}
});
Template.hello.events({
'click input': function () {
// template data, if any, is available in 'this'
if (typeof console !== 'undefined')
console.log("You pressed the button");
'click button': function () {
// increment the counter when button is clicked
Session.set("counter", Session.get("counter") + 1);
}
});
}

View File

@@ -2,6 +2,7 @@ var _ = require('underscore');
var release = require('./release.js');
var uniload = require('./uniload.js');
var config = require('./config.js');
var utils = require('./utils.js');
var randomString = function (charsCount) {
var chars = 'abcdefghijklmnopqrstuvwxyz';
@@ -12,7 +13,7 @@ var randomString = function (charsCount) {
return str;
};
exports.accountsCommandTimeoutSecs = 15;
exports.accountsCommandTimeoutSecs = 15 * utils.timeoutScaleFactor;
exports.randomString = randomString;

View File

@@ -357,3 +357,9 @@ exports.ensureOnlyExactVersions = function (dependencies) {
});
};
// Allow a simple way to scale up all timeouts from the command line
var timeoutScaleFactor = 1.0;
if (process.env.TIMEOUT_SCALE_FACTOR) {
timeoutScaleFactor = parseFloat(process.env.TIMEOUT_SCALE_FACTOR);
}
exports.timeoutScaleFactor = timeoutScaleFactor;