From 04cf441c5ae18400c6b4865b0b37a71718dc9d46 Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Thu, 31 Jul 2014 19:29:33 -0300 Subject: Add web-ui based on previous code --- web-ui/.bowerrc | 3 + web-ui/.jshintrc | 37 + web-ui/.ruby-gemset | 1 + web-ui/.ruby-version | 1 + web-ui/Gemfile | 4 + web-ui/Gemfile.lock | 18 + web-ui/Gruntfile.js | 465 +++++ web-ui/README.md | 2 + web-ui/app/404.html | 157 ++ web-ui/app/favicon.ico | 0 web-ui/app/fonts/NewsCycleBold.ttf | Bin 0 -> 73912 bytes web-ui/app/fonts/NewsCycleRegular.ttf | Bin 0 -> 193996 bytes web-ui/app/fonts/OpenSans-Bold.woff | Bin 0 -> 14504 bytes web-ui/app/fonts/OpenSans-BoldItalic.woff | Bin 0 -> 15488 bytes web-ui/app/fonts/OpenSans-Extrabold.woff | Bin 0 -> 15312 bytes web-ui/app/fonts/OpenSans-ExtraboldItalic.woff | Bin 0 -> 15932 bytes web-ui/app/fonts/OpenSans-Italic.woff | Bin 0 -> 15768 bytes web-ui/app/fonts/OpenSans-Light.woff | Bin 0 -> 15048 bytes web-ui/app/fonts/OpenSans-Semibold.woff | Bin 0 -> 15236 bytes web-ui/app/fonts/OpenSans-SemiboldItalic.woff | Bin 0 -> 15736 bytes web-ui/app/fonts/OpenSans.woff | Bin 0 -> 14604 bytes web-ui/app/fonts/OpenSansLight-Italic.woff | Bin 0 -> 15956 bytes web-ui/app/index.html | 84 + web-ui/app/js/dispatchers/left_pane_dispatcher.js | 51 + .../app/js/dispatchers/middle_pane_dispatcher.js | 36 + web-ui/app/js/dispatchers/right_pane_dispatcher.js | 93 + web-ui/app/js/foundation/off_canvas.js | 21 + web-ui/app/js/helpers/contenttype.js | 164 ++ web-ui/app/js/helpers/iterator.js | 43 + web-ui/app/js/helpers/triggering.js | 13 + web-ui/app/js/helpers/view_helper.js | 145 ++ web-ui/app/js/lib/highlightRegex.js | 127 ++ web-ui/app/js/lib/html-sanitizer.js | 1064 ++++++++++ web-ui/app/js/lib/html4-defs.js | 640 ++++++ web-ui/app/js/lib/html_whitelister.js | 70 + web-ui/app/js/mail_list/domain/refresher.js | 25 + web-ui/app/js/mail_list/ui/mail_item_factory.js | 49 + .../app/js/mail_list/ui/mail_items/draft_item.js | 55 + .../mail_list/ui/mail_items/generic_mail_item.js | 97 + web-ui/app/js/mail_list/ui/mail_items/mail_item.js | 63 + web-ui/app/js/mail_list/ui/mail_items/sent_item.js | 62 + web-ui/app/js/mail_list/ui/mail_list.js | 185 ++ .../app/js/mail_list_actions/ui/compose_trigger.js | 31 + .../js/mail_list_actions/ui/delete_many_trigger.js | 31 + .../js/mail_list_actions/ui/mail_list_actions.js | 50 + .../mail_list_actions/ui/mark_as_unread_trigger.js | 31 + .../ui/mark_many_as_read_trigger.js | 31 + .../js/mail_list_actions/ui/pagination_trigger.js | 50 + .../app/js/mail_list_actions/ui/refresh_trigger.js | 28 + .../ui/toggle_check_all_trigger.js | 33 + web-ui/app/js/mail_view/data/mail_builder.js | 79 + web-ui/app/js/mail_view/data/mail_sender.js | 74 + web-ui/app/js/mail_view/ui/compose_box.js | 63 + web-ui/app/js/mail_view/ui/draft_box.js | 74 + web-ui/app/js/mail_view/ui/draft_save_status.js | 25 + web-ui/app/js/mail_view/ui/forward_box.js | 66 + web-ui/app/js/mail_view/ui/mail_actions.js | 70 + web-ui/app/js/mail_view/ui/mail_view.js | 242 +++ .../js/mail_view/ui/no_message_selected_pane.js | 25 + web-ui/app/js/mail_view/ui/recipients/recipient.js | 36 + .../app/js/mail_view/ui/recipients/recipients.js | 127 ++ .../js/mail_view/ui/recipients/recipients_input.js | 140 ++ .../mail_view/ui/recipients/recipients_iterator.js | 41 + web-ui/app/js/mail_view/ui/reply_box.js | 102 + web-ui/app/js/mail_view/ui/reply_section.js | 102 + web-ui/app/js/mail_view/ui/send_button.js | 85 + web-ui/app/js/main.js | 58 + web-ui/app/js/mixins/with_compose_inline.js | 63 + .../app/js/mixins/with_enable_disable_on_event.js | 34 + web-ui/app/js/mixins/with_hide_and_show.js | 14 + web-ui/app/js/mixins/with_mail_edit_base.js | 182 ++ web-ui/app/js/monkey_patching/all.js | 1 + web-ui/app/js/monkey_patching/array.js | 11 + web-ui/app/js/monkey_patching/post_message.js | 16 + web-ui/app/js/page/default.js | 87 + web-ui/app/js/page/events.js | 168 ++ web-ui/app/js/page/pane_contract_expand.js | 34 + web-ui/app/js/page/router.js | 51 + web-ui/app/js/page/router/url_params.js | 40 + web-ui/app/js/search/results_highlighter.js | 53 + web-ui/app/js/search/search_trigger.js | 68 + web-ui/app/js/services/delete_service.js | 43 + web-ui/app/js/services/mail_service.js | 254 +++ web-ui/app/js/services/model/mail.js | 147 ++ web-ui/app/js/tags/data/tags.js | 42 + web-ui/app/js/tags/ui/tag.js | 94 + web-ui/app/js/tags/ui/tag_base.js | 24 + web-ui/app/js/tags/ui/tag_list.js | 93 + web-ui/app/js/tags/ui/tag_shortcut.js | 68 + web-ui/app/js/user_alerts/ui/user_alerts.js | 35 + web-ui/app/js/views/i18n.js | 18 + web-ui/app/js/views/recipientListFormatter.js | 16 + web-ui/app/js/views/templates.js | 46 + web-ui/app/locales/en/translation.json | 69 + web-ui/app/locales/pt/translation.json | 20 + web-ui/app/locales/sv/translation.json | 66 + web-ui/app/robots.txt | 3 + web-ui/app/scss/_alerts.scss | 14 + web-ui/app/scss/_colors.scss | 13 + web-ui/app/scss/_compose.scss | 92 + web-ui/app/scss/_mascot.scss | 32 + web-ui/app/scss/_mixins.scss | 205 ++ web-ui/app/scss/_read.scss | 105 + web-ui/app/scss/_reply.scss | 36 + web-ui/app/scss/_security.scss | 47 + web-ui/app/scss/foundation.scss | 2066 ++++++++++++++++++++ web-ui/app/scss/main.scss | 46 + web-ui/app/scss/news-cycle.scss | 13 + web-ui/app/scss/opensans.scss | 61 + web-ui/app/scss/reset.scss | 421 ++++ web-ui/app/scss/styles.scss | 610 ++++++ web-ui/app/templates/compose/compose_box.hbs | 23 + web-ui/app/templates/compose/fixed_recipient.hbs | 6 + web-ui/app/templates/compose/inline_box.hbs | 18 + web-ui/app/templates/compose/recipient_input.hbs | 1 + web-ui/app/templates/compose/recipients.hbs | 19 + web-ui/app/templates/compose/reply_section.hbs | 6 + web-ui/app/templates/mail_actions/actions_box.hbs | 6 + .../app/templates/mail_actions/compose_trigger.hbs | 3 + .../templates/mail_actions/pagination_trigger.hbs | 3 + .../app/templates/mail_actions/refresh_trigger.hbs | 3 + web-ui/app/templates/mails/full_view.hbs | 87 + web-ui/app/templates/mails/mail_actions.hbs | 6 + web-ui/app/templates/mails/sent.hbs | 23 + web-ui/app/templates/mails/single.hbs | 19 + web-ui/app/templates/no_message_selected.hbs | 3 + web-ui/app/templates/search/search_trigger.hbs | 3 + web-ui/app/templates/tags/shortcut.hbs | 9 + web-ui/app/templates/tags/tag.hbs | 3 + web-ui/app/templates/tags/tag_inner.hbs | 4 + web-ui/app/templates/tags/tag_list.hbs | 3 + web-ui/app/templates/user_alerts/message.hbs | 1 + web-ui/bower.json | 21 + web-ui/control-tower.yml | 3 + web-ui/go | 3 + web-ui/karma.conf.js | 85 + web-ui/package.json | 58 + .../spec/dispatchers/left_pane_dispatcher.spec.js | 79 + .../dispatchers/middle_pane_dispatchers.spec.js | 27 + .../spec/dispatchers/right_pane_dispatcher.spec.js | 90 + web-ui/test/spec/helpers/view_helper.spec.js | 76 + .../ui/mail_items/generic_mail_item.spec.js | 136 ++ .../spec/mail_list/ui/mail_items/mail_item.spec.js | 40 + web-ui/test/spec/mail_list/ui/mail_list.spec.js | 275 +++ .../mail_list_actions/ui/compose_trigger.spec.js | 16 + .../spec/mail_list_actions/ui/mail_actions.spec.js | 9 + .../ui/pagination_trigger.spec.js | 26 + .../test/spec/mail_view/data/mail_builder.spec.js | 110 ++ .../test/spec/mail_view/data/mail_sender.spec.js | 70 + web-ui/test/spec/mail_view/ui/compose_box.spec.js | 132 ++ web-ui/test/spec/mail_view/ui/draft_box.spec.js | 67 + .../spec/mail_view/ui/draft_save_status.spec.js | 26 + web-ui/test/spec/mail_view/ui/forward_box.spec.js | 90 + web-ui/test/spec/mail_view/ui/mail_actions.spec.js | 63 + web-ui/test/spec/mail_view/ui/mail_view.spec.js | 247 +++ .../mail_view/ui/recipients/recipients.spec.js | 36 + .../ui/recipients/recipients_input.spec.js | 104 + .../ui/recipients/recipients_iterator.spec.js | 101 + web-ui/test/spec/mail_view/ui/reply_box.spec.js | 105 + .../test/spec/mail_view/ui/reply_section.spec.js | 97 + web-ui/test/spec/mail_view/ui/send_button.spec.js | 91 + .../test/spec/mixins/with_mail_edit_base.spec.js | 94 + web-ui/test/spec/page/pane_contract_expand.spec.js | 67 + web-ui/test/spec/page/router.spec.js | 70 + web-ui/test/spec/page/router/url_params.spec.js | 68 + web-ui/test/spec/search/search_trigger.spec.js | 78 + web-ui/test/spec/services/delete_service.spec.js | 54 + web-ui/test/spec/services/mail_service.spec.js | 307 +++ web-ui/test/spec/services/model/mail.spec.js | 116 ++ web-ui/test/spec/tags/data/tags.spec.js | 39 + web-ui/test/spec/tags/ui/tag.spec.js | 151 ++ web-ui/test/spec/tags/ui/tag_list.spec.js | 75 + web-ui/test/spec/tags/ui/tag_shortcut.spec.js | 35 + .../test/spec/user_alerts/ui/user_alerts.spec.js | 23 + web-ui/test/test-main.js | 57 + web-ui/test/test_data.js | 170 ++ 176 files changed, 15026 insertions(+) create mode 100644 web-ui/.bowerrc create mode 100644 web-ui/.jshintrc create mode 100644 web-ui/.ruby-gemset create mode 100644 web-ui/.ruby-version create mode 100644 web-ui/Gemfile create mode 100644 web-ui/Gemfile.lock create mode 100644 web-ui/Gruntfile.js create mode 100644 web-ui/README.md create mode 100644 web-ui/app/404.html create mode 100644 web-ui/app/favicon.ico create mode 100644 web-ui/app/fonts/NewsCycleBold.ttf create mode 100644 web-ui/app/fonts/NewsCycleRegular.ttf create mode 100644 web-ui/app/fonts/OpenSans-Bold.woff create mode 100644 web-ui/app/fonts/OpenSans-BoldItalic.woff create mode 100644 web-ui/app/fonts/OpenSans-Extrabold.woff create mode 100644 web-ui/app/fonts/OpenSans-ExtraboldItalic.woff create mode 100644 web-ui/app/fonts/OpenSans-Italic.woff create mode 100644 web-ui/app/fonts/OpenSans-Light.woff create mode 100644 web-ui/app/fonts/OpenSans-Semibold.woff create mode 100644 web-ui/app/fonts/OpenSans-SemiboldItalic.woff create mode 100644 web-ui/app/fonts/OpenSans.woff create mode 100644 web-ui/app/fonts/OpenSansLight-Italic.woff create mode 100644 web-ui/app/index.html create mode 100644 web-ui/app/js/dispatchers/left_pane_dispatcher.js create mode 100644 web-ui/app/js/dispatchers/middle_pane_dispatcher.js create mode 100644 web-ui/app/js/dispatchers/right_pane_dispatcher.js create mode 100644 web-ui/app/js/foundation/off_canvas.js create mode 100644 web-ui/app/js/helpers/contenttype.js create mode 100644 web-ui/app/js/helpers/iterator.js create mode 100644 web-ui/app/js/helpers/triggering.js create mode 100644 web-ui/app/js/helpers/view_helper.js create mode 100644 web-ui/app/js/lib/highlightRegex.js create mode 100644 web-ui/app/js/lib/html-sanitizer.js create mode 100644 web-ui/app/js/lib/html4-defs.js create mode 100644 web-ui/app/js/lib/html_whitelister.js create mode 100644 web-ui/app/js/mail_list/domain/refresher.js create mode 100644 web-ui/app/js/mail_list/ui/mail_item_factory.js create mode 100644 web-ui/app/js/mail_list/ui/mail_items/draft_item.js create mode 100644 web-ui/app/js/mail_list/ui/mail_items/generic_mail_item.js create mode 100644 web-ui/app/js/mail_list/ui/mail_items/mail_item.js create mode 100644 web-ui/app/js/mail_list/ui/mail_items/sent_item.js create mode 100644 web-ui/app/js/mail_list/ui/mail_list.js create mode 100644 web-ui/app/js/mail_list_actions/ui/compose_trigger.js create mode 100644 web-ui/app/js/mail_list_actions/ui/delete_many_trigger.js create mode 100644 web-ui/app/js/mail_list_actions/ui/mail_list_actions.js create mode 100644 web-ui/app/js/mail_list_actions/ui/mark_as_unread_trigger.js create mode 100644 web-ui/app/js/mail_list_actions/ui/mark_many_as_read_trigger.js create mode 100644 web-ui/app/js/mail_list_actions/ui/pagination_trigger.js create mode 100644 web-ui/app/js/mail_list_actions/ui/refresh_trigger.js create mode 100644 web-ui/app/js/mail_list_actions/ui/toggle_check_all_trigger.js create mode 100644 web-ui/app/js/mail_view/data/mail_builder.js create mode 100644 web-ui/app/js/mail_view/data/mail_sender.js create mode 100644 web-ui/app/js/mail_view/ui/compose_box.js create mode 100644 web-ui/app/js/mail_view/ui/draft_box.js create mode 100644 web-ui/app/js/mail_view/ui/draft_save_status.js create mode 100644 web-ui/app/js/mail_view/ui/forward_box.js create mode 100644 web-ui/app/js/mail_view/ui/mail_actions.js create mode 100644 web-ui/app/js/mail_view/ui/mail_view.js create mode 100644 web-ui/app/js/mail_view/ui/no_message_selected_pane.js create mode 100644 web-ui/app/js/mail_view/ui/recipients/recipient.js create mode 100644 web-ui/app/js/mail_view/ui/recipients/recipients.js create mode 100644 web-ui/app/js/mail_view/ui/recipients/recipients_input.js create mode 100644 web-ui/app/js/mail_view/ui/recipients/recipients_iterator.js create mode 100644 web-ui/app/js/mail_view/ui/reply_box.js create mode 100644 web-ui/app/js/mail_view/ui/reply_section.js create mode 100644 web-ui/app/js/mail_view/ui/send_button.js create mode 100644 web-ui/app/js/main.js create mode 100644 web-ui/app/js/mixins/with_compose_inline.js create mode 100644 web-ui/app/js/mixins/with_enable_disable_on_event.js create mode 100644 web-ui/app/js/mixins/with_hide_and_show.js create mode 100644 web-ui/app/js/mixins/with_mail_edit_base.js create mode 100644 web-ui/app/js/monkey_patching/all.js create mode 100644 web-ui/app/js/monkey_patching/array.js create mode 100644 web-ui/app/js/monkey_patching/post_message.js create mode 100644 web-ui/app/js/page/default.js create mode 100644 web-ui/app/js/page/events.js create mode 100644 web-ui/app/js/page/pane_contract_expand.js create mode 100644 web-ui/app/js/page/router.js create mode 100644 web-ui/app/js/page/router/url_params.js create mode 100644 web-ui/app/js/search/results_highlighter.js create mode 100644 web-ui/app/js/search/search_trigger.js create mode 100644 web-ui/app/js/services/delete_service.js create mode 100644 web-ui/app/js/services/mail_service.js create mode 100644 web-ui/app/js/services/model/mail.js create mode 100644 web-ui/app/js/tags/data/tags.js create mode 100644 web-ui/app/js/tags/ui/tag.js create mode 100644 web-ui/app/js/tags/ui/tag_base.js create mode 100644 web-ui/app/js/tags/ui/tag_list.js create mode 100644 web-ui/app/js/tags/ui/tag_shortcut.js create mode 100644 web-ui/app/js/user_alerts/ui/user_alerts.js create mode 100644 web-ui/app/js/views/i18n.js create mode 100644 web-ui/app/js/views/recipientListFormatter.js create mode 100644 web-ui/app/js/views/templates.js create mode 100644 web-ui/app/locales/en/translation.json create mode 100644 web-ui/app/locales/pt/translation.json create mode 100644 web-ui/app/locales/sv/translation.json create mode 100644 web-ui/app/robots.txt create mode 100644 web-ui/app/scss/_alerts.scss create mode 100644 web-ui/app/scss/_colors.scss create mode 100644 web-ui/app/scss/_compose.scss create mode 100644 web-ui/app/scss/_mascot.scss create mode 100644 web-ui/app/scss/_mixins.scss create mode 100644 web-ui/app/scss/_read.scss create mode 100644 web-ui/app/scss/_reply.scss create mode 100644 web-ui/app/scss/_security.scss create mode 100644 web-ui/app/scss/foundation.scss create mode 100644 web-ui/app/scss/main.scss create mode 100644 web-ui/app/scss/news-cycle.scss create mode 100644 web-ui/app/scss/opensans.scss create mode 100644 web-ui/app/scss/reset.scss create mode 100644 web-ui/app/scss/styles.scss create mode 100644 web-ui/app/templates/compose/compose_box.hbs create mode 100644 web-ui/app/templates/compose/fixed_recipient.hbs create mode 100644 web-ui/app/templates/compose/inline_box.hbs create mode 100644 web-ui/app/templates/compose/recipient_input.hbs create mode 100644 web-ui/app/templates/compose/recipients.hbs create mode 100644 web-ui/app/templates/compose/reply_section.hbs create mode 100644 web-ui/app/templates/mail_actions/actions_box.hbs create mode 100644 web-ui/app/templates/mail_actions/compose_trigger.hbs create mode 100644 web-ui/app/templates/mail_actions/pagination_trigger.hbs create mode 100644 web-ui/app/templates/mail_actions/refresh_trigger.hbs create mode 100644 web-ui/app/templates/mails/full_view.hbs create mode 100644 web-ui/app/templates/mails/mail_actions.hbs create mode 100644 web-ui/app/templates/mails/sent.hbs create mode 100644 web-ui/app/templates/mails/single.hbs create mode 100644 web-ui/app/templates/no_message_selected.hbs create mode 100644 web-ui/app/templates/search/search_trigger.hbs create mode 100644 web-ui/app/templates/tags/shortcut.hbs create mode 100644 web-ui/app/templates/tags/tag.hbs create mode 100644 web-ui/app/templates/tags/tag_inner.hbs create mode 100644 web-ui/app/templates/tags/tag_list.hbs create mode 100644 web-ui/app/templates/user_alerts/message.hbs create mode 100644 web-ui/bower.json create mode 100644 web-ui/control-tower.yml create mode 100755 web-ui/go create mode 100644 web-ui/karma.conf.js create mode 100644 web-ui/package.json create mode 100644 web-ui/test/spec/dispatchers/left_pane_dispatcher.spec.js create mode 100644 web-ui/test/spec/dispatchers/middle_pane_dispatchers.spec.js create mode 100644 web-ui/test/spec/dispatchers/right_pane_dispatcher.spec.js create mode 100644 web-ui/test/spec/helpers/view_helper.spec.js create mode 100644 web-ui/test/spec/mail_list/ui/mail_items/generic_mail_item.spec.js create mode 100644 web-ui/test/spec/mail_list/ui/mail_items/mail_item.spec.js create mode 100644 web-ui/test/spec/mail_list/ui/mail_list.spec.js create mode 100644 web-ui/test/spec/mail_list_actions/ui/compose_trigger.spec.js create mode 100644 web-ui/test/spec/mail_list_actions/ui/mail_actions.spec.js create mode 100644 web-ui/test/spec/mail_list_actions/ui/pagination_trigger.spec.js create mode 100644 web-ui/test/spec/mail_view/data/mail_builder.spec.js create mode 100644 web-ui/test/spec/mail_view/data/mail_sender.spec.js create mode 100644 web-ui/test/spec/mail_view/ui/compose_box.spec.js create mode 100644 web-ui/test/spec/mail_view/ui/draft_box.spec.js create mode 100644 web-ui/test/spec/mail_view/ui/draft_save_status.spec.js create mode 100644 web-ui/test/spec/mail_view/ui/forward_box.spec.js create mode 100644 web-ui/test/spec/mail_view/ui/mail_actions.spec.js create mode 100644 web-ui/test/spec/mail_view/ui/mail_view.spec.js create mode 100644 web-ui/test/spec/mail_view/ui/recipients/recipients.spec.js create mode 100644 web-ui/test/spec/mail_view/ui/recipients/recipients_input.spec.js create mode 100644 web-ui/test/spec/mail_view/ui/recipients/recipients_iterator.spec.js create mode 100644 web-ui/test/spec/mail_view/ui/reply_box.spec.js create mode 100644 web-ui/test/spec/mail_view/ui/reply_section.spec.js create mode 100644 web-ui/test/spec/mail_view/ui/send_button.spec.js create mode 100644 web-ui/test/spec/mixins/with_mail_edit_base.spec.js create mode 100644 web-ui/test/spec/page/pane_contract_expand.spec.js create mode 100644 web-ui/test/spec/page/router.spec.js create mode 100644 web-ui/test/spec/page/router/url_params.spec.js create mode 100644 web-ui/test/spec/search/search_trigger.spec.js create mode 100644 web-ui/test/spec/services/delete_service.spec.js create mode 100644 web-ui/test/spec/services/mail_service.spec.js create mode 100644 web-ui/test/spec/services/model/mail.spec.js create mode 100644 web-ui/test/spec/tags/data/tags.spec.js create mode 100644 web-ui/test/spec/tags/ui/tag.spec.js create mode 100644 web-ui/test/spec/tags/ui/tag_list.spec.js create mode 100644 web-ui/test/spec/tags/ui/tag_shortcut.spec.js create mode 100644 web-ui/test/spec/user_alerts/ui/user_alerts.spec.js create mode 100644 web-ui/test/test-main.js create mode 100644 web-ui/test/test_data.js diff --git a/web-ui/.bowerrc b/web-ui/.bowerrc new file mode 100644 index 00000000..5773025b --- /dev/null +++ b/web-ui/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "app/bower_components" +} diff --git a/web-ui/.jshintrc b/web-ui/.jshintrc new file mode 100644 index 00000000..a3714ba9 --- /dev/null +++ b/web-ui/.jshintrc @@ -0,0 +1,37 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": false, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "smarttabs": true, + "strict": true, + "trailing": true, + "undef": true, + "validthis": true, + "predef": [ + "$", + "jQuery", + "before", + "beforeEach", + "define", + "describe", + "describeComponent", + "describeMixin", + "expect", + "it", + "requirejs", + "setupComponent", + "spyOn", + "spyOnEvent" + ] +} diff --git a/web-ui/.ruby-gemset b/web-ui/.ruby-gemset new file mode 100644 index 00000000..66924d5a --- /dev/null +++ b/web-ui/.ruby-gemset @@ -0,0 +1 @@ +smail-front diff --git a/web-ui/.ruby-version b/web-ui/.ruby-version new file mode 100644 index 00000000..eca07e4c --- /dev/null +++ b/web-ui/.ruby-version @@ -0,0 +1 @@ +2.1.2 diff --git a/web-ui/Gemfile b/web-ui/Gemfile new file mode 100644 index 00000000..e9cb427d --- /dev/null +++ b/web-ui/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem 'compass' +gem 'flight-control-tower' diff --git a/web-ui/Gemfile.lock b/web-ui/Gemfile.lock new file mode 100644 index 00000000..f926d679 --- /dev/null +++ b/web-ui/Gemfile.lock @@ -0,0 +1,18 @@ +GEM + remote: https://rubygems.org/ + specs: + chunky_png (1.3.1) + compass (0.12.6) + chunky_png (~> 1.2) + fssm (>= 0.2.7) + sass (~> 3.2.19) + flight-control-tower (0.2.1) + fssm (0.2.10) + sass (3.2.19) + +PLATFORMS + ruby + +DEPENDENCIES + compass + flight-control-tower diff --git a/web-ui/Gruntfile.js b/web-ui/Gruntfile.js new file mode 100644 index 00000000..01c53007 --- /dev/null +++ b/web-ui/Gruntfile.js @@ -0,0 +1,465 @@ +'use strict'; + +// # Globbing +// for performance reasons we're only matching one level down: +// 'test/spec/{,*/}*.js' +// use this if you want to recursively match all subfolders: +// 'test/spec/**/*.js' + +module.exports = function (grunt) { + var exec = require('child_process').exec; + + // Load grunt tasks automatically + require('load-grunt-tasks')(grunt); + + // Time how long tasks take. Can help when optimizing build times + require('time-grunt')(grunt); + + // Define the configuration for all the tasks + grunt.initConfig({ + compress: { + main: { + options: { + archive: 'archive.zip' + }, + files: [ + {src: ['dist/**/*']} // includes files in path + ] + } + }, + + // Project settings + yeoman: { + // configurable paths + app: require('./bower.json').appPath || 'app', + dist: 'dist' + }, + + // Watches files for changes and runs tasks based on the changed files + watch: { + js: { + files: ['{.tmp,<%= yeoman.app %>}/js/{,*/}*.js'], + tasks: ['newer:jshint:all', 'update-control-tower'] + }, + jsTest: { + files: ['test/spec/{,*/}*.js'], + tasks: ['newer:jshint:test', 'karma'] + }, + compass: { + files: ['<%= yeoman.app %>/scss/{,*/}*.scss'], + tasks: ['compass:dev'] + }, + html: { + files: ['<%= yeoman.app %>/index.html'], + livereload: true + }, + handlebars: { + files: ['<%= yeoman.app %>/templates/**/*.hbs'], + tasks: ['handlebars:dev'] + }, + gruntfile: { + files: ['Gruntfile.js'] + }, + options: { + livereload: true + } + }, + + compass: { + dist: { + options: { + sassDir: '<%= yeoman.app %>/scss', + cssDir: '<%= yeoman.dist %>/css', + environment: 'production' + } + }, + dev: { + options: { + sassDir: '<%= yeoman.app %>/scss', + cssDir: '.tmp/css' + } + } + }, + + // The actual grunt server settings + connect: { + options: { + port: 9000, + // Change this from '0.0.0.0' to 'localhost' to limit access from outside. + hostname: '0.0.0.0', + livereload: true + }, + dev: { + options: { + base: [ + '.tmp', + '<%= yeoman.app %>' + ] + }, + livereload: true + }, + test: { + options: { + port: 9001, + base: [ + '.tmp', + 'test', + '<%= yeoman.app %>' + ] + } + }, + dist: { + options: { + base: '<%= yeoman.dist %>' + } + } + }, + + // Make sure code styles are up to par and there are no obvious mistakes + jshint: { + options: { + jshintrc: '.jshintrc', + reporter: require('jshint-stylish') + }, + all: [ + 'Gruntfile.js', + '<%= yeoman.app %>/scripts/{,*/}*.js' + ], + test: { + options: { + jshintrc: '.jshintrc' + }, + src: ['test/spec/{,*/}*.js'] + } + }, + + // Empties folders to start fresh + clean: { + dist: { + files: [{ + dot: true, + src: [ + '.tmp', + '<%= yeoman.dist %>/*', + ] + }] + }, + server: ['.tmp', '<%= yeoman.app %>/js/generated'] + }, + + uglify: { + dist: { + files: [{ + expand: true, + cwd: '.tmp', + src: 'app.min.js', + dest: 'dist' + }] + } + }, + + cssmin: { + minify: { + expand: true, + cwd: '<%= yeoman.app %>/css/', + src: ['*.css', '!*.min.css'], + dest: '<%= yeoman.dist %>/css/' + } + }, + + // The following *-min tasks produce minified files in the dist folder + imagemin: { + dist: { + files: [{ + expand: true, + cwd: '<%= yeoman.app %>/images', + src: '{,*/}*.{png,jpg,jpeg,gif}', + dest: '<%= yeoman.dist %>/images' + }] + } + }, + + svgmin: { + dist: { + files: [{ + expand: true, + cwd: '<%= yeoman.app %>/images', + src: '{,*/}*.svg', + dest: '<%= yeoman.dist %>/images' + }] + } + }, + + htmlmin: { + dist: { + options: { + // Optional configurations that you can uncomment to use + // removeCommentsFromCDATA: true, + // collapseBooleanAttributes: true, + // removeAttributeQuotes: true, + // removeRedundantAttributes: true, + // useShortDoctype: true, + // removeEmptyAttributes: true, + // removeOptionalTags: true*/ + }, + files: [{ + expand: true, + cwd: '<%= yeoman.app %>', + src: ['*.html', 'templates/*.html'], + dest: '<%= yeoman.dist %>' + }] + } + }, + + // Allow the use of non-minsafe AngularJS files. Automatically makes it + // minsafe compatible so Uglify does not destroy the ng references + // Replace Google CDN references + cdnify: { + dist: { + html: ['<%= yeoman.dist %>/*.html'] + } + }, + + // Copies remaining files to places other tasks can use + copy: { + dist: { + files: [{ + expand: true, + dot: true, + cwd: '<%= yeoman.app %>', + dest: '<%= yeoman.dist %>', + src: [ + '*.{ico,png,txt}', + '.htaccess', + 'images/{,*/}*.{webp}', + 'fonts/*', + 'templates/*', + 'locales/**/*', + 'bower_components/font-awesome/css/font-awesome.min.css', + 'bower_components/font-awesome/fonts/*' + ] + }, { + expand: true, + cwd: '.tmp/images', + dest: '<%= yeoman.dist %>/images', + src: [ + 'generated/*' + ] + }] + }, + styles: { + expand: true, + cwd: '<%= yeoman.app %>/styles', + dest: '.tmp/css/', + src: '{,*/}*.css' + } + }, + + // Run some tasks in parallel to speed up the build process + concurrent: { + server: [ + 'copy:styles' + ], + test: [ + 'copy:styles' + ], + dist: [ + 'copy:styles', + 'imagemin', + 'svgmin', + 'htmlmin' + ] + }, + requirejs: { + compile: { + options: { + baseUrl: 'app', + wrap: true, + almond: true, + optimize: 'none', + mainConfigFile: 'app/js/main.js', + out: '.tmp/app.concatenated.js', + include: ['js/main'], + name: 'bower_components/almond/almond' + + } + } + }, + concat: { + options: { + separator: ';', + }, + dist: { + src: [ + 'app/bower_components/modernizr/modernizr.js', + 'app/bower_components/lodash/dist/lodash.js', + 'app/bower_components/jquery/dist/jquery.js', + 'app/js/lib/highlightRegex.js', + 'app/bower_components/handlebars/handlebars.min.js', + 'app/bower_components/typeahead.js/dist/typeahead.bundle.min.js', + 'app/bower_components/foundation/js/foundation.js', + 'app/bower_components/foundation/js/foundation/foundation.reveal.js', + 'app/bower_components/foundation/js/foundation/foundation.offcanvas.js', + '.tmp/app.concatenated.js', + ], + dest: '.tmp/app.min.js', + }, + }, + useminPrepare: { + html: 'dist/index.html', + options: { + dest: 'dist' + } + }, + usemin: { + html: 'dist/index.html' + }, + 'regex-replace': { + dist: { + src: ['dist/index.html'], + actions: [ + { + name: 'remove-requirejs-from-index', + search: 'remove-in-build.*end-remove-in-build', + replace: function(match){ + return ''; + }, + flags: 'g' + } + ] + } + }, + handlebars: { + dist: { + options: { + namespace: 'Smail' + }, + files: { + '<%= yeoman.dist %>/js/templates.js': '<%= yeoman.app %>/templates/**/*.hbs' + } + }, + dev: { + options: { + namespace: 'Smail' + }, + files: { + '<%= yeoman.app %>/js/generated/hbs/templates.js': '<%= yeoman.app %>/templates/**/*.hbs' + } + } + }, + + // Test settings + karma: { + options: { + configFile: 'karma.conf.js' + }, + + ci: { + singleRun: true, + autoWatch: false, + colors: false, + reporters: ['junit'] + }, + + single: { + singleRun: true, + autoWatch: false + }, + + dev: { + singleRun: false, + browsers: ['PhantomJS'] + }, + debug: { + singleRun: false, + browsers: ['Chrome'] + } + + } + + }); + + grunt.loadNpmTasks('grunt-contrib-requirejs'); + grunt.loadNpmTasks('grunt-contrib-concat'); + + grunt.registerTask('serve', function (target) { + if (target === 'dist') { + return grunt.task.run(['package', 'connect:dist:keepalive']); + } + + grunt.task.run([ + 'clean:server', + 'compass:dev', + 'handlebars:dev', + 'concurrent:server', + 'update-control-tower', + 'connect:dev', + 'watch' + ]); + }); + + grunt.registerTask('test-watch', [ + 'clean:server', + 'handlebars:dev', + 'connect:test', + 'karma:dev' + ]); + + grunt.registerTask('debug', [ + 'clean:server', + 'handlebars:dev', + 'connect:test', + 'karma:debug' + ]); + + grunt.registerTask('test-ci', [ + 'clean:server', + 'stopbrowsers', + 'handlebars:dev', + 'concurrent:test', + 'connect:test', + 'karma:ci' + ]); + + grunt.registerTask('test', [ + 'clean:server', + 'handlebars:dev', + 'connect:test', + 'karma:single' + ]); + + grunt.registerTask('package', [ + 'clean:dist', + 'useminPrepare', + 'compass:dist', + 'handlebars:dist', + 'concurrent:dist', + 'copy:dist', + 'cdnify', + 'cssmin', + 'requirejs', + 'concat:dist', + 'uglify', + 'usemin', + 'regex-replace:dist', + 'compress' + ]); + + grunt.registerTask('default', [ + 'newer:jshint', + 'test', + 'package' + ]); + + grunt.registerTask('stopbrowsers', function () { + ['phantom', 'firefox'].forEach(function (browser) { + console.log('killing all ' + browser + ' instances'); + exec('pgrep -f "' + browser + '" | xargs kill'); + console.log('... done'); + }); + }); + + grunt.registerTask('update-control-tower', function () { + exec('bash -c "flight-control-tower control-tower.yml && mv control_tower.html .tmp/"'); + }); +}; diff --git a/web-ui/README.md b/web-ui/README.md new file mode 100644 index 00000000..74dadb95 --- /dev/null +++ b/web-ui/README.md @@ -0,0 +1,2 @@ +smail-front +=========== diff --git a/web-ui/app/404.html b/web-ui/app/404.html new file mode 100644 index 00000000..fdace4ab --- /dev/null +++ b/web-ui/app/404.html @@ -0,0 +1,157 @@ + + + + + Page Not Found :( + + + +
+

Not found :(

+

Sorry, but the page you were trying to view does not exist.

+

It looks like this was the result of either:

+ + + +
+ + diff --git a/web-ui/app/favicon.ico b/web-ui/app/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/web-ui/app/fonts/NewsCycleBold.ttf b/web-ui/app/fonts/NewsCycleBold.ttf new file mode 100644 index 00000000..8265217f Binary files /dev/null and b/web-ui/app/fonts/NewsCycleBold.ttf differ diff --git a/web-ui/app/fonts/NewsCycleRegular.ttf b/web-ui/app/fonts/NewsCycleRegular.ttf new file mode 100644 index 00000000..9fbfd346 Binary files /dev/null and b/web-ui/app/fonts/NewsCycleRegular.ttf differ diff --git a/web-ui/app/fonts/OpenSans-Bold.woff b/web-ui/app/fonts/OpenSans-Bold.woff new file mode 100644 index 00000000..dacf3c9c Binary files /dev/null and b/web-ui/app/fonts/OpenSans-Bold.woff differ diff --git a/web-ui/app/fonts/OpenSans-BoldItalic.woff b/web-ui/app/fonts/OpenSans-BoldItalic.woff new file mode 100644 index 00000000..a4e29c0f Binary files /dev/null and b/web-ui/app/fonts/OpenSans-BoldItalic.woff differ diff --git a/web-ui/app/fonts/OpenSans-Extrabold.woff b/web-ui/app/fonts/OpenSans-Extrabold.woff new file mode 100644 index 00000000..7a2e352b Binary files /dev/null and b/web-ui/app/fonts/OpenSans-Extrabold.woff differ diff --git a/web-ui/app/fonts/OpenSans-ExtraboldItalic.woff b/web-ui/app/fonts/OpenSans-ExtraboldItalic.woff new file mode 100644 index 00000000..ce3ab2e7 Binary files /dev/null and b/web-ui/app/fonts/OpenSans-ExtraboldItalic.woff differ diff --git a/web-ui/app/fonts/OpenSans-Italic.woff b/web-ui/app/fonts/OpenSans-Italic.woff new file mode 100644 index 00000000..c5f6bac1 Binary files /dev/null and b/web-ui/app/fonts/OpenSans-Italic.woff differ diff --git a/web-ui/app/fonts/OpenSans-Light.woff b/web-ui/app/fonts/OpenSans-Light.woff new file mode 100644 index 00000000..eb601d70 Binary files /dev/null and b/web-ui/app/fonts/OpenSans-Light.woff differ diff --git a/web-ui/app/fonts/OpenSans-Semibold.woff b/web-ui/app/fonts/OpenSans-Semibold.woff new file mode 100644 index 00000000..56c44944 Binary files /dev/null and b/web-ui/app/fonts/OpenSans-Semibold.woff differ diff --git a/web-ui/app/fonts/OpenSans-SemiboldItalic.woff b/web-ui/app/fonts/OpenSans-SemiboldItalic.woff new file mode 100644 index 00000000..3a439fc3 Binary files /dev/null and b/web-ui/app/fonts/OpenSans-SemiboldItalic.woff differ diff --git a/web-ui/app/fonts/OpenSans.woff b/web-ui/app/fonts/OpenSans.woff new file mode 100644 index 00000000..77706fa6 Binary files /dev/null and b/web-ui/app/fonts/OpenSans.woff differ diff --git a/web-ui/app/fonts/OpenSansLight-Italic.woff b/web-ui/app/fonts/OpenSansLight-Italic.woff new file mode 100644 index 00000000..3f9f088f Binary files /dev/null and b/web-ui/app/fonts/OpenSansLight-Italic.woff differ diff --git a/web-ui/app/index.html b/web-ui/app/index.html new file mode 100644 index 00000000..568cff8a --- /dev/null +++ b/web-ui/app/index.html @@ -0,0 +1,84 @@ + + + + + +Pixelated Mail + + + + + + + + + + +
+
+ +
+
+ +
+
+ +
    +
+ +
+ +
+ +
+ +
+
+
+
+
+
+
+
+
+
    +
    + +
    +
      +
    +
    +
    + +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + diff --git a/web-ui/app/js/dispatchers/left_pane_dispatcher.js b/web-ui/app/js/dispatchers/left_pane_dispatcher.js new file mode 100644 index 00000000..8fd2b81d --- /dev/null +++ b/web-ui/app/js/dispatchers/left_pane_dispatcher.js @@ -0,0 +1,51 @@ +define( + [ + 'flight/lib/component', + 'page/router/url_params', + 'page/events' + ], + + function(defineComponent, urlParams, events) { + 'use strict'; + + return defineComponent(leftPaneDispatcher); + + function leftPaneDispatcher() { + var initialized = false; + + this.refreshTagList = function () { + this.trigger(document, events.tags.want, { caller: this.$node }); + }; + + this.loadTags = function (ev, data) { + this.trigger(document, events.ui.tagList.load, data); + }; + + this.selectTag = function (ev, data) { + var tag = (data && data.tag) || urlParams.getTag(); + this.trigger(document, events.ui.tag.select, { tag: tag }); + }; + + this.pushUrlState = function (ev, data) { + if (initialized) { + this.trigger(document, events.router.pushState, data); + } + initialized = true; + + if (data.skipMailListRefresh) { + return; + } + + this.trigger(document, events.ui.mails.fetchByTag, data); + }; + + this.after('initialize', function () { + this.on(this.$node, events.tags.received, this.loadTags); + this.on(document, events.dispatchers.tags.refreshTagList, this.refreshTagList); + this.on(document, events.ui.tags.loaded, this.selectTag); + this.on(document, events.ui.tag.selected, this.pushUrlState); + this.trigger(document, events.tags.want, { caller: this.$node } ); + }); + } + } +); diff --git a/web-ui/app/js/dispatchers/middle_pane_dispatcher.js b/web-ui/app/js/dispatchers/middle_pane_dispatcher.js new file mode 100644 index 00000000..26c32235 --- /dev/null +++ b/web-ui/app/js/dispatchers/middle_pane_dispatcher.js @@ -0,0 +1,36 @@ +define(['flight/lib/component', 'page/events', 'helpers/triggering'], function(defineComponent, events, triggering) { + 'use strict'; + + return defineComponent(function() { + this.defaultAttrs({ + middlePane: '#middle-pane' + }); + + this.refreshMailList = function (ev, data) { + this.trigger(document, events.ui.mails.fetchByTag, data); + }; + + this.cleanSelected = function(ev, data) { + this.trigger(document, events.ui.mails.cleanSelected); + }; + + this.resetScroll = function() { + this.select('middlePane').scrollTop(0); + }; + + this.updateMiddlePaneHeight = function() { + var vh = $(window).height(); + var top = $("#main").outerHeight() + $("#top-pane").outerHeight(); + this.select('middlePane').css({height: (vh - top) + 'px'}); + }; + + this.after('initialize', function () { + this.on(document, events.dispatchers.middlePane.refreshMailList, this.refreshMailList); + this.on(document, events.dispatchers.middlePane.cleanSelected, this.cleanSelected); + this.on(document, events.dispatchers.middlePane.resetScroll, this.resetScroll); + + this.updateMiddlePaneHeight(); + $(window).on('resize', this.updateMiddlePaneHeight.bind(this)); + }); + }); +}); diff --git a/web-ui/app/js/dispatchers/right_pane_dispatcher.js b/web-ui/app/js/dispatchers/right_pane_dispatcher.js new file mode 100644 index 00000000..3e62e581 --- /dev/null +++ b/web-ui/app/js/dispatchers/right_pane_dispatcher.js @@ -0,0 +1,93 @@ +/*global Smail */ + +define( + [ + 'flight/lib/component', + 'mail_view/ui/compose_box', + 'mail_view/ui/mail_view', + 'mail_view/ui/reply_section', + 'mail_view/ui/draft_box', + 'mail_view/ui/no_message_selected_pane', + 'page/events' + ], + + function(defineComponent, ComposeBox, MailView, ReplySection, DraftBox, NoMessageSelectedPane, events) { + 'use strict'; + + return defineComponent(rightPaneDispatcher); + + function rightPaneDispatcher() { + this.defaultAttrs({ + rightPane: '#right-pane', + composeBox: 'compose-box', + mailView: 'mail-view', + noMessageSelectedPane: 'no-message-selected-pane', + replySection: 'reply-section', + draftBox: 'draft-box', + currentTag: '' + }); + + this.createAndAttach = function(newContainer) { + var stage = $('
    ', { id: newContainer }); + this.select('rightPane').append(stage); + return stage; + }; + + this.reset = function (newContainer) { + this.trigger(document, events.dispatchers.rightPane.clear); + this.select('rightPane').empty(); + var stage = this.createAndAttach(newContainer); + return stage; + }; + + this.openComposeBox = function() { + var stage = this.reset(this.attr.composeBox); + ComposeBox.attachTo(stage, {currentTag: this.attr.currentTag}); + }; + + this.openMail = function(ev, data) { + var stage = this.reset(this.attr.mailView); + MailView.attachTo(stage, data); + + var replySectionContainer = this.createAndAttach(this.attr.replySection); + ReplySection.attachTo(replySectionContainer, { ident: data.ident }); + }; + + this.initializeNoMessageSelectedPane = function () { + var stage = this.reset(this.attr.noMessageSelectedPane); + NoMessageSelectedPane.attachTo(stage); + this.trigger(document, events.dispatchers.middlePane.cleanSelected); + }; + + this.openNoMessageSelectedPane = function(ev, data) { + this.initializeNoMessageSelectedPane(); + + this.trigger(document, events.router.pushState, { tag: this.attr.currentTag, isDisplayNoMessageSelected: true }); + }; + + this.openDraft = function (ev, data) { + var stage = this.reset(this.attr.draftBox); + DraftBox.attachTo(stage, { mailIdent: data.ident, currentTag: this.attr.currentTag }); + }; + + this.selectTag = function(ev, data) { + this.trigger(document, events.ui.tags.loaded, {tag: data.tag}); + }; + + this.saveTag = function(ev, data) { + this.attr.currentTag = data.tag; + }; + + this.after('initialize', function () { + this.on(document, events.dispatchers.rightPane.openComposeBox, this.openComposeBox); + this.on(document, events.dispatchers.rightPane.openDraft, this.openDraft); + this.on(document, events.ui.mail.open, this.openMail); + this.on(document, events.dispatchers.rightPane.openNoMessageSelected, this.openNoMessageSelectedPane); + this.on(document, events.dispatchers.rightPane.selectTag, this.selectTag); + this.on(document, events.ui.tag.selected, this.saveTag); + this.on(document, events.dispatchers.rightPane.openNoMessageSelectedWithoutPushState, this.initializeNoMessageSelectedPane); + this.initializeNoMessageSelectedPane(); + }); + } + } +); diff --git a/web-ui/app/js/foundation/off_canvas.js b/web-ui/app/js/foundation/off_canvas.js new file mode 100644 index 00000000..7dfd6f34 --- /dev/null +++ b/web-ui/app/js/foundation/off_canvas.js @@ -0,0 +1,21 @@ +define(['flight/lib/component', 'page/events'], function (defineComponent, events) { + + return defineComponent(function() { + + this.toggleSlider = function (){ + $('.off-canvas-wrap').foundation('offcanvas', 'toggle', 'move-right'); + }; + + this.closeSlider = function (){ + if ($('.off-canvas-wrap').attr('class').indexOf('move-right') > -1) { + $('.off-canvas-wrap').foundation('offcanvas', 'toggle', 'move-right'); + } + }; + + this.after('initialize', function () { + this.on($('.left-off-canvas-toggle'), 'click', this.toggleSlider); + this.on($('#middle-pane-container'), 'click', this.closeSlider); + this.on($('#right-pane'), 'click', this.closeSlider); + }); + }); +}); diff --git a/web-ui/app/js/helpers/contenttype.js b/web-ui/app/js/helpers/contenttype.js new file mode 100644 index 00000000..81519452 --- /dev/null +++ b/web-ui/app/js/helpers/contenttype.js @@ -0,0 +1,164 @@ +define([], function () { + var exports = {}; + + // Licence: PUBLIC DOMAIN + // Author: Austin Wright + + function MediaType(s, p){ + this.type = ''; + this.params = {}; + if(typeof s=='string'){ + var c = splitQuotedString(s); + this.type = c.shift(); + for(var i=0; i=0){ + offset = [delim,quote].reduce(findNextChar, 1/0); + if(offset===1/0) break; + switch(str[offset]){ + case quote: + // Skip to end of quoted string + while(1){ + offset=str.indexOf(quote, offset+1); + if(offset<0) break; + if(str[offset-1]==='\\') continue; + break; + } + continue; + case delim: + res.push(str.substr(start, offset-start).trim()); + start = ++offset; + break; + } + } + res.push(str.substr(start).trim()); + return res; + } + exports.splitQuotedString = splitQuotedString; + + // Split a list of content types found in an Accept header + // Maybe use it like: splitContentTypes(request.headers.accept).map(parseMedia) + function splitContentTypes(str){ + return splitQuotedString(str, ','); + } + exports.splitContentTypes = splitContentTypes; + + function parseMedia(str){ + var o = new MediaType(str); + if(o.q===undefined) o.q=1; + return o; + } + exports.parseMedia = parseMedia; + + // Pick an ideal representation to send given a list of representations to choose from and the client-preferred list + function select(reps, accept){ + var cr = {q:0}; + var ca = {q:0}; + var cq = 0; + for(var i=0; i=0){ + if(aq*rq>cq){ + ca = a; + cr = r; + cq = ca.q*cr.q; + if(cq===1 && cr.type) return cr; + } + } + } + } + return cr.type&&cr; + } + exports.select = select; + + // Determine if one media type is a subset of another + // If a is a superset of b (b is smaller than a), return 1 + // If b is a superset of a, return -1 + // If they are the exact same, return 0 + // If they are disjoint, return null + function mediaCmp(a, b){ + if(a.type==='*/*' && b.type!=='*/*') return 1; + else if(a.type!=='*/*' && b.type==='*/*') return -1; + var ac = (a.type||'').split('/'); + var bc = (b.type||'').split('/'); + if(ac[0]=='*' && bc[0]!='*') return 1; + if(ac[0]!='*' && bc[0]=='*') return -1; + if(a.type!==b.type) return null; + var ap = a.params || {}; + var bp = b.params || {}; + var ak = Object.keys(ap); + var bk = Object.keys(bp); + if(ak.length < bk.length) return 1; + if(ak.length > bk.length) return -1; + var k = ak.concat(bk).sort(); + var dir = 0; + for(var n in ap){ + if(ap[n] && !bp[n]){ if(dir<0) return null; else dir=1; } + if(!ap[n] && bp[n]){ if(dir>0) return null; else dir=-1; } + } + return dir; + } + exports.mediaCmp = mediaCmp; + + return exports; +}); diff --git a/web-ui/app/js/helpers/iterator.js b/web-ui/app/js/helpers/iterator.js new file mode 100644 index 00000000..9d8358a7 --- /dev/null +++ b/web-ui/app/js/helpers/iterator.js @@ -0,0 +1,43 @@ +define(function () { + + return Iterator; + + function Iterator(elems, startingIndex) { + + this.index = startingIndex || 0; + this.elems = elems; + + this.hasPrevious = function () { + return this.index != 0; + }; + + this.hasNext = function () { + return this.index < this.elems.length - 1; + }; + + this.previous = function () { + return this.elems[--this.index]; + }; + + this.next = function () { + return this.elems[++this.index]; + }; + + this.current = function () { + return this.elems[this.index]; + }; + + this.hasElements = function () { + return this.elems.length > 0; + }; + + this.removeCurrent = function () { + var removed = this.current(), + toRemove = this.index; + + !this.hasNext() && this.index--; + this.elems.remove(toRemove); + return removed; + }; + } +}); \ No newline at end of file diff --git a/web-ui/app/js/helpers/triggering.js b/web-ui/app/js/helpers/triggering.js new file mode 100644 index 00000000..7c8ae136 --- /dev/null +++ b/web-ui/app/js/helpers/triggering.js @@ -0,0 +1,13 @@ +define([], function() { + 'use strict'; + + return function(that, event, data, on) { + return function() { + if(on) { + that.trigger(on, event, data || {}); + } else { + that.trigger(event, data || {}); + } + }; + }; +}); diff --git a/web-ui/app/js/helpers/view_helper.js b/web-ui/app/js/helpers/view_helper.js new file mode 100644 index 00000000..3fa9edc1 --- /dev/null +++ b/web-ui/app/js/helpers/view_helper.js @@ -0,0 +1,145 @@ +define( + [ + 'helpers/contenttype', + 'lib/html_whitelister', + 'views/i18n', + 'quoted-printable/quoted-printable' + ], + function(contentType, htmlWhitelister, i18n_lib, quotedPrintable) { + 'use strict'; + + function formatStatusClasses(ss) { + return _.map(ss, function(s) { + return 'status-' + s; + }).join(' '); + } + + function addParagraphsToPlainText(plainTextBodyPart) { + return _.map(plainTextBodyPart.split('\n'), function (paragraph) { + return '

    ' + paragraph + '

    '; + }).join(''); + } + + function isQuotedPrintableBodyPart (bodyPart) { + return bodyPart.headers['Content-Transfer-Encoding'] && bodyPart.headers['Content-Transfer-Encoding'] === 'quoted-printable'; + } + + function getHtmlContentType (mail) { + return _.find(mail.availableBodyPartsContentType(), function (contentType) { + return contentType.indexOf('text/html') >= 0; + }); + } + + function getSanitizedAndDecodedMailBody (bodyPart) { + var body; + + if (isQuotedPrintableBodyPart(bodyPart)) { + body = quotedPrintable.decode(bodyPart.body); + } else { + body = bodyPart.body; + } + + return htmlWhitelister.sanitize(body, htmlWhitelister.tagPolicy); + } + + function formatMailBody (mail) { + if (mail.isMailMultipartAlternative()) { + var htmlContentType; + + htmlContentType = getHtmlContentType(mail); + + if (htmlContentType) { + return $(getSanitizedAndDecodedMailBody(mail.getMailPartByContentType(htmlContentType))); + } + + return $(addParagraphsToPlainText(mail.getMailMultiParts[0])); + } + + return $(addParagraphsToPlainText(mail.body)); + + /* + var body; + // probably parse MIME parts and ugliness here + // content_type: "multipart/alternative; boundary="----=_Part_1115_17865397.1370312509342"" + var mediaType = new contentType.MediaType(mail.header.content_type); + if(mediaType.type === 'multipart/alternative') { + var parsedBodyParts = getMailMultiParts(mail.body, mediaType); + var selectedBodyPart = getHtmlMailPart(parsedBodyParts) || getPlainTextMailPart(parsedBodyParts) || parsedBodyParts[0]; + body = selectedBodyPart.body; + + if (isQuotedPrintableBodyPart(selectedBodyPart)) { + body = quotedPrintable.decode(body); + } + } else { + body = addParagraphsToPlainText(mail.body); + } + return $(htmlWhitelister.sanitize(body, htmlWhitelister.tagPolicy)); + */ + } + + function moveCaretToEnd(el) { + if (typeof el.selectionStart == "number") { + el.selectionStart = el.selectionEnd = el.value.length; + } else if (typeof el.createTextRange != "undefined") { + el.focus(); + var range = el.createTextRange(); + range.collapse(false); + range.select(); + } + } + + function fixedSizeNumber(num, size) { + var res = num.toString(); + while(res.length < size) { + res = "0" + res; + } + return res; + } + + function getFormattedDate(date){ + var today = createTodayDate(); + if (date.getTime() > today.getTime()) { + return fixedSizeNumber(date.getHours(), 2) + ":" + fixedSizeNumber(date.getMinutes(), 2); + } else { + return "" + date.getFullYear() + "-" + fixedSizeNumber(date.getMonth() + 1, 2) + "-" + fixedSizeNumber(date.getDate(), 2); + } + } + + function createTodayDate() { + var today = new Date(); + today.setHours(0); + today.setMinutes(0); + today.setSeconds(0); + return today; + } + + function moveCaretToEndOfText() { + var self = this; + + moveCaretToEnd(self); + window.setTimeout(function() { + moveCaretToEnd(self); + }, 1); + } + + function quoteMail(mail) { + var quotedLines = _.map(mail.body.split('\n'), function (line) { + return '> ' + line; + }); + + return '\n\n' + quotedLines.join('\n'); + } + + function i18n(text) { + return i18n_lib.get(text); + } + + return { + formatStatusClasses: formatStatusClasses, + formatMailBody: formatMailBody, + moveCaretToEndOfText: moveCaretToEndOfText, + getFormattedDate: getFormattedDate, + quoteMail: quoteMail, + i18n: i18n + }; +}); diff --git a/web-ui/app/js/lib/highlightRegex.js b/web-ui/app/js/lib/highlightRegex.js new file mode 100644 index 00000000..17caaa23 --- /dev/null +++ b/web-ui/app/js/lib/highlightRegex.js @@ -0,0 +1,127 @@ +/* + * jQuery Highlight Regex Plugin v0.1.2 + * + * Based on highlight v3 by Johann Burkard + * http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html + * + * (c) 2009-13 Jacob Rothstein + * MIT license + */ + +;(function( $ ) { + + + + var normalize = function( node ) { + if ( ! ( node && node.childNodes )) return + + var children = $.makeArray( node.childNodes ) + , prevTextNode = null + + $.each( children, function( i, child ) { + if ( child.nodeType === 3 ) { + if ( child.nodeValue === "" ) { + + node.removeChild( child ) + + } else if ( prevTextNode !== null ) { + + prevTextNode.nodeValue += child.nodeValue; + node.removeChild( child ) + + } else { + + prevTextNode = child + + } + } else { + prevTextNode = null + + if ( child.childNodes ) { + normalize( child ) + } + } + }) + } + + + + + $.fn.highlightRegex = function( regex, options ) { + + if ( typeof regex === 'object' && !(regex.constructor.name == 'RegExp' || regex instanceof RegExp ) ) { + options = regex + regex = undefined + } + + if ( typeof options === 'undefined' ) options = {} + + options.className = options.className || 'highlight' + options.tagType = options.tagType || 'span' + options.attrs = options.attrs || {} + + if ( typeof regex === 'undefined' || regex.source === '' ) { + + $( this ).find( options.tagType + '.' + options.className ).each( function() { + + $( this ).replaceWith( $( this ).text() ) + + normalize( $( this ).parent().get( 0 )) + + }) + + } else { + + $( this ).each( function() { + + var elt = $( this ).get( 0 ) + + normalize( elt ) + + $.each( $.makeArray( elt.childNodes ), function( i, searchnode ) { + + var spannode, middlebit, middleclone, pos, match, parent + + normalize( searchnode ) + + if ( searchnode.nodeType == 3 ) { + + // don't re-highlight the same node over and over + if ( $(searchnode).parent(options.tagType + '.' + options.className).length ) { + return; + } + + while ( searchnode.data && + ( pos = searchnode.data.search( regex )) >= 0 ) { + + match = searchnode.data.slice( pos ).match( regex )[ 0 ] + + if ( match.length > 0 ) { + + spannode = document.createElement( options.tagType ) + spannode.className = options.className + $(spannode).attr(options.attrs) + + parent = searchnode.parentNode + middlebit = searchnode.splitText( pos ) + searchnode = middlebit.splitText( match.length ) + middleclone = middlebit.cloneNode( true ) + + spannode.appendChild( middleclone ) + parent.replaceChild( spannode, middlebit ) + + } else break + } + + } else { + + $( searchnode ).highlightRegex( regex, options ) + + } + }) + }) + } + + return $( this ) + } +})( jQuery ); diff --git a/web-ui/app/js/lib/html-sanitizer.js b/web-ui/app/js/lib/html-sanitizer.js new file mode 100644 index 00000000..80fb0041 --- /dev/null +++ b/web-ui/app/js/lib/html-sanitizer.js @@ -0,0 +1,1064 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview + * An HTML sanitizer that can satisfy a variety of security policies. + * + *

    + * The HTML sanitizer is built around a SAX parser and HTML element and + * attributes schemas. + * + * If the cssparser is loaded, inline styles are sanitized using the + * css property and value schemas. Else they are remove during + * sanitization. + * + * If it exists, uses parseCssDeclarations, sanitizeCssProperty, cssSchema + * + * @author mikesamuel@gmail.com + * @author jasvir@gmail.com + * \@requires html4, URI + * \@overrides window + * \@provides html, html_sanitize + */ + +// The Turkish i seems to be a non-issue, but abort in case it is. +if ('I'.toLowerCase() !== 'i') { throw 'I/i problem'; } + +/** + * \@namespace + */ +define(['lib/html4-defs'], function (html4) { +var html = (function(html4) { + + // For closure compiler + var parseCssDeclarations, sanitizeCssProperty, cssSchema; + if ('undefined' !== typeof window) { + parseCssDeclarations = window['parseCssDeclarations']; + sanitizeCssProperty = window['sanitizeCssProperty']; + cssSchema = window['cssSchema']; + } + + // The keys of this object must be 'quoted' or JSCompiler will mangle them! + // This is a partial list -- lookupEntity() uses the host browser's parser + // (when available) to implement full entity lookup. + // Note that entities are in general case-sensitive; the uppercase ones are + // explicitly defined by HTML5 (presumably as compatibility). + var ENTITIES = { + 'lt': '<', + 'LT': '<', + 'gt': '>', + 'GT': '>', + 'amp': '&', + 'AMP': '&', + 'quot': '"', + 'apos': '\'', + 'nbsp': '\240' + }; + + // Patterns for types of entity/character reference names. + var decimalEscapeRe = /^#(\d+)$/; + var hexEscapeRe = /^#x([0-9A-Fa-f]+)$/; + // contains every entity per http://www.w3.org/TR/2011/WD-html5-20110113/named-character-references.html + var safeEntityNameRe = /^[A-Za-z][A-za-z0-9]+$/; + // Used as a hook to invoke the browser's entity parsing. + +{{> recipients }} + +

    + +
    + + + +
    +
    diff --git a/web-ui/app/templates/compose/fixed_recipient.hbs b/web-ui/app/templates/compose/fixed_recipient.hbs new file mode 100644 index 00000000..2f773c76 --- /dev/null +++ b/web-ui/app/templates/compose/fixed_recipient.hbs @@ -0,0 +1,6 @@ +
    + +
    {{ address }}
    +
    + +
    diff --git a/web-ui/app/templates/compose/inline_box.hbs b/web-ui/app/templates/compose/inline_box.hbs new file mode 100644 index 00000000..eb339c21 --- /dev/null +++ b/web-ui/app/templates/compose/inline_box.hbs @@ -0,0 +1,18 @@ +
    +

    {{subject}}

    + +
    + + + + {{t 'To'}}: {{formatRecipients recipients}} + + +{{> recipients }} + +
    + + + +
    +
    diff --git a/web-ui/app/templates/compose/recipient_input.hbs b/web-ui/app/templates/compose/recipient_input.hbs new file mode 100644 index 00000000..9416f11f --- /dev/null +++ b/web-ui/app/templates/compose/recipient_input.hbs @@ -0,0 +1 @@ + diff --git a/web-ui/app/templates/compose/recipients.hbs b/web-ui/app/templates/compose/recipients.hbs new file mode 100644 index 00000000..6ec29ae5 --- /dev/null +++ b/web-ui/app/templates/compose/recipients.hbs @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/web-ui/app/templates/compose/reply_section.hbs b/web-ui/app/templates/compose/reply_section.hbs new file mode 100644 index 00000000..9e833ffe --- /dev/null +++ b/web-ui/app/templates/compose/reply_section.hbs @@ -0,0 +1,6 @@ +
    + + + + +
    diff --git a/web-ui/app/templates/mail_actions/actions_box.hbs b/web-ui/app/templates/mail_actions/actions_box.hbs new file mode 100644 index 00000000..b6dc2f53 --- /dev/null +++ b/web-ui/app/templates/mail_actions/actions_box.hbs @@ -0,0 +1,6 @@ +
  • +
  • +
  • +
  • +
  • +
  • diff --git a/web-ui/app/templates/mail_actions/compose_trigger.hbs b/web-ui/app/templates/mail_actions/compose_trigger.hbs new file mode 100644 index 00000000..ccdb4df0 --- /dev/null +++ b/web-ui/app/templates/mail_actions/compose_trigger.hbs @@ -0,0 +1,3 @@ +
    + {{t 'compose' }} +
    diff --git a/web-ui/app/templates/mail_actions/pagination_trigger.hbs b/web-ui/app/templates/mail_actions/pagination_trigger.hbs new file mode 100644 index 00000000..cbd8a089 --- /dev/null +++ b/web-ui/app/templates/mail_actions/pagination_trigger.hbs @@ -0,0 +1,3 @@ + +{{ currentPage }} + diff --git a/web-ui/app/templates/mail_actions/refresh_trigger.hbs b/web-ui/app/templates/mail_actions/refresh_trigger.hbs new file mode 100644 index 00000000..68685442 --- /dev/null +++ b/web-ui/app/templates/mail_actions/refresh_trigger.hbs @@ -0,0 +1,3 @@ +
    + +
    diff --git a/web-ui/app/templates/mails/full_view.hbs b/web-ui/app/templates/mails/full_view.hbs new file mode 100644 index 00000000..a466308d --- /dev/null +++ b/web-ui/app/templates/mails/full_view.hbs @@ -0,0 +1,87 @@ + +
    + +
    + + + + +
    + +
    + + {{t signatureStatus }} + + + {{t encryptionStatus }} + +
    +
    + {{ header.formattedDate }} +
    +
    + + {{#if header.from }} + {{ header.from }} + {{else}} + {{t 'you'}} + {{/if}} + + + {{{formatRecipients header}}} +
    + +
    +

    + {{ header.subject }} + +
    +
      + {{#each tags }} +
    • {{ this }}
    • + {{/each }} + +
    • + +
    • +
    • + +
    • +
    +
    +

    + +
    + +
    +
    + +
    +

    {{t 'You are trying to delete the last tag on this message.'}}

    + +

    {{t 'What would you like to do?'}}

    + + + × + {{t 'Trash:'}} {{t 'we will keep this message for 30 days, then delete it forever.'}} + + + {{t 'Archive:'}} {{t 'we will remove all the tags, but keep it in your account in case you need it.'}} + +
    + +
    + {{#each body }} +

    {{ this }}

    + {{/each }} +
    +
    + diff --git a/web-ui/app/templates/mails/mail_actions.hbs b/web-ui/app/templates/mails/mail_actions.hbs new file mode 100644 index 00000000..8933db79 --- /dev/null +++ b/web-ui/app/templates/mails/mail_actions.hbs @@ -0,0 +1,6 @@ + + +
      +
    • {{t 'Reply to All'}}
    • +
    • {{t 'Trash this message'}}
    • +
    diff --git a/web-ui/app/templates/mails/sent.hbs b/web-ui/app/templates/mails/sent.hbs new file mode 100644 index 00000000..826a66d5 --- /dev/null +++ b/web-ui/app/templates/mails/sent.hbs @@ -0,0 +1,23 @@ + + + + + + {{ header.formattedDate }} + +
    {{t 'to:'}} {{#if header.to }}{{ + header.to }}{{else}}{{t 'no_recipient'}}{{/if}}
    +
    +
      + {{#each tagsForListView }} +
    • {{ this }}
    • + {{/each }} +
    + {{#if header.subject }} + {{header.subject}} + {{else}} + {{t 'no_subject'}} + {{/if}} +
    +
    +
    diff --git a/web-ui/app/templates/mails/single.hbs b/web-ui/app/templates/mails/single.hbs new file mode 100644 index 00000000..9a054c79 --- /dev/null +++ b/web-ui/app/templates/mails/single.hbs @@ -0,0 +1,19 @@ + + + + + + {{ header.formattedDate }} + +
    {{#if header.from }}{{ header.from }}{{else}}{{t "you"}}{{/if}}
    +
    +
      + {{#each tagsForListView }} +
    • {{ this }}
    • + {{/each }} +
    + + {{ header.subject }} +
    +
    +
    diff --git a/web-ui/app/templates/no_message_selected.hbs b/web-ui/app/templates/no_message_selected.hbs new file mode 100644 index 00000000..0442192d --- /dev/null +++ b/web-ui/app/templates/no_message_selected.hbs @@ -0,0 +1,3 @@ +
    +
    {{t 'NOTHING SELECTED'}}.
    +
    diff --git a/web-ui/app/templates/search/search_trigger.hbs b/web-ui/app/templates/search/search_trigger.hbs new file mode 100644 index 00000000..fbf24170 --- /dev/null +++ b/web-ui/app/templates/search/search_trigger.hbs @@ -0,0 +1,3 @@ +
    + +
    diff --git a/web-ui/app/templates/tags/shortcut.hbs b/web-ui/app/templates/tags/shortcut.hbs new file mode 100644 index 00000000..49ddfdb2 --- /dev/null +++ b/web-ui/app/templates/tags/shortcut.hbs @@ -0,0 +1,9 @@ +
  • + + {{#if displayBadge }} + {{ count }} + {{/if}} + +
    {{ tagName }}
    +
    +
  • \ No newline at end of file diff --git a/web-ui/app/templates/tags/tag.hbs b/web-ui/app/templates/tags/tag.hbs new file mode 100644 index 00000000..c645f782 --- /dev/null +++ b/web-ui/app/templates/tags/tag.hbs @@ -0,0 +1,3 @@ +
  • + {{> tag_inner }} +
  • diff --git a/web-ui/app/templates/tags/tag_inner.hbs b/web-ui/app/templates/tags/tag_inner.hbs new file mode 100644 index 00000000..2e0958cb --- /dev/null +++ b/web-ui/app/templates/tags/tag_inner.hbs @@ -0,0 +1,4 @@ +{{ tagName }} +{{#if displayBadge }} +{{ count }} +{{/if}} diff --git a/web-ui/app/templates/tags/tag_list.hbs b/web-ui/app/templates/tags/tag_list.hbs new file mode 100644 index 00000000..e2e97833 --- /dev/null +++ b/web-ui/app/templates/tags/tag_list.hbs @@ -0,0 +1,3 @@ +
      +

      {{t 'Tags'}}

      +
        diff --git a/web-ui/app/templates/user_alerts/message.hbs b/web-ui/app/templates/user_alerts/message.hbs new file mode 100644 index 00000000..d2fff04a --- /dev/null +++ b/web-ui/app/templates/user_alerts/message.hbs @@ -0,0 +1 @@ +{{ message }} diff --git a/web-ui/bower.json b/web-ui/bower.json new file mode 100644 index 00000000..f8eebac7 --- /dev/null +++ b/web-ui/bower.json @@ -0,0 +1,21 @@ +{ + "name": "flightjs", + "version": "0.0.0", + "dependencies": { + "jquery": "2.x.x", + "flight": "~1.1.0", + "requirejs": "~2.1.5", + "lodash": "~2.4.1", + "foundation": "5.3.0", + "i18next": "~1.7.3", + "font-awesome": "~4.1.0", + "quoted-printable": "0.2.1" + }, + "devDependencies": { + "jasmine-flight": "~2.2.0", + "jasmine-jquery": "~1.1.3", + "handlebars": "~1.3.0", + "typeahead.js": "~0.10.2", + "almond": "~0.2.9" + } +} diff --git a/web-ui/control-tower.yml b/web-ui/control-tower.yml new file mode 100644 index 00000000..874e1b64 --- /dev/null +++ b/web-ui/control-tower.yml @@ -0,0 +1,3 @@ +--- +include_pattern: 'app/js/**/*.js' +exclude_pattern: '' diff --git a/web-ui/go b/web-ui/go new file mode 100755 index 00000000..ad575723 --- /dev/null +++ b/web-ui/go @@ -0,0 +1,3 @@ +#!/bin/bash + +node_modules/grunt-cli/bin/grunt $GRUNT_OPTS $* diff --git a/web-ui/karma.conf.js b/web-ui/karma.conf.js new file mode 100644 index 00000000..6a4463fe --- /dev/null +++ b/web-ui/karma.conf.js @@ -0,0 +1,85 @@ +// Karma configuration +// +// For all available config options and default values, see: +// http://karma-runner.github.io/0.10/config/configuration-file.html + +module.exports = function (config) { + 'use strict'; + + config.set({ + + // base path, that will be used to resolve files and exclude + basePath: '', + + // frameworks to use + frameworks: ['jasmine'], + + // list of files / patterns to load in the browser + files: [ + // loaded without require + 'app/bower_components/es5-shim/es5-shim.js', + 'app/bower_components/es5-shim/es5-sham.js', + 'app/bower_components/lodash/dist/lodash.js', + 'app/bower_components/jquery/dist/jquery.js', + 'app/bower_components/jasmine-jquery/lib/jasmine-jquery.js', + 'app/bower_components/jasmine-flight/lib/jasmine-flight.js', + 'app/bower_components/jasmine-jquery/lib/jasmine-jquery.js', + 'app/bower_components/handlebars/handlebars.min.js', + 'app//bower_components/modernizr/modernizr.js', + 'app/bower_components/foundation/js/foundation.js', + 'app/bower_components/foundation/js/foundation/foundation.reveal.js', + 'app/bower_components/foundation/js/foundation/foundation.offcanvas.js', + + // hack to load RequireJS after the shim libs + 'node_modules/requirejs/require.js', + 'node_modules/karma-requirejs/lib/adapter.js', + + // loaded with require + {pattern: 'app/bower_components/flight/**/*.js', included: false}, + {pattern: 'app/bower_components/i18next/**/*.js', included: false}, + {pattern: 'app/bower_components/quoted-printable/*.js', included: false}, + {pattern: 'app/locales/**/*.json', included: false}, + {pattern: 'app/js/**/*.js', included: false}, + {pattern: 'test/test_data.js', included: false}, + {pattern: 'test/spec/**/*.spec.js', included: false}, + + 'test/test-main.js' + ], + + // list of files to exclude + exclude: [ + 'app/js/main.js' + ], + + // test results reporter to use + // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' + reporters: ['progress'], + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera + // - Safari (only Mac) + // - PhantomJS + // - IE (only Windows) + browsers: [ + 'PhantomJS' + ], + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 5000, + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: false, + + // Karma will report all the tests that are slower than given time limit (in + // ms). + reportSlowerThan: 500, + + }); +}; diff --git a/web-ui/package.json b/web-ui/package.json new file mode 100644 index 00000000..a57ef1ca --- /dev/null +++ b/web-ui/package.json @@ -0,0 +1,58 @@ +{ + "name": "smail-front", + "version": "0.0.0", + "devDependencies": { + "bower": "^1.3.5", + "connect-livereload": "^0.4.0", + "express": "^4.3.2", + "generator-flight": "~0.7.3", + "grunt": "^0.4.5", + "grunt-autoprefixer": "~0.4.0", + "grunt-build-control": "^0.1.3", + "grunt-cli": "^0.1.13", + "grunt-concurrent": "~0.4.1", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-coffee": "~0.7.0", + "grunt-contrib-compass": "~0.6.0", + "grunt-contrib-compress": "^0.9.1", + "grunt-contrib-concat": "~0.3.0", + "grunt-contrib-connect": "~0.5.0", + "grunt-contrib-copy": "~0.4.1", + "grunt-contrib-cssmin": "~0.7.0", + "grunt-contrib-handlebars": "^0.8.0", + "grunt-contrib-htmlmin": "~0.1.3", + "grunt-contrib-imagemin": "~0.3.0", + "grunt-contrib-jshint": "~0.7.1", + "grunt-contrib-requirejs": "~0.4.4", + "grunt-contrib-uglify": "~0.2.0", + "grunt-contrib-watch": "~0.5.2", + "grunt-google-cdn": "~0.2.0", + "grunt-karma": "^0.8.3", + "grunt-newer": "~0.5.4", + "grunt-ngmin": "~0.0.2", + "grunt-regex-replace": "^0.2.6", + "grunt-rev": "~0.1.0", + "grunt-svgmin": "~0.2.0", + "grunt-usemin": "^2.3.0", + "handlebars": "^2.0.0-alpha.4", + "jshint-stylish": "^0.2.0", + "karma": "~0.12.0", + "karma-chrome-launcher": "~0.1.0", + "karma-firefox-launcher": "~0.1.0", + "karma-ie-launcher": "~0.1.1", + "karma-jasmine": "~0.1.0", + "karma-junit-reporter": "^0.2.2", + "karma-phantomjs-launcher": "~0.1.0", + "karma-requirejs": "~0.2.1", + "karma-safari-launcher": "~0.1.1", + "load-grunt-tasks": "~0.2.0", + "node-static": "~0.7.3", + "requirejs": "~2.1.11", + "time-grunt": "~0.2.1", + "tiny-lr": "0.0.5" + }, + "scripts": { + "test": "karma start --single-run --browsers PhantomJS", + "watch-test": "karma start" + } +} diff --git a/web-ui/test/spec/dispatchers/left_pane_dispatcher.spec.js b/web-ui/test/spec/dispatchers/left_pane_dispatcher.spec.js new file mode 100644 index 00000000..fb5b169a --- /dev/null +++ b/web-ui/test/spec/dispatchers/left_pane_dispatcher.spec.js @@ -0,0 +1,79 @@ +/*global Smail */ + +describeComponent('dispatchers/left_pane_dispatcher', function () { + 'use strict'; + + describe('initialize', function () { + it('asks for tags', function () { + var tagWantEvent = spyOnEvent(document, Smail.events.tags.want); + + setupComponent(); + + expect(tagWantEvent).toHaveBeenTriggeredOn(document); + expect(tagWantEvent.mostRecentCall.data.caller[0]).toEqual(this.$node[0]); + }); + }); + + describe('after initialization', function () { + beforeEach(function () { + setupComponent(); + }); + + it('pushes the url state when a tag is selected but not for the first tag', function () { + var pushStateEvent = spyOnEvent(document, Smail.events.router.pushState); + + $(document).trigger(Smail.events.ui.tag.selected, { tag: 'Drafts'}); + $(document).trigger(Smail.events.ui.tag.selected, { tag: 'inbox'}); + + expect(pushStateEvent).toHaveBeenTriggeredOn(document, { tag: 'inbox'}); + }); + + it('fetches mails by tag when a tag is selected', function () { + var fetchByTagEvent = spyOnEvent(document, Smail.events.ui.mails.fetchByTag); + + $(document).trigger(Smail.events.ui.tag.selected, { tag: 'Drafts'}); + + expect(fetchByTagEvent).toHaveBeenTriggeredOn(document, { tag: 'Drafts'}); + }); + + it('doesnt fetch mails by tag when skipMailListRefresh is sent on tag.selected', function () { + var fetchByTagEvent = spyOnEvent(document, Smail.events.ui.mails.fetchByTag); + + $(document).trigger(Smail.events.ui.tag.selected, { tag: 'Drafts', skipMailListRefresh: true}); + + expect(fetchByTagEvent).not.toHaveBeenTriggeredOn(document, { tag: 'Drafts'}); + }); + + it('asks for more tags when refreshTagList is fired', function () { + var tagWantEvent = spyOnEvent(document, Smail.events.tags.want); + + $(document).trigger(Smail.events.dispatchers.tags.refreshTagList); + + expect(tagWantEvent).toHaveBeenTriggeredOn(document); + }); + + it('fires tagLoad when the tags are received', function () { + var tagListLoadEvent = spyOnEvent(document, Smail.events.ui.tagList.load); + + this.$node.trigger(Smail.events.tags.received, { tags: ['tags']}); + + expect(tagListLoadEvent).toHaveBeenTriggeredOn(document, { tags: ['tags']}); + }); + + it('on tags loaded selects the inbox tag if no data is provided', function () { + var selectTagEvent = spyOnEvent(document, Smail.events.ui.tag.select); + + $(document).trigger(Smail.events.ui.tags.loaded); + + expect(selectTagEvent).toHaveBeenTriggeredOnAndWith(document, { tag: 'inbox' }); + }); + + it('on tags loaded selects the a different tag if tag is provided', function () { + var selectTagEvent = spyOnEvent(document, Smail.events.ui.tag.select); + + $(document).trigger(Smail.events.ui.tags.loaded, { tag: 'Drafts' }); + + expect(selectTagEvent).toHaveBeenTriggeredOnAndWith(document, { tag: 'Drafts' }); + }); + }); +}); diff --git a/web-ui/test/spec/dispatchers/middle_pane_dispatchers.spec.js b/web-ui/test/spec/dispatchers/middle_pane_dispatchers.spec.js new file mode 100644 index 00000000..2dd0de2e --- /dev/null +++ b/web-ui/test/spec/dispatchers/middle_pane_dispatchers.spec.js @@ -0,0 +1,27 @@ +/*global Smail */ + +describeComponent('dispatchers/middle_pane_dispatcher', function () { + 'use strict'; + + beforeEach(function() { + setupComponent('
        '); + }); + + it ('listens to refresh mail list event', function() { + var mailsListRefreshEventSpy = spyOnEvent(document, Smail.events.ui.mails.fetchByTag); + this.component.trigger(document, Smail.events.dispatchers.middlePane.refreshMailList); + expect(mailsListRefreshEventSpy).toHaveBeenTriggeredOn(document); + }); + + it ('listens to unselect event', function() { + var mailListUnselectEvent = spyOnEvent(document, Smail.events.ui.mails.cleanSelected); + this.component.trigger(document, Smail.events.dispatchers.middlePane.cleanSelected); + expect(mailListUnselectEvent).toHaveBeenTriggeredOn(document); + }); + + it('resets the scrollTop value when asked to', function() { + this.component.select('middlePane').scrollTop(200); + this.component.trigger(document, Smail.events.dispatchers.middlePane.resetScroll); + expect(this.component.select('middlePane').scrollTop()).toEqual(0); + }); +}); diff --git a/web-ui/test/spec/dispatchers/right_pane_dispatcher.spec.js b/web-ui/test/spec/dispatchers/right_pane_dispatcher.spec.js new file mode 100644 index 00000000..5fc7ecd7 --- /dev/null +++ b/web-ui/test/spec/dispatchers/right_pane_dispatcher.spec.js @@ -0,0 +1,90 @@ +/*global Smail */ + +describeComponent('dispatchers/right_pane_dispatcher', function () { + 'use strict'; + + describe('after initialization', function () { + beforeEach(function () { + setupComponent(); + }); + + it('listens to open compose box event and creates a compose box', function () { + var composeBox = require('mail_view/ui/compose_box'); + spyOn(composeBox, 'attachTo'); + + this.component.trigger(document, Smail.events.dispatchers.rightPane.openComposeBox); + + expect(composeBox.attachTo).toHaveBeenCalled(); + }); + + describe('no message selected', function () { + var noMessageSelectedPane; + beforeEach(function () { + noMessageSelectedPane = require('mail_view/ui/no_message_selected_pane'); + spyOn(noMessageSelectedPane, 'attachTo'); + }); + + it('listen to open no message selected event and creates a no-message-selected-pane', function () { + this.component.trigger(document, Smail.events.dispatchers.rightPane.openNoMessageSelected); + + expect(noMessageSelectedPane.attachTo).toHaveBeenCalled(); + }); + + it('sends an dispatchers.middlePane.unselect event', function () { + var unselectEvent = spyOnEvent(document, Smail.events.dispatchers.middlePane.cleanSelected); + this.component.trigger(document, Smail.events.dispatchers.rightPane.openNoMessageSelected); + + expect(unselectEvent).toHaveBeenTriggeredOn(document); + }); + + it('pushes the current state with the current tag', function () { + var pushStateEvent = spyOnEvent(document, Smail.events.router.pushState); + + this.component.attr.currentTag = 'sometag'; + this.component.trigger(document, Smail.events.dispatchers.rightPane.openNoMessageSelected); + + expect(pushStateEvent).toHaveBeenTriggeredOnAndWith(document, jasmine.objectContaining({tag: this.component.attr.currentTag })); + }); + + it('pushes the current state stating that it meant to close the right pane', function () { + var pushStateEvent = spyOnEvent(document, Smail.events.router.pushState); + + this.component.attr.currentTag = 'sometag'; + this.component.trigger(document, Smail.events.dispatchers.rightPane.openNoMessageSelected); + + expect(pushStateEvent).toHaveBeenTriggeredOnAndWith(document, jasmine.objectContaining({ isDisplayNoMessageSelected: true })); + }); + + + }); + + it('listens to open a draft and creates it', function () { + var draftBox = require('mail_view/ui/draft_box'); + spyOn(draftBox, 'attachTo'); + + this.component.trigger(document, Smail.events.dispatchers.rightPane.openDraft, { ident: '1' }); + + expect(draftBox.attachTo).toHaveBeenCalled(); + }); + }); + + describe('on initialization', function () { + var noMessageSelectedPane; + + beforeEach(function () { + noMessageSelectedPane = require('mail_view/ui/no_message_selected_pane'); + spyOn(noMessageSelectedPane, 'attachTo'); + }); + + it('opens the no message selected pane but doesnt push the state', function () { + var pushStateEvent = spyOnEvent(document, Smail.events.router.pushState); + + setupComponent(); + + expect(noMessageSelectedPane.attachTo).toHaveBeenCalled(); + expect(pushStateEvent).not.toHaveBeenTriggeredOn(document); + + }); + }); + +}); diff --git a/web-ui/test/spec/helpers/view_helper.spec.js b/web-ui/test/spec/helpers/view_helper.spec.js new file mode 100644 index 00000000..7fa10e56 --- /dev/null +++ b/web-ui/test/spec/helpers/view_helper.spec.js @@ -0,0 +1,76 @@ +/*global Smail */ + +define(['helpers/view_helper'], function (viewHelper) { + 'use strict'; + + var testData; + describe('view helper', function() { + beforeEach(function () { + testData = Smail.testData(); + }); + + describe('quote email', function() { + it('should add > to body text', function() { + testData.rawMail.mail.body = 'First Line\nSecond Line'; + + var quotedMail = viewHelper.quoteMail(testData.rawMail.mail); + + expect(quotedMail).toContain('> First Line\n> Second Line'); + }); + }); + + describe('getFormmattedDate', function() { + it('formats correctly a Date for today', function() { + var d = new Date(); + var dtest = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 14, 2, 36); + + var res = viewHelper.getFormattedDate(dtest); + + expect(res).toEqual('14:02'); + + }); + + it('formats correctly a Date for a specific day', function() { + var dtest = new Date(2013, 2, 13, 7, 56, 1); + + var res = viewHelper.getFormattedDate(dtest); + + // This expectation is weird for the month - JS Dates have date numbers be zero-indexed, thus the discrepency + // Specifically, the 2 in the constructor DOES match the 3 in the expectation below. + expect(res).toEqual('2013-03-13'); + }); + }); + + describe('format status classes', function () { + it('formats all the status of the email to css classes', function () { + var statuses = ['read', 'banana']; + + expect(viewHelper.formatStatusClasses(statuses)).toEqual('status-read status-banana'); + }); + + it('formats a single status of the email to a css class', function () { + var statuses = ['read']; + + expect(viewHelper.formatStatusClasses(statuses)).toEqual('status-read'); + }); + }); + + it('formats the body of a multipart email', function () { + expect(viewHelper.formatMailBody(testData.parsedMail.html)).toContainHtml('

        Hello everyone!

        '); + }); + + it('decodes a quoted-printable email body', function () { + var result = viewHelper.formatMailBody(testData.parsedMail.htmlQuotedPrintable); + + expect(result).toContainHtml('

        Hello everyone!

        '); + }); + + it('move caret to the end of text after 1ms', function () { + spyOn(window, 'setTimeout'); + + viewHelper.moveCaretToEndOfText(); + + expect(window.setTimeout.calls[0].args[1]).toEqual(1); + }); + }); +}); diff --git a/web-ui/test/spec/mail_list/ui/mail_items/generic_mail_item.spec.js b/web-ui/test/spec/mail_list/ui/mail_items/generic_mail_item.spec.js new file mode 100644 index 00000000..b49bc7f0 --- /dev/null +++ b/web-ui/test/spec/mail_list/ui/mail_items/generic_mail_item.spec.js @@ -0,0 +1,136 @@ +describeComponent('mail_list/ui/mail_items/generic_mail_item', function () { + 'use strict'; + + var mail; + + beforeEach(function () { + mail = Smail.testData().parsedMail.simpleTextPlain; + mail.tags = ['inbox']; + + setupComponent('
      • ', { + mail: mail, + selected: false, + tag: 'inbox' + }); + }); + + it('should trigger ui:openMail on click', function () { + var spyEvent = spyOnEvent(document, Smail.events.ui.mail.open); + + this.component.$node.find('a').click(); + + expect(spyEvent).toHaveBeenTriggeredOn(document); + expect(spyEvent.mostRecentCall.data).toEqual({ ident: mail.ident }); + }); + + it('should add selected class when selecting', function () { + this.$node.find('a').click(); + + expect(this.$node).toHaveClass('selected'); + }); + + it('should remove selected class when selecting a different mail', function () { + $(document).trigger(Smail.events.ui.mail.updateSelected, { ident: 2 }); + + expect(this.$node).not.toHaveClass('selected'); + }); + + it('should remove selected class when enabling compose box', function () { + this.$node.find('a').click(); + + $(document).trigger(Smail.events.ui.composeBox.newMessage); + + expect(this.$node).not.toHaveClass('selected'); + }); + + it('should have the href link with mail ident and tag name', function () { + expect(this.$node.find('a')[0].href).toMatch('inbox/mail/' + mail.ident); + }); + + describe('clicking on a mail', function () { + + function createClickEvent(options) { + var clickEvent = $.Event('click'); + _.merge(clickEvent, options); + spyOn(clickEvent, 'preventDefault'); + return clickEvent; + } + + it('triggers mail open and pushes the state', function () { + var clickEvent = createClickEvent(); + var mailOpenEvent = spyOnEvent(document, Smail.events.ui.mail.open); + var pushStateEvent = spyOnEvent(document, Smail.events.router.pushState); + + $(this.$node.find('a')).trigger(clickEvent); + + expect(mailOpenEvent).toHaveBeenTriggeredOnAndWith(document, { ident: mail.ident }); + expect(pushStateEvent).toHaveBeenTriggeredOnAndWith(document, { mailIdent: mail.ident }); + expect(clickEvent.preventDefault).toHaveBeenCalled(); + }); + + describe('when opening on a new tab', function () { + + _.each([ + {metaKey: true}, + {which: 2}, + {ctrlKey: true} + ], function (specialKey) { + it('doesnt trigger mail open and nor pushes the state', function () { + var clickEvent = createClickEvent(specialKey); + var mailOpenEvent = spyOnEvent(document, Smail.events.ui.mail.open); + var pushStateEvent = spyOnEvent(document, Smail.events.router.pushState); + + $(this.$node.find('a')).trigger(clickEvent); + + expect(mailOpenEvent).not.toHaveBeenTriggeredOnAndWith(document, { ident: mail.ident }); + expect(pushStateEvent).not.toHaveBeenTriggeredOnAndWith(document, { mailIdent: mail.ident }); + expect(clickEvent.preventDefault).not.toHaveBeenCalled(); + }); + + it('marks the email as read', function () { + debugger; + var mailReadEvent = spyOnEvent(document, Smail.events.mail.read); + var clickEvent = createClickEvent(specialKey); + + $(this.$node.find('a')).trigger(clickEvent); + + expect(this.component.attr.mail.status).toContain(this.component.status.READ); + expect(this.$node.attr('class')).toMatch('status-read'); + expect(mailReadEvent).toHaveBeenTriggeredOnAndWith(document, { ident: mail.ident, tags: ['inbox'] }); + }); + + }); + + }); + + }); + + describe('marking emails as read', function () { + it('should trigger mail:read event when unread is clicked', function () { + var mailReadEvent = spyOnEvent(document, Smail.events.mail.read); + + this.$node.find('a').click(); + + expect(mailReadEvent).toHaveBeenTriggeredOnAndWith(document, jasmine.objectContaining({ident: mail.ident})); + }); + + it('should not trigger mail:read event when clicking mail that is already read', function () { + var mailReadEvent = spyOnEvent(document, Smail.events.mail.read); + this.component.attr.mail.status.push(this.component.status.READ); + + this.$node.find('a').click(); + + expect(mailReadEvent).not.toHaveBeenTriggeredOnAndWith(document, {ident: mail.ident}); + }); + + it('should add status-read class to email when clicking an unread email', function () { + this.$node.find('a').click(); + + expect(this.$node).toHaveClass('status-read'); + }); + + it('should not have status-read class when initializing email without read status', function () { + expect(this.$node).not.toHaveClass('status-read'); + }); + }); +}); diff --git a/web-ui/test/spec/mail_list/ui/mail_items/mail_item.spec.js b/web-ui/test/spec/mail_list/ui/mail_items/mail_item.spec.js new file mode 100644 index 00000000..1b5899a1 --- /dev/null +++ b/web-ui/test/spec/mail_list/ui/mail_items/mail_item.spec.js @@ -0,0 +1,40 @@ +/*global Smail */ + +describeMixin('mail_list/ui/mail_items/mail_item', function () { + 'use strict'; + + beforeEach(function () { + var mail = Smail.testData().parsedMail.simpleTextPlain; + mail.tags = ['inbox']; + + setupComponent('
      • ', { + mail: mail, + selected: false, + tag: 'inbox' + }); + }); + + describe('mail checkbox', function () { + var mailCheckedEvent, mailUncheckedEvent, checkbox; + beforeEach(function () { + mailCheckedEvent = spyOnEvent(document, Smail.events.ui.mail.checked); + mailUncheckedEvent = spyOnEvent(document, Smail.events.ui.mail.unchecked); + checkbox = this.component.$node.find('input[type=checkbox]'); + }); + + it('checkCheckbox checks it and triggers events.ui.mail.checked', function () { + this.component.checkCheckbox(); + + expect(checkbox.prop('checked')).toBe(true); + expect(mailCheckedEvent).toHaveBeenTriggeredOn(document); + }); + + it('uncheckCheckbox checks it and triggers events.ui.mail.checked', function () { + checkbox.prop('checked', true); + this.component.uncheckCheckbox(); + + expect(checkbox.prop('checked')).toBe(false); + expect(mailUncheckedEvent).toHaveBeenTriggeredOn(document); + }); + }); +}); diff --git a/web-ui/test/spec/mail_list/ui/mail_list.spec.js b/web-ui/test/spec/mail_list/ui/mail_list.spec.js new file mode 100644 index 00000000..f383d540 --- /dev/null +++ b/web-ui/test/spec/mail_list/ui/mail_list.spec.js @@ -0,0 +1,275 @@ +/*global Smail */ + +describeComponent('mail_list/ui/mail_list', function () { + 'use strict'; + + var mailList; + + beforeEach(function () { + setupComponent('
        ', { + urlParams: { + hasMailIdent: function () { + return false; + } + } + }); + mailList = + [ + createMail('the mail subject', 'from@mail.com', '1', '2012-12-26T01:38:46-08:00'), + createMail('another mail subject', 'from_another@mail.com', '2', '2012-12-28T01:38:46-08:00') + ]; + }); + + + it('should open mail at first mail:available if there is a mailIdent in the url hash', function () { + this.component.attr.urlParams = { + hasMailIdent: function () { + return true; + }, + getMailIdent: function () { + return '10'; + } + }; + var openMailEvent = spyOnEvent(document, Smail.events.ui.mail.open); + + this.$node.trigger(Smail.events.mails.available, { mails: mailList }); + expect(openMailEvent).toHaveBeenTriggeredOnAndWith(document, { ident: '10' }); + + this.$node.trigger(Smail.events.mails.available, { mails: mailList }); + expect(openMailEvent.calls.length).toEqual(1); + }); + + it('should push the state if there is a mail ident in the hash url', function () { + this.component.attr.urlParams = { + hasMailIdent: function () { + return true; + }, + getMailIdent: function () { + return '10'; + } + }; + var pushState = spyOnEvent(document, Smail.events.router.pushState); + this.component.attr.currentTag = 'inbox'; + + this.$node.trigger(Smail.events.mails.available, { mails: mailList }); + + expect(pushState).toHaveBeenTriggeredOnAndWith(document, { tag: 'inbox', mailIdent: '10' }); + }); + + describe('checking/unchecking mails in the list', function () { + + it('keeps a list with the currently checked mails', function () { + var checkedMails = {}; + + this.component.attr.checkedMails = {}; + + $(document).trigger(Smail.events.ui.mail.checked, {mail: mailList[0]}); + + checkedMails[mailList[0].ident] = mailList[0]; + + expect(this.component.attr.checkedMails).toEqual(checkedMails); + }); + + it('returns the list of checked mails to whomever requests them', function () { + var caller = {}; + this.component.attr.checkedMails = {'1': {}}; + var mailHereCheckedEvent = spyOnEvent(caller, Smail.events.ui.mail.hereChecked); + + $(document).trigger(Smail.events.ui.mail.wantChecked, caller); + + expect(mailHereCheckedEvent).toHaveBeenTriggeredOnAndWith(caller, { checkedMails: {'1': {} }}); + }); + + it('returns an empty list to whomever requests the checked mails if there are no checked mails', function () { + var caller = {}; + var mailHereCheckedEvent = spyOnEvent(caller, Smail.events.ui.mail.hereChecked); + + $(document).trigger(Smail.events.ui.mail.wantChecked, caller); + + expect(mailHereCheckedEvent).toHaveBeenTriggeredOnAndWith(caller, { checkedMails: {} }); + }); + + it('removes for the checked mails when mail is unchecked', function () { + this.component.attr.checkedMails = { + '1': {}, + '2': {}, + '3': {} + }; + + $(document).trigger(Smail.events.ui.mail.unchecked, {mail: {ident: '1'}}); + + expect(this.component.attr.checkedMails).toEqual({'2': {}, '3': {} }); + }); + + it('checks the check all checkbox if at least one mail is checked', function () { + var setCheckAllCheckboxEvent = spyOnEvent(document, Smail.events.ui.mails.hasMailsChecked); + + $(document).trigger(Smail.events.ui.mail.checked, {mail: mailList[0]}); + + expect(setCheckAllCheckboxEvent).toHaveBeenTriggeredOnAndWith(document, true); + }); + + it('unchecks the check all checkbox if no mail is left checked', function () { + this.component.attr.checkedMails = {1: {}}; + + var setCheckAllCheckboxEvent = spyOnEvent(document, Smail.events.ui.mails.hasMailsChecked); + + $(document).trigger(Smail.events.ui.mail.unchecked, {mail: {ident: '1'}}); + + expect(setCheckAllCheckboxEvent).toHaveBeenTriggeredOnAndWith(document, false); + }); + }); + + describe('when mails are available', function () { + it('should open email if popstate event happened (when mailIdent isnt undefined)', function () { + var openMailEvent = spyOnEvent(document, Smail.events.ui.mail.open); + + this.component.$node.trigger(Smail.events.mails.available, { mails: mailList, mailIdent: '30' }); + + expect(openMailEvent).toHaveBeenTriggeredOnAndWith(document, { ident: '30'}); + }); + + it('should open draft in popstate event if tag is Drafts', function () { + var openDraftEvent = spyOnEvent(document, Smail.events.dispatchers.rightPane.openDraft); + + this.component.$node.trigger(Smail.events.mails.available, { mails: mailList, mailIdent: '30', tag: 'drafts' }); + + expect(openDraftEvent).toHaveBeenTriggeredOnAndWith(document, { ident: '30'}); + }); + }); + + it('should not append emails when another mails:available event is triggered', function () { + this.component.$node.trigger(Smail.events.mails.available, { mails: mailList }); + + expect(this.component.$node.find('a').length).toEqual(2); + + this.component.$node.trigger(Smail.events.mails.available, { mails: mailList }); + + expect(this.component.$node.find('a').length).toEqual(2); + }); + + it('resets scroll when opening a new tag or choosing a new tag', function () { + var eventSpy = spyOnEvent(document, Smail.events.dispatchers.middlePane.resetScroll); + this.component.$node.trigger(Smail.events.mails.available, { mails: mailList }); + expect(eventSpy).toHaveBeenTriggeredOn(document); + }); + + describe('rendering the mails', function () { + + describe('when mails are available for refreshing', function () { + it('renders the new mails', function () { + this.component.$node.trigger(Smail.events.mails.availableForRefresh, { mails: mailList }); + + matchMail(mailList[0], this.component.$node); + matchMail(mailList[1], this.component.$node); + }); + + }); + + it('should render all mails sent in ui:mails:show event', function () { + this.component.$node.trigger(Smail.events.mails.available, { mails: mailList }); + + matchMail(mailList[0], this.component.$node); + matchMail(mailList[1], this.component.$node); + }); + + it('should select the current email when mails are available', function () { + this.component.attr.currentMailIdent = '1'; + + this.component.trigger(Smail.events.mails.available, { mails: mailList }); + + matchSelectedMail(mailList[0], this.component.$node); + matchMail(mailList[1], this.component.$node); + }); + + it('should keep the mail checked when it was previously checked (so refresh works)', function () { + var checkbox, mailIdent; + + mailIdent = mailList[0].ident; + this.component.attr.checkedMails[mailIdent] = mailList[0]; + this.component.$node.trigger(Smail.events.mails.available, { mails: [mailList[0]] }); + checkbox = this.$node.find('input[type=checkbox]'); + + expect(checkbox.prop('checked')).toBe(true); + }); + + it('should render links for the emails', function () { + this.component.$node.trigger(Smail.events.mails.available, { mails: mailList, tag: 'inbox' }); + + expect(this.$node.html()).toMatch('href="/#/inbox/mail/1'); + expect(this.$node.html()).toMatch('href="/#/inbox/mail/2'); + }); + + it('should clean the selected email', function () { + this.component.attr.currentMailIdent = '1'; + this.component.trigger(Smail.events.ui.mails.cleanSelected); + + expect(this.component.attr.currentMailIdent).toEqual(''); + }); + + function matchMail(mail, node) { + expect(node.html()).toMatch('id="mail-' + mail.ident + '"'); + expect(node.html()).toMatch('
        '); + expect(node.html()).toMatch('
        ' + mail.header.from + '
        '); + expect(node.html()).toMatch('' + mail.header.formattedDate + ''); + } + + function matchSelectedMail(mail, node) { + expect(node.html()).toMatch(['id="mail-', mail.ident, '" class="selected"'].join('')); + } + }); + + describe('when saving a draft', function () { + it('refreshes the list if the current tag is drafts', function () { + this.component.attr.currentTag = 'drafts'; + var spyRefresh = spyOnEvent(document, Smail.events.ui.mails.refresh); + var spyScroll = spyOnEvent(document, Smail.events.dispatchers.middlePane.resetScroll); + this.component.trigger(Smail.events.mail.draftSaved, {ident: 1}); + expect(spyRefresh).toHaveBeenTriggeredOn(document); + expect(spyScroll).toHaveBeenTriggeredOn(document); + }); + + it('does not refresh the list if the current tag is not drafts', function() { + this.component.attr.currentTag = 'sent'; + var spyRefresh = spyOnEvent(document, Smail.events.ui.mails.refresh); + var spyScroll = spyOnEvent(document, Smail.events.dispatchers.middlePane.resetScroll); + this.component.trigger(Smail.events.mail.draftSaved, {ident: 1}); + expect(spyRefresh).not.toHaveBeenTriggeredOn(document); + expect(spyScroll).not.toHaveBeenTriggeredOn(document); + }); + }); + + describe('when sending a mail', function () { + it('refreshes the list if the current tag is drafts', function () { + this.component.attr.currentTag = 'drafts'; + var spyRefresh = spyOnEvent(document, Smail.events.ui.mails.refresh); + var spyScroll = spyOnEvent(document, Smail.events.dispatchers.middlePane.resetScroll); + this.component.trigger(Smail.events.mail.sent); + expect(spyRefresh).toHaveBeenTriggeredOn(document); + expect(spyScroll).toHaveBeenTriggeredOn(document); + }); + + it('refreshes the list if the current tag is sent', function() { + this.component.attr.currentTag = 'sent'; + var spyRefresh = spyOnEvent(document, Smail.events.ui.mails.refresh); + var spyScroll = spyOnEvent(document, Smail.events.dispatchers.middlePane.resetScroll); + this.component.trigger(Smail.events.mail.sent); + expect(spyRefresh).toHaveBeenTriggeredOn(document); + expect(spyScroll).toHaveBeenTriggeredOn(document); + }); + }); + + function createMail(subject, from, ident, date) { + var mail = Smail.testData().parsedMail.simpleTextPlain; + + return _.merge(mail, { + header: { + subject: subject, + from: from, + date: date + }, + ident: ident, + tags: ['inbox'] + }); + } +}); diff --git a/web-ui/test/spec/mail_list_actions/ui/compose_trigger.spec.js b/web-ui/test/spec/mail_list_actions/ui/compose_trigger.spec.js new file mode 100644 index 00000000..4942b5b6 --- /dev/null +++ b/web-ui/test/spec/mail_list_actions/ui/compose_trigger.spec.js @@ -0,0 +1,16 @@ +describeComponent('mail_list_actions/ui/compose_trigger', function () { + 'use strict'; + + beforeEach(function () { + setupComponent('
        '); + }); + + it('triggers the enableComposebox event when clicked', function () { + var spyEvent = spyOnEvent(document, Smail.events.dispatchers.rightPane.openComposeBox); + + this.component.trigger('click'); + + expect(spyEvent).toHaveBeenTriggeredOn(document); + }); + +}); diff --git a/web-ui/test/spec/mail_list_actions/ui/mail_actions.spec.js b/web-ui/test/spec/mail_list_actions/ui/mail_actions.spec.js new file mode 100644 index 00000000..f7a0ed72 --- /dev/null +++ b/web-ui/test/spec/mail_list_actions/ui/mail_actions.spec.js @@ -0,0 +1,9 @@ +describeComponent('mail_list_actions/ui/mail_actions', function () { + 'use strict'; + + beforeEach(function () { + setupComponent(); + }); + + +}); diff --git a/web-ui/test/spec/mail_list_actions/ui/pagination_trigger.spec.js b/web-ui/test/spec/mail_list_actions/ui/pagination_trigger.spec.js new file mode 100644 index 00000000..67c2f3ef --- /dev/null +++ b/web-ui/test/spec/mail_list_actions/ui/pagination_trigger.spec.js @@ -0,0 +1,26 @@ +describeComponent('mail_list_actions/ui/pagination_trigger', function () { + 'use strict'; + + beforeEach(function () { + setupComponent(); + }); + + it('triggers the ui:page:previous event when the left arrow is clicked', function () { + var eventSpy = spyOnEvent(document, Smail.events.ui.page.previous); + this.component.select('previous').click(); + expect(eventSpy).toHaveBeenTriggeredOn(document); + }); + + + it('triggers the ui:page:next event when the right arrow is clicked', function () { + var eventSpy = spyOnEvent(document, Smail.events.ui.page.next); + this.component.select('next').click(); + expect(eventSpy).toHaveBeenTriggeredOn(document); + }); + + it('re-renders with current page number when page changes', function () { + this.component.trigger(document, Smail.events.ui.page.changed, {currentPage: 0}); + + expect(this.component.select('currentPage').text()).toBe('1'); + }); +}); diff --git a/web-ui/test/spec/mail_view/data/mail_builder.spec.js b/web-ui/test/spec/mail_view/data/mail_builder.spec.js new file mode 100644 index 00000000..bf17d598 --- /dev/null +++ b/web-ui/test/spec/mail_view/data/mail_builder.spec.js @@ -0,0 +1,110 @@ +define(['mail_view/data/mail_builder'], function (mailBuilder) { + describe('mail builder', function () { + 'use strict'; + + it('sets ident if passed to constructor', function() { + var mail = mailBuilder.newMail('12345').build(); + + expect(mail.ident).toBe('12345'); + }); + + it('sets ident to empty if not passed to constructor', function() { + var mail = mailBuilder.newMail().build(); + + expect(mail.ident).toBe(''); + }); + + it('sets the subject', function() { + var mail = mailBuilder.newMail().subject("subject").build(); + + expect(mail.header.subject).toBe("subject"); + }); + + it('sets the body', function() { + var mail = mailBuilder.newMail().body("some body text").build(); + + expect(mail.body).toBe("some body text"); + }); + + describe('to field', function() { + it('adds a single address', function() { + var mail = mailBuilder.newMail().to('foo@bar.com').build(); + + expect(mail.header.to).toContain('foo@bar.com'); + }); + + it('adds multiple addresses', function() { + var mail = mailBuilder.newMail().to('foo@bar.com bar@foo.com').build(); + + expect(mail.header.to).toContain('foo@bar.com'); + expect(mail.header.to).toContain('bar@foo.com'); + }); + + it('accepts undefined without breaking', function() { + var mail = mailBuilder.newMail().to(undefined).build(); + + expect(mail.header.to).toEqual([]); + }); + }); + + describe('cc field', function() { + it('adds a single address', function() { + var mail = mailBuilder.newMail().cc('foo@bar.com').build(); + + expect(mail.header.cc).toContain('foo@bar.com'); + }); + + it('adds multiple addresses', function() { + var mail = mailBuilder.newMail().cc('foo@bar.com bar@foo.com').build(); + + expect(mail.header.cc).toContain('foo@bar.com'); + expect(mail.header.cc).toContain('bar@foo.com'); + }); + + it('accepts undefined without breaking', function() { + var mail = mailBuilder.newMail().cc(undefined).build(); + + expect(mail.header.cc).toEqual([]); + }); + }); + + describe('bcc field', function() { + it('adds a single address', function() { + var mail = mailBuilder.newMail().bcc('foo@bar.com').build(); + + expect(mail.header.bcc).toContain('foo@bar.com'); + }); + + it('adds multiple addresses', function() { + var mail = mailBuilder.newMail().bcc('foo@bar.com bar@foo.com').build(); + + expect(mail.header.bcc).toContain('foo@bar.com'); + expect(mail.header.bcc).toContain('bar@foo.com'); + }); + + it('accepts undefined without breaking', function() { + var mail = mailBuilder.newMail().bcc(undefined).build(); + + expect(mail.header.bcc).toEqual([]); + }); + }); + + it('adds arbitrary headers', function() { + var mail = mailBuilder.newMail() + .header('Reply-To', 'something') + .header('In-Reply-To', '12345') + .build(); + + expect(mail.header['Reply-To']).toBe('something'); + expect(mail.header['In-Reply-To']).toBe('12345'); + }); + + it('adds tag', function() { + var mail = mailBuilder.newMail() + .tag('tag1') + .build(); + + expect(mail.tags).toContain('tag1'); + }); + }); +}); diff --git a/web-ui/test/spec/mail_view/data/mail_sender.spec.js b/web-ui/test/spec/mail_view/data/mail_sender.spec.js new file mode 100644 index 00000000..3bdc1934 --- /dev/null +++ b/web-ui/test/spec/mail_view/data/mail_sender.spec.js @@ -0,0 +1,70 @@ +/*global Smail */ + +describeComponent('mail_view/data/mail_sender', function () { + 'use strict'; + + var mailBuilder; + var mail; + + beforeEach(function () { + mailBuilder = require('mail_view/data/mail_builder'); + mail = Smail.testData().parsedMail.simpleTextPlain; + setupComponent(); + }); + + it('sends mail data with a POST to the server when asked to send email', function() { + var mailSentEventSpy = spyOnEvent(document, Smail.events.mail.sent); + var g; + + spyOn($, 'ajax').andReturn({done: function(f) { g = f; return {fail: function(){}};}}); + + this.component.trigger(Smail.events.mail.send, mail); + + g(); + + expect(mailSentEventSpy).toHaveBeenTriggeredOn(document); + + expect($.ajax.mostRecentCall.args[0]).toEqual('/mails'); + expect($.ajax.mostRecentCall.args[1].type).toEqual('POST'); + expect(JSON.parse($.ajax.mostRecentCall.args[1].data).header).toEqual(mail.header); + expect(JSON.parse($.ajax.mostRecentCall.args[1].data).body).toEqual(mail.body); + }); + + it('save draft data with a POST to the server when asked to save draft for the first time', function() { + var draftSavedEventSpy = spyOnEvent(document, Smail.events.mail.draftSaved); + var g; + + spyOn($, 'ajax').andReturn({done: function(f) { g = f; return {fail: function(){}};}}); + + mail.ident = ''; + this.component.trigger(Smail.events.mail.saveDraft, mail); + + g(); + + expect(draftSavedEventSpy).toHaveBeenTriggeredOn(document); + + expect($.ajax.mostRecentCall.args[0]).toEqual('/mails'); + expect($.ajax.mostRecentCall.args[1].type).toEqual('POST'); + expect(JSON.parse($.ajax.mostRecentCall.args[1].data).header).toEqual(mail.header); + expect(JSON.parse($.ajax.mostRecentCall.args[1].data).body).toEqual(mail.body); + }); + + it('save draft data with a PUT to the server when asked to save draft for the second time', function() { + var draftSavedEventSpy = spyOnEvent(document, Smail.events.mail.draftSaved); + var g; + + spyOn($, 'ajax').andReturn({done: function(f) { g = f; return {fail: function(){}};}}); + + mail.ident = 0; + this.component.trigger(Smail.events.mail.saveDraft, mail); + + g(); + + expect(draftSavedEventSpy).toHaveBeenTriggeredOn(document); + + expect($.ajax.mostRecentCall.args[0]).toEqual('/mails'); + expect($.ajax.mostRecentCall.args[1].type).toEqual('PUT'); + expect(JSON.parse($.ajax.mostRecentCall.args[1].data).header).toEqual(mail.header); + expect(JSON.parse($.ajax.mostRecentCall.args[1].data).body).toEqual(mail.body); + }); +}); diff --git a/web-ui/test/spec/mail_view/ui/compose_box.spec.js b/web-ui/test/spec/mail_view/ui/compose_box.spec.js new file mode 100644 index 00000000..a131d2cf --- /dev/null +++ b/web-ui/test/spec/mail_view/ui/compose_box.spec.js @@ -0,0 +1,132 @@ +/*global jasmine */ +/*global Smail */ + +describeComponent('mail_view/ui/compose_box', function () { + 'use strict'; + beforeEach(function () { + Smail.mockBloodhound(); + setupComponent('
        '); + }); + + + describe('compose new mail', function() { + + it('only sends if all the recipients are valid emails', function() { + $(document).trigger(Smail.events.ui.recipients.updated, {recipientsName: 'to', newRecipients: ['valid@email.example']}); + + var eventSpy = spyOnEvent(document, Smail.events.mail.send); + + $(document).trigger(Smail.events.ui.mail.send); + + expect(eventSpy).toHaveBeenTriggeredOn(document); + }); + + it('sends the recipient entered', function () { + $(document).trigger(Smail.events.ui.recipients.updated, {recipientsName: 'to', newRecipients: ['fox@somewhere.com']}); + + var eventSpy = spyOnEvent(document, Smail.events.mail.send); + + $(document).trigger(Smail.events.ui.mail.send); + + expect(eventSpy).toHaveBeenTriggeredOn(document); + expect(eventSpy.mostRecentCall.data.header).toEqual(jasmine.objectContaining({ + to: ['fox@somewhere.com'] + })); + }); + + it('sends the multiple recipients entered', function () { + $(document).trigger(Smail.events.ui.recipients.updated, {recipientsName: 'to', newRecipients: ['fox@somewhere.com', 'blarg@someone.com', 'fox2@google.se']}); + var eventSpy = spyOnEvent(document, Smail.events.mail.send); + + $(document).trigger(Smail.events.ui.mail.send); + + expect(eventSpy).toHaveBeenTriggeredOn(document); + expect(eventSpy.mostRecentCall.data.header).toEqual(jasmine.objectContaining({ + to: ['fox@somewhere.com', 'blarg@someone.com', 'fox2@google.se'] + })); + }); + + it('sends the subject line entered', function () { + $(document).trigger(Smail.events.ui.recipients.updated, {recipientsName: 'to', newRecipients: ['aa@aa.com']}); + this.component.select('subjectBox').val('A new fancy subject!'); + var eventSpy = spyOnEvent(document, Smail.events.mail.send); + + $(document).trigger(Smail.events.ui.mail.send); + + expect(eventSpy).toHaveBeenTriggeredOn(document); + expect(eventSpy.mostRecentCall.data.header).toEqual(jasmine.objectContaining({ + subject: 'A new fancy subject!' + })); + }); + + it('sends the multiple CCs entered', function () { + $(document).trigger(Smail.events.ui.recipients.updated, {recipientsName: 'cc', newRecipients: ['cc1@foo.bar', 'cc2@bar.foo', 'cc3@zz.top']}); + var eventSpy = spyOnEvent(document, Smail.events.mail.send); + + $(document).trigger(Smail.events.ui.mail.send); + + expect(eventSpy).toHaveBeenTriggeredOn(document); + expect(eventSpy.mostRecentCall.data.header).toEqual(jasmine.objectContaining({ + cc: ['cc1@foo.bar', 'cc2@bar.foo', 'cc3@zz.top'] + })); + }); + + it('sends the multiple BCCs entered', function () { + $(document).trigger(Smail.events.ui.recipients.updated, {recipientsName: 'bcc', newRecipients: ['bcc1@foo.bar', 'bcc2@bar.foo', 'bcc3@zz.top']}); + var eventSpy = spyOnEvent(document, Smail.events.mail.send); + + $(document).trigger(Smail.events.ui.mail.send); + + expect(eventSpy).toHaveBeenTriggeredOn(document); + expect(eventSpy.mostRecentCall.data.header).toEqual(jasmine.objectContaining({ + bcc: ['bcc1@foo.bar', 'bcc2@bar.foo', 'bcc3@zz.top'] + })); + }); + + it('shows no message selected pane when deleting the email being composed', function() { + var openNoMessageSelectedPaneEvent = spyOnEvent(document, Smail.events.dispatchers.rightPane.openNoMessageSelected); + var mails = [{ident: 123}]; + this.component.attr.ident = 123; + + this.component.trigger(document, Smail.events.mail.deleted, {mails: mails}); + + expect(openNoMessageSelectedPaneEvent).toHaveBeenTriggeredOn(document); + }); + + it('does not show no message selected pane when deleting a different set of emails', function() { + var openNoMessageSelectedPaneEvent = spyOnEvent(document, Smail.events.dispatchers.rightPane.openNoMessageSelected); + var mails = [{ident: 321}]; + this.component.attr.ident = 123; + + this.component.trigger(document, Smail.events.mail.deleted, {mails: mails}); + + expect(openNoMessageSelectedPaneEvent).not.toHaveBeenTriggeredOn(document); + }); + }); + + describe('close button behavior', function() { + + it('should fire Show no message selected if the close button is clicked', function() { + var spy = spyOnEvent(document, Smail.events.dispatchers.rightPane.openNoMessageSelected); + this.component.select('closeButton').click(); + expect(spy).toHaveBeenTriggeredOn(document); + }); + + }); + + describe('draft compose box', function() { + it('should save a draft when click on draft button', function () { + $(document).trigger(Smail.events.ui.recipients.updated, {recipientsName: 'to', newRecipients: ['fox@somewhere.com']}); + + this.component.select('subjectBox').val('A new fancy subject!'); + var eventSpy = spyOnEvent(document, Smail.events.mail.saveDraft); + + this.component.select('draftButton').click(); + + expect(eventSpy).toHaveBeenTriggeredOn(document); + expect(eventSpy.mostRecentCall.data.header).toEqual(jasmine.objectContaining({ + to: ['fox@somewhere.com'] + })); + }); + }); +}); diff --git a/web-ui/test/spec/mail_view/ui/draft_box.spec.js b/web-ui/test/spec/mail_view/ui/draft_box.spec.js new file mode 100644 index 00000000..3e02f752 --- /dev/null +++ b/web-ui/test/spec/mail_view/ui/draft_box.spec.js @@ -0,0 +1,67 @@ +/* global Smail */ + +describeComponent('mail_view/ui/draft_box', function () { + 'use strict'; + + var mail; + + beforeEach(function () { + Smail.mockBloodhound(); + mail = Smail.testData().parsedMail.simpleTextPlain; + }); + + describe('when initializing', function () { + it('fetches the email to draft', function () { + var mailWantEvent = spyOnEvent(document, Smail.events.mail.want); + + setupComponent({mailIdent: '1'}); + + expect(mailWantEvent).toHaveBeenTriggeredOnAndWith(document, { + mail: '1', caller: this.component + }); + }); + }); + + describe('after initialize', function () { + beforeEach(function () { + setupComponent({mailIdent: '1'}); + }); + + it('renders the compose box when mail is received', function () { + var templates = require('views/templates'); + + spyOn(this.component, 'render'); + + this.component.trigger(this.component, Smail.events.mail.here, { mail: mail}); + + expect(this.component.render).toHaveBeenCalledWith(templates.compose.box, { + recipients: { to: mail.header.to, cc: mail.header.cc, bcc: mail.header.bcc }, + subject: mail.header.subject, + body: mail.body + }); + }); + + }); + + it('sending a draft sends the correct mailIdent', function () { + setupComponent({mailIdent: mail.ident}); + this.component.trigger(this.component, Smail.events.mail.here, { mail: mail}); + + var sendDraftEvent = spyOnEvent(document, Smail.events.mail.saveDraft); + this.component.select('draftButton').click(); + + expect(sendDraftEvent).toHaveBeenTriggeredOnAndWith(document, jasmine.objectContaining({ident: mail.ident})); + }); + + it('shows no message selected pane when draft is sent', function() { + var openNoMessageSelectedEvent = spyOnEvent(document, Smail.events.dispatchers.rightPane.openNoMessageSelected); + + setupComponent({mailIdent: mail.ident}); + this.component.trigger(this.component, Smail.events.mail.here, { mail: mail}); + + this.component.trigger(document, Smail.events.mail.sent); + + expect(openNoMessageSelectedEvent).toHaveBeenTriggeredOn(document); + }); + +}); diff --git a/web-ui/test/spec/mail_view/ui/draft_save_status.spec.js b/web-ui/test/spec/mail_view/ui/draft_save_status.spec.js new file mode 100644 index 00000000..fb989f4c --- /dev/null +++ b/web-ui/test/spec/mail_view/ui/draft_save_status.spec.js @@ -0,0 +1,26 @@ +/* global Smail */ + +describeComponent('mail_view/ui/draft_save_status', function () { + 'use strict'; + + beforeEach(setupComponent); + + it('should be empty on initialization', function() { + expect(this.$node.text()).toBe(''); + }); + + it('should display status when saving a draft', function() { + $(document).trigger(Smail.events.mail.saveDraft); + expect(this.$node.text()).toBe('Saving to Drafts...'); + }); + + it('should display status when draft is saved', function() { + $(document).trigger(Smail.events.mail.draftSaved); + expect(this.$node.text()).toBe('Draft Saved.'); + }); + + it('should reset status when mail is changed since last save', function() { + $(document).trigger(Smail.events.ui.mail.changedSinceLastSave); + expect(this.$node.text()).toBe(''); + }); +}); diff --git a/web-ui/test/spec/mail_view/ui/forward_box.spec.js b/web-ui/test/spec/mail_view/ui/forward_box.spec.js new file mode 100644 index 00000000..fc878447 --- /dev/null +++ b/web-ui/test/spec/mail_view/ui/forward_box.spec.js @@ -0,0 +1,90 @@ +/*global jasmine */ +/*global Smail */ + +describeComponent('mail_view/ui/forward_box', function () { + 'use strict'; + + var attrs; + var testMail; + beforeEach(function () { + testMail = Smail.testData().parsedMail.simpleTextPlain; + + Smail.mockBloodhound(); + }); + + it('should have a subject of Fwd: ', function() { + testMail.header.subject = 'Very interesting'; + setupComponent({ mail: testMail }); + + expect(this.component.select('subjectDisplay').text()).toEqual('Fwd: '+ testMail.header.subject); + }); + + it('should have no recipients', function () { + var Recipients = require('mail_view/ui/recipients/recipients'); + spyOn(Recipients, 'attachTo'); + + setupComponent({ mail: testMail }); + + expect(Recipients.attachTo.calls[0].args[1]).toEqual({name: 'to', addresses: []}); + expect(Recipients.attachTo.calls[1].args[1]).toEqual({name: 'cc', addresses: []}); + expect(Recipients.attachTo.calls[2].args[1]).toEqual({name: 'bcc', addresses: []}); + }); + + it('should populate body text area with quote of email being forwarded', function() { + var viewHelper = require('helpers/view_helper'); + spyOn(viewHelper, 'quoteMail').andReturn('quoted email'); + + setupComponent({ mail: testMail }); + + expect(viewHelper.quoteMail).toHaveBeenCalledWith(testMail); + expect(this.component.select('bodyBox').val()).toBe('quoted email'); + }); + + it('should show subject field when clicking on subject display', function() { + setupComponent({ mail: testMail }); + + this.component.select('subjectDisplay').click(); + + expect(this.component.select('subjectInput')).not.toBeHidden(); + expect(this.component.select('subjectDisplay')).toBeHidden(); + }); + + it('should copy original message headers', function() { + var mailSendEvent = spyOnEvent(document, Smail.events.mail.send); + + testMail.header.bcc = 'original_bcc@email.com'; + testMail.header.cc = 'original_cc@email.com'; + testMail.header.date = 'original_date'; + testMail.header.from = 'original_from'; + testMail.header.message_id = 'original_message_id'; + testMail.header.reply_to = 'original_reply_to@email.com'; + testMail.header.sender = 'original_sender'; + testMail.header.to = 'original_to@email.com'; + + setupComponent({ mail: testMail }); + + this.component.attr.recipientValues.to.push('forward_to@email.com'); + $(document).trigger(Smail.events.ui.mail.send); + + expect(mailSendEvent).toHaveBeenTriggeredOn(document); + var sentMail = mailSendEvent.mostRecentCall.data; + + expect(sentMail.header).toEqual(jasmine.objectContaining({ + resent_bcc: 'original_bcc@email.com', + resent_cc: 'original_cc@email.com', + resent_date: 'original_date', + resent_from: 'original_from', + resent_message_id: 'original_message_id', + resent_reply_to: 'original_reply_to@email.com', + resent_sender: 'original_sender', + resent_to: 'original_to@email.com' + })); + }); + + it('triggers openMail when email is sent', function() { + var eventSpy = spyOnEvent(document, Smail.events.ui.mail.open); + setupComponent({ mail: testMail }); + $(document).trigger(Smail.events.mail.sent, {ident: testMail.ident}); + expect(eventSpy).toHaveBeenTriggeredOnAndWith(document, {ident: testMail.ident}); + }); +}); diff --git a/web-ui/test/spec/mail_view/ui/mail_actions.spec.js b/web-ui/test/spec/mail_view/ui/mail_actions.spec.js new file mode 100644 index 00000000..93db0193 --- /dev/null +++ b/web-ui/test/spec/mail_view/ui/mail_actions.spec.js @@ -0,0 +1,63 @@ +/*global Smail */ + +describeComponent('mail_view/ui/mail_actions', function () { + 'use strict'; + + var testData; + + beforeEach(function(){ + testData = Smail.testData(); + setupComponent(testData); + }); + + it('verifies if more actions list is hidden when rendering mail view', function() { + + var moreActionsComponent = this.component.select('moreActions'); + expect(moreActionsComponent.attr('style').trim()).toEqual('display: none;'); + + }); + + it('show more actions list when click on view more actions button', function(){ + + this.component.select('viewMoreActions').click(); + + var moreActionsComponent = this.component.select('moreActions'); + expect(moreActionsComponent.attr('style').trim()).not.toEqual('display: none;'); + }); + + it('triggers a show reply box event when clicking on reply-button-top', function(){ + + var showReplyBoxEvent = spyOnEvent(document, Smail.events.ui.replyBox.showReply); + + this.component.select('replyButtonTop').click(); + + expect(showReplyBoxEvent).toHaveBeenTriggeredOn(document); + }); + + it('triggers a show reply all box event when clicking on reply-button-top and hide more actions list', function(){ + + var showReplyAllEvent = spyOnEvent(document, Smail.events.ui.replyBox.showReplyAll); + + this.component.select('viewMoreActions').click(); + this.component.select('replyAllButtonTop').click(); + + expect(showReplyAllEvent).toHaveBeenTriggeredOn(document); + + var moreActionsComponent = this.component.select('moreActions'); + expect(moreActionsComponent.attr('style').trim()).toEqual('display: none;'); + }); + + it('triggers a delete event when clicking on delete-button-top', function(){ + + var deleteEvent = spyOnEvent(document, Smail.events.ui.mail.delete); + + this.component.select('viewMoreActions').click(); + this.component.select('deleteButtonTop').click(); + + expect(deleteEvent).toHaveBeenTriggeredOnAndWith(document, {mail: testData.mail}); + + var moreActionsComponent = this.component.select('moreActions'); + expect(moreActionsComponent.attr('style').trim()).toEqual('display: none;'); + }); + +}); diff --git a/web-ui/test/spec/mail_view/ui/mail_view.spec.js b/web-ui/test/spec/mail_view/ui/mail_view.spec.js new file mode 100644 index 00000000..80f253e0 --- /dev/null +++ b/web-ui/test/spec/mail_view/ui/mail_view.spec.js @@ -0,0 +1,247 @@ +/*global Smail */ + +describeComponent('mail_view/ui/mail_view', function () { + 'use strict'; + + var mail; + + var testData; + + beforeEach(function () { + mail = {ident: 1, header: { date: '12/12/12T12:12' }, tags: ['inbox']}; + testData = {mail: Smail.testData().parsedMail.simpleTextPlain}; + Smail.mockBloodhound(); + setupComponent('
        ', {mail: mail}); + }); + + it('triggers mail:want on ui:openMail', function () { + var spyEvent = spyOnEvent(document, Smail.events.mail.want); + + setupComponent('
        ', {ident: mail.ident }); + + expect(spyEvent).toHaveBeenTriggeredOn(document); + expect(spyEvent.mostRecentCall.data.mail).toEqual(1); + }); + + it('triggers dispatchers.rightPane.openNoMessageSelected when getting mail.notFound', function () { + var openNoMessageSelectedEvent = spyOnEvent(document, Smail.events.dispatchers.rightPane.openNoMessageSelected); + + this.component.trigger(this.component, Smail.events.mail.notFound); + + expect(openNoMessageSelectedEvent).toHaveBeenTriggeredOn(document); + }); + + + it('removes the tag from the mail when the tag label is clicked', function() { + var updateSpy = spyOnEvent(document, Smail.events.mail.tags.update); + + this.component.displayMail({}, testData); + this.component.removeTag('inbox'); + + expect(updateSpy).toHaveBeenTriggeredOn(document); + }); + + it('verifies if new tag input is hidden when rendering mail view', function() { + + this.component.displayMail({}, testData); + + var newTagInputComponent = this.component.select('newTagInput'); + expect(newTagInputComponent.attr('style').trim()).toEqual('display: none;'); + }); + + it('verifies if new tag input is shown when clicking on new tag button ', function() { + this.component.displayMail({}, testData); + + var newTagInputComponent = this.component.select('newTagInput'); + var addNewComponent = this.component.select('addNew'); + + this.component.select('newTagButton').click(); + + expect(newTagInputComponent.attr('style').trim()).not.toEqual('display: none;'); + expect(addNewComponent.attr('style').trim()).toEqual('display: none;'); + }); + + it('hides new tag button when pressing esc key', function(){ + this.component.displayMail({}, testData); + this.component.select('newTagButton').click(); + + var e = creatingEvent("keydown", 27); + var newTagInputComponent = this.component.select('newTagInput'); + var addNewComponent = this.component.select('addNew'); + + newTagInputComponent.trigger(e); + + expect(newTagInputComponent.attr('style').trim()).toEqual('display: none;'); + expect(addNewComponent.attr('style').trim()).not.toEqual('display: none;'); + }); + + it('assumes that the mail is encrypted and valid if at least one of the locks are valid', function() { + var email = testData; + email.security_casing = {locks: [{state: 'valid'}, {state: 'failure'}]}; + expect(this.component.checkEncrypted(email)).toEqual('encrypted encryption-valid'); + }); + + it('assumes that the mail is encrypted and failure if all the locks are failed', function() { + var email = testData; + email.security_casing = {locks: [{state: 'failure'}, {state: 'failure'}]}; + expect(this.component.checkEncrypted(email)).toEqual('encrypted encryption-failure'); + }); + + it('assumes that the mail is not encrypted if it doesn\'t have any locks', function() { + var email = testData; + email.security_casing = {locks: []}; + expect(this.component.checkEncrypted(email)).toEqual('not-encrypted'); + }); + + it('assumes that the mail is signed only if all imprints are valid', function() { + var email = testData; + email.security_casing = {imprints: [{state: 'valid', seal: {trust: 'marginal', validity: 'marginal'}}, {state: 'valid', seal: {trust: 'marginal', validity: 'marginal'}}]}; + expect(this.component.checkSigned(email)).toEqual('signed'); + }); + + it('assumes that the mail is signed with failures if there is a revoke or expire', function() { + var email = testData; + email.security_casing = {imprints: [{state: 'valid', seal: {trust: 'marginal', validity: 'marginal'}}, {state: 'from_revoked', seal: {trust: 'marginal', validity: 'marginal'}}]}; + expect(this.component.checkSigned(email)).toEqual('signed signature-revoked'); + }); + + it('assumes that mail is not trusted if its signature contains no_trust from the user', function() { + var email = testData; + email.security_casing = {imprints: [{seal: {trust: "no_trust", validity: "ultimate"}}]}; + expect(this.component.checkSigned(email)).toEqual('signed signature-not-trusted'); + }); + + it('uses validity when trust is not present', function() { + var email = testData; + email.security_casing = {imprints: [{seal: { validity: "no_trust"}}]}; + expect(this.component.checkSigned(email)).toEqual('signed signature-not-trusted'); + }); + + it('assumes not trusted when the signature is not found', function(){ + var email = testData; + email.security_casing = {imprints: [{seal: null}]}; + expect(this.component.checkSigned(email)).toEqual('signed signature-not-trusted'); + }); + + it('assumes that the mail is not signed if there are no imprints', function() { + var email = testData + email.security_casing = {imprints: []} + expect(this.component.checkSigned(email)).toEqual('not-signed'); + }); + + it('shows that mail is encrypted if it is', function() { + spyOn(this.component, 'checkEncrypted').andReturn('encrypted'); + this.component.displayMail({}, testData); + expect(this.component.$node.find('.encrypted')).toExist(); + }); + + it('shows that mail is signed if it is', function() { + spyOn(this.component, 'checkSigned').andReturn('signed'); + this.component.displayMail({}, testData); + expect(this.component.$node.find('.signed')).toExist(); + }); + + it('shows that mail is not encrypted if it isn\'t', function() { + spyOn(this.component, 'checkEncrypted').andReturn('not-encrypted'); + this.component.displayMail({}, testData); + expect(this.component.$node.find('.not-encrypted')).toExist(); + }); + + it('shows that mail is not signed if it isn\'t', function() { + spyOn(this.component, 'checkEncrypted').andReturn('not-signed'); + this.component.displayMail({}, testData); + expect(this.component.$node.find('.not-signed')).toExist(); + }); + + it('creates new tag when pressing Enter key on new tag input', function(){ + var tagsUpdateEvent = spyOnEvent(document, Smail.events.mail.tags.update); + var tagListRefreshEvent = spyOnEvent(document, Smail.events.dispatchers.tags.refreshTagList); + var e = creatingEvent("keydown", 13); + + this.component.displayMail({}, testData); + this.component.select('newTagButton').click(); + + var newTagInputComponent = this.component.select('newTagInput'); + newTagInputComponent.val('Test'); + newTagInputComponent.trigger(e); + + var tags = testData.mail.tags.slice(); + tags.push('Test'); + + expect(tagListRefreshEvent).toHaveBeenTriggeredOn(document); + expect(tagsUpdateEvent).toHaveBeenTriggeredOnAndWith(document, { ident: testData.mail.ident, tags: tags}); + }); + + it('trigger mail delete event when moving email to trash', function(){ + var mailDeleteEvent = spyOnEvent(document, Smail.events.ui.mail.delete); + + Foundation.global.namespace = ''; + $(document).foundation(); + + this.component.displayMail({}, testData); + this.component.moveToTrash(); + + expect(mailDeleteEvent).toHaveBeenTriggeredOnAndWith(document, { mail: this.component.attr.mail }); + }); + + it('shows no message selected pane when deleting the email being composed', function() { + var openNoMessageSelectedPaneEvent = spyOnEvent(document, Smail.events.dispatchers.rightPane.openNoMessageSelected); + var mails = [{ident: 123}]; + this.component.attr.mail = mails[0]; + + this.component.trigger(document, Smail.events.mail.deleted, {mails: mails}); + + expect(openNoMessageSelectedPaneEvent).toHaveBeenTriggeredOn(document); + }); + + it('does not show no message selected pane when deleting a different set of emails', function() { + var openNoMessageSelectedPaneEvent = spyOnEvent(document, Smail.events.dispatchers.rightPane.openNoMessageSelected); + var mails = [{ident: 321}]; + this.component.attr.mail = {ident: 123}; + + this.component.trigger(document, Smail.events.mail.deleted, {mails: mails}); + + expect(openNoMessageSelectedPaneEvent).not.toHaveBeenTriggeredOn(document); + }); + + describe('archiving email', function() { + it('trigger tag updates events with no tags', function(){ + var tagsUpdateEvent = spyOnEvent(document, Smail.events.mail.tags.update); + + Foundation.global.namespace = ''; + $(document).foundation(); + + this.component.displayMail({}, testData); + this.component.archiveIt(); + + expect(tagsUpdateEvent).toHaveBeenTriggeredOnAndWith(document, { ident: testData.mail.ident, tags: []}); + }); + + it('opens no message selected pane', function() { + var openNoMessageSelectedEvent = spyOnEvent(document, Smail.events.dispatchers.rightPane.openNoMessageSelected); + + Foundation.global.namespace = ''; + $(document).foundation(); + + this.component.displayMail({}, testData); + this.component.archiveIt(); + + expect(openNoMessageSelectedEvent).toHaveBeenTriggeredOn(document); + }); + }); + + it('opens the no message selected pane when clicking the close button', function() { + var openNoMessageSelectedEvent = spyOnEvent(document, Smail.events.dispatchers.rightPane.openNoMessageSelected); + + this.component.displayMail({}, testData); + this.component.select('closeMailButton').click(); + + expect(openNoMessageSelectedEvent).toHaveBeenTriggeredOn(document); + }); + + function creatingEvent(event, keyCode) { + var e = $.Event(event); + e.which = keyCode; + return e; + } +}); diff --git a/web-ui/test/spec/mail_view/ui/recipients/recipients.spec.js b/web-ui/test/spec/mail_view/ui/recipients/recipients.spec.js new file mode 100644 index 00000000..692bf0eb --- /dev/null +++ b/web-ui/test/spec/mail_view/ui/recipients/recipients.spec.js @@ -0,0 +1,36 @@ +/* global Smail */ + +describeComponent('mail_view/ui/recipients/recipients',function () { + 'use strict'; + var recipientsUpdatedEvent; + + describe('initialization', function() { + it('adds recipients', function() { + setupComponent({name: 'to', addresses: ['foobar@gmail.com'] }); + expect(this.component.attr.recipients.length).toBe(1); + }); + + it('does not trigger recipients updated events on initialization', function() { + recipientsUpdatedEvent = spyOnEvent(document, Smail.events.ui.recipients.updated); + + setupComponent({name: 'to', addresses: ['foobar@gmail.com'] }); + expect(recipientsUpdatedEvent).not.toHaveBeenTriggeredOn(document); + }); + }); + + describe('adding recipients from the ui', function() { + beforeEach(function () { + setupComponent(); + recipientsUpdatedEvent = spyOnEvent(document, Smail.events.ui.recipients.updated); + this.component.trigger(Smail.events.ui.recipients.entered, {name: 'to', addresses: ['foobar@gmail.com'] }); + }); + + it('triggers recipients updated', function() { + expect(recipientsUpdatedEvent).toHaveBeenTriggeredOn(document); + }); + + it('adds recipients', function() { + expect(this.component.attr.recipients.length).toBe(1); + }); + }); +}); diff --git a/web-ui/test/spec/mail_view/ui/recipients/recipients_input.spec.js b/web-ui/test/spec/mail_view/ui/recipients/recipients_input.spec.js new file mode 100644 index 00000000..0f98d007 --- /dev/null +++ b/web-ui/test/spec/mail_view/ui/recipients/recipients_input.spec.js @@ -0,0 +1,104 @@ +/* global Smail */ + +describeComponent('mail_view/ui/recipients/recipients_input',function () { + 'use strict'; + + beforeEach(function () { + setupComponent({name: 'to'}); + }); + + describe('keys that finish address input', function () { + + _.each([ + [186, 'semicolon'], + [188, 'comma'], + [32, 'space'] + + ], function (keycode) { + + it(': ' + keycode[1], function () { + var addressEnteredEvent = spyOnEvent(this.$node, Smail.events.ui.recipients.entered); + + var enterAddressKeyPressEvent = $.Event('keydown', { which: keycode[0] }); + this.$node.val('a@b.c'); + this.$node.trigger(enterAddressKeyPressEvent); + + expect(addressEnteredEvent).toHaveBeenTriggeredOnAndWith(this, { name: 'to', address: 'a@b.c' }); + }); + + it('wont add address if val is empty: ' + keycode[1], function () { + var addressEnteredEvent = spyOnEvent(this.$node, Smail.events.ui.recipients.entered); + + var enterAddressKeyPressEvent = $.Event('keydown', { which: keycode[0] }); + this.$node.val(''); + this.$node.trigger(enterAddressKeyPressEvent); + + expect(addressEnteredEvent).not.toHaveBeenTriggeredOnAndWith(this, { name: 'to', address: '' }); + }); + + it('prevents event default regardless on input val when key is ' + keycode[1], function () { + var enterAddressKeyPressEvent = $.Event('keydown', { which: keycode[0] }); + spyOn(enterAddressKeyPressEvent, 'preventDefault'); + + this.$node.val('') + this.$node.trigger(enterAddressKeyPressEvent); + expect(enterAddressKeyPressEvent.preventDefault).toHaveBeenCalled(); + + enterAddressKeyPressEvent.preventDefault.reset(); + this.$node.val('anything') + this.$node.trigger(enterAddressKeyPressEvent); + expect(enterAddressKeyPressEvent.preventDefault).toHaveBeenCalled(); + }); + + }); + + describe('when tab is pressed', function () { + it('enters an address and prevents event default if there is an input val', function () { + var addressEnteredEvent = spyOnEvent(this.$node, Smail.events.ui.recipients.entered); + + var tabKeyPressEvent = $.Event('keydown', { which: 9}); + spyOn(tabKeyPressEvent, 'preventDefault'); + + this.$node.val('a@b.c'); + this.$node.trigger(tabKeyPressEvent); + + expect(tabKeyPressEvent.preventDefault).toHaveBeenCalled(); + expect(addressEnteredEvent).toHaveBeenTriggeredOnAndWith(this, { name: 'to', address: 'a@b.c'}); + }); + + it('doesnt enter an address and doesnt prevent event default if input val is empty (so tab moves it to the next input)', function () { + var addressEnteredEvent = spyOnEvent(this.$node, Smail.events.ui.recipients.entered); + + var tabKeyPressEvent = $.Event('keydown', { which: 9}); + spyOn(tabKeyPressEvent, 'preventDefault'); + + this.$node.val(''); + this.$node.trigger(tabKeyPressEvent); + + expect(tabKeyPressEvent.preventDefault).not.toHaveBeenCalled(); + expect(addressEnteredEvent).not.toHaveBeenTriggeredOnAndWith(this, { name: 'to', address: ''}); + }); + }); + }); + + describe('on keyup', function () { + it('triggers inputHasNoMail if input is empty', function () { + var inputHasNoMailEvent = spyOnEvent(document, Smail.events.ui.recipients.inputHasNoMail); + this.$node.val(''); + + this.$node.trigger('keyup'); + + expect(inputHasNoMailEvent).toHaveBeenTriggeredOn(document); + }); + + it('triggers inputHasMail if input is not empty', function () { + var inputHasMailEvent = spyOnEvent(document, Smail.events.ui.recipients.inputHasMail); + this.$node.val('lalala'); + + this.$node.trigger('keyup'); + + expect(inputHasMailEvent).toHaveBeenTriggeredOn(document, { name: 'to' }); + }); + }); + +}); diff --git a/web-ui/test/spec/mail_view/ui/recipients/recipients_iterator.spec.js b/web-ui/test/spec/mail_view/ui/recipients/recipients_iterator.spec.js new file mode 100644 index 00000000..480ea256 --- /dev/null +++ b/web-ui/test/spec/mail_view/ui/recipients/recipients_iterator.spec.js @@ -0,0 +1,101 @@ +/* global Smail */ + +define(['mail_view/ui/recipients/recipients_iterator'], function (RecipientsIterator) { + 'use strict'; + + function createRecipient() { + return jasmine.createSpyObj('recipient', ['select', 'unselect', 'destroy']); + } + + var recipientsIterator, + exitInput; + + function createIterator(elements) { + return recipientsIterator = new RecipientsIterator({ elements: elements, exitInput: exitInput }); + } + + function resetMock(m) { + m.destroy.reset();m.select.reset();m.unselect.reset(); + } + + beforeEach(function () { + exitInput = $(''); + }); + + describe('moving left', function () { + it('unselects the current element and selects the element in the left if there is one', function () { + var elements = _.times(2, createRecipient); + + recipientsIterator = createIterator(elements); + recipientsIterator.moveLeft(); + + expect(elements[0].select).toHaveBeenCalled(); + expect(elements[1].unselect).toHaveBeenCalled(); + }); + + it('doesnt do anything if there are no elements in the left', function () { + var elements = _.times(2, createRecipient); + recipientsIterator = createIterator(elements); + recipientsIterator.moveLeft(); + _.each(elements, resetMock); + + recipientsIterator.moveLeft(); + + expect(elements[0].select).not.toHaveBeenCalled(); + expect(elements[0].unselect).not.toHaveBeenCalled(); + expect(elements[1].select).not.toHaveBeenCalled(); + expect(elements[1].unselect).not.toHaveBeenCalled(); + }); + + }); + + describe('moving right', function () { + it('unselects the current element and selects the one in the right if there is one', function () { + var elements = _.times(2, createRecipient); + recipientsIterator = createIterator(elements); + recipientsIterator.moveLeft(); + _.each(elements, resetMock); + + recipientsIterator.moveRight(); + + expect(elements[0].unselect).toHaveBeenCalled(); + expect(elements[1].select).toHaveBeenCalled(); + }); + + it('unselects current element and focus on exit input if there are no elements on the right', function () { + var elements = _.times(2, createRecipient); + spyOn(exitInput, 'focus'); + + recipientsIterator = createIterator(elements); + recipientsIterator.moveRight(); + + expect(elements[1].unselect).toHaveBeenCalled(); + expect(exitInput.focus).toHaveBeenCalled(); + }); + }); + + describe('delete current', function () { + it('selects what is left in the right after deleting the current element', function () { + var elements = _.times(2, createRecipient); + var toBeDeleted = elements[0]; + recipientsIterator = createIterator(elements); + recipientsIterator.moveLeft(); + + recipientsIterator.deleteCurrent(); + + expect(toBeDeleted.destroy).toHaveBeenCalled(); + expect(elements[0].select).toHaveBeenCalled(); + }); + + it('focus on the input if there are no more elements', function () { + recipientsIterator = createIterator([createRecipient()]); + spyOn(exitInput, 'focus'); + + recipientsIterator.deleteCurrent(); + + expect(exitInput.focus).toHaveBeenCalled(); + }); + }); + + +}); \ No newline at end of file diff --git a/web-ui/test/spec/mail_view/ui/reply_box.spec.js b/web-ui/test/spec/mail_view/ui/reply_box.spec.js new file mode 100644 index 00000000..c6db4de3 --- /dev/null +++ b/web-ui/test/spec/mail_view/ui/reply_box.spec.js @@ -0,0 +1,105 @@ +/*global jasmine */ +/*global Smail */ + +describeComponent('mail_view/ui/reply_box', function () { + 'use strict'; + + var attrs, i18n; + beforeEach(function () { + attrs = { + mail: Smail.testData().parsedMail.simpleTextPlain + }; + setupComponent(attrs); + i18n = require('views/i18n'); + }); + + describe('reply compose box', function() { + it('should display subject of the reply', function() { + expect(this.component.select('subjectDisplay').text()).toBe(i18n('Re: ') + attrs.mail.header.subject); + }); + + it('should show recipient fields when clicking on recipient display', function() { + this.component.select('recipientsDisplay').click(); + + expect(this.component.select('recipientsFields')).not.toBeHidden(); + expect(this.component.select('recipientsDisplay')).toBeHidden(); + }); + + it('should show subject field when clicking on subject display', function() { + this.component.select('subjectDisplay').click(); + + expect(this.component.select('subjectInput')).not.toBeHidden(); + expect(this.component.select('subjectDisplay')).toBeHidden(); + }); + + it('should use the from field when Reply-To header does not exist', function() { + attrs.mail.header.reply_to = undefined; + + setupComponent(attrs); + + expect(this.component.attr.recipientValues['to']).toEqual([attrs.mail.header.from]); + }); + + it('should have a subject of Re: ', function() { + attrs.mail.header.subject = 'Very interesting'; + + setupComponent(attrs); + + expect(this.component.select('subjectDisplay').text()).toEqual(i18n('Re: ')+ attrs.mail.header.subject); + }); + + it('should use set In-Reply-To header when Message-Id header is set', function() { + var mailSendEvent = spyOnEvent(document, Smail.events.mail.send); + + attrs.mail.header.message_id = '12345'; + setupComponent(attrs); + + $(document).trigger(Smail.events.ui.mail.send); + + expect(mailSendEvent).toHaveBeenTriggeredOn(document); + expect(mailSendEvent.mostRecentCall.data.header).toEqual(jasmine.objectContaining({ + in_reply_to: '12345' + })); + }); + + it('keeps the List-Id header when it exists', function() { + var mailSendEvent = spyOnEvent(document, Smail.events.mail.send); + attrs.mail.header.list_id = 'somelist'; + + setupComponent(attrs); + $(document).trigger(Smail.events.ui.mail.send); + + expect(mailSendEvent.mostRecentCall.data.header).toEqual(jasmine.objectContaining({ + list_id: 'somelist' + })); + }); + + it('populates body text area with quote of email being replied', function() { + var viewHelper = require('helpers/view_helper'); + spyOn(viewHelper, 'quoteMail').andReturn('quoted email'); + + setupComponent(attrs); + + expect(viewHelper.quoteMail).toHaveBeenCalledWith(attrs.mail); + expect(this.component.select('bodyBox').val()).toBe('quoted email'); + }); + + it('triggers mail when cancelling a reply', function () { + var mailSaveEvent = spyOnEvent(document, Smail.events.mail.save); + + this.component.select('trashButton').click(); + + expect(mailSaveEvent).toHaveBeenTriggeredOn(document); + }); + + it('reopens the mail after the reply is sent', function () { + var mailOpenEvent = spyOnEvent(document, Smail.events.ui.mail.open); + + this.component.trigger(document, Smail.events.mail.sent); + + expect(mailOpenEvent).toHaveBeenTriggeredOnAndWith(document, { + ident: this.component.attr.mail.ident + }); + }); + }); +}); diff --git a/web-ui/test/spec/mail_view/ui/reply_section.spec.js b/web-ui/test/spec/mail_view/ui/reply_section.spec.js new file mode 100644 index 00000000..e5571e2c --- /dev/null +++ b/web-ui/test/spec/mail_view/ui/reply_section.spec.js @@ -0,0 +1,97 @@ +/*global jasmine */ +/*global Smail */ + +describeComponent('mail_view/ui/reply_section', function () { + 'use strict'; + + beforeEach(function () { + setupComponent(); + }); + + describe('clicking reply buttons', function() { + var mailWantEvent, expectEventData; + + beforeEach(function () { + mailWantEvent = spyOnEvent(document, Smail.events.mail.want); + expectEventData = { + mail: '12345', + caller: this.component + }; + this.component.attr.ident = '12345'; + }); + + it('should ask for email when clicking on reply button', function() { + this.component.select('replyButton').click(); + + expect(mailWantEvent).toHaveBeenTriggeredOnAndWith(document, expectEventData); + }); + + it('should ask for email when clicking on replyAll button', function() { + this.component.select('replyAllButton').click(); + + expect(mailWantEvent).toHaveBeenTriggeredOnAndWith(document, expectEventData); + }); + }); + + describe('creating reply box when getting email back', function() { + var mailData, ReplyBox, ForwardBox; + + beforeEach(function () { + mailData = Smail.testData().mail; + ReplyBox = require('mail_view/ui/reply_box'); + ForwardBox = require('mail_view/ui/forward_box'); + spyOn(ReplyBox, 'attachTo'); + spyOn(ForwardBox, 'attachTo'); + }); + + it('for normal reply', function() { + this.component.attr.replyType = 'reply'; + this.component.trigger(this.component, Smail.events.mail.here, { mail: mailData }); + + expect(ReplyBox.attachTo).toHaveBeenCalledWith(jasmine.any(Object), { + mail: mailData, + replyType: 'reply' + }); + }); + + it('for reply to all', function() { + this.component.attr.replyType = 'replyall'; + this.component.trigger(this.component, Smail.events.mail.here, { mail: mailData }); + + expect(ReplyBox.attachTo).toHaveBeenCalledWith(jasmine.any(Object), { + mail: mailData, + replyType: 'replyall' + }); + }); + + it('creates a forward box', function() { + this.component.attr.replyType = 'forward'; + this.component.trigger(this.component, Smail.events.mail.here, { mail: mailData }); + + expect(ForwardBox.attachTo).toHaveBeenCalledWith(jasmine.any(Object), { + mail: mailData + }); + }); + }); + + it('hides the buttons when clicked', function() { + this.component.attr.mailIdent = 12345; + + this.component.select('replyButton').click(); + + expect(this.component.select('replyButton')).toBeHidden(); + expect(this.component.select('replyAllButton')).toBeHidden(); + expect(this.component.select('forwardButton')).toBeHidden(); + }); + + it('shows the buttons when reply is cancelled', function() { + this.component.attr.mailIdent = 12345; + this.component.select('replyButton').click(); + + $(document).trigger(Smail.events.ui.composeBox.trashReply); + + expect(this.component.select('replyButton')).not.toBeHidden(); + expect(this.component.select('replyAllButton')).not.toBeHidden(); + expect(this.component.select('forwardButton')).not.toBeHidden(); + }); +}); diff --git a/web-ui/test/spec/mail_view/ui/send_button.spec.js b/web-ui/test/spec/mail_view/ui/send_button.spec.js new file mode 100644 index 00000000..27bee0f3 --- /dev/null +++ b/web-ui/test/spec/mail_view/ui/send_button.spec.js @@ -0,0 +1,91 @@ +describeComponent('mail_view/ui/send_button', function () { + + describe('send button', function () { + beforeEach(function () { + setupComponent('