From e7e894a2e36e042be6d62af56b0f3126f4e4fc77 Mon Sep 17 00:00:00 2001 From: Elliott Sprehn Date: Mon, 18 Oct 2010 14:02:18 -0700 Subject: Significantly clean up the way the scenario DSL works and implement many more DSL statements. - "this" always means the current chain scope inside a DSL - addFutureAction callbacks now take ($window, $document, done) - $document has a special method elements() that uses the currently selected nodes in the document as defined by using() statements. - $document.elements() allows placeholder insertion into selectors to make them more readable. ex. $document.elements('input[name="$1"]', myVar) will substitute the value of myVar for $1 in the selector. Subsequent arguments are $2 and so on. - $document.elements() results have a special method trigger(event) which should be used to events. This method implements some hacks to make sure browser UI controls update and the correct angular events fire. - futures now allow custom formatting. By default any chain that results in a future can use toJson() or fromJson() to convert the future value to and from json. A custom parser can be provided with parsedWith(fn) where fn is a callback(value) that must return the parsed result. Note: The entire widgets.html UI is now able to be controlled and asserted through DSL statements!!! Victory! :) --- test/scenario/ApplicationSpec.js | 13 +- test/scenario/DSLSpec.js | 233 ------------------------- test/scenario/FutureSpec.js | 38 ++++- test/scenario/SpecRunnerSpec.js | 17 +- test/scenario/dslSpec.js | 361 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 409 insertions(+), 253 deletions(-) delete mode 100644 test/scenario/DSLSpec.js create mode 100644 test/scenario/dslSpec.js (limited to 'test/scenario') diff --git a/test/scenario/ApplicationSpec.js b/test/scenario/ApplicationSpec.js index 706fbc36..2fb9881f 100644 --- a/test/scenario/ApplicationSpec.js +++ b/test/scenario/ApplicationSpec.js @@ -18,20 +18,21 @@ describe('angular.scenario.Application', function() { testDocument = $document; }); app.navigateTo('http://www.google.com/'); - app.executeAction(function($document, $window) { + app.executeAction(function($window, $document) { expect($window).not.toEqual(testWindow); expect($document).not.toEqual(testDocument); }); }); - it('should execute callback on $window of frame', function() { + it('should execute callback with correct arguments', function() { var testWindow = {document: {}}; app.getWindow = function() { return testWindow; }; - app.executeAction(function($document, $window) { - expect(this).toEqual($window); - expect(this).toEqual(testWindow); + app.executeAction(function($window, $document) { + expect(this).toEqual(app); + expect($document).toEqual(_jQuery($window.document)); + expect($window).toEqual(testWindow); }); }); @@ -52,7 +53,7 @@ describe('angular.scenario.Application', function() { it('should call onload handler when frame loads', function() { var called; - app.getFrame = function() { + app.getFrame = function() { // Mock a little jQuery var result = { remove: function() { diff --git a/test/scenario/DSLSpec.js b/test/scenario/DSLSpec.js deleted file mode 100644 index b144a3ce..00000000 --- a/test/scenario/DSLSpec.js +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Very basic Mock of angular. - */ -function AngularMock() { - this.reset(); - this.service = this; -} - -AngularMock.prototype.reset = function() { - this.log = []; -}; - -AngularMock.prototype.element = function(node) { - this.log.push('element(' + node.nodeName.toLowerCase() + ')'); - return this; -}; - -AngularMock.prototype.trigger = function(value) { - this.log.push('element().trigger(' + value + ')'); -}; - -AngularMock.prototype.$browser = function() { - this.log.push('$brower()'); - return this; -}; - -AngularMock.prototype.poll = function() { - this.log.push('$brower.poll()'); - return this; -}; - -AngularMock.prototype.notifyWhenNoOutstandingRequests = function(fn) { - this.log.push('$brower.notifyWhenNoOutstandingRequests()'); - fn(); -}; - -describe("angular.scenario.dsl", function() { - var $window; - var $root; - var application; - - beforeEach(function() { - $window = { - document: _jQuery("
"), - angular: new AngularMock() - }; - $root = angular.scope({}, angular.service); - $root.futures = []; - $root.addFuture = function(name, fn) { - this.futures.push(name); - fn.call(this, function(error, result) { - $root.futureError = error; - $root.futureResult = result; - }); - }; - $root.application = new angular.scenario.Application($window.document); - $root.application.getWindow = function() { - return $window; - }; - $root.application.navigateTo = function(url, callback) { - $window.location = url; - callback(); - }; - // Just use the real one since it delegates to this.addFuture - $root.addFutureAction = angular.scenario. - SpecRunner.prototype.addFutureAction; - }); - - describe('Pause', function() { - beforeEach(function() { - $root.setTimeout = function(fn, value) { - $root.timerValue = value; - fn(); - }; - }); - - it('should pause for specified seconds', function() { - angular.scenario.dsl.pause.call($root).call($root, 10); - expect($root.timerValue).toEqual(10000); - expect($root.futureResult).toEqual(10000); - }); - }); - - describe('Expect', function() { - it('should chain and execute matcher', function() { - var future = {value: 10}; - var result = angular.scenario.dsl.expect.call($root).call($root, future); - result.toEqual(10); - expect($root.futureError).toBeUndefined(); - expect($root.futureResult).toBeUndefined(); - result = angular.scenario.dsl.expect.call($root).call($root, future); - result.toEqual(20); - expect($root.futureError).toBeDefined(); - }); - }); - - describe('NavigateTo', function() { - it('should allow a string url', function() { - angular.scenario.dsl.navigateTo.call($root).call($root, 'http://myurl'); - expect($window.location).toEqual('http://myurl'); - expect($root.futureResult).toEqual('http://myurl'); - }); - - it('should allow a future url', function() { - var future = {name: 'future name', value: 'http://myurl'}; - angular.scenario.dsl.navigateTo.call($root).call($root, future); - expect($window.location).toEqual('http://myurl'); - expect($root.futureResult).toEqual('http://myurl'); - }); - - it('should complete if angular is missing from app frame', function() { - delete $window.angular; - angular.scenario.dsl.navigateTo.call($root).call($root, 'http://myurl'); - expect($window.location).toEqual('http://myurl'); - expect($root.futureResult).toEqual('http://myurl'); - }); - - it('should wait for angular notify when no requests pending', function() { - angular.scenario.dsl.navigateTo.call($root).call($root, 'url'); - expect($window.angular.log).toContain('$brower.poll()'); - expect($window.angular.log). - toContain('$brower.notifyWhenNoOutstandingRequests()'); - }); - }); - - describe('Element Finding', function() { - var doc; - //TODO(esprehn): Work around a bug in jQuery where attribute selectors - // only work if they are executed on a real document, not an element. - // - // ex. jQuery('#foo').find('[name="bar"]') // fails - // ex. jQuery('#foo [name="bar"]') // works, wtf? - // - beforeEach(function() { - doc = _jQuery('
'); - _jQuery(document.body).html('').append(doc); - $window.document = window.document; - }); - - afterEach(function() { - _jQuery(document.body). - find('#angular-scenario-binding'). - remove(); - }); - - describe('Binding', function() { - it('should select binding by name', function() { - doc.append('some value'); - angular.scenario.dsl.binding.call($root).call($root, 'foo.bar'); - expect($root.futureResult).toEqual('some value'); - }); - - it('should return error if no binding exists', function() { - angular.scenario.dsl.binding.call($root).call($root, 'foo.bar'); - expect($root.futureError).toMatch(/does not exist/); - }); - }); - - describe('Input', function() { - it('should change value in text input', function() { - doc.append(''); - var chain = angular.scenario.dsl.input. - call($root).call($root, 'test.input'); - chain.enter('foo'); - expect($window.angular.log).toContain('element(input)'); - expect($window.angular.log).toContain('element().trigger(change)'); - expect(_jQuery('input[name="test.input"]').val()).toEqual('foo'); - }); - - it('should return error if no input exists', function() { - var chain = angular.scenario.dsl.input. - call($root).call($root, 'test.input'); - chain.enter('foo'); - expect($root.futureError).toMatch(/does not exist/); - }); - - it('should toggle checkbox state', function() { - doc.append(''); - expect(_jQuery('input[name="test.input"]'). - attr('checked')).toBeTruthy(); - var chain = angular.scenario.dsl.input. - call($root).call($root, 'test.input'); - chain.check(); - expect($window.angular.log).toContain('element(input)'); - expect($window.angular.log).toContain('element().trigger(click)'); - expect(_jQuery('input[name="test.input"]'). - attr('checked')).toBeFalsy(); - $window.angular.reset(); - chain.check(); - expect($window.angular.log).toContain('element(input)'); - expect($window.angular.log).toContain('element().trigger(click)'); - expect(_jQuery('input[name="test.input"]'). - attr('checked')).toBeTruthy(); - }); - - it('should return error if checkbox does not exist', function() { - var chain = angular.scenario.dsl.input. - call($root).call($root, 'test.input'); - chain.check(); - expect($root.futureError).toMatch(/does not exist/); - }); - - it('should select option from radio group', function() { - doc.append( - '' + - ''); - // HACK! We don't know why this is sometimes false on chrome - _jQuery('input[name="0@test.input"][value="bar"]').attr('checked', true); - expect(_jQuery('input[name="0@test.input"][value="bar"]'). - attr('checked')).toBeTruthy(); - expect(_jQuery('input[name="0@test.input"][value="foo"]'). - attr('checked')).toBeFalsy(); - var chain = angular.scenario.dsl.input. - call($root).call($root, 'test.input'); - chain.select('foo'); - expect($window.angular.log).toContain('element(input)'); - expect($window.angular.log).toContain('element().trigger(click)'); - expect(_jQuery('input[name="0@test.input"][value="bar"]'). - attr('checked')).toBeFalsy(); - expect(_jQuery('input[name="0@test.input"][value="foo"]'). - attr('checked')).toBeTruthy(); - }); - - it('should return error if radio button does not exist', function() { - var chain = angular.scenario.dsl.input. - call($root).call($root, 'test.input'); - chain.select('foo'); - expect($root.futureError).toMatch(/does not exist/); - }); - }); - }); - -}); diff --git a/test/scenario/FutureSpec.js b/test/scenario/FutureSpec.js index ae475779..1e6af7a1 100644 --- a/test/scenario/FutureSpec.js +++ b/test/scenario/FutureSpec.js @@ -1,13 +1,14 @@ describe('angular.scenario.Future', function() { var future; - it('should set the name and behavior', function() { + it('should set the sane defaults', function() { var behavior = function() {}; var future = new angular.scenario.Future('test name', behavior); expect(future.name).toEqual('test name'); expect(future.behavior).toEqual(behavior); expect(future.value).toBeUndefined(); expect(future.fulfilled).toBeFalsy(); + expect(future.parser).toEqual(angular.identity); }); it('should be fulfilled after execution and done callback', function() { @@ -35,4 +36,39 @@ describe('angular.scenario.Future', function() { future.execute(angular.noop); expect(future.value).toEqual(10); }); + + it('should parse json with fromJson', function() { + var future = new angular.scenario.Future('test name', function(done) { + done(null, "{test: 'foo'}"); + }); + future.fromJson().execute(angular.noop); + expect(future.value).toEqual({test: 'foo'}); + }); + + it('should convert to json with toJson', function() { + var future = new angular.scenario.Future('test name', function(done) { + done(null, {test: 'foo'}); + }); + future.toJson().execute(angular.noop); + expect(future.value).toEqual('{"test":"foo"}'); + }); + + it('should convert with custom parser', function() { + var future = new angular.scenario.Future('test name', function(done) { + done(null, 'foo'); + }); + future.parsedWith(function(value) { + return value.toUpperCase(); + }).execute(angular.noop); + expect(future.value).toEqual('FOO'); + }); + + it('should pass error if parser fails', function() { + var future = new angular.scenario.Future('test name', function(done) { + done(null, '{'); + }); + future.fromJson().execute(function(error, result) { + expect(error).toBeDefined(); + }); + }); }); diff --git a/test/scenario/SpecRunnerSpec.js b/test/scenario/SpecRunnerSpec.js index 0926c3f8..e62bb392 100644 --- a/test/scenario/SpecRunnerSpec.js +++ b/test/scenario/SpecRunnerSpec.js @@ -42,7 +42,7 @@ function ApplicationMock($window) { } ApplicationMock.prototype = { executeAction: function(callback) { - callback.call(this.$window); + callback.call(this.$window, _jQuery(this.$window.document), this.$window); } }; @@ -59,15 +59,15 @@ describe('angular.scenario.SpecRunner', function() { it('should bind futures to the spec', function() { runner.addFuture('test future', function(done) { - this.application.value = 10; + this.value = 10; done(); }); runner.futures[0].execute(angular.noop); - expect(runner.application.value).toEqual(10); + expect(runner.value).toEqual(10); }); it('should pass done to future action behavior', function() { - runner.addFutureAction('test future', function(done) { + runner.addFutureAction('test future', function($window, $document, done) { expect(angular.isFunction(done)).toBeTruthy(); done(10, 20); }); @@ -77,15 +77,6 @@ describe('angular.scenario.SpecRunner', function() { }); }); - it('should pass execute future action on the $window', function() { - runner.addFutureAction('test future', function(done) { - this.test = 'test value'; - done(); - }); - runner.futures[0].execute(angular.noop); - expect($window.test).toEqual('test value'); - }); - it('should execute spec function and notify UI', function() { var finished = false; var ui = new UIMock(); diff --git a/test/scenario/dslSpec.js b/test/scenario/dslSpec.js new file mode 100644 index 00000000..a30fe165 --- /dev/null +++ b/test/scenario/dslSpec.js @@ -0,0 +1,361 @@ +/** + * Very basic Mock of angular. + */ +function AngularMock() { + this.reset(); + this.service = this; +} + +AngularMock.prototype.reset = function() { + this.log = []; +}; + +AngularMock.prototype.element = function(node) { + this.log.push('element(' + node.nodeName.toLowerCase() + ')'); + var mock = this; + return { + selector: '', + attr: function(name, value) { + mock.log.push('attr(' + name + (angular.isDefined(value) ? ',' + value : '') + ')'); + return _jQuery.fn.attr.apply(_jQuery(node), arguments); + }, + trigger: function(type) { + mock.log.push('element().trigger(' + type + ')'); + //TODO(esprehn): See the HACK!! in the SpecRunner. This avoids + // triggering the second part of the hack in tests + delete this.selector; + } + }; +}; + +AngularMock.prototype.$browser = function() { + this.log.push('$brower()'); + return this; +}; + +AngularMock.prototype.poll = function() { + this.log.push('$brower.poll()'); + return this; +}; + +AngularMock.prototype.notifyWhenNoOutstandingRequests = function(fn) { + this.log.push('$brower.notifyWhenNoOutstandingRequests()'); + fn(); +}; + +describe("angular.scenario.dsl", function() { + var $window; + var $root; + var application; + + beforeEach(function() { + $window = { + document: _jQuery("
"), + angular: new AngularMock() + }; + $root = angular.scope({}, angular.service); + $root.futures = []; + $root.addFuture = function(name, fn) { + this.futures.push(name); + fn.call(this, function(error, result) { + $root.futureError = error; + $root.futureResult = result; + }); + }; + $root.dsl = {}; + angular.foreach(angular.scenario.dsl, function(fn, name) { + $root.dsl[name] = function() { + return fn.call($root).apply($root, arguments); + }; + }); + $root.application = new angular.scenario.Application($window.document); + $root.application.getWindow = function() { + return $window; + }; + $root.application.navigateTo = function(url, callback) { + $window.location = url; + callback(); + }; + // Just use the real one since it delegates to this.addFuture + $root.addFutureAction = angular.scenario. + SpecRunner.prototype.addFutureAction; + }); + + describe('Pause', function() { + beforeEach(function() { + $root.setTimeout = function(fn, value) { + $root.timerValue = value; + fn(); + }; + }); + + it('should pause for specified seconds', function() { + $root.dsl.pause(10); + expect($root.timerValue).toEqual(10000); + expect($root.futureResult).toEqual(10000); + }); + }); + + describe('Expect', function() { + it('should chain and execute matcher', function() { + var future = {value: 10}; + var result = $root.dsl.expect(future); + result.toEqual(10); + expect($root.futureError).toBeUndefined(); + expect($root.futureResult).toBeUndefined(); + result = $root.dsl.expect(future); + result.toEqual(20); + expect($root.futureError).toBeDefined(); + }); + }); + + describe('NavigateTo', function() { + it('should allow a string url', function() { + $root.dsl.navigateTo('http://myurl'); + expect($window.location).toEqual('http://myurl'); + expect($root.futureResult).toEqual('http://myurl'); + }); + + it('should allow a future url', function() { + var future = {name: 'future name', value: 'http://myurl'}; + $root.dsl.navigateTo(future); + expect($window.location).toEqual('http://myurl'); + expect($root.futureResult).toEqual('http://myurl'); + }); + + it('should complete if angular is missing from app frame', function() { + delete $window.angular; + $root.dsl.navigateTo('http://myurl'); + expect($window.location).toEqual('http://myurl'); + expect($root.futureResult).toEqual('http://myurl'); + }); + + it('should wait for angular notify when no requests pending', function() { + $root.dsl.navigateTo('url'); + expect($window.angular.log).toContain('$brower.poll()'); + expect($window.angular.log). + toContain('$brower.notifyWhenNoOutstandingRequests()'); + }); + }); + + describe('Element Finding', function() { + var doc; + //TODO(esprehn): Work around a bug in jQuery where attribute selectors + // only work if they are executed on a real document, not an element. + // + // ex. jQuery('#foo').find('[name="bar"]') // fails + // ex. jQuery('#foo [name="bar"]') // works, wtf? + // + beforeEach(function() { + doc = _jQuery('
'); + _jQuery(document.body).html('').append(doc); + $window.document = window.document; + }); + + afterEach(function() { + _jQuery(document.body). + find('#angular-scenario-binding'). + remove(); + }); + + describe('Select', function() { + it('should select single option', function() { + doc.append( + '' + ); + $root.dsl.select('test').option('A'); + expect(_jQuery('[name="test"]').val()).toEqual('A'); + }); + + it('should select multiple options', function() { + doc.append( + '' + ); + $root.dsl.select('test').options('A', 'B'); + expect(_jQuery('[name="test"]').val()).toEqual(['A','B']); + }); + + it('should fail to select multiple options on non-multiple select', function() { + doc.append(''); + $root.dsl.select('test').options('A', 'B'); + expect($root.futureError).toMatch(/did not match/); + }); + }); + + describe('Element', function() { + it('should execute click', function() { + var clicked; + doc.append(''); + doc.find('a').click(function() { + clicked = true; + }); + $root.dsl.element('a').click(); + }); + + it('should get attribute', function() { + doc.append('
'); + $root.dsl.element('#test').attr('class'); + expect($root.futureResult).toEqual('foo'); + }); + + it('should set attribute', function() { + doc.append('
'); + $root.dsl.element('#test').attr('class', 'bam'); + expect(doc.find('div').attr('class')).toEqual('bam'); + }); + + it('should get val', function() { + doc.append(''); + $root.dsl.element('input').val(); + expect($root.futureResult).toEqual('bar'); + }); + + it('should set val', function() { + doc.append(''); + $root.dsl.element('input').val('baz'); + expect(doc.find('input').val()).toEqual('baz'); + }); + + }); + + describe('Repeater', function() { + var chain; + beforeEach(function() { + doc.append( + '' + ); + chain = $root.dsl.repeater('ul li'); + }); + + it('should get the row count', function() { + chain.count(); + expect($root.futureResult).toEqual(2); + }); + + it('should get a row of bindings', function() { + chain.row(1); + expect($root.futureResult).toEqual(['felisa', 'female']); + }); + + it('should get a column of bindings', function() { + chain.column('gender'); + expect($root.futureResult).toEqual(['male', 'female']); + }); + }); + + describe('Binding', function() { + it('should select binding by name', function() { + doc.append('some value'); + $root.dsl.binding('foo.bar'); + expect($root.futureResult).toEqual('some value'); + }); + + it('should select binding in template by name', function() { + doc.append('
foo some baz
'); + $root.dsl.binding('bar'); + expect($root.futureResult).toEqual('foo some baz'); + }); + + it('should return error if no binding exists', function() { + $root.dsl.binding('foo.bar'); + expect($root.futureError).toMatch(/did not match/); + }); + }); + + describe('Using', function() { + it('should prefix selector in $document.elements()', function() { + var chain; + doc.append( + '
' + + '
' + ); + chain = $root.dsl.using('div#test2'); + chain.input('test.input').enter('foo'); + expect($window.angular.log).toContain('element(input)'); + expect($window.angular.log).toContain('element().trigger(change)'); + var inputs = _jQuery('input[name="test.input"]'); + expect(inputs.first().val()).toEqual('something'); + expect(inputs.last().val()).toEqual('foo'); + }); + }); + + describe('Input', function() { + it('should change value in text input', function() { + doc.append(''); + var chain = $root.dsl.input('test.input'); + chain.enter('foo'); + expect($window.angular.log).toContain('element(input)'); + expect($window.angular.log).toContain('element().trigger(change)'); + expect(_jQuery('input[name="test.input"]').val()).toEqual('foo'); + }); + + it('should return error if no input exists', function() { + var chain = $root.dsl.input('test.input'); + chain.enter('foo'); + expect($root.futureError).toMatch(/did not match/); + }); + + it('should toggle checkbox state', function() { + doc.append(''); + expect(_jQuery('input[name="test.input"]'). + attr('checked')).toBeTruthy(); + var chain = $root.dsl.input('test.input'); + chain.check(); + expect($window.angular.log).toContain('element(input)'); + expect($window.angular.log).toContain('element().trigger(click)'); + expect(_jQuery('input[name="test.input"]'). + attr('checked')).toBeFalsy(); + $window.angular.reset(); + chain.check(); + expect($window.angular.log).toContain('element(input)'); + expect($window.angular.log).toContain('element().trigger(click)'); + expect(_jQuery('input[name="test.input"]'). + attr('checked')).toBeTruthy(); + }); + + it('should return error if checkbox did not match', function() { + var chain = $root.dsl.input('test.input'); + chain.check(); + expect($root.futureError).toMatch(/did not match/); + }); + + it('should select option from radio group', function() { + doc.append( + '' + + '' + ); + // HACK! We don't know why this is sometimes false on chrome + _jQuery('input[name="0@test.input"][value="bar"]').attr('checked', true); + expect(_jQuery('input[name="0@test.input"][value="bar"]'). + attr('checked')).toBeTruthy(); + expect(_jQuery('input[name="0@test.input"][value="foo"]'). + attr('checked')).toBeFalsy(); + var chain = $root.dsl.input('test.input'); + chain.select('foo'); + expect($window.angular.log).toContain('element(input)'); + expect($window.angular.log).toContain('element().trigger(click)'); + expect(_jQuery('input[name="0@test.input"][value="bar"]'). + attr('checked')).toBeFalsy(); + expect(_jQuery('input[name="0@test.input"][value="foo"]'). + attr('checked')).toBeTruthy(); + }); + + it('should return error if radio button did not match', function() { + var chain = $root.dsl.input('test.input'); + chain.select('foo'); + expect($root.futureError).toMatch(/did not match/); + }); + }); + + }); +}); -- cgit v1.2.3