diff --git a/.gitignore b/.gitignore index a3896d7..2b85d59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ .DS_Store .project +.idea .children *.dart.js *.dart.js_ *.dart.js.map +*.iml packages packages/ pubspec.lock diff --git a/lib/dropdownToggle.dart b/lib/dropdownToggle.dart new file mode 100644 index 0000000..599b631 --- /dev/null +++ b/lib/dropdownToggle.dart @@ -0,0 +1,55 @@ +library angular.ui.dropdownToggle; + +import 'dart:html' as dom; +import "package:angular/angular.dart"; + +/** + * DropdownToggle Module. + */ +class DropdownToggleModule extends Module { + DropdownToggleModule() { + type(DropdownToggle); + } +} + +@NgDirective( + selector: '[dropdown-toggle]' +) +class DropdownToggle { + static dom.Element _openElement; + static var _closeMenu = (dom.MouseEvent evt) => {}; + + dom.Element element; + Scope scope; + + DropdownToggle(this.element, this.scope) { + this.element.parent.onClick.listen((dom.MouseEvent evt) => _closeMenu(evt)); + this.element.onClick.listen(toggleDropDown); + } + + void toggleDropDown(dom.MouseEvent event) { + bool elementWasOpen = (element == _openElement); + + event.preventDefault(); + event.stopPropagation(); + + if (_openElement != null) { + _closeMenu(null); + } + + if (!elementWasOpen && !element.classes.contains('disabled') && !element.attributes['disabled']) { + element.parent.classes.add('open'); + _openElement = element; + _closeMenu = (dom.MouseEvent event) { + if (event != null) { + event.preventDefault(); + event.stopPropagation(); + } + element.parent.classes.remove('open'); + _closeMenu = (dom.MouseEvent evt) => {}; + _openElement = null; + }; + dom.document.onClick.first.then((dom.MouseEvent evt) => _closeMenu(evt)); + } + } +} \ No newline at end of file diff --git a/test/_specs.dart b/test/_specs.dart new file mode 100644 index 0000000..7881f0d --- /dev/null +++ b/test/_specs.dart @@ -0,0 +1,243 @@ +library ng_specs; + + +import 'dart:html'; +import 'dart:mirrors' as mirror; +import 'package:unittest/unittest.dart' as unit; +import 'package:angular/angular.dart'; +import 'package:angular/mock/module.dart'; + +import 'jasmine_syntax.dart'; + +export 'dart:html'; +export 'jasmine_syntax.dart' hide main; +export 'package:unittest/unittest.dart'; +export 'package:unittest/mock.dart'; +export 'package:di/dynamic_injector.dart'; +export 'package:angular/angular.dart'; +export 'package:angular/mock/module.dart'; +export 'package:perf_api/perf_api.dart'; + +es(String html) { + var div = new DivElement(); + div.setInnerHtml(html, treeSanitizer: new NullTreeSanitizer()); + return div.nodes; +} + +e(String html) => es(html).first; + +renderedText(n, [bool notShadow = false]) { + if (n is List) { + return n.map((nn) => renderedText(nn)).join(""); + } + + if (n is Comment) return ''; + + if (!notShadow && n is Element && n.shadowRoot != null) { + var shadowText = n.shadowRoot.text; + var domText = renderedText(n, true); + return shadowText.replaceFirst("SHADOW-CONTENT", domText); + } + + if (n.nodes == null || n.nodes.length == 0) return n.text; + + return n.nodes.map((cn) => renderedText(cn)).join(""); +} + +Expect expect(actual, [unit.Matcher matcher = null]) { + if (matcher != null) { + unit.expect(actual, matcher); + } + return new Expect(actual); +} + +class Expect { + var actual; + var not; + Expect(this.actual) { + not = new NotExpect(this); + } + + toEqual(expected) => unit.expect(actual, unit.equals(expected)); + toContain(expected) => unit.expect(actual, unit.contains(expected)); + toBe(expected) => unit.expect(actual, + unit.predicate((actual) => identical(expected, actual), '$expected')); + toThrow([exception]) => unit.expect(actual, exception == null ? unit.throws : unit.throwsA(new ExceptionContains(exception))); + toBeFalsy() => unit.expect(actual, (v) => v == null ? true : v is bool ? v == false : false); + toBeTruthy() => unit.expect(actual, (v) => v is bool ? v == true : true); + toBeDefined() => unit.expect(actual, (v) => v != null); + toBeNull() => unit.expect(actual, unit.isNull); + toBeNotNull() => unit.expect(actual, unit.isNotNull); + + toHaveBeenCalled() => unit.expect(actual.called, true, reason: 'method not called'); + toHaveBeenCalledOnce() => unit.expect(actual.count, 1, reason: 'method invoked ${actual.count} expected once'); + toHaveBeenCalledWith([a,b,c,d,e,f]) => + unit.expect(actual.firstArgsMatch(a,b,c,d,e,f), true, + reason: 'method invoked with correct arguments'); + toHaveBeenCalledOnceWith([a,b,c,d,e,f]) => + unit.expect(actual.count == 1 && actual.firstArgsMatch(a,b,c,d,e,f), + true, + reason: 'method invoked once with correct arguments. (Called ${actual.count} times)'); + + toHaveClass(cls) => unit.expect(actual.classes.contains(cls), true, reason: ' Expected ${actual} to have css class ${cls}'); + + toEqualSelect(options) { + var actualOptions = []; + + for(var option in actual.querySelectorAll('option')) { + if (option.selected) { + actualOptions.add([option.value]); + } else { + actualOptions.add(option.value); + } + } + return unit.expect(actualOptions, options); + } + + toEqualValid() { +// TODO: implement onece we have forms + } + toEqualInvalid() { +// TODO: implement onece we have forms + } + toEqualPristine() { +// TODO: implement onece we have forms + } + toEqualDirty() { +// TODO: implement onece we have forms + } +} + +class NotExpect { + Expect expect; + get actual => expect.actual; + NotExpect(this.expect); + + toHaveBeenCalled() => unit.expect(actual.called, false, reason: 'method called'); + toThrow() => actual(); + + toHaveClass(cls) => unit.expect(actual.classes.contains(cls), false, reason: ' Expected ${actual} to not have css class ${cls}'); + toBe(expected) => unit.expect(actual, + unit.predicate((actual) => !identical(expected, actual), '$expected')); + toEqual(expected) => unit.expect(actual, + unit.predicate((actual) => expected != actual, '$expected')); + toContain(expected) => unit.expect(actual, + unit.predicate((actual) => !actual.contains(expected), '$expected')); +} + +class ExceptionContains extends unit.Matcher { + + final _expected; + + const ExceptionContains(this._expected); + + bool matches(item, Map matchState) { + if (item is String) { + return item.indexOf(_expected) >= 0; + } + return matches('$item', matchState); + } + + unit.Description describe(unit.Description description) => + description.add('exception contains ').addDescriptionOf(_expected); + + unit.Description describeMismatch(item, unit.Description mismatchDescription, + Map matchState, bool verbose) { + return super.describeMismatch('$item', mismatchDescription, matchState, + verbose); + } +} + +$(selector) { + return new JQuery(selector); +} + + +class GetterSetter { + Getter getter(String key) => null; + Setter setter(String key) => null; +} +var getterSetter = new GetterSetter(); + +class JQuery implements List { + List _list = []; + + JQuery([selector]) { + if (selector == null) { +// do nothing; + } else if (selector is String) { + _list.addAll(es(selector)); + } else if (selector is List) { + _list.addAll(selector); + } else if (selector is Node) { + add(selector); + } else { + throw selector; + } + } + + noSuchMethod(Invocation invocation) => mirror.reflect(_list).delegate(invocation); + + _toHtml(node, [bool outer = false]) { + if (node is Comment) { + return ''; + } else { + return outer ? node.outerHtml : node.innerHtml; + } + } + + accessor(Function getter, Function setter, [value, single=false]) { +// TODO(dart): ?value does not work, since value was passed. :-( + var setterMode = value != null; + var result = setterMode ? this : ''; + _list.forEach((node) { + if (setterMode) { + setter(node, value); + } else { + result = single ? getter(node) : '$result${getter(node)}'; + } + }); + return result; + } + + html([String html]) => accessor( + (n) => _toHtml(n), + (n, v) => n.setInnerHtml(v, treeSanitizer: new NullTreeSanitizer()), + html); + val([String text]) => accessor((n) => n.value, (n, v) => n.value = v); + text([String text]) => accessor((n) => n.text, (n, v) => n.text = v, text); + contents() => fold(new JQuery(), (jq, node) => jq..addAll(node.nodes)); + toString() => fold('', (html, node) => '$html${_toHtml(node, true)}'); + eq(num childIndex) => $(this[childIndex]); + remove(_) => forEach((n) => n.remove()); + attr([String name, String value]) => accessor( + (n) => n.attributes[name], + (n, v) => n.attributes[name] = v, + value, + true); + prop([String name]) => accessor( + (n) => getterSetter.getter(name)(n), + (n, v) => getterSetter.setter(name)(n, v), + null, + true); + textWithShadow() => fold('', (t, n) => '${t}${renderedText(n)}'); + find(selector) => fold(new JQuery(), (jq, n) => jq..addAll( + (n is Element ? (n as Element).querySelectorAll(selector) : []))); + hasClass(String name) => fold(false, (hasClass, node) => + hasClass || (node is Element && (node as Element).classes.contains(name))); + addClass(String name) => _list.forEach((node) => + (node is Element) ? (node as Element).classes.add(name) : null); + removeClass(String name) => _list.forEach((node) => + (node is Element) ? (node as Element).classes.remove(name) : null); + css(String name, [String value]) => accessor( + (Element n) => n.style.getPropertyValue(name), + (Element n, v) => n.style.setProperty(name, value), value); + children() => new JQuery(this[0].childNodes); +} + + +main() { + beforeEach(setUpInjector); + beforeEach(() => wrapFn(sync)); + afterEach(tearDownInjector); +} \ No newline at end of file diff --git a/test/jasmine_syntax.dart b/test/jasmine_syntax.dart new file mode 100644 index 0000000..6a4ce8c --- /dev/null +++ b/test/jasmine_syntax.dart @@ -0,0 +1,153 @@ +library jamine; + +import 'package:unittest/unittest.dart' as unit; +import 'package:angular/utils.dart' as utils; + +Function _wrapFn; + +_maybeWrapFn(fn) => () { + if (_wrapFn != null) { + _wrapFn(fn)(); + } else { + fn(); + } +}; + +it(name, fn) => unit.test(name, _maybeWrapFn(fn)); +iit(name, fn) => unit.solo_test(name, _maybeWrapFn(fn)); +xit(name, fn) {} +xdescribe(name, fn) {} +ddescribe(name, fn) => describe(name, fn, true); + + +class Describe { + Describe parent; + String name; + bool exclusive; + List beforeEachFns = []; + List afterEachFns = []; + + Describe(this.name, this.parent, [bool this.exclusive=false]) { + if (parent != null && parent.exclusive) { + exclusive = true; + } + } + + setUp() { + beforeEachFns.forEach((fn) => fn()); + } + + tearDown() { + afterEachFns.forEach((fn) => fn()); + } +} + +Describe currentDescribe = new Describe('', null); +bool ddescribeActive = false; + +describe(name, fn, [bool exclusive=false]) { + var lastDescribe = currentDescribe; + currentDescribe = new Describe(name, lastDescribe, exclusive); + if (exclusive) { + name = 'DDESCRIBE: $name'; + ddescribeActive = true; + } + try { + unit.group(name, () { + unit.setUp(currentDescribe.setUp); + fn(); + unit.tearDown(currentDescribe.tearDown); + }); + } finally { + currentDescribe = lastDescribe; + } +} + +beforeEach(fn) => currentDescribe.beforeEachFns.add(fn); +afterEach(fn) => currentDescribe.afterEachFns.insert(0, fn); + +wrapFn(fn) => _wrapFn = fn; + +var jasmine = new Jasmine(); + +class SpyFunctionInvocationResult { + final List args; + SpyFunctionInvocationResult(this.args); +} + +class SpyFunction { + String name; + List> invocations = []; + List> invocationsWithoutTrailingNulls = []; + var _andCallFakeFn; + + SpyFunction([this.name]); + call([a0, a1, a2, a3, a4, a5]) { + var args = []; + args.add(a0); + args.add(a1); + args.add(a2); + args.add(a3); + args.add(a4); + args.add(a5); + invocations.add(args); + + var withoutNulls = new List.from(args); + while (!withoutNulls.isEmpty && withoutNulls.last == null) { + withoutNulls.removeLast(); + } + invocationsWithoutTrailingNulls.add(withoutNulls); + + if (_andCallFakeFn != null) { + utils.relaxFnApply(_andCallFakeFn, args); + } + } + + andCallFake(fn) { + _andCallFakeFn = fn; + return this; + } + + reset() => invocations = []; + + num get count => invocations.length; + bool get called => count > 0; + + num get callCount => count; + get argsForCall => invocationsWithoutTrailingNulls; + + firstArgsMatch(a,b,c,d,e,f) { + var fi = invocations.first; + assert(fi.length == 6); + if ("${fi[0]}" != "$a") return false; + if ("${fi[1]}" != "$b") return false; + if ("${fi[2]}" != "$c") return false; + if ("${fi[3]}" != "$d") return false; + if ("${fi[4]}" != "$e") return false; + if ("${fi[5]}" != "$f") return false; + + return true; + } + + get mostRecentCall { + if (invocations.isEmpty) { + throw ["No calls"]; + } + return new SpyFunctionInvocationResult(invocations.last); + } +} + +class Jasmine { + createSpy([String name]) { + return new SpyFunction(name); + } + + SpyFunction spyOn(receiver, methodName) { + throw ["spyOn not implemented"]; + } +} + +main(){ + unit.setUp(currentDescribe.setUp); + unit.tearDown(currentDescribe.tearDown); +} \ No newline at end of file diff --git a/test/tests/dropdownToggle_tests.dart b/test/tests/dropdownToggle_tests.dart new file mode 100644 index 0000000..0322623 --- /dev/null +++ b/test/tests/dropdownToggle_tests.dart @@ -0,0 +1,59 @@ +part of angular.ui.test; + +void dropdownToggleTests() { + describe('Testing dropdownToggle:', () { + TestBed _; + beforeEach(setUpInjector); + beforeEach(module((Module module) { + module.type(DropdownToggle); + })); + beforeEach(inject((TestBed tb) => _ = tb)); + + afterEach(tearDownInjector); + + dom.Element dropdown() { + return _.compile(''); + } + + it('should toggle on `a` click', () { + dom.Element elm = dropdown(); + expect(elm.classes.contains('open')).toBe(false); + elm.querySelector('a').click(); + expect(elm.classes.contains('open')).toBe(true); + elm.querySelector('a').click(); + expect(elm.classes.contains('open')).toBe(false); + }); + + it('should toggle on `ul` click', () { + dom.Element elm = dropdown(); + expect(elm.classes.contains('open')).toBe(false); + elm.querySelector('ul').click(); + expect(elm.classes.contains('open')).toBe(true); + elm.querySelector('ul').click(); + expect(elm.classes.contains('open')).toBe(false); + }); + + it('should close on elm click', () { + dom.Element elm = dropdown(); + elm.querySelector('a').click(); + elm.click(); + expect(elm.classes.contains('open')).toBe(false); + }); + + it('should close on document click', () { + dom.Element elm = dropdown(); + elm.querySelector('a').click(); + dom.document.body.click(); + expect(elm.classes.contains('open')).toBe(false); + }); + + it('should only allow one dropdown to be open at once', () { + dom.Element elm1 = dropdown(); + dom.Element elm2 = dropdown(); + elm1.querySelector('a').click(); + elm2.querySelector('a').click(); + expect(elm1.classes.contains('open')).toBe(false); + expect(elm2.classes.contains('open')).toBe(true); + }); + }); +} \ No newline at end of file diff --git a/test/ui_tests.dart b/test/ui_tests.dart index 16ab463..45e64ad 100644 --- a/test/ui_tests.dart +++ b/test/ui_tests.dart @@ -10,22 +10,20 @@ library angular.ui.test; import 'dart:html' as dom; // -import 'package:unittest/unittest.dart'; import 'package:unittest/html_enhanced_config.dart'; - -import 'package:di/di.dart'; -import 'package:angular/angular.dart'; -import 'package:angular/mock/module.dart'; +import '_specs.dart'; //import 'package:angular_ui/position.dart'; import 'package:angular_ui/transition.dart'; import 'package:angular_ui/buttons.dart'; import 'package:angular_ui/collapse.dart'; +import 'package:angular_ui/dropdownToggle.dart'; //part 'tests/position_tests.dart'; part 'tests/transition_tests.dart'; part 'tests/buttons_tests.dart'; part 'tests/collapse_tests.dart'; +part 'tests/dropdownToggle_tests.dart'; void main() { print('Running unit tests for Angular UI library.'); @@ -34,6 +32,7 @@ void main() { // test('Position', () => positionTests()); test('Transition', () => transitionTests()); test('Buttons', () => buttonsTests()); + test('DropdownToggle', () => dropdownToggleTests()); // test('Collapse', () => collapseTests()); }); } \ No newline at end of file