aboutsummaryrefslogtreecommitdiffstats
path: root/documentation/themes/themes-css.asciidoc
blob: d7ba87c700fc0efdd6502a55898a0ae620bb2bf7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
---
title: Introduction to Cascading Style Sheets
order: 2
layout: page
---

[[themes.css]]
= Introduction to Cascading Style Sheets

((("CSS", "introduction", id="term.themes.css", range="startofrange")))

Cascading Style Sheets or CSS is the basic technique to separate the appearance
of a web page from the content represented in HTML. In this section, we give an
introduction to CSS and look how they are relevant to software development with
Vaadin.

As we can only give a short intruction in this book, we encourage you to refer
to the rich literature on CSS and the many resources available in the web. You
can find the authoratitative specifications of CSS standards from the
link:http://www.w3.org/Style/CSS/[W3C
website]
ifdef::web[]
and other literature, references, and tutorials from the
link:http://www.dmoz.org/Computers/Data_Formats/Style_Sheets/CSS/[Open Directory
Project page on CSS], as well as from other
sources
endif::web[]
.

[[themes.css.basics]]
== Applying CSS to HTML

Let us consider the following HTML document that contains various markup
elements for formatting text. Vaadin UIs work in essentially similar documents,
even though they use somewhat different elements to draw the user interface.

[subs="normal"]
----
<html>
    <head>
        <title>My Page</title>
        <link rel="stylesheet" type="text/css"
              href="mystylesheet.css"/>
    </head>
    <body>
        **<p>**This is a paragraph**</p>**
        **<p>**This is another paragraph**</p>**
        <table>
            <tr>
                **<td>**This is a table cell**</td>**
                **<td>**This is another table cell**</td>**
            </tr>
        </table>
    </body>
</html>
----
The HTML elements that will be styled later by matching CSS rules are emphasized
above.

The [elementname]#link# element in the HTML header defines the CSS stylesheet.
The definition is automatically generated by Vaadin in the HTML page
that loads the UI of the application. A stylesheet can also be embedded in the
HTML document itself, as is done when optimizing their loading in Vaadin
TouchKit, for example.


[[themes.css.basics]]
== Basic CSS Rules

A stylesheet contains a set of __rules__ that can match the HTML elements in the
page. Each rule consists of one or more __selectors__, separated with commas,
and a __declaration block__ enclosed in curly braces. A declaration block
contains a list of __property__ statements. Each property has a label and a
value, separated with a colon. A property statement ends with a semicolon.

Let us look at an example that matches certain elements in the simple HTML
document given in the previous section:

[source, css]
----
p, td {
  color: blue;
}

td {
  background: yellow;
  font-weight: bold;
}
----

The [literal]#++p++# and [literal]#++td++# are element type selectors that match
with [elementname]#p# and [elementname]#td# elements in HTML, respectively.
The first rule matches with both elements, while the second matches only with
[elementname]#td# elements.
Let us assume that you have saved the above style sheet with the name [filename]#mystylesheet.css# and consider the following HTML file located in the same folder.

[[figure.themes.basic.1]]
.Simple Styling by Element Type
image::img/themes-css-match-1.png[]

[[themes.css.basics.inheritance]]
=== Style Inheritance in CSS

CSS has __inheritance__ where contained elements inherit the properties of their
parent elements. For example, let us change the above example and define it
instead as follows:

[source, css]
----
table {
    color: blue;
    background: yellow;
}
----

All elements contained in the [elementname]#table# element would have the same properties.
For example, the text in the contained [elementname]#td# elements would be in blue color.


[[themes.css.basics.element-types]]
=== HTML Element Types

HTML has a number of element types, each of which accepts a specific set of
properties. The [elementname]#div# elements are generic elements that can be
used to create almost any layout and formatting that can be created with a
specific HTML element type. Vaadin uses [elementname]#div# elements
extensively to draw the UI, especially in layout components.

((("Google Web Toolkit", "themeing")))
Matching elements by their type as shown above is, however, rarely if ever used
in style sheets for Vaadin applications. We used it above, because it is the
normal way in regular HTML documents that use the various HTML elements for
formatting text, but it is not applicable in Vaadin UIs that consist mostly of
[elementname]#div# elements. Instead, you need to match by element class, as
described next.



[[themes.css.matching-by-class]]
== Matching by Element Class

Matching HTML elements by the __class__ attribute is the most common form of
matching in Vaadin stylesheets. It is also possible to match with the
__identifier__ of a unique HTML element.

The class of an HTML element is defined with the [parameter]#class# attribute as
follows:

[subs="normal"]
----
<html>
  <body>
    **<p class="normal">**This is the first paragraph**</p>**

    **<p class="another">**This is the second paragraph**</p>**

    <table>
      <tr>
        **<td class="normal">**This is a table cell**</td>**
        **<td class="another">**This is another table cell**</td>**
      </tr>
    </table>
  </body>
</html>
----

The class attributes of HTML elements can be matched in CSS rules with a
selector notation where the class name is written after a period following the
element name. This gives us full control of matching elements by their type and
class.


[source, css]
----
p.normal   {color: red;}
p.another  {color: blue;}
td.normal  {background: pink;}
td.another {background: yellow;}
----

The page would look as shown below:

.Matching HTML Element Type and Class
image::img/themes-css-match-class-2.png[]

We can also match solely by the class by using the universal selector
[literal]#++*++# for the element name, for example [literal]#++*.normal++#. The
universal selector can also be left out altogether so that we use just the class
name following the period, for example [literal]#++.normal++#.


[source, css]
----
.normal {
    color: red;
}

.another {
    blackground: yellow;
}
----

In this case, the rule will match with all elements of the same class regardless
of the element type. The result is shown in <<figure.themes.match.class>>. This
example illustrates a technique to make style sheets compatible regardless of
the exact HTML element used in drawing a component.

[[figure.themes.match.class]]
.Matching Only HTML Element Class
image::img/themes-css-match-class-3.png[]

To ensure future compatibility, we recommend that you use only matching based on
the classes and __do not__ match for specific HTML element types in CSS rules,
because Vaadin may change the exact HTML implementation how components are drawn
in the future.
For example, Vaadin earlier used [elementname]#div# element to draw [classname]#Button# components, but later it was changed to use the special-purpose [elementname]#button# element in HTML.
Because of using the [literal]#++v-button++# style class in the CSS rules for the button, styling it has changed only very little.


[[themes.css.matching-by-descendants]]
== Matching by Descendant Relationship

CSS allows matching HTML by their containment relationship. For example,
consider the following HTML fragment:

[subs="normal"]
----
&lt;body&gt;
  &lt;p class="mytext"&gt;Here is some text inside a
                    paragraph element&lt;/p&gt;
  &lt;table class="**mytable**"&gt;
    &lt;tr&gt;
      &lt;td class="**mytext**"&gt;Here is text inside
                      a table and inside a td element.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/table&gt;
&lt;/body&gt;
----
Matching by the class name [literal]#++.mytext++# alone would match both the [elementname]#p# and [elementname]#td# elements.
If we want to match only the table cell, we could use the following selector:

[source, css]
----
.mytable .mytext {color: blue;}
----

To match, a class listed in a rule does not have to be an immediate descendant
of the previous class, but just a descendant. For example, the selector "
[literal]#++.v-panel .v-button++#" would match all elements with class
[literal]#++.v-button++# somewhere inside an element with class
[literal]#++.v-panel++#.


[[themes.css.cascading]]
== Importance of Cascading

CSS or Cascading Stylesheets are, as the name implies, about __cascading__
stylesheets, which means applying the stylesheet rules according to their
origin, importance, scope, specifity, and order.

For exact rules for cascading in CSS, see the section
link:http://www.w3.org/TR/css3-cascade/#cascading[Cascading] in the CSS
specification.

[[themes.css.cascading.importance]]
=== Importance

Declarations in CSS rules can be made override declarations with otherwise
higher priority by annotating them as [literal]#++!important++#. For example, an
inline style setting made in the [literal]#++style++# attribute of an HTML
element has a higher specificity than any rule in a CSS stylesheet.


[source, css]
----
<div class="v-button" style="height: 20px;">...
----

You can override the higher specificity with the [literal]#++!important++#
annotation as follows:


[source, css]
----
.v-button {height: 30px !important;}
----


[[themes.css.cascading.specificity]]
=== Specificity

A rule that specifies an element with selectors more closely overrides ones that
specify it less specifically. With respect to the element class selectors most
commonly used in Vaadin themes, the specificity is determined by the number of
class selectors in the selector.


[source, css]
----
.v-button {}
.v-verticallayout .v-button {}
.v-app .v-verticallayout .v-button {}
----

In the above example, the last rule would have the highest specificity and would
match.

As noted earlier, style declarations given in the style attribute of a HTML
element have higher specificity than declarations in a CSS rule, except if the
[literal]#++!important++# annotation is given.

See the CSS3 link:http://www.w3.org/TR/selectors/#specificity[selectors module
specification] for details regarding how the specificity is computed.


[[themes.css.cascading.order]]
=== Order

CSS rules given later have higher priority than ones given earlier. For example,
in the following, the latter rule overrides the former and the color will be
black:


[source, css]
----
.v-button {color: white}
.v-button {color: black}
----

As specificity has a higher cascading priority than order, you could make the
first rule have higher priority by adding specificity as follows:


[source, css]
----
.v-app .v-button {color: white}
.v-button {color: black}
----

The order is important to notice in certain cases, because Vaadin does not
guarantee the order in which CSS stylesheets are loaded in the browser, which
can in fact be random and result in very unexpected behavior. This is not
relevant for Sass stylesheets, which are compiled to a single stylesheet. For
plain CSS stylesheets, such as add-on or TouchKit stylesheets, the order can be
relevant.



[[themes.css.hierarchy]]
== Style Class Hierarchy of a Vaadin UI

Let us give a real case in a Vaadin UI by considering a simple Vaadin UI with a
label and a button inside a vertical layout:


[source, java]
----
// UI has v-ui style class
@Theme("mytheme")
public class HelloWorld extends UI {
    @Override
    protected void init(VaadinRequest request) {
        // VerticalLayout has v-verticallayout style
        VerticalLayout content = new VerticalLayout();
        setContent(content);

        // Label has v-label style
        content.addComponent(new Label("Hello World!"));

        // Button has v-button style
        content.addComponent(new Button("Push Me!",
            new Button.ClickListener() {
            @Override
            public void buttonClick(ClickEvent event) {
                Notification.show("Pushed!");
            }
        }));
    }
}
----

The UI will look by default as shown in <<figure.themes.css.hierarchy.initial>>.
By using a HTML inspector such as Firebug, you can view the HTML tree and the
element classes and applied styles for each element.

[[figure.themes.css.hierarchy.initial]]
.An Unthemed Vaadin UI
image::img/example-ui-default.png[]

Now, let us look at the HTML element class structure of the UI, as we can see it
in the HTML inspector:

[subs="normal"]
----
&lt;body class="**v-generated-body v-ff v-ff20 v-ff200 v-gecko v-lin**"
      scroll="auto"&gt;
  &lt;div id="bookexamplesvaadin7helloworld-447164942"
       class="**v-app mytheme**"&gt;
    &lt;div class="**v-ui v-scrollable**"
         tabindex="1" style="height: 100%; width: 100%;"&gt;
      &lt;div class="**v-loading-indicator first**"
           style="position: absolute; display: none;"&gt;&lt;/div&gt;
      &lt;div class="**v-verticallayout v-layout v-vertical v-widget v-has-width**"
           style="width: 100%;"&gt;
        &lt;div class="**v-slot**"&gt;
          &lt;div class="**v-label v-widget v-has-width**"
               style="width: 100%;"&gt;Hello World!&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="**v-slot**"&gt;
          &lt;div class="**v-button v-widget**"
               tabindex="0" role="button"&gt;
            &lt;span class="**v-button-wrap**"&gt;
              &lt;span class="**v-button-caption**"&gt;Push Me!&lt;/span&gt;
            &lt;/span&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  ...
&lt;body&gt;
----
Now, consider the following theme where we set the colors and margins of various
elements. The theme is actually a Sass theme.


[source, css]
----
@import "../valo/valo.scss";

@mixin mytheme {
  @include valo;

  /* White background for the entire UI */
  .v-ui {
    background: white;
  }

  /* All labels have white text on black background */
  .v-label {
    background: black;
    color: white;
    font-size: 24pt;
    line-height: 24pt;
    padding: 5px;
  }

  /* All buttons have blue caption and some margin */
  .v-button {
    margin: 10px;

    /* A nested selector to increase specificity */
    .v-button-caption {
      color: blue;
    }
  }
}
----

The look has changed as shown in <<figure.themes.css.hierarchy.themed>>.

[[figure.themes.css.hierarchy.themed]]
.Themed Vaadin UI
image::img/example-ui-themed.png[]

An element can have multiple classes separated with a space. With multiple
classes, a CSS rule matches an element if any of the classes match. This feature
is used in many Vaadin components to allow matching based on the state of the
component. For example, when the mouse is over a [classname]#Link# component,
[literal]#++over++# class is added to the component. Most of such styling is a
feature of Google Web Toolkit.


[[themes.css.compatibility]]
== Notes on Compatibility

((("CSS", "compatibility")))
((("compatibility")))
CSS is a standard continuously under development. It was first proposed in 1994.
The specification of CSS is maintained by the CSS Working Group of World Wide
Web Consortium (W3C). Versioned with backward-compatible "levels", CSS Level 1
was published in 1996, Level 2 in 1998, and the ongoing development of CSS Level
3 started in 1998. CSS3 is divided into a number of separate modules, each
developed and progressing separately, and many of the modules are already Level
4.

While the support for CSS has been universal in all graphical web browsers since
at least 1995, the support has been very incomplete at times and there still
exists an unfortunate number of incompatibilities between browsers. While we
have tried to take these incompatibilities into account in the built-in themes
in Vaadin, you need to consider them while developing your own themes.
Compatibility issues are detailed in various CSS handbooks.


(((range="endofrange", startref="term.themes.css")))
/span>=> '#1' # link to a changeset assert_select 'a[href=?][title=?]', 'https://mydomain.foo/projects/ecookbook/repository/revisions/2', 'This commit fixes #1, #2 and references #1 & #3', :text => 'r2' # link to a description diff assert_select 'a[href^=?][title=?]', # should be https://mydomain.foo/journals/diff/3?detail_id=4 # but the Rails 4.2 DOM assertion doesn't handle the ? in the # attribute value 'https://mydomain.foo/journals/diff/3', 'View differences', :text => 'diff' # link to an attachment assert_select 'a[href=?]', 'https://mydomain.foo/attachments/download/4/source.rb', :text => 'source.rb' end end def test_generated_links_with_prefix relative_url_root = Redmine::Utils.relative_url_root Setting.host_name = 'mydomain.foo/rdm' Setting.protocol = 'http' journal = Journal.find(3) assert Mailer.deliver_issue_edit(journal) mail = last_email assert_not_nil mail assert_select_email do # link to the main ticket assert_select 'a[href=?]', 'http://mydomain.foo/rdm/issues/2#change-3', :text => 'Feature request #2: Add ingredients categories' # link to a referenced ticket assert_select 'a[href=?][title=?]', 'http://mydomain.foo/rdm/issues/1', "Bug: Cannot print recipes (New)", :text => '#1' # link to a changeset assert_select 'a[href=?][title=?]', 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2', 'This commit fixes #1, #2 and references #1 & #3', :text => 'r2' # link to a description diff assert_select 'a[href^=?][title=?]', # should be http://mydomain.foo/rdm/journals/diff/3?detail_id=4 # but the Rails 4.2 DOM assertion doesn't handle the ? in the # attribute value 'http://mydomain.foo/rdm/journals/diff/3', 'View differences', :text => 'diff' # link to an attachment assert_select 'a[href=?]', 'http://mydomain.foo/rdm/attachments/download/4/source.rb', :text => 'source.rb' end end def test_generated_links_with_port_and_prefix with_settings :host_name => '10.0.0.1:81/redmine', :protocol => 'http' do Mailer.test_email(User.find(1)).deliver mail = last_email assert_not_nil mail assert_include 'http://10.0.0.1:81/redmine', mail_body(mail) end end def test_generated_links_with_port with_settings :host_name => '10.0.0.1:81', :protocol => 'http' do Mailer.test_email(User.find(1)).deliver mail = last_email assert_not_nil mail assert_include 'http://10.0.0.1:81', mail_body(mail) end end def test_issue_edit_should_generate_url_with_hostname_for_relations journal = Journal.new(:journalized => Issue.find(1), :user => User.find(1), :created_on => Time.now) journal.details << JournalDetail.new(:property => 'relation', :prop_key => 'label_relates_to', :value => 2) Mailer.deliver_issue_edit(journal) assert_not_nil last_email assert_select_email do assert_select 'a[href=?]', 'http://mydomain.foo/issues/2', :text => 'Feature request #2' end end def test_generated_links_with_prefix_and_no_relative_url_root relative_url_root = Redmine::Utils.relative_url_root Setting.host_name = 'mydomain.foo/rdm' Setting.protocol = 'http' Redmine::Utils.relative_url_root = nil journal = Journal.find(3) assert Mailer.deliver_issue_edit(journal) mail = last_email assert_not_nil mail assert_select_email do # link to the main ticket assert_select 'a[href=?]', 'http://mydomain.foo/rdm/issues/2#change-3', :text => 'Feature request #2: Add ingredients categories' # link to a referenced ticket assert_select 'a[href=?][title=?]', 'http://mydomain.foo/rdm/issues/1', "Bug: Cannot print recipes (New)", :text => '#1' # link to a changeset assert_select 'a[href=?][title=?]', 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2', 'This commit fixes #1, #2 and references #1 & #3', :text => 'r2' # link to a description diff assert_select 'a[href^=?][title=?]', # should be http://mydomain.foo/rdm/journals/diff/3?detail_id=4 # but the Rails 4.2 DOM assertion doesn't handle the ? in the # attribute value 'http://mydomain.foo/rdm/journals/diff/3', 'View differences', :text => 'diff' # link to an attachment assert_select 'a[href=?]', 'http://mydomain.foo/rdm/attachments/download/4/source.rb', :text => 'source.rb' end ensure # restore it Redmine::Utils.relative_url_root = relative_url_root end def test_email_headers issue = Issue.find(1) Mailer.deliver_issue_add(issue) mail = last_email assert_not_nil mail assert_equal 'All', mail.header['X-Auto-Response-Suppress'].to_s assert_equal 'auto-generated', mail.header['Auto-Submitted'].to_s assert_equal '<redmine.example.net>', mail.header['List-Id'].to_s end def test_email_headers_should_include_sender issue = Issue.find(1) Mailer.deliver_issue_add(issue) mail = last_email assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s end def test_plain_text_mail Setting.plain_text_mail = 1 journal = Journal.find(2) Mailer.deliver_issue_edit(journal) mail = last_email assert_equal "text/plain; charset=UTF-8", mail.content_type assert_equal 0, mail.parts.size assert !mail.encoded.include?('href') end def test_html_mail Setting.plain_text_mail = 0 journal = Journal.find(2) Mailer.deliver_issue_edit(journal) mail = last_email assert_equal 2, mail.parts.size assert mail.encoded.include?('href') end def test_from_header with_settings :mail_from => 'redmine@example.net' do Mailer.test_email(User.find(1)).deliver end mail = last_email assert_equal 'redmine@example.net', mail.from_addrs.first end def test_from_header_with_phrase with_settings :mail_from => 'Redmine app <redmine@example.net>' do Mailer.test_email(User.find(1)).deliver end mail = last_email assert_equal 'redmine@example.net', mail.from_addrs.first assert_equal 'Redmine app <redmine@example.net>', mail.header['From'].to_s end def test_should_not_send_email_without_recipient news = News.first user = news.author # Remove members except news author news.project.memberships.each {|m| m.destroy unless m.user == user} user.pref.no_self_notified = false user.pref.save User.current = user Mailer.news_added(news.reload).deliver assert_equal 1, last_email.bcc.size # nobody to notify user.pref.no_self_notified = true user.pref.save User.current = user ActionMailer::Base.deliveries.clear Mailer.news_added(news.reload).deliver assert ActionMailer::Base.deliveries.empty? end def test_issue_add_message_id issue = Issue.find(2) Mailer.deliver_issue_add(issue) mail = last_email assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id assert_include "redmine.issue-2.20060719190421@example.net", mail.references end def test_issue_edit_message_id journal = Journal.find(3) journal.issue = Issue.find(2) Mailer.deliver_issue_edit(journal) mail = last_email assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id assert_include "redmine.issue-2.20060719190421@example.net", mail.references assert_select_email do # link to the update assert_select "a[href=?]", "http://mydomain.foo/issues/#{journal.journalized_id}#change-#{journal.id}" end end def test_message_posted_message_id message = Message.find(1) Mailer.message_posted(message).deliver mail = last_email assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id assert_include "redmine.message-1.20070512151532@example.net", mail.references assert_select_email do # link to the message assert_select "a[href=?]", "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}", :text => message.subject end end def test_reply_posted_message_id message = Message.find(3) Mailer.message_posted(message).deliver mail = last_email assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id assert_include "redmine.message-1.20070512151532@example.net", mail.references assert_select_email do # link to the reply assert_select "a[href=?]", "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.root.id}?r=#{message.id}#message-#{message.id}", :text => message.subject end end test "#issue_add should notify project members" do issue = Issue.find(1) assert Mailer.deliver_issue_add(issue) assert last_email.bcc.include?('dlopper@somenet.foo') end def test_issue_add_should_send_mail_to_all_user_email_address EmailAddress.create!(:user_id => 3, :address => 'otheremail@somenet.foo') issue = Issue.find(1) assert Mailer.deliver_issue_add(issue) assert last_email.bcc.include?('dlopper@somenet.foo') assert last_email.bcc.include?('otheremail@somenet.foo') end test "#issue_add should not notify project members that are not allow to view the issue" do issue = Issue.find(1) Role.find(2).remove_permission!(:view_issues) assert Mailer.deliver_issue_add(issue) assert !last_email.bcc.include?('dlopper@somenet.foo') end test "#issue_add should notify issue watchers" do issue = Issue.find(1) user = User.find(9) # minimal email notification options user.pref.no_self_notified = '1' user.pref.save user.mail_notification = false user.save Watcher.create!(:watchable => issue, :user => user) assert Mailer.deliver_issue_add(issue) assert last_email.bcc.include?(user.mail) end test "#issue_add should not notify watchers not allowed to view the issue" do issue = Issue.find(1) user = User.find(9) Watcher.create!(:watchable => issue, :user => user) Role.non_member.remove_permission!(:view_issues) assert Mailer.deliver_issue_add(issue) assert !last_email.bcc.include?(user.mail) end def test_issue_add_should_include_enabled_fields issue = Issue.find(2) assert Mailer.deliver_issue_add(issue) assert_mail_body_match '* Target version: 1.0', last_email assert_select_email do assert_select 'li', :text => 'Target version: 1.0' end end def test_issue_add_should_not_include_disabled_fields issue = Issue.find(2) tracker = issue.tracker tracker.core_fields -= ['fixed_version_id'] tracker.save! assert Mailer.deliver_issue_add(issue) assert_mail_body_no_match 'Target version', last_email assert_select_email do assert_select 'li', :text => /Target version/, :count => 0 end end # test mailer methods for each language def test_issue_add issue = Issue.find(1) with_each_language_as_default do assert Mailer.deliver_issue_add(issue) end end def test_issue_edit journal = Journal.find(1) with_each_language_as_default do assert Mailer.deliver_issue_edit(journal) end end def test_issue_edit_should_send_private_notes_to_users_with_permission_only journal = Journal.find(1) journal.private_notes = true journal.save! Role.find(2).add_permission! :view_private_notes Mailer.deliver_issue_edit(journal) assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort Role.find(2).remove_permission! :view_private_notes Mailer.deliver_issue_edit(journal) assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort end def test_issue_edit_should_send_private_notes_to_watchers_with_permission_only Issue.find(1).set_watcher(User.find_by_login('someone')) journal = Journal.find(1) journal.private_notes = true journal.save! Role.non_member.add_permission! :view_private_notes Mailer.deliver_issue_edit(journal) assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort Role.non_member.remove_permission! :view_private_notes Mailer.deliver_issue_edit(journal) assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort end def test_issue_edit_should_mark_private_notes journal = Journal.find(2) journal.private_notes = true journal.save! with_settings :default_language => 'en' do Mailer.deliver_issue_edit(journal) end assert_mail_body_match '(Private notes)', last_email end def test_issue_edit_with_relation_should_notify_users_who_can_see_the_related_issue issue = Issue.generate! issue.init_journal(User.find(1)) private_issue = Issue.generate!(:is_private => true) IssueRelation.create!(:issue_from => issue, :issue_to => private_issue, :relation_type => 'relates') issue.reload assert_equal 1, issue.journals.size journal = issue.journals.first ActionMailer::Base.deliveries.clear Mailer.deliver_issue_edit(journal) last_email.bcc.each do |email| user = User.find_by_mail(email) assert private_issue.visible?(user), "Issue was not visible to #{user}" end end def test_document_added document = Document.find(1) with_each_language_as_default do assert Mailer.document_added(document).deliver end end def test_attachments_added attachements = [ Attachment.find_by_container_type('Document') ] with_each_language_as_default do assert Mailer.attachments_added(attachements).deliver end end def test_version_file_added attachements = [ Attachment.find_by_container_type('Version') ] assert Mailer.attachments_added(attachements).deliver assert_not_nil last_email.bcc assert last_email.bcc.any? assert_select_email do assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files" end end def test_project_file_added attachements = [ Attachment.find_by_container_type('Project') ] assert Mailer.attachments_added(attachements).deliver assert_not_nil last_email.bcc assert last_email.bcc.any? assert_select_email do assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files" end end def test_news_added news = News.first with_each_language_as_default do assert Mailer.news_added(news).deliver end end def test_news_added_should_notify_project_news_watchers user1 = User.generate! user2 = User.generate! news = News.find(1) news.project.enabled_module('news').add_watcher(user1) Mailer.news_added(news).deliver assert_include user1.mail, last_email.bcc assert_not_include user2.mail, last_email.bcc end def test_news_comment_added comment = Comment.find(2) with_each_language_as_default do assert Mailer.news_comment_added(comment).deliver end end def test_message_posted message = Message.first recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author} recipients = recipients.compact.uniq with_each_language_as_default do assert Mailer.message_posted(message).deliver end end def test_wiki_content_added content = WikiContent.find(1) with_each_language_as_default do assert_difference 'ActionMailer::Base.deliveries.size' do assert Mailer.wiki_content_added(content).deliver assert_select_email do assert_select 'a[href=?]', 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation', :text => 'CookBook documentation' end end end end def test_wiki_content_updated content = WikiContent.find(1) with_each_language_as_default do assert_difference 'ActionMailer::Base.deliveries.size' do assert Mailer.wiki_content_updated(content).deliver assert_select_email do assert_select 'a[href=?]', 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation', :text => 'CookBook documentation' end end end end def test_account_information user = User.find(2) valid_languages.each do |lang| user.update_attribute :language, lang.to_s user.reload assert Mailer.account_information(user, 'pAsswORd').deliver end end def test_lost_password token = Token.find(2) valid_languages.each do |lang| token.user.update_attribute :language, lang.to_s token.reload assert Mailer.lost_password(token).deliver end end def test_register token = Token.find(1) Setting.host_name = 'redmine.foo' Setting.protocol = 'https' valid_languages.each do |lang| token.user.update_attribute :language, lang.to_s token.reload ActionMailer::Base.deliveries.clear assert Mailer.register(token).deliver mail = last_email assert_select_email do assert_select "a[href=?]", "https://redmine.foo/account/activate?token=#{token.value}", :text => "https://redmine.foo/account/activate?token=#{token.value}" end end end def test_test user = User.find(1) valid_languages.each do |lang| user.update_attribute :language, lang.to_s assert Mailer.test_email(user).deliver end end def test_reminders Mailer.reminders(:days => 42) assert_equal 1, ActionMailer::Base.deliveries.size mail = last_email assert mail.bcc.include?('dlopper@somenet.foo') assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail assert_equal '1 issue(s) due in the next 42 days', mail.subject end def test_reminders_should_not_include_closed_issues with_settings :default_language => 'en' do Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 5, :subject => 'Closed issue', :assigned_to_id => 3, :due_date => 5.days.from_now, :author_id => 2) ActionMailer::Base.deliveries.clear Mailer.reminders(:days => 42) assert_equal 1, ActionMailer::Base.deliveries.size mail = last_email assert mail.bcc.include?('dlopper@somenet.foo') assert_mail_body_no_match 'Closed issue', mail end end def test_reminders_for_users Mailer.reminders(:days => 42, :users => ['5']) assert_equal 0, ActionMailer::Base.deliveries.size # No mail for dlopper Mailer.reminders(:days => 42, :users => ['3']) assert_equal 1, ActionMailer::Base.deliveries.size # No mail for dlopper mail = last_email assert mail.bcc.include?('dlopper@somenet.foo') assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail end def test_reminder_should_include_issues_assigned_to_groups with_settings :default_language => 'en' do group = Group.generate! group.users << User.find(2) group.users << User.find(3) Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Assigned to group', :assigned_to => group, :due_date => 5.days.from_now, :author_id => 2) ActionMailer::Base.deliveries.clear Mailer.reminders(:days => 7) assert_equal 2, ActionMailer::Base.deliveries.size assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.map(&:bcc).flatten.sort ActionMailer::Base.deliveries.each do |mail| assert_mail_body_match 'Assigned to group', mail end end end def test_reminders_with_version_option with_settings :default_language => 'en' do version = Version.generate!(:name => 'Acme', :project_id => 1) Issue.generate!(:assigned_to => User.find(2), :due_date => 5.days.from_now) Issue.generate!(:assigned_to => User.find(3), :due_date => 5.days.from_now, :fixed_version => version) ActionMailer::Base.deliveries.clear Mailer.reminders(:days => 42, :version => 'acme') assert_equal 1, ActionMailer::Base.deliveries.size mail = last_email assert mail.bcc.include?('dlopper@somenet.foo') end end def test_reminders_should_only_include_issues_the_user_can_see with_settings :default_language => 'en' do user = User.find(3) member = Member.create!(:project_id => 2, :principal => user, :role_ids => [1]) Issue.create!(:project_id => 2, :tracker_id => 1, :status_id => 1, :subject => 'Issue dlopper should not see', :assigned_to_id => 3, :due_date => 5.days.from_now, :author_id => 2) member.destroy ActionMailer::Base.deliveries.clear Mailer.reminders(:days => 42) assert_equal 1, ActionMailer::Base.deliveries.size mail = last_email assert mail.bcc.include?('dlopper@somenet.foo') assert_mail_body_no_match 'Issue dlopper should not see', mail end end def test_mailer_should_not_change_locale # Set current language to italian set_language_if_valid 'it' # Send an email to a french user user = User.find(1) user.language = 'fr' Mailer.account_activated(user).deliver mail = last_email assert_mail_body_match 'Votre compte', mail assert_equal :it, current_language end def test_with_deliveries_off Mailer.with_deliveries false do Mailer.test_email(User.find(1)).deliver end assert ActionMailer::Base.deliveries.empty? # should restore perform_deliveries assert ActionMailer::Base.perform_deliveries end def test_token_for_should_strip_trailing_gt_from_address_with_full_name with_settings :mail_from => "Redmine Mailer<no-reply@redmine.org>" do assert_match /\Aredmine.issue-\d+\.\d+\.[0-9a-f]+@redmine.org\z/, Mailer.token_for(Issue.generate!) end end def test_layout_should_include_the_emails_header with_settings :emails_header => "*Header content*" do with_settings :plain_text_mail => 0 do assert Mailer.test_email(User.find(1)).deliver assert_select_email do assert_select ".header" do assert_select "strong", :text => "Header content" end end end with_settings :plain_text_mail => 1 do assert Mailer.test_email(User.find(1)).deliver mail = last_email assert_not_nil mail assert_include "*Header content*", mail.body.decoded end end end def test_layout_should_not_include_empty_emails_header with_settings :emails_header => "", :plain_text_mail => 0 do assert Mailer.test_email(User.find(1)).deliver assert_select_email do assert_select ".header", false end end end def test_layout_should_include_the_emails_footer with_settings :emails_footer => "*Footer content*" do with_settings :plain_text_mail => 0 do assert Mailer.test_email(User.find(1)).deliver assert_select_email do assert_select ".footer" do assert_select "strong", :text => "Footer content" end end end with_settings :plain_text_mail => 1 do assert Mailer.test_email(User.find(1)).deliver mail = last_email assert_not_nil mail assert_include "\n-- \n", mail.body.decoded assert_include "*Footer content*", mail.body.decoded end end end def test_layout_should_not_include_empty_emails_footer with_settings :emails_footer => "" do with_settings :plain_text_mail => 0 do assert Mailer.test_email(User.find(1)).deliver assert_select_email do assert_select ".footer", false end end with_settings :plain_text_mail => 1 do assert Mailer.test_email(User.find(1)).deliver mail = last_email assert_not_nil mail assert_not_include "\n-- \n", mail.body.decoded end end end def test_should_escape_html_templates_only Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'Subject with a <tag>') mail = last_email assert_equal 2, mail.parts.size assert_include '<tag>', text_part.body.encoded assert_include '&lt;tag&gt;', html_part.body.encoded end def test_should_raise_delivery_errors_when_raise_delivery_errors_is_true mail = Mailer.test_email(User.find(1)) mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error")) ActionMailer::Base.raise_delivery_errors = true assert_raise Exception, "delivery error" do mail.deliver end ensure ActionMailer::Base.raise_delivery_errors = false end def test_should_log_delivery_errors_when_raise_delivery_errors_is_false mail = Mailer.test_email(User.find(1)) mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error")) Rails.logger.expects(:error).with("Email delivery error: delivery error") ActionMailer::Base.raise_delivery_errors = false assert_nothing_raised do mail.deliver end end def test_with_synched_deliveries_should_yield_with_synced_deliveries ActionMailer::Base.delivery_method = :async_smtp ActionMailer::Base.async_smtp_settings = {:foo => 'bar'} Mailer.with_synched_deliveries do assert_equal :smtp, ActionMailer::Base.delivery_method assert_equal({:foo => 'bar'}, ActionMailer::Base.smtp_settings) end assert_equal :async_smtp, ActionMailer::Base.delivery_method ensure ActionMailer::Base.delivery_method = :test end def test_email_addresses_should_keep_addresses assert_equal ["foo@example.net"], Mailer.email_addresses("foo@example.net") assert_equal ["foo@example.net", "bar@example.net"], Mailer.email_addresses(["foo@example.net", "bar@example.net"]) end def test_email_addresses_should_replace_users_with_their_email_addresses assert_equal ["admin@somenet.foo"], Mailer.email_addresses(User.find(1)) assert_equal ["admin@somenet.foo", "jsmith@somenet.foo"], Mailer.email_addresses(User.where(:id => [1,2])).sort end def test_email_addresses_should_include_notified_emails_addresses_only EmailAddress.create!(:user_id => 2, :address => "another@somenet.foo", :notify => false) EmailAddress.create!(:user_id => 2, :address => "another2@somenet.foo") assert_equal ["another2@somenet.foo", "jsmith@somenet.foo"], Mailer.email_addresses(User.find(2)).sort end private def last_email mail = ActionMailer::Base.deliveries.last assert_not_nil mail mail end def text_part last_email.parts.detect {|part| part.content_type.include?('text/plain')} end def html_part last_email.parts.detect {|part| part.content_type.include?('text/html')} end def with_each_language_as_default(&block) valid_languages.each do |lang| with_settings :default_language => lang.to_s do yield lang end end end end