~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/testing/windmill/widgets.py

  • Committer: Curtis Hovey
  • Date: 2011-05-12 18:25:06 UTC
  • mto: This revision was merged to the branch mainline in revision 13038.
  • Revision ID: curtis.hovey@canonical.com-20110512182506-098n1wovp9m1av59
Renamed licence_reviewed to project_reviewed.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
 
2
# GNU Affero General Public License version 3 (see the file LICENSE).
 
3
 
 
4
"""Test helpers for common AJAX widgets."""
 
5
 
 
6
__metaclass__ = type
 
7
__all__ = [
 
8
    'FormPickerWidgetTest',
 
9
    'InlineEditorWidgetTest',
 
10
    'InlinePickerWidgetButtonTest',
 
11
    'InlinePickerWidgetSearchTest',
 
12
    'OnPageWidget',
 
13
    'search_and_select_picker_widget',
 
14
    'search_picker_widget',
 
15
    ]
 
16
 
 
17
 
 
18
from windmill.authoring import WindmillTestClient
 
19
 
 
20
from lp.testing.windmill import (
 
21
    constants,
 
22
    lpuser,
 
23
    )
 
24
 
 
25
 
 
26
class OnPageWidget:
 
27
    """A class that represents and interacts with an on-page JavaScript widget.
 
28
 
 
29
    The widget is assumed to be a YUI widget controlled by yui-X-hidden classes.
 
30
    """
 
31
 
 
32
    def __init__(self, client, widget_name):
 
33
        """Constructor.
 
34
 
 
35
        :param client: A WindmillTestClient instance for interacting with pages.
 
36
        :param widget_name: The class name of the YUI widget, like 'yui3-picker'.
 
37
        """
 
38
        self.client = client
 
39
        self.widget_name = widget_name
 
40
 
 
41
    @property
 
42
    def xpath(self):
 
43
        """The XPath of this widget, not including the hidden or visible state.
 
44
        """
 
45
        # We include a space after the widget name because @class matches the
 
46
        # /beginning/ of text strings, not whole words!
 
47
        return u"//div[contains(@class, '%s ')]" % self.widget_name
 
48
 
 
49
    @property
 
50
    def visible_xpath(self):
 
51
        """The XPath of the widget when it is visible on page."""
 
52
        subs = dict(name=self.widget_name)
 
53
        # We include a space after the widget name because @class matches the
 
54
        # /beginning/ of text strings, not whole words!
 
55
        return (u"//div[contains(@class, '%(name)s ') "
 
56
                "and not(contains(@class, '%(name)s-hidden'))]" % subs)
 
57
 
 
58
    @property
 
59
    def hidden_xpath(self):
 
60
        """The XPath of the widget when it is hidden."""
 
61
        # We include a space after the widget name because @class matches the
 
62
        # /beginning/ of text strings, not whole words!
 
63
        subs = dict(name=self.widget_name)
 
64
        return (u"//div[contains(@class, '%(name)s ') "
 
65
                "and contains(@class, '%(name)s-hidden')]" % subs)
 
66
 
 
67
    def should_be_visible(self):
 
68
        """Check to see if the widget is visible on screen."""
 
69
        self.client.waits.forElement(xpath=self.visible_xpath,
 
70
                                     timeout=constants.FOR_ELEMENT)
 
71
 
 
72
    def should_be_hidden(self):
 
73
        """Check to see if the widget is hidden on screen."""
 
74
        self.client.waits.forElement(xpath=self.hidden_xpath,
 
75
                                     timeout=constants.FOR_ELEMENT)
 
76
 
 
77
 
 
78
class SearchPickerWidget(OnPageWidget):
 
79
    """A proxy for the yui3-picker widget from lazr-js."""
 
80
 
 
81
    def __init__(self, client):
 
82
        """Constructor.
 
83
 
 
84
        :param client: A WindmillTestClient instance.
 
85
        """
 
86
        super(SearchPickerWidget, self).__init__(client, 'yui3-picker')
 
87
        self.search_input_xpath = (
 
88
            self.visible_xpath + "//input[@class='yui3-picker-search']")
 
89
        self.search_button_xpath = (
 
90
            self.visible_xpath +
 
91
            "//div[@class='yui3-picker-search-box']/button")
 
92
 
 
93
    def _get_result_xpath_by_number(self, item_number):
 
94
        """Return the XPath for the given search result number."""
 
95
        item_xpath = "//ul[@class='yui3-picker-results']/li[%d]/span" % item_number
 
96
        return self.visible_xpath + item_xpath
 
97
 
 
98
    def do_search(self, text):
 
99
        """Enter some text in the search field and click the search button.
 
100
 
 
101
        :param text: The text we want to search for.
 
102
        """
 
103
        self.client.waits.forElement(xpath=self.search_input_xpath,
 
104
                                timeout=constants.FOR_ELEMENT)
 
105
        self.client.type(xpath=self.search_input_xpath, text=text)
 
106
        self.client.click(xpath=self.search_button_xpath,
 
107
                          timeout=constants.FOR_ELEMENT)
 
108
 
 
109
    def click_result_by_number(self, item_number):
 
110
        """Click on the given result number in the results list.
 
111
 
 
112
        :param item_number: The item in the results list we should click on.
 
113
        """
 
114
        item_xpath = self._get_result_xpath_by_number(item_number)
 
115
        self.client.waits.forElement(xpath=item_xpath,
 
116
                                     timeout=constants.FOR_ELEMENT)
 
117
        self.client.click(xpath=item_xpath)
 
118
 
 
119
 
 
120
class WidgetTest:
 
121
    """A base class to provide logon capability."""
 
122
    def getLoggedInClient(self):
 
123
        """Return a new client, and the url that it has loaded."""
 
124
        client = WindmillTestClient(self.suite_name)
 
125
        email = self.user.email
 
126
        password = self.user.password
 
127
        client.open(url=lpuser.get_basic_login_url(email, password))
 
128
        client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
 
129
        client.open(url=self.url)
 
130
        client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
 
131
        return client
 
132
 
 
133
 
 
134
class InlineEditorWidgetTest(WidgetTest):
 
135
    """Test that the inline editor widget is working properly on a page."""
 
136
 
 
137
    def __init__(self, url, widget_id, expected_value, new_value, name=None,
 
138
                 suite_name='inline_editor', user=lpuser.NO_PRIV,
 
139
                 widget_tag='h1'):
 
140
        """Create a new InlineEditorWidgetTest.
 
141
 
 
142
        :param url: The URL to the page on which the widget lives.
 
143
        :param widget_id: The HTML id of the widget.
 
144
        :param expected_value: The current expected value of the widget.
 
145
        :param new_value: The value to change the field to.
 
146
        :param suite: The suite in which this test is part of.
 
147
        :param user: The user who should be logged in.
 
148
        :param widget_tag: Element tag the widget is inside of.
 
149
        """
 
150
        self.url = url
 
151
        if name is None:
 
152
            self.__name__ = ('test_%s_inline_edit'
 
153
                             % widget_id.replace('-', '_'))
 
154
        else:
 
155
            self.__name__ = name
 
156
        self.widget_id = widget_id
 
157
        self.expected_value = expected_value
 
158
        self.new_value = new_value
 
159
        self.suite_name = suite_name
 
160
        self.user = user
 
161
        self.widget_tag = widget_tag
 
162
 
 
163
    def __call__(self):
 
164
        """Tests the widget is hooked and works properly.
 
165
 
 
166
        The test:
 
167
        * opens the url;
 
168
        * asserts that the widget is initialized to the expected value;
 
169
        * uses the inline editor to change to the new value;
 
170
        * asserts that the page was updated with the new value;
 
171
        * reloads and verifies that the new value sticked.
 
172
        """
 
173
        client = self.getLoggedInClient()
 
174
        widget_base = u"//%s[@id='%s']" % (self.widget_tag, self.widget_id)
 
175
        client.waits.forElement(
 
176
            xpath=widget_base + '/a', timeout=constants.FOR_ELEMENT)
 
177
        client.asserts.assertText(
 
178
            xpath=widget_base + '/span[1]', validator=self.expected_value)
 
179
        client.click(xpath=widget_base + '/a')
 
180
        client.waits.forElement(
 
181
            xpath=widget_base + '/a', timeout=constants.FOR_ELEMENT)
 
182
        client.waits.forElement(
 
183
            xpath=widget_base + '//textarea', timeout=constants.FOR_ELEMENT)
 
184
        client.type(
 
185
            xpath=widget_base + '//textarea', text=self.new_value)
 
186
        client.click(xpath=widget_base + '//button[last()]')
 
187
        client.waits.forElement(
 
188
            xpath=widget_base + '/span[1][text()="' + self.new_value + '"]',
 
189
            timeout=constants.FOR_ELEMENT)
 
190
 
 
191
 
 
192
def search_picker_widget(client, search_text):
 
193
    """Search using an on-page picker widget."""
 
194
    picker = SearchPickerWidget(client)
 
195
    picker.should_be_visible()
 
196
    picker.do_search(search_text)
 
197
 
 
198
 
 
199
def search_and_select_picker_widget(client, search_text, result_index):
 
200
    """Search using an on-page picker widget and click a search result."""
 
201
    picker = SearchPickerWidget(client)
 
202
    picker.should_be_visible()
 
203
    picker.do_search(search_text)
 
204
    picker.click_result_by_number(result_index)
 
205
 
 
206
 
 
207
class InlinePickerWidgetSearchTest(WidgetTest):
 
208
    """Test that the Picker widget edits a value inline."""
 
209
 
 
210
    def __init__(self, url, activator_id, search_text, result_index,
 
211
                 new_value, name=None, suite_name='inline_picker_search_test',
 
212
                 user=lpuser.FOO_BAR):
 
213
        """Create a new InlinePickerSearchWidgetTest.
 
214
 
 
215
        :param url: The URL to the page on which the widget lives.
 
216
        :param activator_id: The HTML id of the activator widget.
 
217
        :param search_text: Picker search value.
 
218
        :param result_index: Item in picker result to select.
 
219
        :param new_value: The value to change the field to.
 
220
        :param name: Override the test name, if necessary.
 
221
        :param suite: The suite in which this test is part of.
 
222
        :param user: The user who should be logged in.
 
223
        """
 
224
        self.url = url
 
225
        if name is None:
 
226
            self.__name__ = 'test_%s_inline_picker' % (
 
227
                activator_id.replace('-', '_'),)
 
228
        else:
 
229
            self.__name__ = name
 
230
        self.activator_id = activator_id
 
231
        self.search_text = search_text
 
232
        self.result_index = result_index
 
233
        self.new_value = new_value
 
234
        self.suite_name = suite_name
 
235
        self.user = user
 
236
 
 
237
    def __call__(self):
 
238
        # Load page.
 
239
        client = self.getLoggedInClient()
 
240
 
 
241
        # Click on edit button.
 
242
        button_xpath = (
 
243
            u"//span[@id='%s']"
 
244
             "/button[not(contains(@class, 'yui3-activator-hidden'))]"
 
245
             % self.activator_id)
 
246
        client.waits.forElement(
 
247
            xpath=button_xpath,
 
248
            timeout=constants.FOR_ELEMENT)
 
249
        client.click(xpath=button_xpath)
 
250
 
 
251
        # Search picker.
 
252
        search_and_select_picker_widget(
 
253
            client, self.search_text, self.result_index)
 
254
 
 
255
        # Verify update.
 
256
        client.waits.sleep(milliseconds=u'2000')
 
257
        client.asserts.assertText(
 
258
            xpath=u"//span[@id='%s']//a" % self.activator_id,
 
259
            validator=self.new_value)
 
260
 
 
261
        # Reload the page to verify that the selected value is persisted.
 
262
        client.open(url=self.url)
 
263
        client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
 
264
 
 
265
        # Verify update, again.
 
266
        client.waits.forElement(
 
267
            xpath=u"//span[@id='%s']//a" % self.activator_id,
 
268
            timeout=constants.FOR_ELEMENT)
 
269
        client.asserts.assertText(
 
270
            xpath=u"//span[@id='%s']//a" % self.activator_id,
 
271
            validator=self.new_value)
 
272
 
 
273
 
 
274
class InlinePickerWidgetButtonTest(WidgetTest):
 
275
    """Test custom buttons/links added to the Picker."""
 
276
 
 
277
    def __init__(self, url, activator_id, button_class, new_value,
 
278
                 name=None, suite_name='inline_picker_button_test',
 
279
                 user=lpuser.FOO_BAR):
 
280
        """Create a new InlinePickerWidgetButtonTest.
 
281
 
 
282
        :param url: The URL to the page on which the widget lives.
 
283
        :param activator_id: The HTML id of the activator widget.
 
284
        :param button_class: The CSS class identifying the button.
 
285
        :param new_value: The value to change the field to.
 
286
        :param name: Override the test name, if necessary.
 
287
        :param suite: The suite in which this test is part of.
 
288
        :param user: The user who should be logged in.
 
289
        """
 
290
        self.url = url
 
291
        self.activator_id = activator_id
 
292
        self.button_class = button_class
 
293
        self.new_value = new_value
 
294
        self.suite_name = suite_name
 
295
        self.user = user
 
296
        if name is None:
 
297
            self.__name__ = 'test_%s_inline_picker' % (
 
298
                activator_id.replace('-', '_'),)
 
299
        else:
 
300
            self.__name__ = name
 
301
 
 
302
    def __call__(self):
 
303
        # Load page.
 
304
        client = self.getLoggedInClient()
 
305
 
 
306
        # Click on edit button.
 
307
        button_xpath = (
 
308
            u"//span[@id='%s']"
 
309
             "/button[not(contains(@class, 'yui3-activator-hidden'))]"
 
310
             % self.activator_id)
 
311
        client.waits.forElement(xpath=button_xpath, timeout=u'25000')
 
312
        client.click(xpath=button_xpath)
 
313
 
 
314
        # Click on remove button.
 
315
        remove_button_xpath = (
 
316
            u"//div[contains(@class, 'yui3-picker ') "
 
317
             "and not(contains(@class, 'yui3-picker-hidden'))]"
 
318
             "//*[contains(@class, '%s')]" % self.button_class)
 
319
        client.waits.forElement(xpath=remove_button_xpath, timeout=u'25000')
 
320
        client.click(xpath=remove_button_xpath)
 
321
        client.waits.sleep(milliseconds=u'2000')
 
322
 
 
323
        # Verify removal.
 
324
        client.asserts.assertText(
 
325
            xpath=u"//span[@id='%s']/span[@class='yui3-activator-data-box']"
 
326
                  % self.activator_id,
 
327
            validator=self.new_value)
 
328
 
 
329
        # Reload the page to verify that the selected value is persisted.
 
330
        client.open(url=self.url)
 
331
        client.waits.forPageLoad(timeout=u'25000')
 
332
 
 
333
        # Verify removal, again.
 
334
        client.waits.forElement(
 
335
            xpath=u"//span[@id='%s']/span[@class='yui3-activator-data-box']"
 
336
                  % self.activator_id,
 
337
            timeout=u'25000')
 
338
        client.asserts.assertText(
 
339
            xpath=u"//span[@id='%s']/span[@class='yui3-activator-data-box']"
 
340
                  % self.activator_id,
 
341
            validator=self.new_value)
 
342
 
 
343
 
 
344
class FormPickerWidgetTest(WidgetTest):
 
345
    """Test that the Picker widget edits a form value properly."""
 
346
 
 
347
    def __init__(self, url, short_field_name, search_text, result_index,
 
348
                 new_value, name=None, suite_name='form_picker',
 
349
                 user=lpuser.FOO_BAR):
 
350
        """Create a new FormPickerWidgetTest.
 
351
 
 
352
        :param url: The URL to the page on which the widget lives.
 
353
        :param short_field_name: The name of the Zope attribute. For example,
 
354
                                 'owner' which has the id 'field.owner'.
 
355
        :param search_text: Picker search value.
 
356
        :param result_index: Item in picker result to select.
 
357
        :param new_value: The value to change the field to.
 
358
        :param name: Override the test name, if necessary.
 
359
        :param suite: The suite in which this test is part of.
 
360
        :param user: The user who should be logged in.
 
361
        """
 
362
        self.url = url
 
363
        if name is None:
 
364
            self.__name__ = 'test_%s_form_picker' % (
 
365
                short_field_name.replace('-', '_'),)
 
366
        else:
 
367
            self.__name__ = name
 
368
        self.search_text = search_text
 
369
        self.result_index = result_index
 
370
        self.new_value = new_value
 
371
        self.suite_name = suite_name
 
372
        self.user = user
 
373
        self.choose_link_id = 'show-widget-field-%s' % short_field_name
 
374
        self.field_id = 'field.%s' % short_field_name
 
375
 
 
376
    def __call__(self):
 
377
        # Load page.
 
378
        client = self.getLoggedInClient()
 
379
 
 
380
        # Click on "Choose" link to show picker for the given field.
 
381
        client.waits.forElement(
 
382
            id=self.choose_link_id, timeout=constants.PAGE_LOAD)
 
383
        client.click(id=self.choose_link_id)
 
384
 
 
385
        # Search picker.
 
386
        search_and_select_picker_widget(
 
387
            client, self.search_text, self.result_index)
 
388
 
 
389
        # Verify value.
 
390
        client.asserts.assertProperty(
 
391
            id=self.field_id, validator=u"value|%s" % self.new_value)