diff --git a/.github/workflows/blaze-tests.yml b/.github/workflows/blaze-tests.yml
index 0dd1c3024..c0390b95d 100644
--- a/.github/workflows/blaze-tests.yml
+++ b/.github/workflows/blaze-tests.yml
@@ -79,4 +79,80 @@ jobs:
pkill -TERM -P $(cat /tmp/meteor_test_pid)
fi
shell: bash
- working-directory: test-app
\ No newline at end of file
+ working-directory: test-app
+
+ # this re-runs the app tests with the test-in-console driver
+ # but in this case we include jQuery via --extra-packages=jquery,
+ # which causes the tests to run with jQuery back-compatibility mode.
+ # This should be removed, once we fully dropped jQuery support in Blaze 4.x
+ test-app-jquery:
+ needs: test-app
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ with:
+ submodules: recursive
+
+ - name: Setup Node.js environment
+ uses: actions/setup-node@v6
+ with:
+ node-version: 22
+
+ - name: Install Meteor
+ run: |
+ npx meteor@latest
+ echo "$HOME/.meteor" >> $GITHUB_PATH
+
+ - name: Link packages directory
+ run: ln -sfn ../packages ./packages
+ working-directory: test-app
+
+ - name: Meteor npm install
+ run: meteor npm install
+ working-directory: test-app
+
+ - name: Start meteor test-packages (background)
+ run: |
+ export URL='http://localhost:4096/'
+ meteor test-packages --driver-package test-in-console -p 4096 --extra-packages=jquery --exclude "${TEST_PACKAGES_EXCLUDE:-}" > /tmp/meteor_test_output.log 2>&1 &
+ echo $! > /tmp/meteor_test_pid
+ working-directory: test-app
+
+ # Wait for test-in-console to be ready for up to 6 minutes
+ # in order to accommodate slower CI environments
+ - name: Wait for test-in-console to be ready
+ run: |
+
+ for i in {1..360}; do
+ if grep -q 'test-in-console listening' /tmp/meteor_test_output.log; then
+ echo "test-in-console is ready."
+ break
+ fi
+ echo "Waiting for test-in-console... attempt $i"
+ sleep 1
+ done
+ # Fail if the service didn't start
+ if ! grep -q 'test-in-console listening' /tmp/meteor_test_output.log; then
+ echo "test-in-console did not start in time."
+ cat /tmp/meteor_test_output.log # Print the log for debugging
+ exit 1
+ fi
+ shell: bash
+ working-directory: test-app
+
+ - name: Run puppeteerRunner.js
+ run: meteor node puppeteerRunner.js
+ env:
+ URL: http://localhost:4096/
+ working-directory: test-app
+
+ - name: Kill meteor test-packages process
+ if: always()
+ run: |
+ if [ -f /tmp/meteor_test_pid ]; then
+ pkill -TERM -P $(cat /tmp/meteor_test_pid)
+ fi
+ shell: bash
+ working-directory: test-app
diff --git a/packages/blaze/.versions b/packages/blaze/.versions
index e51307103..3cdab109f 100644
--- a/packages/blaze/.versions
+++ b/packages/blaze/.versions
@@ -29,7 +29,6 @@ html-tools@2.0.0
htmljs@2.0.1
id-map@1.2.0
inter-process-messaging@0.1.2
-jquery@3.0.2
local-test:blaze@3.0.1
logging@1.3.5
meteor@2.0.1
diff --git a/packages/blaze/blaze.d.ts b/packages/blaze/blaze.d.ts
index 9a2896ca6..c27e6d04c 100644
--- a/packages/blaze/blaze.d.ts
+++ b/packages/blaze/blaze.d.ts
@@ -1,5 +1,3 @@
-import * as $ from 'jquery';
-
import { Tracker } from 'meteor/tracker';
import { Meteor } from 'meteor/meteor';
declare module 'meteor/blaze' {
diff --git a/packages/blaze/dombackend.js b/packages/blaze/dombackend.js
index ca2eba9e3..c9218498f 100644
--- a/packages/blaze/dombackend.js
+++ b/packages/blaze/dombackend.js
@@ -1,23 +1,34 @@
const DOMBackend = {};
Blaze._DOMBackend = DOMBackend;
-const $jq = (typeof jQuery !== 'undefined' ? jQuery :
- (typeof Package !== 'undefined' && Package.jquery ?
- (Package.jquery.jQuery || Package.jquery.$) : null));
+let $jq;
+let $jqSource;
-const _hasJQuery = !!$jq;
+if (!$jq && typeof jQuery !== 'undefined') {
+ $jq = jQuery;
+ $jqSource = 'global scope';
+}
+
+if (!$jq && typeof Package !== 'undefined' && Package.jquery) {
+ $jq = Package.jquery.jQuery ?? Package.jquery.$ ?? null;
+ $jqSource = 'Meteor packages';
+}
+const _hasJQuery = !!$jq;
if (_hasJQuery && typeof console !== 'undefined') {
+ const version = $jq.fn?.jquery ?? ' ';
+ console.info(
+ `[Blaze] jQuery ${version} detected as DOM backend. Native DOM backend is available — ` +
+ 'remove jquery to enable native DOM backend. jQuery support will be removed in Blaze 4.0.'
+ );
console.info(
- '[Blaze] jQuery detected as DOM backend. Native DOM backend is available — ' +
- 'remove the jquery package to enable it. jQuery support will be removed in Blaze 4.0.'
+ `[Blaze] jQuery was loaded via ${$jqSource}`
);
}
DOMBackend._$jq = $jq; // null when absent
DOMBackend._hasJQuery = _hasJQuery;
-
DOMBackend.getContext = function () {
if (DOMBackend._context) return DOMBackend._context;
// jQuery may need the legacy check; native path always supports createHTMLDocument
@@ -37,7 +48,7 @@ DOMBackend.parseHTML = function (html) {
if (_hasJQuery) {
return $jq.parseHTML(html, DOMBackend.getContext()) || [];
}
- const template = document.createElement('template');
+ const template = DOMBackend.getContext().createElement('template');
template.innerHTML = html;
return Array.from(template.content.childNodes);
};
@@ -63,19 +74,7 @@ DOMBackend.Events = {
// Alias non-bubbling events to their bubbling equivalents
eventType = _delegateEventAlias[eventType] || eventType;
- const wrapper = (event) => {
- // event.target can be a text node (nodeType 3) — walk to parent element first
- const origin = event.target;
- const target = origin.nodeType === 1 ? origin.closest(selector) : origin.parentElement?.closest(selector);
- if (target && elem.contains(target)) {
- // Mimic jQuery's delegated event behavior
- Object.defineProperty(event, 'currentTarget', {
- value: target,
- configurable: true,
- });
- handler.call(target, event);
- }
- };
+ const wrapper = createWrapper(elem, type, selector, handler);
if (!_delegateMap.has(elem)) {
_delegateMap.set(elem, new Map());
@@ -123,20 +122,7 @@ DOMBackend.Events = {
handler._meteorui_wrapper = wrapper;
} else {
- const wrapper = (event) => {
- // event.target can be a text node — walk to parent element first
- const origin = event.target;
- const matched = origin.nodeType === 1 ? origin.closest(selector) : origin.parentElement?.closest(selector);
- if (matched && elem.contains(matched)) {
- Object.defineProperty(event, 'currentTarget', {
- value: matched,
- configurable: true,
- });
- handler.call(elem, event);
- }
- };
-
- handler._meteorui_wrapper = wrapper;
+ handler._meteorui_wrapper = createWrapper(elem, type, selector, handler);
}
type = DOMBackend.Events.parseEventType(type);
@@ -158,6 +144,46 @@ DOMBackend.Events = {
}
};
+const createWrapper = (elem, type, selector, handler) => {
+ return (event) => {
+ // event.target can be a text node (nodeType 3) — walk to parent element first
+ const origin = event.target;
+ const target = origin.nodeType === 1 ? origin.closest(selector) : origin.parentElement?.closest(selector);
+
+ // we need to manually check, if a selector left the template scope
+ // which jQuery would do automatically for us.
+ // for this we traverse nodes that still match the selector
+ // and compare the final to see, if the event bubbled up to
+ // a parent view that is out of scope
+ let node = origin;
+ while (node && node !== elem && node instanceof Element && node.matches(selector)) {
+ node = node.parentElement;
+ }
+
+ const root = elem?.['$blaze_range']?.view?.name;
+ const scope = node?.['$blaze_range']?.view?.name;
+
+ let inScope = true;
+ if (root && scope && root === scope) {
+ inScope = false;
+ }
+
+ if (target && elem.contains(target) && inScope) {
+ // Mimic jQuery's delegated event behavior
+ Object.defineProperty(event, 'currentTarget', {
+ value: target,
+ configurable: true,
+ });
+ // mimic jQuery event return false behavior
+ const value = handler.call(target, event);
+ if (value === false) {
+ event.preventDefault();
+ event.stopPropagation();
+ event.stopImmediatePropagation();
+ }
+ }
+ };
+}
///// Removal detection and interoperability.
@@ -274,6 +300,17 @@ if (_hasJQuery) {
_executeTeardownCallbacks(this);
}
};
+} else {
+ // in native DOM Backend we need to extend the native remove function
+ // to call the TearDown callbacks, registered during materializing
+ // for the element and its full descendant subtree.
+ (function(removeFn) {
+ HTMLElement.prototype.remove = function () {
+ DOMBackend.Teardown.tearDownElement(this);
+ return removeFn.apply(this, arguments);
+ };
+ })(HTMLElement.prototype.remove);
+
}
diff --git a/packages/blaze/materializer.js b/packages/blaze/materializer.js
index 092de816b..3d1ec4463 100644
--- a/packages/blaze/materializer.js
+++ b/packages/blaze/materializer.js
@@ -97,7 +97,7 @@ const isPromiseLike = x => !!x && typeof x.then === 'function';
function then(maybePromise, fn) {
if (isPromiseLike(maybePromise)) {
- maybePromise.then(fn, Blaze._reportException);
+ maybePromise.then(fn, Blaze._reportException)
} else {
fn(maybePromise);
}
diff --git a/packages/blaze/package.js b/packages/blaze/package.js
index 50f85c7ea..fb92a6545 100644
--- a/packages/blaze/package.js
+++ b/packages/blaze/package.js
@@ -6,14 +6,13 @@ Package.describe({
});
Package.onUse(function (api) {
- api.use('jquery@1.11.9 || 3.0.0', { weak: true }); // should be a weak dep, by having multiple "DOM backends"
api.use('tracker@1.3.2');
api.use('check@1.0.12');
api.use('observe-sequence@2.0.0');
api.use('reactive-var@1.0.12');
api.use('ordered-dict@1.2.0');
api.use('ecmascript@0.16.9');
-
+ api.use('jquery@3.0.0', 'client', { weak: true });
api.export([
'Blaze',
'UI',
@@ -53,8 +52,6 @@ Package.onTest(function (api) {
api.use('ecmascript@0.16.9');
api.use('tinytest');
api.use('test-helpers');
- api.use('jquery@1.11.9 || 3.0.0'); // strong dependency, for testing jQuery backend
-
api.use('reactive-var@1.0.12');
api.use('tracker@1.3.2');
diff --git a/packages/blaze/render_tests.js b/packages/blaze/render_tests.js
index 67df25b72..9e907ba9c 100644
--- a/packages/blaze/render_tests.js
+++ b/packages/blaze/render_tests.js
@@ -1,7 +1,7 @@
import { BlazeTools } from 'meteor/blaze-tools';
const toCode = BlazeTools.toJS;
-
+const hasJquery = Blaze._DOMBackend._hasJQuery;
const P = HTML.P;
const CharRef = HTML.CharRef;
const DIV = HTML.DIV;
@@ -267,8 +267,8 @@ Tinytest.add("blaze - render - view GC", function (test) {
(function () {
const R = ReactiveVar('Hello');
const test1 = P(Blaze.View(function () { return R.get(); }));
-
const div = document.createElement("DIV");
+
materialize(test1, div);
test.equal(canonicalizeHtml(div.innerHTML), "
Hello
");
@@ -278,7 +278,7 @@ Tinytest.add("blaze - render - view GC", function (test) {
test.equal(R._numListeners(), 1);
- $(div).remove();
+ if (hasJquery) { $(div).remove() } else { div.remove() }
test.equal(R._numListeners(), 0);
@@ -325,7 +325,7 @@ Tinytest.add("blaze - render - reactive attributes", function (test) {
test.equal(canonicalizeHtml(div.innerHTML), '');
test.equal(R._numListeners(), 1);
- $(div).remove();
+ if (hasJquery) { $(div).remove() } else { div.remove() }
test.equal(R._numListeners(), 0);
})();
@@ -354,7 +354,7 @@ Tinytest.add("blaze - render - reactive attributes", function (test) {
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), '');
- $(div).remove();
+ if (hasJquery) { $(div).remove() } else { div.remove() }
test.equal(style._numListeners(), 0);
})();
@@ -393,7 +393,7 @@ Tinytest.add("blaze - render - reactive attributes", function (test) {
test.equal(canonicalizeHtml(div.innerHTML), '');
test.equal(R._numListeners(), 1);
- $(div).remove();
+ if (hasJquery) { $(div).remove() } else { div.remove() }
test.equal(R._numListeners(), 0);
})();
@@ -482,7 +482,7 @@ Tinytest.add("blaze - render - reactive attributes", function (test) {
Tracker.flush();
test.equal(canonicalizeHtml(div.innerHTML), '');
- $(div).remove();
+ if (hasJquery) { $(div).remove() } else { div.remove() }
test.equal(R._numListeners(), 0);
})();
@@ -574,7 +574,7 @@ Tinytest.add("blaze - render - templates and views", function (test) {
test.equal(canonicalizeHtml(div.innerHTML), '123
');
buf.length = 0;
- $(div).remove();
+ if (hasJquery) { $(div).remove() } else { div.remove() }
buf.sort();
test.equal(buf, ['destroyed 1', 'destroyed 2', 'destroyed 3']);
@@ -617,10 +617,12 @@ Tinytest.add("blaze - render - findAll", function (test) {
Blaze.render(myTemplate, div);
Tracker.flush();
- test.equal(Array.isArray(found), true);
- test.equal(Array.isArray($found), false);
test.equal(found.length, 2);
test.equal($found.length, 2);
+ const foundIsArray = Array.isArray(found);
+ const $foundIsArray = Array.isArray($found);
+ test.equal(foundIsArray, true);
+ test.equal($foundIsArray, !hasJquery);
});
Tinytest.add("blaze - render - reactive attributes 2", function (test) {
@@ -670,7 +672,7 @@ Tinytest.add("blaze - render - reactive attributes 2", function (test) {
// clean up
- $(div).remove();
+ if (hasJquery) { $(div).remove() } else { div.remove() }
test.equal(R1._numListeners(), 0);
test.equal(R2._numListeners(), 0);
@@ -778,7 +780,7 @@ if (typeof MutationObserver !== 'undefined') {
// We do not update anything after initial rendering, so only one mutation is there.
test.equal(observedMutations.length, 1);
- $(div).remove();
+ if (hasJquery) { $(div).remove() } else { div.remove() }
observer.disconnect();
onComplete();
diff --git a/packages/observe-sequence/observe_sequence_tests.js b/packages/observe-sequence/observe_sequence_tests.js
index c40ffbe92..01c471922 100644
--- a/packages/observe-sequence/observe_sequence_tests.js
+++ b/packages/observe-sequence/observe_sequence_tests.js
@@ -145,23 +145,23 @@ function runInVM(code) {
return res;
}
-Tinytest.add('observe-sequence - initial data for all sequence types', function (test) {
- runOneObserveSequenceTestCase(test, function () {
+Tinytest.addAsync('observe-sequence - initial data for all sequence types', async function (test) {
+ await runOneObserveSequenceTestCase(test, function () {
return null;
}, function () {}, []);
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
return [];
}, function () {}, []);
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
return [{foo: 1}, {bar: 2}];
}, function () {}, [
{addedAt: [0, {foo: 1}, 0, null]},
{addedAt: [1, {bar: 2}, 1, null]}
]);
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
return [{_id: "13", foo: 1}, {_id: "37", bar: 2}];
}, function () {}, [
{addedAt: ["13", {_id: "13", foo: 1}, 0, null]},
@@ -169,7 +169,7 @@ Tinytest.add('observe-sequence - initial data for all sequence types', function
]);
// sub-classed arrays
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
return new ArraySubclass({_id: "13", foo: 1}, {_id: "37", bar: 2});
}, function () {}, [
{addedAt: ["13", {_id: "13", foo: 1}, 0, null]},
@@ -177,14 +177,14 @@ Tinytest.add('observe-sequence - initial data for all sequence types', function
]);
// Execute in VM
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
return new runInVM('new Array({_id: "13", foo: 1}, {_id: "37", bar: 2})');
}, function () {}, [
{addedAt: ["13", {_id: "13", foo: 1}, 0, null]},
{addedAt: ["37", {_id: "37", bar: 2}, 1, null]}
]);
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
const coll = new Mongo.Collection(null);
coll.insert({_id: "13", foo: 1});
coll.insert({_id: "37", bar: 2});
@@ -197,7 +197,7 @@ Tinytest.add('observe-sequence - initial data for all sequence types', function
// shouldn't break on array with duplicate _id's, and the ids sent
// in the callbacks should be distinct
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
return [
{_id: "13", foo: 1},
{_id: "13", foo: 2}
@@ -208,12 +208,12 @@ Tinytest.add('observe-sequence - initial data for all sequence types', function
], /*numExpectedWarnings = */1);
// non-array iterable (empty)
- if(typeof Map == 'function') runOneObserveSequenceTestCase(test, function () {
+ if(typeof Map == 'function') await runOneObserveSequenceTestCase(test, function () {
return new Map();
}, function () {}, []);
// non-array iterable (non-empty)
- if(typeof Set == 'function') runOneObserveSequenceTestCase(test, function () {
+ if(typeof Set == 'function') await runOneObserveSequenceTestCase(test, function () {
return new Set([{foo: 1}, {bar: 2}]);
}, function () {}, [
{addedAt: [0, {foo: 1}, 0, null]},
@@ -221,11 +221,11 @@ Tinytest.add('observe-sequence - initial data for all sequence types', function
]);
});
-Tinytest.add('observe-sequence - array to other array', function (test) {
+Tinytest.addAsync('observe-sequence - array to other array', async function (test) {
const dep = new Tracker.Dependency;
let seq = [{_id: "13", foo: 1}, {_id: "37", bar: 2}];
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -240,11 +240,11 @@ Tinytest.add('observe-sequence - array to other array', function (test) {
]);
});
-Tinytest.add('observe-sequence - array to other array, strings', function (test) {
+Tinytest.addAsync('observe-sequence - array to other array, strings', async function (test) {
const dep = new Tracker.Dependency;
let seq = ["A", "B"];
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -258,8 +258,8 @@ Tinytest.add('observe-sequence - array to other array, strings', function (test)
]);
});
-Tinytest.add('observe-sequence - bug #7850 array with null values', function (test) {
- runOneObserveSequenceTestCase(test, function () {
+Tinytest.addAsync('observe-sequence - bug #7850 array with null values', async function (test) {
+ await runOneObserveSequenceTestCase(test, function () {
return [1, null];
}, function () {}, [
{addedAt: [1, 1, 0, null]},
@@ -267,11 +267,11 @@ Tinytest.add('observe-sequence - bug #7850 array with null values', function (te
]);
});
-Tinytest.add('observe-sequence - array to other array, objects without ids', function (test) {
+Tinytest.addAsync('observe-sequence - array to other array, objects without ids', async function (test) {
const dep = new Tracker.Dependency;
let seq = [{foo: 1}, {bar: 2}];
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -285,11 +285,11 @@ Tinytest.add('observe-sequence - array to other array, objects without ids', fun
]);
});
-Tinytest.add('observe-sequence - array to other array, changes', function (test) {
+Tinytest.addAsync('observe-sequence - array to other array, changes', async function (test) {
const dep = new Tracker.Dependency;
let seq = [{_id: "13", foo: 1}, {_id: "37", bar: 2}, {_id: "42", baz: 42}];
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -308,11 +308,11 @@ Tinytest.add('observe-sequence - array to other array, changes', function (test)
]);
});
-Tinytest.add('observe-sequence - array to other array, movedTo', function (test) {
+Tinytest.addAsync('observe-sequence - array to other array, movedTo', async function (test) {
const dep = new Tracker.Dependency;
let seq = [{_id: "13", foo: 1}, {_id: "37", bar: 2}, {_id: "42", baz: 42}, {_id: "43", baz: 43}];
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -334,11 +334,11 @@ Tinytest.add('observe-sequence - array to other array, movedTo', function (test)
]);
});
-Tinytest.add('observe-sequence - array to other array, movedTo the end', function (test) {
+Tinytest.addAsync('observe-sequence - array to other array, movedTo the end', async function (test) {
const dep = new Tracker.Dependency;
let seq = [{_id: "0"}, {_id: "1"}, {_id: "2"}, {_id: "3"}];
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -358,11 +358,11 @@ Tinytest.add('observe-sequence - array to other array, movedTo the end', functio
]);
});
-Tinytest.add('observe-sequence - array to other array, movedTo later position but not the latest #2845', function (test) {
+Tinytest.addAsync('observe-sequence - array to other array, movedTo later position but not the latest #2845', async function (test) {
const dep = new Tracker.Dependency;
let seq = [{_id: "0"}, {_id: "1"}, {_id: "2"}, {_id: "3"}];
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -383,11 +383,11 @@ Tinytest.add('observe-sequence - array to other array, movedTo later position bu
]);
});
-Tinytest.add('observe-sequence - array to other array, movedTo earlier position but not the first', function (test) {
+Tinytest.addAsync('observe-sequence - array to other array, movedTo earlier position but not the first', async function (test) {
const dep = new Tracker.Dependency;
let seq = [{_id: "0"}, {_id: "1"}, {_id: "2"}, {_id: "3"}, {_id: "4"}];
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -410,11 +410,11 @@ Tinytest.add('observe-sequence - array to other array, movedTo earlier position
]);
});
-Tinytest.add('observe-sequence - array to null', function (test) {
+Tinytest.addAsync('observe-sequence - array to null', async function (test) {
const dep = new Tracker.Dependency;
let seq = [{_id: "13", foo: 1}, {_id: "37", bar: 2}];
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -428,11 +428,11 @@ Tinytest.add('observe-sequence - array to null', function (test) {
]);
});
-Tinytest.add('observe-sequence - array to cursor', function (test) {
+Tinytest.addAsync('observe-sequence - array to cursor', async function (test) {
const dep = new Tracker.Dependency;
let seq = [{_id: "13", foo: 1}, {_id: "37", bar: 2}];
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -452,7 +452,7 @@ Tinytest.add('observe-sequence - array to cursor', function (test) {
});
-Tinytest.add('observe-sequence - cursor to null', function (test) {
+Tinytest.addAsync('observe-sequence - cursor to null', async function (test) {
const dep = new Tracker.Dependency;
const coll = new Mongo.Collection(null);
coll.insert({_id: "13", foo: 1});
@@ -460,7 +460,7 @@ Tinytest.add('observe-sequence - cursor to null', function (test) {
const cursor = coll.find({}, {sort: {_id: 1}});
let seq = cursor;
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -474,14 +474,14 @@ Tinytest.add('observe-sequence - cursor to null', function (test) {
]);
});
-Tinytest.add('observe-sequence - cursor to array', function (test) {
+Tinytest.addAsync('observe-sequence - cursor to array', async function (test) {
const dep = new Tracker.Dependency;
const coll = new Mongo.Collection(null);
coll.insert({_id: "13.5", foo: 1});
const cursor = coll.find({}, {sort: {_id: 1}});
let seq = cursor;
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -497,13 +497,13 @@ Tinytest.add('observe-sequence - cursor to array', function (test) {
]);
});
-Tinytest.add('observe-sequence - cursor', function (test) {
+Tinytest.addAsync('observe-sequence - cursor', async function (test) {
const coll = new Mongo.Collection(null);
coll.insert({_id: "13", rank: 1});
const cursor = coll.find({}, {sort: {rank: 1}});
const seq = cursor;
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
return seq;
}, async function () {
await coll.insertAsync({_id: "37", rank: 2});
@@ -527,14 +527,14 @@ Tinytest.add('observe-sequence - cursor', function (test) {
]);
});
-Tinytest.add('observe-sequence - cursor to other cursor', function (test) {
+Tinytest.addAsync('observe-sequence - cursor to other cursor', async function (test) {
const dep = new Tracker.Dependency;
const coll = new Mongo.Collection(null);
coll.insert({_id: "13", foo: 1});
const cursor = coll.find({}, {sort: {_id: 1}});
let seq = cursor;
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -555,7 +555,7 @@ Tinytest.add('observe-sequence - cursor to other cursor', function (test) {
]);
});
-Tinytest.add('observe-sequence - cursor to other cursor with transform', function (test) {
+Tinytest.addAsync('observe-sequence - cursor to other cursor with transform', async function (test) {
const dep = new Tracker.Dependency;
const transform = function(doc) {
return Object.assign({idCopy: doc._id}, doc);
@@ -566,7 +566,7 @@ Tinytest.add('observe-sequence - cursor to other cursor with transform', functio
const cursor = coll.find({}, {sort: {_id: 1}});
let seq = cursor;
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -587,14 +587,14 @@ Tinytest.add('observe-sequence - cursor to other cursor with transform', functio
]);
});
-Tinytest.add('observe-sequence - cursor to same cursor', function (test) {
+Tinytest.addAsync('observe-sequence - cursor to same cursor', async function (test) {
const coll = new Mongo.Collection(null);
coll.insert({_id: "13", rank: 1});
const cursor = coll.find({}, {sort: {rank: 1}});
const seq = cursor;
const dep = new Tracker.Dependency;
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -613,11 +613,11 @@ Tinytest.add('observe-sequence - cursor to same cursor', function (test) {
]);
});
-Tinytest.add('observe-sequence - string arrays', function (test) {
+Tinytest.addAsync('observe-sequence - string arrays', async function (test) {
let seq = ['A', 'B'];
const dep = new Tracker.Dependency;
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -631,11 +631,11 @@ Tinytest.add('observe-sequence - string arrays', function (test) {
]);
});
-Tinytest.add('observe-sequence - number arrays', function (test) {
+Tinytest.addAsync('observe-sequence - number arrays', async function (test) {
let seq = [1, 1, 2];
const dep = new Tracker.Dependency;
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -651,11 +651,11 @@ Tinytest.add('observe-sequence - number arrays', function (test) {
]);
});
-Tinytest.add('observe-sequence - subclassed number arrays', function (test) {
+Tinytest.addAsync('observe-sequence - subclassed number arrays', async function (test) {
let seq = new ArraySubclass(1, 1, 2);
const dep = new Tracker.Dependency;
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -671,11 +671,11 @@ Tinytest.add('observe-sequence - subclassed number arrays', function (test) {
]);
});
-Tinytest.add('observe-sequence - vm generated number arrays', function (test) {
+Tinytest.addAsync('observe-sequence - vm generated number arrays', async function (test) {
let seq = runInVM('new Array(1, 1, 2)');
const dep = new Tracker.Dependency;
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -691,11 +691,11 @@ Tinytest.add('observe-sequence - vm generated number arrays', function (test) {
]);
});
-Tinytest.add('observe-sequence - number arrays, _id:0 correctly handled, no duplicate ids warning #4049', function (test) {
+Tinytest.addAsync('observe-sequence - number arrays, _id:0 correctly handled, no duplicate ids warning #4049', async function (test) {
let seq = [...Array(3).keys()].map(function (i) { return { _id: i}; });
const dep = new Tracker.Dependency;
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
@@ -716,7 +716,7 @@ Tinytest.add('observe-sequence - number arrays, _id:0 correctly handled, no dupl
]);
});
-Tinytest.add('observe-sequence - cursor to other cursor, same collection', function (test) {
+Tinytest.addAsync('observe-sequence - cursor to other cursor, same collection', async function (test) {
const dep = new Tracker.Dependency;
const coll = new Mongo.Collection(null);
coll.insert({_id: "13", foo: 1});
@@ -724,7 +724,7 @@ Tinytest.add('observe-sequence - cursor to other cursor, same collection', funct
const cursor = coll.find({foo: 1});
let seq = cursor;
- runOneObserveSequenceTestCase(test, function () {
+ await runOneObserveSequenceTestCase(test, function () {
dep.depend();
return seq;
}, function () {
diff --git a/packages/spacebars-tests/.versions b/packages/spacebars-tests/.versions
index 1d782853c..b311664f4 100644
--- a/packages/spacebars-tests/.versions
+++ b/packages/spacebars-tests/.versions
@@ -30,7 +30,6 @@ html-tools@2.0.0
htmljs@2.0.1
id-map@1.2.0
inter-process-messaging@0.1.2
-jquery@3.0.2
local-test:spacebars-tests@2.0.1
logging@1.3.5
markdown@2.0.0
diff --git a/packages/spacebars-tests/old_templates_tests.js b/packages/spacebars-tests/old_templates_tests.js
index e2d6406de..bfea0a6dd 100644
--- a/packages/spacebars-tests/old_templates_tests.js
+++ b/packages/spacebars-tests/old_templates_tests.js
@@ -2,7 +2,7 @@
// This file is used to ensure old built templates still work with the
// new Blaze APIs. More in a comment at the top of old_templates.js
//
-
+const hasJquery = Blaze._DOMBackend._hasJQuery;
const divRendersTo = function (test, div, html) {
Tracker.flush({ _throwFirstError: true });
const actual = canonicalizeHtml(div.innerHTML);
@@ -107,7 +107,7 @@ Tinytest.add(
};
const div = renderToDiv(tmpl);
- test.equal($(div).find('div')[0].className, 'aaa124zzz');
+ test.equal(div.querySelector('div').className, 'aaa124zzz');
}
);
@@ -126,7 +126,9 @@ Tinytest.add(
};
const div = renderToDiv(tmpl);
- const span = $(div).find('span')[0];
+ const span = hasJquery
+ ? $(div).find('span')[0]
+ : div.querySelector('span');
test.equal(span.innerHTML, 'hi');
test.isTrue(span.hasAttribute('selected'));
test.equal(span.getAttribute('x'), 'X');
@@ -153,7 +155,9 @@ Tinytest.add(
};
let div = renderToDiv(tmpl);
- let elems = $(div).find('> *');
+ let elems = hasJquery
+ ? $(div).find('> *')
+ : div.querySelectorAll(':scope > *');
test.equal(elems.length, 1);
test.equal(elems[0].nodeName, 'SPAN');
let span = elems[0];
@@ -162,13 +166,17 @@ Tinytest.add(
R.set('asdf');
Tracker.flush();
- elems = $(div).find('> *');
+ elems = hasJquery
+ ? $(div).find('> *')
+ : div.querySelectorAll(':scope > *');
test.equal(elems.length, 0);
test.equal(canonicalizeHtml(div.innerHTML), 'asdf');
R.set('blah');
Tracker.flush();
- elems = $(div).find('> *');
+ elems = hasJquery
+ ? $(div).find('> *')
+ : div.querySelectorAll(':scope > *');
test.equal(elems.length, 1);
test.equal(elems[0].nodeName, 'SPAN');
span = elems[0];
@@ -575,7 +583,7 @@ Tinytest.add(
}
);
-Tinytest.add(
+Tinytest.addAsync(
'spacebars-tests - old - template_tests - each on cursor',
async function (test) {
const tmpl = Template.old_spacebars_template_test_each;
@@ -680,7 +688,7 @@ Tinytest.add('spacebars-tests - old - template_tests - ..', function (test) {
);
});
-Tinytest.add(
+Tinytest.addAsync(
'spacebars-tests - old - template_tests - select tags',
async function (test) {
const tmpl = Template.old_spacebars_template_test_select_tag;
@@ -702,7 +710,9 @@ Tinytest.add(
};
const div = renderToDiv(tmpl);
- const selectEl = $(div).find('select')[0];
+ const selectEl = hasJquery
+ ? $(div).find('select')[0]
+ : div.querySelector('select');
// returns canonicalized contents of `div` in the form eg
// [""]. strip out selected attributes -- we
@@ -751,8 +761,8 @@ Tinytest.add(
'',
]);
test.equal(selectEl.value, 'value2');
- test.equal($(selectEl).find('option')[0].selected, false);
- test.equal($(selectEl).find('option')[1].selected, true);
+ test.equal(selectEl.querySelectorAll('option')[0].selected, false);
+ test.equal(selectEl.querySelectorAll('option')[1].selected, true);
// swap selection
await options.updateAsync({ value: 'value1' }, { $set: { selected: true } });
@@ -770,8 +780,8 @@ Tinytest.add(
'',
]);
test.equal(selectEl.value, 'value1');
- test.equal($(selectEl).find('option')[0].selected, true);
- test.equal($(selectEl).find('option')[1].selected, false);
+ test.equal(selectEl.querySelectorAll('option')[0].selected, true);
+ test.equal(selectEl.querySelectorAll('option')[1].selected, false);
// change value and label
await options.updateAsync({ value: 'value1' }, { $set: { value: 'value1.0' } });
@@ -789,8 +799,8 @@ Tinytest.add(
'',
]);
test.equal(selectEl.value, 'value1.0');
- test.equal($(selectEl).find('option')[0].selected, true);
- test.equal($(selectEl).find('option')[1].selected, false);
+ test.equal(selectEl.querySelectorAll('option')[0].selected, true);
+ test.equal(selectEl.querySelectorAll('option')[1].selected, false);
// unselect and then select both options. normally, the second is
// selected (since it got selected later). then switch to ',
]);
test.equal(selectEl.value, 'value2');
- test.equal($(selectEl).find('option')[0].selected, false);
- test.equal($(selectEl).find('option')[1].selected, true);
+ test.equal(selectEl.querySelectorAll('option')[0].selected, false);
+ test.equal(selectEl.querySelectorAll('option')[1].selected, true);
// swap selection
await options.updateAsync({ value: 'value2' }, { $set: { selected: false } });
@@ -885,8 +886,8 @@ Tinytest.add('spacebars-tests - template_tests - select tags', async function (t
'',
]);
test.equal(selectEl.value, 'value1');
- test.equal($(selectEl).find('option')[0].selected, true);
- test.equal($(selectEl).find('option')[1].selected, false);
+ test.equal(selectEl.querySelectorAll('option')[0].selected, true);
+ test.equal(selectEl.querySelectorAll('option')[1].selected, false);
// change value and label
await options.updateAsync({ value: 'value1' }, { $set: { value: 'value1.0' } });
@@ -904,8 +905,8 @@ Tinytest.add('spacebars-tests - template_tests - select tags', async function (t
'',
]);
test.equal(selectEl.value, 'value1.0');
- test.equal($(selectEl).find('option')[0].selected, true);
- test.equal($(selectEl).find('option')[1].selected, false);
+ test.equal(selectEl.querySelectorAll('option')[0].selected, true);
+ test.equal(selectEl.querySelectorAll('option')[1].selected, false);
// unselect and then select both options. normally, the second is
// selected (since it got selected later). then switch to | Foo |
');
});
-Tinytest.add(
- 'spacebars-tests - template_tests - jQuery.trigger extraParameters are passed to the event callback',
- function (test) {
- const tmpl = Template.spacebars_test_jquery_events;
- let captured = false;
- const args = ['param1', 'param2', { option: 1 }, 1, 2, 3];
-
- tmpl.events({
- someCustomEvent: function (...args1) {
- let i;
- for (i = 0; i < args.length; i++) {
- // expect the arguments to be just after template
- test.equal(args1[i + 2], args[i]);
- }
- captured = true;
- },
- });
+if (hasJquery){
+ Tinytest.add(
+ 'spacebars-tests - template_tests - jQuery.trigger extraParameters are passed to the event callback',
+ function (test) {
+ const tmpl = Template.spacebars_test_jquery_events;
+ let captured = false;
+ const args = ['param1', 'param2', { option: 1 }, 1, 2, 3];
+
+ tmpl.events({
+ someCustomEvent: function (...args1) {
+ let i;
+ for (i = 0; i < args.length; i++) {
+ // expect the arguments to be just after template
+ test.equal(args1[i + 2], args[i]);
+ }
+ captured = true;
+ },
+ });
- tmpl.rendered = function () {
- $(this.find('button')).trigger('someCustomEvent', args);
- };
+ tmpl.rendered = function () {
+ $(this.find('button')).trigger('someCustomEvent', args);
+ };
- renderToDiv(tmpl);
- Tracker.flush();
- test.equal(captured, true);
- }
-);
+ renderToDiv(tmpl);
+ Tracker.flush();
+ test.equal(captured, true);
+ }
+ );
+}
Tinytest.add('spacebars-tests - template_tests - toHTML', function (test) {
// run once, verifying that autoruns are stopped
@@ -2828,7 +2831,7 @@ Tinytest.add(
);
// https://github.com/meteor/meteor/issues/2156
-Tinytest.add(
+Tinytest.addAsync(
'spacebars-tests - template_tests - each with inserts inside autorun',
async function (test) {
const tmpl = Template.spacebars_test_each_with_autorun_insert;
@@ -3039,7 +3042,11 @@ Tinytest.add(
test.equal(helperCalled, true);
helperCalled = false;
- $(div).find('.test-with-cleanup').remove();
+ if (hasJquery) {
+ $(div).find('.test-with-cleanup').remove();
+ } else {
+ div.querySelector('.test-with-cleanup').remove();
+ }
rv.set('second');
Tracker.flush();
@@ -3107,8 +3114,7 @@ Tinytest.add('spacebars - SVG elements', function (test) {
const tmpl = Template.spacebars_test_svg_anchor;
const div = renderToDiv(tmpl);
-
- const anchNamespace = $(div).find('a').get(0).namespaceURI;
+ const anchNamespace = div.querySelector('a').namespaceURI;
test.equal(anchNamespace, 'http://www.w3.org/2000/svg');
});
@@ -3152,7 +3158,7 @@ Tinytest.add(
divRendersTo(test, div, 'C
');
test.equal(buf, 'CaRaDaCbRbDbCcRc');
- $(div).remove();
+ if (hasJquery) { $(div).remove() } else { div.remove() }
test.equal(buf, 'CaRaDaCbRbDbCcRcDc');
}
);
@@ -3225,18 +3231,20 @@ Tinytest.add(
}
);
-Tinytest.add(
- 'spacebars-tests - template_tests - Blaze.render fails on jQuery objects',
- function (test) {
- const tmpl = Template.spacebars_test_ui_render;
- test.throws(function () {
- Blaze.render(tmpl, $('body'));
- }, /'parentElement' must be a DOM node/);
- test.throws(function () {
- Blaze.render(tmpl, document.body, $('body'));
- }, /'nextNode' must be a DOM node/);
- }
-);
+if (hasJquery) {
+ Tinytest.add(
+ 'spacebars-tests - template_tests - Blaze.render fails on jQuery objects',
+ function (test) {
+ const tmpl = Template.spacebars_test_ui_render;
+ test.throws(function () {
+ Blaze.render(tmpl, $('body'));
+ }, /'parentElement' must be a DOM node/);
+ test.throws(function () {
+ Blaze.render(tmpl, document.body, $('body'));
+ }, /'nextNode' must be a DOM node/);
+ }
+ );
+}
Tinytest.add(
'spacebars-tests - template_tests - UI.getElementData',
@@ -3309,11 +3317,21 @@ Tinytest.add(
// Now see that removing the DOM with jQuery, below
// the level of the entire template, stops everything.
- $(div.querySelector('.toremove')).remove();
+ if (hasJquery) {
+ $(div.querySelector('.toremove')).remove();
+ } else {
+ div.querySelector('.toremove').remove();
+ }
assertCallsAndListeners(0, 0, 0, 0);
}
);
+const trigger = ({ el, eventType, bubbles = true, options }) => {
+ const event = new Event(eventType, { bubbles, cancelable: true });
+ if (options) Object.assign(event, options);
+ el.dispatchEvent(event);
+};
+
Tinytest.add(
'spacebars-tests - template_tests - focus/blur with clean-up',
function (test) {
@@ -3360,11 +3378,21 @@ Tinytest.add(
'You might need to defocus the Chrome Dev Tools to get a more accurate run of this test!',
});
borken = true;
- $(input).trigger('focus');
+ if (hasJquery) {
+ $(input).trigger('focus');
+ } else {
+ trigger({ el: input, eventType: 'focusin',bubbles: true });
+ }
}
test.equal(buf.join(), 'FOCUS');
blurElement(div.querySelector('input'));
- if (buf.length === 1) $(input).trigger('blur');
+ if (buf.length === 1) {
+ if (hasJquery) {
+ $(input).trigger('blur');
+ } else {
+ trigger({ el: input, eventType: 'focusout', bubbles: true });
+ }
+ }
test.equal(buf.join(), 'FOCUS,BLUR');
// now switch the IF and check again. The failure mode
@@ -3379,7 +3407,13 @@ Tinytest.add(
Tracker.flush();
test.equal(div.querySelectorAll('input').length, 1);
focusElement((input = div.querySelector('input')));
- if (borken) $(input).trigger('focus');
+ if (borken) {
+ if (hasJquery) {
+ $(input).trigger('focus');
+ } else {
+ trigger({ el: input, eventType: 'focusin', bubbles: true });
+ }
+ }
test.equal(buf.join(), 'FOCUS');
blurElement(div.querySelector('input'));
if (!borken) test.equal(buf.join(), 'FOCUS,BLUR');
@@ -3388,6 +3422,102 @@ Tinytest.add(
}
);
+// this is an explicit additional test for manual event
+// dispatch of focus/blur, in case the previous test did not
+// branch into these cases
+Tinytest.add(
+ 'spacebars-tests - template_tests - manual focus/blur with clean-up',
+ function (test) {
+ const tmpl = Template.spacebars_test_focus_blur_outer;
+ const cond = ReactiveVar(true);
+ tmpl.helpers({
+ cond: function () {
+ return cond.get();
+ },
+ });
+ const buf = [];
+ Template.spacebars_test_focus_blur_inner.events({
+ 'focus input': function () {
+ buf.push('FOCUS');
+ },
+ 'blur input': function () {
+ buf.push('BLUR');
+ },
+ });
+
+ const div = renderToDiv(tmpl);
+ document.body.appendChild(div);
+
+ // check basic focus and blur to make sure
+ // everything is sane
+ test.equal(div.querySelectorAll('input').length, 1);
+ let input = div.querySelector('input');
+ focusElement(input);
+ // We don't get focus events when the Chrome Dev Tools are focused,
+ // unfortunately, as of Chrome 35. I think this is a regression in
+ // Chrome 34. So, the goal is to work whether or not focus is
+ // "borken," where "working" means always failing if DOMBackend isn't
+ // correctly unbinding the old event handlers when we switch the IF,
+ // and always passing if it is. To cause the problem in DOMBackend,
+ // delete the '**' argument to jQuery#off in
+ // DOMBackend.Events.undelegateEvents. The only compromise we are
+ // making here is that if some unrelated bug in Blaze makes
+ // focus/blur not work, the failure might be masked while the Dev
+ // Tools are open.
+ let borken = false;
+ if (buf.length === 0 && document.activeElement === input) {
+ test.ok({
+ note:
+ 'You might need to defocus the Chrome Dev Tools to get a more accurate run of this test!',
+ });
+ borken = true;
+ if (hasJquery) {
+ $(input).trigger('focus');
+ } else {
+ trigger({ el: input, eventType: 'focusin', bubbles: true });
+ }
+ }
+ test.equal(buf.join(), 'FOCUS');
+ input = div.querySelector('input')
+ blurElement(input);
+ if (buf.length === 1) {
+ if (hasJquery) {
+ $(input).trigger('blur');
+ } else {
+ trigger({ el: input, eventType: 'focusout', bubbles: true });
+ }
+ }
+ test.equal(buf.join(), 'FOCUS,BLUR');
+
+ // now switch the IF and check again. The failure mode
+ // we observed was that DOMBackend would not correctly
+ // unbind the old event listener at the jQuery level,
+ // so the old event listener would fire and cause an
+ // exception inside Blaze ("Must be attached" in
+ // DOMRange#containsElement), which would show up in
+ // the console and cause our handler not to fire.
+ cond.set(false);
+ buf.length = 0;
+ Tracker.flush();
+ test.equal(div.querySelectorAll('input').length, 1);
+ input = div.querySelector('input')
+ focusElement(input);
+ if (borken) {
+ if (hasJquery) {
+ $(input).trigger('focus');
+ } else {
+ trigger({ el: input, eventType: 'focusin', bubbles: true });
+ }
+ }
+ test.equal(buf.join(), 'FOCUS');
+ input = div.querySelector('input')
+ blurElement(input);
+ if (!borken) test.equal(buf.join(), 'FOCUS,BLUR');
+
+ document.body.removeChild(div);
+ }
+);
+
// We used to remove event handlers on DOMRange detached, but when
// tearing down a view, we don't "detach" all the DOMRanges recursively.
// Mainly, we destroy the View. Destroying a View should remove its
@@ -3478,7 +3608,7 @@ Tinytest.add(
test.equal(canonicalizeHtml(div.innerHTML), 'blah');
document.body.appendChild(div);
clickElement(div.querySelector('span'));
- $(div).remove();
+ if (hasJquery) { $(div).remove() } else { div.remove() }
test.isTrue(currentView);
test.equal(currentData, 'blah');
@@ -3557,7 +3687,7 @@ Tinytest.add(
// One of the templates has a separate attribute in addition to
// an attributes dictionary.
if (tmplInfo === tmplWithContentsAndMoreAttrs) {
- test.equal($(textarea).attr('class'), 'bar');
+ test.equal(textarea.getAttribute('class'), 'bar');
}
// Change the id, check that the attribute updates reactively.
@@ -3575,7 +3705,7 @@ Tinytest.add(
);
if (tmplInfo === tmplWithContentsAndMoreAttrs) {
- test.equal($(textarea).attr('class'), 'bar');
+ test.equal(textarea.getAttribute('class'), 'bar');
}
}
);
@@ -4131,7 +4261,7 @@ Tinytest.add(
}
);
-Tinytest.add(
+Tinytest.addAsync(
'spacebars-tests - template_tests - #each @index',
async function (test) {
const tmpl = Template.spacebars_template_test_each_index;
diff --git a/packages/spacebars-tests/templating_tests.js b/packages/spacebars-tests/templating_tests.js
index 6e1c2a335..75282a74f 100644
--- a/packages/spacebars-tests/templating_tests.js
+++ b/packages/spacebars-tests/templating_tests.js
@@ -1,4 +1,4 @@
-
+const hasJquery = Blaze._DOMBackend._hasJQuery;
// for events to bubble an element needs to be in the DOM.
// @return {Function} call this for cleanup
const addToBody = function (el) {
@@ -188,16 +188,22 @@ if (document.addEventListener) {
video2Played = 0;
};
- simulateEvent($(containerDiv).find(".video1").get(0),
- "play", {}, {bubbles: false});
+ let video1 = hasJquery
+ ? $(containerDiv).find(".video1").get(0)
+ : containerDiv.querySelector(".video1");
+ simulateEvent(video1, "play", {}, {bubbles: false});
checkAndResetEvents(1, 0);
- simulateEvent($(containerDiv).find(".video2").get(0),
- "play", {}, {bubbles: false});
+ let video2 = hasJquery
+ ? $(containerDiv).find(".video2").get(0)
+ : containerDiv.querySelector(".video2");
+ simulateEvent(video2, "play", {}, {bubbles: false});
checkAndResetEvents(0, 1);
- simulateEvent($(containerDiv).find(".video2").get(1),
- "play", {}, {bubbles: false});
+ video2 = hasJquery
+ ? $(containerDiv).find(".video2").get(1)
+ : containerDiv.querySelectorAll(".video2")[1];
+ simulateEvent(video2, "play", {}, {bubbles: false});
checkAndResetEvents(0, 1);
// clean up DOM
@@ -410,7 +416,10 @@ Tinytest.add("spacebars-tests - templating_tests - rendered template", function(
});
let div = renderToDiv(Template.test_render_a, {x: 123});
- test.equal($(div).text().match(/\S+/)[0], "124");
+ let expectedText = hasJquery
+ ? $(div).text().match(/\S+/)[0]
+ : div.textContent.match(/\S+/)[0];
+ test.equal(expectedText, "124");
let br1 = div.getElementsByTagName('br')[0];
let hr1 = div.getElementsByTagName('hr')[0];
@@ -438,7 +447,10 @@ Tinytest.add("spacebars-tests - templating_tests - rendered template", function(
}});
div = renderToDiv(Template.test_render_b, {x: 123});
- test.equal($(div).text().match(/\S+/)[0], "201");
+ expectedText = hasJquery
+ ? $(div).text().match(/\S+/)[0]
+ : div.textContent.match(/\S+/)[0];
+ test.equal(expectedText, "201");
br1 = div.getElementsByTagName('br')[0];
hr1 = div.getElementsByTagName('hr')[0];
@@ -498,9 +510,15 @@ Tinytest.add("spacebars-tests - templating_tests - template arg", function (test
const div = renderToDiv(Template.test_template_arg_a, {food: "pie"});
const cleanupDiv = addToBody(div);
Tracker.flush(); // cause `rendered` to be called
- test.equal($(div).text(), "Greetings 1-bold Line");
+ let text = hasJquery
+ ? $(div).text()
+ : div.textContent;
+ test.equal(text, "Greetings 1-bold Line");
clickElement(div.querySelector('i'));
- test.equal($(div).text(), "Hello 3-element World (the secret is strawberry pie)");
+ text = hasJquery
+ ? $(div).text()
+ : div.textContent;
+ test.equal(text, "Hello 3-element World (the secret is strawberry pie)");
cleanupDiv();
Tracker.flush();
@@ -516,7 +534,10 @@ Tinytest.add("spacebars-tests - templating_tests - helpers", function (test) {
tmpl.helpers({foo: 'a', baz: function() { return 'c'; }});
let div = renderToDiv(tmpl);
- test.equal($(div).text().match(/\S+/)[0], 'abc');
+ let text = hasJquery
+ ? $(div).text().match(/\S+/)[0]
+ : div.textContent.match(/\S+/)[0];
+ test.equal(text, 'abc');
Tracker.flush();
tmpl = Template.test_template_helpers_b;
@@ -530,7 +551,9 @@ Tinytest.add("spacebars-tests - templating_tests - helpers", function (test) {
});
div = renderToDiv(tmpl);
- let txt = $(div).text();
+ let txt = hasJquery
+ ? $(div).text()
+ : div.textContent;
txt = txt.replace('[object Object]', 'X'); // IE 8
txt = txt.match(/\S+/)[0];
test.isTrue(txt.match(/^AB[CX]4D$/));
@@ -542,7 +565,10 @@ Tinytest.add("spacebars-tests - templating_tests - helpers", function (test) {
// test that helpers don't "leak"
tmpl = Template.test_template_helpers_c;
div = renderToDiv(tmpl);
- test.equal($(div).text(), 'x');
+ text = hasJquery
+ ? $(div).text()
+ : div.textContent;
+ test.equal(text, 'x');
Tracker.flush();
});
@@ -558,7 +584,10 @@ Tinytest.add("spacebars-tests - templating_tests - events", function (test) {
let div = renderToDiv(tmpl);
let cleanupDiv = addToBody(div);
- clickElement($(div).find('b')[0]);
+ let el = hasJquery
+ ? $(div).find('b')[0]
+ : div.querySelector('b');
+ clickElement(el);
test.equal(buf, ['b']);
cleanupDiv();
Tracker.flush();
@@ -577,8 +606,14 @@ Tinytest.add("spacebars-tests - templating_tests - events", function (test) {
div = renderToDiv(tmpl);
cleanupDiv = addToBody(div);
- clickElement($(div).find('u')[0]);
- clickElement($(div).find('i')[0]);
+ el = hasJquery
+ ? $(div).find('u')[0]
+ : div.querySelector('u');
+ clickElement(el);
+ el = hasJquery
+ ? $(div).find('i')[0]
+ : div.querySelector('i');
+ clickElement(el);
test.equal(buf, ['u', 'i']);
cleanupDiv();
Tracker.flush();
@@ -595,7 +630,10 @@ Tinytest.add("spacebars-tests - templating_tests - events", function (test) {
div = renderToDiv(tmpl);
cleanupDiv = addToBody(div);
- clickElement($(div).find('u')[0]);
+ el = hasJquery
+ ? $(div).find('u')[0]
+ : div.querySelector('u');
+ clickElement(el);
test.equal(buf.length, 2);
test.isTrue(buf.includes('a'));
test.isTrue(buf.includes('b'));
diff --git a/packages/test-in-browser/.gitignore b/packages/test-in-browser/.gitignore
new file mode 100644
index 000000000..a7a45c088
--- /dev/null
+++ b/packages/test-in-browser/.gitignore
@@ -0,0 +1 @@
+.build*
\ No newline at end of file
diff --git a/packages/test-in-browser/.npm/package/.gitignore b/packages/test-in-browser/.npm/package/.gitignore
new file mode 100644
index 000000000..3c3629e64
--- /dev/null
+++ b/packages/test-in-browser/.npm/package/.gitignore
@@ -0,0 +1 @@
+node_modules
diff --git a/packages/test-in-browser/.npm/package/README b/packages/test-in-browser/.npm/package/README
new file mode 100644
index 000000000..3d492553a
--- /dev/null
+++ b/packages/test-in-browser/.npm/package/README
@@ -0,0 +1,7 @@
+This directory and the files immediately inside it are automatically generated
+when you change this package's NPM dependencies. Commit the files in this
+directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
+so that others run the same versions of sub-dependencies.
+
+You should NOT check in the node_modules directory that Meteor automatically
+creates; if you are using git, the .gitignore file tells git to ignore it.
diff --git a/packages/test-in-browser/.npm/package/npm-shrinkwrap.json b/packages/test-in-browser/.npm/package/npm-shrinkwrap.json
new file mode 100644
index 000000000..cc5e89b5b
--- /dev/null
+++ b/packages/test-in-browser/.npm/package/npm-shrinkwrap.json
@@ -0,0 +1,20 @@
+{
+ "lockfileVersion": 4,
+ "dependencies": {
+ "@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
+ },
+ "bootstrap": {
+ "version": "5.3.8",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
+ "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg=="
+ },
+ "diff": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
+ "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="
+ }
+ }
+}
diff --git a/packages/test-in-browser/README.md b/packages/test-in-browser/README.md
new file mode 100644
index 000000000..596144712
--- /dev/null
+++ b/packages/test-in-browser/README.md
@@ -0,0 +1,5 @@
+# test-in-browser
+[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/test-in-browser) | [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/test-in-browser)
+***
+
+> Attention! This package is a stub package and should be removed, once the original package has jquery removed.
\ No newline at end of file
diff --git a/packages/test-in-browser/driver.css b/packages/test-in-browser/driver.css
new file mode 100644
index 000000000..a5fc00347
--- /dev/null
+++ b/packages/test-in-browser/driver.css
@@ -0,0 +1,281 @@
+
+* {
+ /* Variables */
+ /* for reference: https://tailwindcss.com/docs/customizing-colors */
+
+ --bg-black: #18181b;
+ --neutral-black: #343333;
+ --primary-white: #F9FAFB;
+ --red-50: #fef2f2;
+ --red-200: #fecaca;
+ --red-400: #ea5555;
+ --red-600: #dc2626;
+ --red-900: #7f1d1d;
+ --gray-300: #d6d3d1;
+ --gray-400: #a09d9a;
+ --gray-500: #737373;
+
+ --blue-gray-400: #5f6373;
+
+
+ --blue-500: #6366f1;
+ --blue-50: #eef2ff;
+
+ --yellow-400: #facc15;
+
+ --green-600: #16a34a;
+}
+
+body {
+ height: 100vh;
+ width: 100vw;
+ background-color: #18181b !important;
+ background-color: var(--bg-black) !important;
+ color: #F9FAFB !important;
+ color: var(--primary-white) !important;
+
+}
+
+.test-in-browser {
+ display: flex;
+ height: 100%;
+ flex-direction: column;
+}
+
+.test-results {
+ flex: 1;
+ overflow: auto;
+}
+
+#testProgressBar {
+ flex: 1;
+ max-width: 400px;
+ position: relative;
+}
+
+.header {
+ font-family: Arial, sans-serif;
+ font-size: 24px;
+ padding-bottom: 4px;
+}
+
+.header.in-progress {
+ color: #fef2f2;
+ color: var(--red-50);
+}
+
+.header.pass {
+ color: #16a34a;
+ color: var(--green-600); /* green */
+}
+
+.header.fail {
+ color: var(--red-400);
+ color: #ea5555;
+ font-weight: bold;
+}
+
+.header .time {
+ color: #737373;
+ font-size: 14px;
+}
+
+.test_table {
+ font-family: Arial, sans-serif;
+ font-size: 16px;
+}
+
+.test_table .group {
+ border-left: 1px solid #313131;
+ border-left: 1px solid var(--neutral-black);
+}
+
+.test_table .group .group {
+ margin-left: 20px;
+}
+
+.test_table .test {
+ margin-left: 20px;
+}
+
+.test_table .testname {
+ margin-left: 200px;
+ line-height: 24px;
+ vertical-align: middle;
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.test_table .groupname {
+ font-weight: bold;
+ background: var(--neutral-black);
+ background: #313131;
+ padding-left: 5px;
+ line-height: 24px;
+ vertical-align: middle;
+ font-size: 16px;
+ color: var(--primary-white);
+ color: #F9FAFB;
+}
+
+.test_table .event {
+ margin-left: 20px;
+ font-size: 14px;
+ border-left: 2px solid var(--blue-gray-400);
+ border-left: 2px solid #5f6373;
+ padding: 4px;
+ position: relative;
+}
+
+.test_table .test .testrow {
+ position: relative;
+ overflow: hidden; /*hasLayout*/
+}
+
+.test_table .running .testname {
+ color: var(--blue-500);
+ color: #6366f1;
+}
+
+.test_table .failed .testname {
+ color: var(--red-400);
+ color: #ea5555;
+}
+
+.test_table .succeeded .testname {
+ color: var(--green-600);
+ color: #16a34a;
+}
+
+.test_table .teststatus {
+ position: absolute;
+ height: 100%;
+ width: 100px;
+ left: 0px;
+ top: 0;
+ text-align: left;
+ line-height: 24px;
+ vertical-align: middle;
+}
+
+.test_table .testtime {
+ position: absolute;
+ height: 100%;
+ width: 75px;
+ margin-right: 5px;
+ left: 100px;
+ top: 0;
+ text-align: right;
+ line-height: 24px;
+ vertical-align: middle;
+ font-size: 14px;
+ color: var(--gray-500);
+ color: #737373;
+}
+
+.test_table .succeeded .teststatus {
+ color: var(--green-600);
+ background: var(--bg-black);
+ color: #16a34a;
+ background: #18181b;
+}
+
+.test_table .failed .teststatus {
+ color: var(--red-400); /* red */
+ background: var(--bg-black);
+ color: #ea5555; /* red */
+ background: #18181b;
+}
+
+.test_table .running .teststatus {
+ color: var(--red-50);
+ color: #fef2f2;
+}
+
+.test_table .event .expected_fail {
+ color: var(--red-400);
+ color: #ea5555;
+}
+
+.test_table .event .fail {
+ color: var(--red-400);
+ color: #ea5555;
+}
+
+.test_table .event .exception {
+ color: var(--red-400);
+ color: #ea5555;
+}
+
+.exception pre {
+ color: var(--primary-white);
+ color: #F9FAFB;
+}
+
+.test_table .event .nodata {
+ color: var(--red-50);
+ color: #fef2f2;
+ font-style: italic;
+}
+
+.test_table .event .xtimes, .test_table .event .failkey {
+ font-weight: bold;
+}
+
+.test_table .event .debug {
+ display: none;
+}
+
+.test_table .event:hover .debug {
+ display: inline;
+ color: var(--gray-400);
+ color: #a09d9a;
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.in-progress {
+ position: absolute;
+ left: 10px;
+ z-index: 2;
+}
+
+.string_equal {
+ line-height: 1.2;
+ margin-left: 30px;
+}
+
+.string_equal ins {
+ text-decoration: none;
+}
+
+.string_equal_expected ins {
+ background: #f59e0b;
+}
+
+.string_equal_actual ins {
+ color: var(--red-600);
+ background: var(--red-200);
+ color: #dc2626;
+ background: #fecaca;
+}
+
+#current-client-test {
+ color: var(--gray-300);
+ color: #d6d3d1;
+ margin-right: 15px;
+}
+
+.failedTests {
+ color: var(--red-400);
+ color: #ea5555;
+}
+
+#testProgressBar {
+ background: --var(--blue-gray-400);
+ background: #5f6373;
+}
+
+.progress-bar.bg-warning {
+ background-color: #f59e0b !important;
+}
\ No newline at end of file
diff --git a/packages/test-in-browser/driver.html b/packages/test-in-browser/driver.html
new file mode 100644
index 000000000..116024b80
--- /dev/null
+++ b/packages/test-in-browser/driver.html
@@ -0,0 +1,180 @@
+
+
+
+ {{> navBar}}
+
+ {{> uncaughtErrors}}
+ {{> failedTests}}
+ {{> testTable}}
+
+ {{> groupNav}}
+
+
+
+
+
+
+
+
+
+
Passed {{passedCount}} of {{totalCount}}
+
+
+
+
+
+
+
+
+
+
+ {{#if uncaughtErrors}}
+
+
+
+ WARNING: The following uncaught errors might be
+ preventing some client tests from running.
+
+
+ {{#each uncaughtErrors}}
+ - {{this}}
+ {{/each}}
+
+
+
+ {{/if}}
+
+
+
+ {{#if failedTests}}
+
+
+ {{#each failedTests}}
+ - {{this}}
+ {{/each}}
+
+
+ {{/if}}
+
+
+
+
+
+ {{#each testdata}}
+ {{> test_group thisWithDep}}
+ {{/each}}
+
+
+
+
+
+
+
+ {{#each tests}}
+ {{> test thisWithDep}}
+ {{/each}}
+ {{#each groups}}
+ {{> test_group thisWithDep}}
+ {{/each}}
+
+
+
+
+
+
+
+ {{test_status_display}}
+
+
+ {{test_time_display}}
+
+
+ {{#if server}}S:{{else}}C:{{/if}}
+ {{name}}
+
+
+ {{#if expanded}}
+ {{#each eventsArray}}
+ {{> event}}
+ {{else}}
+
+ {{/each}}
+ {{/if}}
+
+
+
+
+
+
+
+ - {{type}}
+ {{#if times}}
+ ({{times}} times)
+ {{/if}}
+ {{#with get_details}}
+ {{#if this}}
+ {{!
+ `type` can be any of the following or a
+ custom assertion type
+
+ * assert_equal
+ * instanceOf
+ * throws
+ * true
+ * null
+ * undefined
+ * NaN
+ * include
+ * length
+ }}
+ {{#if type}}— {{type}}{{/if}}
+ {{#each details}}
+ - {{key}} {{val}}
+ {{/each}}
+ {{/if}}
+ {{#if stack}}{{stack}}{{/if}}
+ {{/with}}
+ {{#if is_debuggable}}
+ [Debug]
+ {{/if}}
+
+
+
+
\ No newline at end of file
diff --git a/packages/test-in-browser/driver.js b/packages/test-in-browser/driver.js
new file mode 100644
index 000000000..e05e8c667
--- /dev/null
+++ b/packages/test-in-browser/driver.js
@@ -0,0 +1,710 @@
+////
+//// Setup
+////
+import { diffChars } from 'diff'
+import 'bootstrap/dist/css/bootstrap.min.css';
+
+const arraysEqual = (a, b) => {
+ return a.length === b.length && a.every((v, i) => v === b[i]);
+}
+
+// dependency for the count of tests running/passed/failed, etc. drives
+// the navbar and the like.
+var countDep = new Tracker.Dependency;
+// things that change on countDep
+var running = true;
+var totalCount = 0;
+var passedCount = 0;
+var failedCount = 0;
+var failedTests = [];
+
+// Dependency for when a new top level group is added. Each group and
+// each test have their own dependency objects.
+var topLevelGroupsDep = new Tracker.Dependency;
+
+// An array of top-level groups.
+//
+// Each group is an object with:
+// - name: string
+// - path: array of strings (names of parent groups)
+// - parent: parent group object (back reference)
+// - dep: Tracker.Dependency object for this group. fires when new tests added.
+// - groups: list of sub-groups
+// - tests: list of tests in this group
+//
+// Each test is an object with:
+// - name: string
+// - parent: parent group object (back reference)
+// - server: boolean
+// - fullName: string
+// - dep: Tracker.Dependency object for this test. fires when the test completes.
+var resultTree = [];
+
+Session.set("uncaughtErrors", []);
+window.onerror = (message, source, line) => {
+ const uncaughtErrors = new Set(Session.get("uncaughtErrors"));
+ uncaughtErrors.add(message);
+ Session.set("uncaughtErrors", Array.from(uncaughtErrors));
+};
+
+var getGroupPathFromURL = function() {
+ var pathname = window.location.pathname;
+ var match = pathname.match(/^\/group\/(.+)$/);
+ if (match) {
+ try {
+ return JSON.parse(decodeURIComponent(match[1]));
+ } catch (e) {
+ console.warn('Invalid group path in URL:', match[1]);
+ }
+ }
+ return ["tinytest"];
+};
+
+var setGroupPathInURL = function(groupPath, pushState = true) {
+ var newURL = '/';
+
+ if (!arraysEqual(groupPath, ['tinytest'])) {
+ newURL = '/group/' + encodeURIComponent(JSON.stringify(groupPath));
+ }
+
+ var historyState = { groupPath: groupPath };
+
+ if (pushState) {
+ window.history.pushState(historyState, '', newURL);
+ } else {
+ window.history.replaceState(historyState, '', newURL);
+ }
+};
+
+// Initialize group path from URL, fallback to default
+var initialGroupPath = getGroupPathFromURL();
+Session.setDefault("groupPath", initialGroupPath);
+Session.set("rerunScheduled", false);
+
+// Safeguards for rapid navigation
+var isNavigating = false;
+var lastNavigationTime = 0;
+var navigationDebounceMs = 200; // Minimum time between navigations
+
+// Handle browser back/forward navigation
+window.addEventListener('popstate', function(event) {
+ var now = Date.now();
+
+ // Safeguard 1: Prevent overlapping navigations
+ if (isNavigating) {
+ console.log('Navigation already in progress, ignoring');
+ return;
+ }
+
+ // Safeguard 2: Debounce rapid successive calls
+ if (now - lastNavigationTime < navigationDebounceMs) {
+ console.log('Navigation too rapid, ignoring');
+ return;
+ }
+
+ var newGroupPath = getGroupPathFromURL();
+ var currentGroupPath = Session.get("groupPath");
+
+ if (!arraysEqual(newGroupPath, currentGroupPath)) {
+ // Set navigation flag
+ isNavigating = true;
+ lastNavigationTime = now;
+
+ // Emulate the EXACT same sequence as changeToPath
+
+ // 1. URL is already changed by browser, but ensure it's correct
+ setGroupPathInURL(newGroupPath, false); // replaceState, don't create new entry
+
+ // 2. Update session state (SAME as changeToPath)
+ Session.set("groupPath", newGroupPath);
+ Session.set("rerunScheduled", true);
+
+ // 3. Clean reload (SAME as changeToPath)
+ Reload._reload();
+ }
+});
+
+// This function is exported. It's called on client startup by the
+// bundle generated by `meteor test` or `meteor test-packages`.
+runTests = function () {
+ // Reset navigation safeguards when app starts
+ isNavigating = false;
+
+ // Reset all test state before starting
+ running = true;
+ totalCount = 0;
+ passedCount = 0;
+ failedCount = 0;
+ failedTests = [];
+ resultTree = [];
+
+ // Reset dependencies to trigger UI updates
+ countDep.changed();
+ topLevelGroupsDep.changed();
+
+ // Get current group path from URL (in case of refresh)
+ var currentGroupPath = getGroupPathFromURL();
+ Session.set("groupPath", currentGroupPath);
+
+ // Only update URL if it's actually different from what we expect
+ var expectedURL;
+ if (currentGroupPath && currentGroupPath.length > 0 && JSON.stringify(currentGroupPath) !== JSON.stringify(["tinytest"])) {
+ expectedURL = '/group/' + encodeURIComponent(JSON.stringify(currentGroupPath));
+ } else {
+ expectedURL = '/';
+ }
+
+ // Only do replaceState if the URL doesn't match what we expect
+ if (window.location.pathname !== expectedURL) {
+ setGroupPathInURL(currentGroupPath, false);
+ }
+
+ document.body.innerHTML = "";
+ document.head.title = "Tests";
+
+ Blaze.render(Template.testInBrowserBody, document.body);
+
+ Tracker.flush();
+ Tinytest._runTestsEverywhere(reportResults, function () {
+ running = false;
+ Meteor.onTestsComplete && Meteor.onTestsComplete();
+ countDep.changed();
+ Tracker.flush();
+
+ Meteor.connection._unsubscribeAll();
+ }, currentGroupPath);
+
+};
+
+
+////
+//// Take incoming results and drive resultsTree
+////
+
+// report a series of events in a single test, or just the existence of
+// that test if no events. this is the entry point for test results to
+// this module.
+var reportResults = function(results) {
+ var test = _findTestForResults(results);
+
+ // Tolerate repeated reports: first undo the effect of any previous report
+ var status = _testStatus(test);
+ if (status === "failed") {
+ failedCount--;
+ countDep.changed();
+ } else if (status === "succeeded") {
+ passedCount--;
+ countDep.changed();
+ }
+
+ // Now process the current report
+ if (Array.isArray(results.events)) {
+ // append events, if present
+ Array.prototype.push.apply((test.events || (test.events = [])),
+ results.events);
+ // sort and de-duplicate, based on sequence number
+ test.events.sort(function (a, b) {
+ return a.sequence - b.sequence;
+ });
+ var out = [];
+ test.events.forEach(function (e) {
+ if (out.length === 0 || out[out.length - 1].sequence !== e.sequence)
+ out.push(e);
+ });
+ test.events = out;
+ }
+ status = _testStatus(test);
+ if (status === "failed") {
+ failedCount++;
+ // Expand a failed test (but only set this if the user hasn't clicked on the
+ // test name yet).
+ if (test.expanded === undefined)
+ test.expanded = true;
+ if (!failedTests.includes(test.fullName))
+ failedTests.push(test.fullName);
+
+ countDep.changed();
+ test.dep.changed();
+ } else if (status === "succeeded") {
+ passedCount++;
+ countDep.changed();
+ test.dep.changed();
+ } else if (test.expanded) {
+ // re-render the test if new results come in and the test is
+ // currently expanded.
+ test.dep.changed();
+ }
+};
+
+// forget all of the events for a particular test
+var forgetEvents = function (results) {
+ var test = _findTestForResults(results);
+ var status = _testStatus(test);
+ if (status === "failed") {
+ failedCount--;
+ countDep.changed();
+ } else if (status === "succeeded") {
+ passedCount--;
+ countDep.changed();
+ }
+ delete test.events;
+ test.dep.changed();
+};
+
+// given a 'results' as delivered via reportResults, find the
+// corresponding leaf object in resultTree, creating one if it doesn't
+// exist. it will be an object with attributes 'name', 'parent', and
+// possibly 'events'.
+var _findTestForResults = function (results) {
+ var groupPath = results.groupPath; // array
+ if ((! Array.isArray(groupPath)) || (groupPath.length < 1)) {
+ throw new Error("Test must be part of a group");
+ }
+
+ var group;
+ var i = 0;
+ groupPath.forEach(function(gname) {
+ var array = (group ? (group.groups || (group.groups = []))
+ : resultTree);
+ var newGroup = array.find(function(g) { return g.name === gname; });
+ if (! newGroup) {
+ newGroup = {
+ name: gname,
+ parent: (group || null),
+ path: groupPath.slice(0, i+1),
+ dep: new Tracker.Dependency
+ }; // create group
+ array.push(newGroup);
+
+ if (group)
+ group.dep.changed();
+ else
+ topLevelGroupsDep.changed();
+ }
+ group = newGroup;
+ i++;
+ });
+
+ var testName = results.test;
+ var server = !!results.server;
+ var test = (group.tests || (group.tests = [])).find(
+ function(t) { return t.name === testName &&
+ t.server === server; });
+ if (! test) {
+ // create test
+ var nameParts = [...groupPath];
+ nameParts.push(testName);
+ var fullName = nameParts.join(' - ');
+ test = {
+ name: testName,
+ parent: group,
+ server: server,
+ fullName: fullName,
+ dep: new Tracker.Dependency
+ };
+ group.tests.push(test);
+ group.dep.changed();
+ totalCount++;
+ countDep.changed();
+ }
+
+ return test;
+};
+
+
+
+////
+//// Helpers on test objects
+////
+
+var _testTime = function(t) {
+ if (t.events && t.events.length > 0) {
+ var lastEvent = t.events[t.events.length - 1];
+ if (lastEvent.type === "finish") {
+ if ((typeof lastEvent.timeMs) === "number") {
+ return lastEvent.timeMs;
+ }
+ }
+ }
+ return null;
+};
+
+var _testStatus = function(t) {
+ var events = t.events || [];
+ if (events.find(function(x) { return x.type === "exception"; })) {
+ // "exception" should be last event, except race conditions on the
+ // server can make this not the case. Technically we can't tell
+ // if the test is still running at this point, but it can only
+ // result in FAIL.
+ return "failed";
+ } else if (events.length == 0 || (events[events.length - 1].type != "finish")) {
+ return "running";
+ } else if (events.some(function(e) {
+ return e.type == "fail" || e.type == "exception"; })) {
+ return "failed";
+ } else {
+ return "succeeded";
+ }
+};
+
+
+
+////
+//// Templates
+////
+
+//// Template - navBars
+
+Template.navBar.helpers({
+ running: function() {
+ countDep.depend();
+ return running;
+ },
+ passed: function() {
+ countDep.depend();
+ return failedCount === 0;
+ },
+ total_test_time: function() {
+ countDep.depend();
+
+ // walk whole tree to get all tests
+ var walk = function (groups) {
+ var total = 0;
+
+ (groups || []).forEach(function (group) {
+ (group.tests || []).forEach(function (t) {
+ total += _testTime(t);
+ });
+
+ total += walk(group.groups);
+ });
+
+ return total;
+ };
+
+ return walk(resultTree);
+ }
+});
+
+
+//// Template - progressBar
+
+Template.progressBar.helpers({
+ running: function () {
+ countDep.depend();
+ return running;
+ },
+ percentPass: function () {
+ countDep.depend();
+ if (totalCount === 0)
+ return 0;
+ return 100*passedCount/totalCount;
+ },
+ totalCount: function () {
+ countDep.depend();
+ return totalCount;
+ },
+ passedCount: function () {
+ countDep.depend();
+ return passedCount;
+ },
+ percentFail: function () {
+ countDep.depend();
+ if (totalCount === 0)
+ return 0;
+ return 100*failedCount/totalCount;
+ },
+ anyFail: function () {
+ countDep.depend();
+ return failedCount > 0;
+ },
+ barOuterClass: function () {
+ countDep.depend();
+ return running ? 'progress-bar-animated progress-bar-striped' : '';
+ },
+ barInnerClass: function () {
+ countDep.depend();
+ return (failedCount > 0 ?
+ 'bg-warning' : 'bg-success');
+ }
+});
+
+//// Template - groupNav
+
+var changeToPath = function (path) {
+ // Update URL with new group path (pushState creates new history entry)
+ setGroupPathInURL(path, true);
+
+ // Update session to trigger UI updates
+ Session.set("groupPath", path);
+ Session.set("rerunScheduled", true);
+ // pretend there's just been a hot code push
+ // so we run the tests completely fresh.
+ Reload._reload();
+};
+
+Template.groupNav.helpers({
+ groupPaths: function () {
+ var groupPath = Session.get("groupPath");
+ var ret = [];
+ for (var i = 1; i <= groupPath.length; i++) {
+ ret.push({path: groupPath.slice(0,i), name: groupPath[i-1]});
+ }
+ return ret;
+ },
+ rerunScheduled: function () {
+ return Session.get("rerunScheduled");
+ },
+ isFiltered: function () {
+ var groupPath = Session.get("groupPath");
+ return groupPath.length > 1 || groupPath[0] !== "tinytest";
+ }
+});
+
+Template.groupNav.events({
+ 'click .group': function () {
+ changeToPath(this.path);
+ },
+ 'click .rerun': function () {
+ Session.set("rerunScheduled", true);
+ Reload._reload();
+ },
+ 'click .run-all': function () {
+ changeToPath(["tinytest"]);
+ }
+});
+
+Template.groupNav.onRendered(function () {
+ Tinytest._onCurrentClientTest = function (name) {
+ name = (name ? 'C: '+name : '');
+ // Set the DOM directly so that it's immediate and we
+ // don't wait for Tracker to flush.
+ var span = document.getElementById('current-client-test');
+ if (span) {
+ span.innerHTML = '';
+ span.appendChild(document.createTextNode(name));
+ }
+ };
+});
+
+//// Template - uncaughtErrors
+
+Template.uncaughtErrors.helpers({
+ uncaughtErrors() {
+ return Session.get("uncaughtErrors");
+ }
+});
+
+//// Template - failedTests
+
+Template.failedTests.helpers({
+ failedTests: function() {
+ countDep.depend();
+ return failedTests;
+ }
+});
+
+//// Template - testTable
+
+Template.testTable.helpers({
+ testdata: function () {
+ topLevelGroupsDep.depend();
+ return resultTree;
+ },
+ thisWithDep: function () {
+ this.dep.depend();
+ return this;
+ }
+});
+
+//// Template - test_group
+
+Template.test_group.helpers({
+ thisWithDep: function () {
+ this.dep.depend();
+ return this;
+ }
+});
+
+Template.test_group.events({
+ 'click .groupname': function (evt) {
+ changeToPath(this.path);
+ // prevent enclosing groups from also triggering on
+ // same groupname. It would be cleaner to think of
+ // this as each group only listening to its *own*
+ // groupname, but currently it listens to all of them.
+ evt.stopImmediatePropagation();
+ }
+});
+
+
+//// Template - test
+
+Template.test.helpers({
+ test_status_display: function() {
+ var status = _testStatus(this);
+ if (status == "failed") {
+ return "FAIL";
+ } else if (status == "succeeded") {
+ return "PASS";
+ } else {
+ return "waiting...";
+ }
+ },
+
+ test_time_display: function() {
+ var time = _testTime(this);
+ return (typeof time === "number") ? time + " ms" : "";
+ },
+
+ test_class: function() {
+ var events = this.events || [];
+ var classes = [_testStatus(this)];
+
+ if (this.expanded) {
+ classes.push("expanded");
+ } else {
+ classes.push("collapsed");
+ }
+
+ return classes.join(' ');
+ },
+
+ eventsArray: function() {
+ var events = this.events.filter(function(e) {
+ return e.type != "finish";
+ });
+
+ var partitionBy = function(seq, func) {
+ var result = [];
+ var lastValue = {};
+ seq.forEach(function(x) {
+ var newValue = func(x);
+ if (newValue === lastValue) {
+ result[result.length-1].push(x);
+ } else {
+ lastValue = newValue;
+ result.push([x]);
+ }
+ });
+ return result;
+ };
+
+ var dupLists = partitionBy(
+ events.map(function(e) {
+ // XXX XXX We need something better than stringify!
+ // stringify([undefined]) === "[null]"
+ e = Object.assign({}, e);
+ delete e.sequence;
+ return {obj: e, str: JSON.stringify(e)};
+ }), function(x) { return x.str; });
+
+ return dupLists.map(function(L) {
+ var obj = L[0].obj;
+ return (L.length > 1) ? Object.assign({times: L.length}, obj) : obj;
+ });
+ }
+});
+
+Template.test.events({
+ 'click .testname': function () {
+ this.expanded = ! this.expanded;
+ this.dep.changed();
+ }
+});
+
+
+//// Template - event
+
+Template.event.events({
+ 'click .debug': function () {
+ // the way we manage groupPath, shortName, cookies, etc, is really
+ // messy. needs to be aggressively refactored.
+ forgetEvents({groupPath: this.cookie.groupPath,
+ test: this.cookie.shortName});
+ Tinytest._debugTest(this.cookie, reportResults);
+ }
+});
+
+// e.g. doDiff('abc', 'bcd') => [[-1, 'a'], [0, 'bc'], [1, 'd']]
+var doDiff = function (str1, str2) {
+ const diff = diffChars(str1, str2);
+
+ return diff.map(part => {
+ if (part.added) return [1, part.value];
+ if (part.removed) return [-1, part.value];
+ return [0, part.value];
+ });
+};
+
+Template.event.helpers({
+ get_details: function() {
+
+ var details = this.details;
+
+ if (! details) {
+ return null;
+ } else {
+
+ var type = details.type;
+ var stack = details.stack;
+
+ details = Array.isArray(details) && [...details] || Object.assign({}, details);
+ delete details.type;
+ delete details.stack;
+
+ var prepare = function(details) {
+ if (type === 'string_equal') {
+ var diff = doDiff(details.actual,
+ details.expected);
+ }
+
+ return Object.entries(details).map(function([key, val]) {
+
+ // make test._stringEqual results print nicely,
+ // in particular for multiline strings
+ if (type === 'string_equal' &&
+ (key === 'actual' || key === 'expected')) {
+ var html = '';
+ diff.forEach(function (piece) {
+ var which = piece[0];
+ var text = piece[1];
+ if (which === 0 ||
+ which === (key === 'actual' ? -1 : 1)) {
+ var htmlBit = Blaze._escape(text).replace(
+ /\n/g, '
');
+ if (which !== 0)
+ htmlBit = '' + htmlBit + '';
+ html += htmlBit;
+ }
+ });
+ html += '';
+ val = new Spacebars.SafeString(html);
+ }
+
+ // You can end up with a an undefined value, e.g. using
+ // isNull without providing a message attribute: isNull(1).
+ // No need to display those.
+ if (typeof val !== 'undefined') {
+ return {
+ key: key,
+ val: val
+ };
+ } else {
+ return undefined;
+ }
+ }).filter(Boolean);
+ };
+
+ return {
+ type: type,
+ stack: stack,
+ details: prepare(details)
+ };
+ }
+ },
+
+ is_debuggable: function() {
+ return !!this.cookie;
+ }
+});
\ No newline at end of file
diff --git a/packages/test-in-browser/package.js b/packages/test-in-browser/package.js
new file mode 100644
index 000000000..5623840c7
--- /dev/null
+++ b/packages/test-in-browser/package.js
@@ -0,0 +1,36 @@
+Package.describe({
+ summary: "Run tests interactively in the browser",
+ version: '1.5.0',
+ documentation: null
+});
+
+Npm.depends({
+ 'bootstrap': '5.3.8',
+ 'diff': '8.0.2'
+});
+
+Package.onUse(function (api) {
+ api.use('ecmascript');
+ api.use('tinytest');
+ api.use('session');
+ api.use('reload');
+ api.use([
+ 'webapp',
+ 'blaze',
+ 'templating',
+ 'spacebars',
+ 'ddp',
+ 'tracker',
+ ], 'client');
+
+ api.addFiles([
+ 'driver.html',
+ 'driver.js',
+ 'driver.css',
+ ], "client");
+
+ api.use("random", "server");
+ api.mainModule("server.js", "server");
+
+ api.export('runTests');
+});
\ No newline at end of file
diff --git a/packages/test-in-browser/server.js b/packages/test-in-browser/server.js
new file mode 100644
index 000000000..1b7ef60ae
--- /dev/null
+++ b/packages/test-in-browser/server.js
@@ -0,0 +1,6 @@
+// If autoupdate is installed, modifying Meteor.settings.public will cause
+// the hashes in Autoupdate.versions to be computed differently, which
+// will trigger a reload in the browser, which is what we want when
+// running test-packages in the browser, since reloading the window is
+// what kicks off both server and client tests again.
+Meteor.settings.public.autoupdateSalt = Random.id();
\ No newline at end of file
diff --git a/test-app/.meteor/packages b/test-app/.meteor/packages
index 3213e9618..2517db45f 100644
--- a/test-app/.meteor/packages
+++ b/test-app/.meteor/packages
@@ -4,16 +4,16 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
-meteor-base@1.5.2-rc300.2 # Packages every Meteor app needs to have
-mobile-experience@1.1.2-rc300.2 # Packages for a great mobile UX
-mongo@2.0.0-rc300.2 # The database Meteor supports right now
-static-html@1.3.3-rc300.2 # Define static page content in .html files
-reactive-var@1.0.13-rc300.2 # Reactive variable for tracker
-tracker@1.3.4-rc300.2 # Meteor's client-side reactive programming library
+meteor-base@1.5.2 # Packages every Meteor app needs to have
+mobile-experience@1.1.2 # Packages for a great mobile UX
+mongo@2.2.0 # The database Meteor supports right now
+static-html@1.5.0 # Define static page content in .html files
+reactive-var@1.0.13 # Reactive variable for tracker
+tracker@1.3.4 # Meteor's client-side reactive programming library
-standard-minifier-css@1.9.3-rc300.2 # CSS minifier run for production mode
-standard-minifier-js@3.0.0-rc300.2 # JS minifier run for production mode
-es5-shim@4.8.1-rc300.2 # ECMAScript 5 compatibility for older browsers
-ecmascript@0.16.9-rc300.2 # Enable ECMAScript2015+ syntax in app code
-typescript@5.4.3-rc300.2 # Enable TypeScript syntax in .ts and .tsx modules
-shell-server@0.6.0-rc300.2 # Server-side component of the `meteor shell` command
+standard-minifier-css@1.10.0 # CSS minifier run for production mode
+standard-minifier-js@3.2.0 # JS minifier run for production mode
+es5-shim@4.8.1 # ECMAScript 5 compatibility for older browsers
+ecmascript@0.17.0 # Enable ECMAScript2015+ syntax in app code
+typescript@5.9.3 # Enable TypeScript syntax in .ts and .tsx modules
+shell-server@0.7.0 # Server-side component of the `meteor shell` command
diff --git a/test-app/.meteor/release b/test-app/.meteor/release
index b229c371c..703a72252 100644
--- a/test-app/.meteor/release
+++ b/test-app/.meteor/release
@@ -1 +1 @@
-METEOR@3.0-rc.2
+METEOR@3.4
diff --git a/test-app/.meteor/versions b/test-app/.meteor/versions
index 589a1f1d2..cc45d72c1 100644
--- a/test-app/.meteor/versions
+++ b/test-app/.meteor/versions
@@ -1,70 +1,64 @@
-allow-deny@2.0.0-rc300.2
-autoupdate@2.0.0-rc300.2
-babel-compiler@7.11.0-rc300.2
-babel-runtime@1.5.2-rc300.2
-base64@1.0.13-rc300.2
-binary-heap@1.0.12-rc300.2
-blaze-tools@2.0.0-rc300.2
-boilerplate-generator@2.0.0-rc300.2
-caching-compiler@2.0.0-rc300.2
-caching-html-compiler@2.0.0-rc300.2
-callback-hook@1.6.0-rc300.2
-check@1.4.2-rc300.2
-core-runtime@1.0.0-rc300.2
-ddp@1.4.2-rc300.2
-ddp-client@3.0.0-rc300.2
-ddp-common@1.4.1-rc300.2
-ddp-server@3.0.0-rc300.2
-diff-sequence@1.1.3-rc300.2
-dynamic-import@0.7.4-rc300.2
-ecmascript@0.16.9-rc300.2
-ecmascript-runtime@0.8.2-rc300.2
-ecmascript-runtime-client@0.12.2-rc300.2
-ecmascript-runtime-server@0.11.1-rc300.2
-ejson@1.1.4-rc300.2
-es5-shim@4.8.1-rc300.2
-facts-base@1.0.2-rc300.2
-fetch@0.1.5-rc300.2
-geojson-utils@1.0.12-rc300.2
-hot-code-push@1.0.5-rc300.2
-html-tools@2.0.0-rc300.2
-htmljs@2.0.0-rc300.2
-id-map@1.2.0-rc300.2
-inter-process-messaging@0.1.2-rc300.2
-launch-screen@2.0.1-rc300.2
-logging@1.3.5-rc300.2
-meteor@2.0.0-rc300.2
-meteor-base@1.5.2-rc300.2
-minifier-css@2.0.0-rc300.2
-minifier-js@3.0.0-rc300.2
-minimongo@2.0.0-rc300.2
-mobile-experience@1.1.2-rc300.2
-mobile-status-bar@1.1.1-rc300.2
-modern-browsers@0.1.11-rc300.2
-modules@0.20.1-rc300.2
-modules-runtime@0.13.2-rc300.2
-mongo@2.0.0-rc300.2
-mongo-decimal@0.1.4-beta300.7
-mongo-dev-server@1.1.1-rc300.2
-mongo-id@1.0.9-rc300.2
-npm-mongo@4.16.2-rc300.2
-ordered-dict@1.2.0-rc300.2
-promise@1.0.0-rc300.2
-random@1.2.2-rc300.2
-react-fast-refresh@0.2.9-rc300.2
-reactive-var@1.0.13-rc300.2
-reload@1.3.2-rc300.2
-retry@1.1.1-rc300.2
-routepolicy@1.1.2-rc300.2
-shell-server@0.6.0-rc300.2
-socket-stream-client@0.5.3-rc300.2
-spacebars-compiler@2.0.0-rc300.2
-standard-minifier-css@1.9.3-rc300.2
-standard-minifier-js@3.0.0-rc300.2
-static-html@1.3.3-rc300.2
-templating-tools@2.0.0-rc300.2
-tracker@1.3.4-rc300.2
-typescript@5.4.3-rc300.2
-underscore@1.6.2-rc300.2
-webapp@2.0.0-rc300.2
-webapp-hashing@1.1.2-rc300.2
+allow-deny@2.1.0
+autoupdate@2.0.1
+babel-compiler@7.13.0
+babel-runtime@1.5.2
+base64@1.0.13
+binary-heap@1.0.12
+boilerplate-generator@2.1.0
+caching-compiler@2.0.1
+callback-hook@1.6.1
+check@1.5.0
+core-runtime@1.0.0
+ddp@1.4.2
+ddp-client@3.1.1
+ddp-common@1.4.4
+ddp-server@3.1.2
+diff-sequence@1.1.3
+dynamic-import@0.7.4
+ecmascript@0.17.0
+ecmascript-runtime@0.8.3
+ecmascript-runtime-client@0.12.3
+ecmascript-runtime-server@0.11.1
+ejson@1.1.5
+es5-shim@4.8.1
+facts-base@1.0.2
+fetch@0.1.6
+geojson-utils@1.0.12
+hot-code-push@1.0.5
+id-map@1.2.0
+inter-process-messaging@0.1.2
+launch-screen@2.0.1
+logging@1.3.6
+meteor@2.2.0
+meteor-base@1.5.2
+minifier-css@2.0.1
+minifier-js@3.1.0
+minimongo@2.0.5
+mobile-experience@1.1.2
+mobile-status-bar@1.1.1
+modern-browsers@0.2.3
+modules@0.20.3
+modules-runtime@0.13.2
+mongo@2.2.0
+mongo-decimal@0.2.0
+mongo-dev-server@1.1.1
+mongo-id@1.0.9
+npm-mongo@6.16.1
+ordered-dict@1.2.0
+promise@1.0.0
+random@1.2.2
+react-fast-refresh@0.3.0
+reactive-var@1.0.13
+reload@1.3.2
+retry@1.1.1
+routepolicy@1.1.2
+shell-server@0.7.0
+socket-stream-client@0.6.1
+standard-minifier-css@1.10.0
+standard-minifier-js@3.2.0
+static-html@1.5.0
+static-html-tools@1.0.0
+tracker@1.3.4
+typescript@5.9.3
+webapp@2.1.0
+webapp-hashing@1.1.2
diff --git a/test-app/package.json b/test-app/package.json
index e392d5ae2..63b44c2d5 100644
--- a/test-app/package.json
+++ b/test-app/package.json
@@ -10,17 +10,17 @@
"setup": "ln -sfn ../packages ./packages",
"lint:check": "npx eslint",
"lint:fix": "npm run eslint --fix",
- "test:watch": "meteor test-packages --raw-logs ./packages/*"
+ "test:watch": "meteor test-packages --raw-logs ./packages/*",
+ "test:jquery": "meteor test-packages --extra-packages=jquery --raw-logs ./packages/*"
},
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.27.1",
- "jquery": "^3.7.1",
- "meteor-node-stubs": "^1.2.17",
- "puppeteer": "^24.8.2"
+ "@babel/runtime": "^7.29.2",
+ "meteor-node-stubs": "^1.2.27",
+ "puppeteer": "^24.40.0"
},
"devDependencies": {
- "@quave/eslint-config-quave": "^3.0.0"
+ "@quave/eslint-config-quave": "^3.0.1"
},
"eslintConfig": {
"extends": [