Skip to content

Commit

Permalink
Merge branch 'components'
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveSanderson committed Apr 7, 2014
2 parents 9b849ee + 2451683 commit 8012096
Show file tree
Hide file tree
Showing 11 changed files with 1,669 additions and 7 deletions.
6 changes: 3 additions & 3 deletions build/fragments/amd-pre.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {
// [1] CommonJS/Node.js
var target = module['exports'] || exports; // module.exports is for Node.js
factory(target);
factory(target, require);
} else if (typeof define === 'function' && define['amd']) {
// [2] AMD anonymous module
define(['exports'], factory);
define(['exports', 'require'], factory);
} else {
// [3] No module loader (plain <script> tag) - put directly in global namespace
factory(window['ko'] = {});
}
}(function(koExports){
}(function(koExports, require){
3 changes: 3 additions & 0 deletions build/fragments/source-references.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ knockoutDebugCallback([
'src/binding/selectExtensions.js',
'src/binding/expressionRewriting.js',
'src/virtualElements.js',
'src/components/loaderRegistry.js',
'src/components/defaultLoader.js',
'src/binding/bindingProvider.js',
'src/binding/bindingAttributeSyntax.js',
'src/components/componentBinding.js',
'src/binding/defaultBindings/attr.js',
'src/binding/defaultBindings/checked.js',
'src/binding/defaultBindings/css.js',
Expand Down
4 changes: 3 additions & 1 deletion build/knockout-raw.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
var DEBUG = true,
jQuery = window.jQuery;
// ensure these variables are defined (even if their values are undefined)
jQuery = window.jQuery,
require = window.require;

// This script adds <script> tags referencing each of the knockout.js source files in the correct order
// It uses JSONP to fetch the list of source files from source-references.js
Expand Down
513 changes: 513 additions & 0 deletions spec/components/componentBindingBehaviors.js

Large diffs are not rendered by default.

434 changes: 434 additions & 0 deletions spec/components/defaultLoaderBehaviors.js

Large diffs are not rendered by default.

294 changes: 294 additions & 0 deletions spec/components/loaderRegistryBehaviors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
describe('Components: Loader registry', function() {
var testAsyncDelay = 20,
testComponentName = 'test-component',
testComponentConfig = {},
testComponentDefinition = { template: {} },
loaderThatDoesNotReturnAnything = {
getConfig: function(name, callback) {
expect(name).toBe(testComponentName);
setTimeout(function() { callback(null) }, testAsyncDelay);
},
loadComponent: function(name, config, callback) {
expect(name).toBe(testComponentName);
expect(config).toBe(testComponentConfig);
setTimeout(function() { callback(null) }, testAsyncDelay);
}
},
loaderThatHasNoHandlers = {},
loaderThatReturnsConfig = {
getConfig: function(name, callback) {
expect(name).toBe(testComponentName);
setTimeout(function() { callback(testComponentConfig) }, testAsyncDelay);
}
},
loaderThatReturnsDefinition = {
loadComponent: function(name, config, callback) {
expect(name).toBe(testComponentName);
expect(config).toBe(testComponentConfig);
setTimeout(function() { callback(testComponentDefinition) }, testAsyncDelay);
}
},
loaderThatShouldNeverBeCalled = {
getConfig: function() { throw new Error('Should not be called'); },
loadComponent: function() { throw new Error('Should not be called'); }
},
loaderThatCompletesSynchronously = {
getConfig: function(name, callback) { callback(testComponentConfig); },
loadComponent: function(name, config, callback) {
expect(config).toBe(testComponentConfig);
callback(testComponentDefinition);
}
},
testLoaderChain = function(spec, chain, options) {
spec.restoreAfter(ko.components, 'loaders');

// Set up a chain of loaders, then query it
ko.components.loaders = chain;

var loadedDefinition = "Not yet loaded";
ko.components.get(testComponentName, function(definition) {
loadedDefinition = definition;
});

var onLoaded = function() {
if ('expectedDefinition' in options) {
expect(loadedDefinition).toBe(options.expectedDefinition);
}
if ('done' in options) {
options.done(loadedDefinition);
}
};

// Wait for and verify result
if (loadedDefinition !== "Not yet loaded") {
// Completed synchronously
onLoaded();
} else {
// Will complete asynchronously
waitsFor(function() { return loadedDefinition !== "Not yet loaded"; }, 300);
runs(onLoaded);
}
};

afterEach(function() {
ko.components.unregister(testComponentName);
});

it('Exposes the list of loaders as an array', function() {
expect(ko.components.loaders instanceof Array).toBe(true);
});

it('Obtains component config and component definition objects by invoking each loader in turn, asynchronously, until one supplies a value', function() {
var loaders = [
loaderThatDoesNotReturnAnything,
loaderThatHasNoHandlers,
loaderThatReturnsDefinition,
loaderThatDoesNotReturnAnything,
loaderThatReturnsConfig,
loaderThatShouldNeverBeCalled
];

testLoaderChain(this, loaders, { expectedDefinition: testComponentDefinition });
});

it('Supplies null if no registered loader returns a config object', function() {
var loaders = [
loaderThatDoesNotReturnAnything,
loaderThatHasNoHandlers,
loaderThatReturnsDefinition,
loaderThatDoesNotReturnAnything
];

testLoaderChain(this, loaders, { expectedDefinition: null });
});

it('Supplies null if no registered loader returns a component for a given config object', function() {
var loaders = [
loaderThatDoesNotReturnAnything,
loaderThatHasNoHandlers,
loaderThatReturnsConfig,
loaderThatDoesNotReturnAnything
];

testLoaderChain(this, loaders, { expectedDefinition: null });
});

it('Aborts if a getConfig call returns a value other than undefined', function() {
// This is just to leave open the option to support synchronous return values in the future.
// We would detect that a getConfig call wants to return synchronously based on getting a
// non-undefined return value, and in that case would not wait for the callback.

var loaders = [
loaderThatReturnsDefinition,
loaderThatDoesNotReturnAnything,
{
getConfig: function(name, callback) {
setTimeout(function() { callback(testComponentDefinition); }, 50);
return testComponentDefinition; // This is what's not allowed
},

// Unfortunately there's no way to catch the async exception, and we don't
// want to clutter up the console during tests, so suppress this
suppressLoaderExceptions: true
},
loaderThatReturnsConfig
];

testLoaderChain(this, loaders, { expectedDefinition: null });
});

it('Aborts if a loadComponent call returns a value other than undefined', function() {
// This is just to leave open the option to support synchronous return values in the future.
// We would detect that a loadComponent call wants to return synchronously based on getting a
// non-undefined return value, and in that case would not wait for the callback.

var loaders = [
loaderThatReturnsConfig,
loaderThatDoesNotReturnAnything,
{
loadComponent: function(name, config, callback) {
setTimeout(function() { callback(testComponentDefinition); }, 50);
return testComponentDefinition; // This is what's not allowed
},

// Unfortunately there's no way to catch the async exception, and we don't
// want to clutter up the console during tests, so suppress this
suppressLoaderExceptions: true
},
loaderThatReturnsDefinition
];

testLoaderChain(this, loaders, { expectedDefinition: null });
});

it('Ensures that the loading process completes asynchronously, even if the loader completed synchronously', function() {
// This behavior is for consistency. Developers calling ko.components.get shouldn't have to
// be concerned about whether the callback fires before or after their next line of code.

var wasAsync = false;

testLoaderChain(this, [loaderThatCompletesSynchronously], {
expectedDefinition: testComponentDefinition,
done: function() {
expect(wasAsync).toBe(true);
}
});

wasAsync = true;
});

it('By default, contains only the default loader', function() {
expect(ko.components.loaders.length).toBe(1);
expect(ko.components.loaders[0]).toBe(ko.components.defaultLoader);
});

it('Caches and reuses loaded component definitions', function() {
// Ensure we leave clean state after the test
this.after(function() {
ko.components.unregister('some-component');
ko.components.unregister('other-component');
});

ko.components.register('some-component', {
viewModel: function() { this.isTheTestComponent = true; }
});
ko.components.register('other-component', {
viewModel: function() { this.isTheOtherComponent = true; }
});

// Fetch the component definition, and see it's the right thing
var definition1;
getComponentDefinition('some-component', function(definition) {
definition1 = definition;
expect(definition1.createViewModel().isTheTestComponent).toBe(true);
});

// Fetch it again, and see the definition was reused
getComponentDefinition('some-component', function(definition2) {
expect(definition2).toBe(definition1);
});

// See that requests for other component names don't reuse the same cache entry
getComponentDefinition('other-component', function(otherDefinition) {
expect(otherDefinition).not.toBe(definition1);
expect(otherDefinition.createViewModel().isTheOtherComponent).toBe(true);
});

// See we can choose to force a refresh by clearing a cache entry before fetching a definition.
// This facility probably won't be used by most applications, but it is helpful for tests.
runs(function() { ko.components.clearCachedDefinition('some-component'); });
getComponentDefinition('some-component', function(definition3) {
expect(definition3).not.toBe(definition1);
expect(definition3.createViewModel().isTheTestComponent).toBe(true);
});

// See that unregistering a component implicitly clears the cache entry too
runs(function() { ko.components.unregister('some-component'); });
getComponentDefinition('some-component', function(definition4) {
expect(definition4).toBe(null);
});
});

it('Only commences a single loading process, even if multiple requests arrive before loading has completed', function() {
// Set up a mock AMD environment that logs calls
var someModuleTemplate = [],
someComponentModule = { template: someModuleTemplate },
requireCallLog = [];
this.restoreAfter(window, 'require');
window.require = function(modules, callback) {
requireCallLog.push(modules.slice(0));
setTimeout(function() { callback(someComponentModule); }, 80);
};

ko.components.register(testComponentName, { require: 'path/testcomponent' });

// Begin loading the module; see it synchronously made a request to the module loader
var definition1 = undefined;
ko.components.get(testComponentName, function(loadedDefinition) {
definition1 = loadedDefinition;
});
expect(requireCallLog).toEqual([['path/testcomponent']]);

// Even a little while later, the module hasn't yet loaded
var definition2 = undefined;
waits(20);
runs(function() {
expect(definition1).toBe(undefined);

// ... but let's make a second request for the same module
ko.components.get(testComponentName, function(loadedDefinition) {
definition2 = loadedDefinition;
});

// This time there was no further request to the module loader
expect(requireCallLog.length).toBe(1);
});

// And when the loading eventually completes, both requests are satisfied with the same definition
waitsFor(function() { return definition1 }, 300);
runs(function() {
expect(definition1.template).toBe(someModuleTemplate);
expect(definition2).toBe(definition1);
});

// Subsequent requests also don't involve calls to the module loader
getComponentDefinition(testComponentName, function(definition3) {
expect(definition3).toBe(definition1);
expect(requireCallLog.length).toBe(1);
});
});

function getComponentDefinition(componentName, assertionCallback) {
var loadedDefinition,
hasCompleted = false;
runs(function() {
ko.components.get(componentName, function(definition) {
loadedDefinition = definition;
hasCompleted = true;
});
expect(hasCompleted).toBe(false); // Should always complete asynchronously
});
waitsFor(function() { return hasCompleted; });
runs(function() { assertionCallback(loadedDefinition); });
}
});
8 changes: 5 additions & 3 deletions spec/lib/jasmine-1.2.0/jasmine.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// NOTE: This is a modified version of Jasmine for use with Knockout.

var isCommonJS = typeof window == "undefined";

/**
Expand Down Expand Up @@ -1589,7 +1591,7 @@ jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMil
return a.runAtMillis - b.runAtMillis;
});
for (var i = 0; i < funcsToRun.length; ++i) {
try {
//try { // mbest: Removed so we can catch errors in asynchronous functions
var funcToRun = funcsToRun[i];
this.nowMillis = funcToRun.runAtMillis;
funcToRun.funcToCall();
Expand All @@ -1599,8 +1601,8 @@ jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMil
funcToRun.millis,
true);
}
} catch(e) {
}
//} catch(e) {
//}
}
this.runFunctionsWithinRange(oldMillis, nowMillis);
}
Expand Down
3 changes: 3 additions & 0 deletions spec/runner.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
<script type="text/javascript" src="jsonPostingBehaviors.js"></script>
<script type="text/javascript" src="nativeTemplateEngineBehaviors.js"></script>
<script type="text/javascript" src="utilsBehaviors.js"></script>
<script type="text/javascript" src="components/loaderRegistryBehaviors.js"></script>
<script type="text/javascript" src="components/defaultLoaderBehaviors.js"></script>
<script type="text/javascript" src="components/componentBindingBehaviors.js"></script>

<!-- Default bindings -->
<script type="text/javascript" src="defaultBindings/attrBehaviors.js"></script>
Expand Down
Loading

0 comments on commit 8012096

Please sign in to comment.