Project

General

Profile

Download (15.2 KB) Statistics
| Branch: | Tag: | Revision:

haketilo / test / haketilo_test / unit / test_text_entry_list.py @ fd9f2fc4

1
# SPDX-License-Identifier: CC0-1.0
2

    
3
"""
4
Haketilo unit tests - list of editable entries
5
"""
6

    
7
# This file is part of Haketilo
8
#
9
# Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the CC0 1.0 Universal License as published by
13
# the Creative Commons Corporation.
14
#
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
# CC0 1.0 Universal License for more details.
19

    
20
import pytest
21
from selenium.webdriver.support.ui import WebDriverWait
22
from selenium.webdriver.common.keys import Keys
23
import inspect
24

    
25
from ..extension_crafting import ExtraHTML
26
from ..script_loader import load_script
27
from .utils import *
28

    
29
list_code_template = '(await blocking_allowing_lists(%%s))[%d]'
30
mode_parameters = [
31
    #add_action        del_action              instantiate_code
32
    ('set_repo',       'del_repo',             'await repo_list(%s)'),
33
    ('set_disallowed', 'set_default_allowing', list_code_template % 0),
34
    ('set_disallowed', 'set_allowed',          list_code_template % 0),
35
    ('set_allowed',    'set_default_allowing', list_code_template % 1),
36
    ('set_allowed',    'set_disallowed',       list_code_template % 1)
37
]
38

    
39
def instantiate_list(to_return):
40
    instantiate_code = inspect.stack()[1].frame.f_locals['instantiate_code']
41
    return inspect.stack()[1].frame.f_locals['execute_in_page'](
42
        f'''
43
        let dialog_ctx = dialog.make(() => {{}}, () => {{}}), list;
44
        async function make_list() {{
45
            list = {instantiate_code % 'dialog_ctx'};
46
            document.body.append(list.main_div, dialog_ctx.main_div);
47
            return [{', '.join(to_return)}];
48
        }}
49
        returnval(make_list());
50
        ''')
51

    
52
dialog_html_append = {'html/text_entry_list.html': '#INCLUDE html/dialog.html'}
53
dialog_html_test_ext_data = {
54
    'background_script': broker_js,
55
    'extra_html': ExtraHTML('html/text_entry_list.html', dialog_html_append),
56
    'navigate_to': 'html/text_entry_list.html'
57
}
58

    
59
@pytest.mark.ext_data(dialog_html_test_ext_data)
60
@pytest.mark.usefixtures('webextension')
61
@pytest.mark.parametrize('mode', mode_parameters)
62
def test_text_entry_list_ordering(driver, execute_in_page, mode):
63
    """
64
    A test case of ordering of repo URLs or URL patterns in the list.
65
    """
66
    add_action, del_action, instantiate_code = mode
67

    
68
    execute_in_page(load_script('html/text_entry_list.js'))
69

    
70
    endings = ['hyd/', 'hydrilla/', 'Hydrilla/', 'HYDRILLA/',
71
               'test/', 'test^it/', 'Test^it/', 'TEST^IT/']
72

    
73
    indexes_added = set()
74

    
75
    for iteration, to_include in enumerate([
76
            set([i for i in range(len(endings)) if is_prime(i)]),
77
            set([i for i in range(len(endings))
78
                 if not is_prime(i) and i & 1]),
79
            set([i for i in range(len(endings)) if i % 3 == 0]),
80
            set([i for i in range(len(endings))
81
                 if i % 3 and not i & 1 and not is_prime(i)]),
82
            set(range(len(endings)))
83
    ]):
84
        endings_to_include = [endings[i] for i in sorted(to_include)]
85
        urls = [f'https://example.com/{e}' for e in endings_to_include]
86

    
87
        def add_urls():
88
            execute_in_page(
89
                '''{
90
                async function add_urls(urls, add_action) {
91
                    for (const url of urls)
92
                        await haketilodb[add_action](url);
93
                }
94
                returnval(add_urls(...arguments));
95
                }''',
96
                urls, add_action)
97

    
98
        def wait_for_completed(wait_id):
99
            """
100
            We add an extra url to IndexedDB and wait for it to appear in the
101
            DOM list. Once this happes, we know other operations must have also
102
            finished.
103
            """
104
            url = f'https://example.org/{iteration}/{wait_id}'
105
            execute_in_page(
106
                '''
107
                returnval(haketilodb[arguments[1]](arguments[0]));
108
                ''',
109
                url, add_action)
110
            WebDriverWait(driver, 10).until(lambda _: url in list_div.text)
111

    
112
        def assert_order(indexes_present, empty_entry_expected=False):
113
            entries_texts = execute_in_page(
114
                '''
115
                returnval([...list.list_div.children].map(n => n.textContent));
116
                ''')
117

    
118
            if empty_entry_expected:
119
                assert 'example' not in entries_texts[0]
120
                entries_texts.pop(0)
121

    
122
            for i, et in zip(sorted(indexes_present), entries_texts):
123
                assert f'https://example.com/{endings[i]}' in et
124

    
125
            for et in entries_texts[len(indexes_present):]:
126
                assert 'example.org' in et
127

    
128
        add_urls()
129

    
130
        if iteration == 0:
131
            list_div, new_entry_but = \
132
                instantiate_list(['list.list_div', 'list.new_but'])
133

    
134
        indexes_added.update(to_include)
135
        wait_for_completed(0)
136
        assert_order(indexes_added)
137

    
138
        execute_in_page(
139
            '''{
140
            async function remove_urls(urls, del_action) {
141
                for (const url of urls)
142
                    await haketilodb[del_action](url);
143
            }
144
            returnval(remove_urls(...arguments));
145
            }''',
146
            urls, del_action)
147
        wait_for_completed(1)
148
        assert_order(indexes_added.difference(to_include))
149

    
150
        # On the last iteration, add a new editable entry before re-additions.
151
        if len(to_include) == len(endings):
152
            new_entry_but.click()
153
            add_urls()
154
            wait_for_completed(2)
155
            assert_order(indexes_added, empty_entry_expected=True)
156
        else:
157
            add_urls()
158

    
159
def active(id):
160
    return inspect.stack()[1].frame.f_locals['execute_in_page']\
161
        (f'returnval(list.active_entry.{id});')
162
def existing(id, entry_nr=0):
163
    return inspect.stack()[1].frame.f_locals['execute_in_page'](
164
        '''
165
        returnval(list.entries_by_text.get(list.shown_texts[arguments[0]])\
166
                  [arguments[1]]);
167
        ''',
168
        entry_nr, id)
169

    
170
@pytest.mark.ext_data(dialog_html_test_ext_data)
171
@pytest.mark.usefixtures('webextension')
172
@pytest.mark.parametrize('mode', [mp for mp in mode_parameters
173
                                  if mp[1] != 'set_default_allowing'])
174
def test_text_entry_list_editing(driver, execute_in_page, mode):
175
    """
176
    A test case of editing entries in repo URLs list.
177
    """
178
    add_action, _, instantiate_code = mode
179

    
180
    execute_in_page(load_script('html/text_entry_list.js'))
181

    
182
    execute_in_page(
183
        '''
184
        let original_loader = dialog.loader, last_loader_msg;
185
        dialog.loader = (ctx, ...msg) => {
186
            last_loader_msg = msg;
187
            return original_loader(ctx, ...msg);
188
        }
189
        ''')
190
    last_loader_msg = lambda: execute_in_page('returnval(last_loader_msg);')
191

    
192
    list_div, new_entry_but = \
193
        instantiate_list(['list.list_div', 'list.new_but'])
194

    
195
    if 'allow' in add_action:
196
        assert last_loader_msg() == ['Loading script blocking settings...']
197
    else:
198
        assert last_loader_msg() == ['Loading repositories...']
199

    
200
    assert execute_in_page('returnval(dialog_ctx.shown);') == False
201

    
202
    # Test adding new item. Submit via button click.
203
    new_entry_but.click()
204
    assert not active('noneditable_view').is_displayed()
205
    assert not active('save_but').is_displayed()
206
    assert active('add_but').is_displayed()
207
    assert active('cancel_but').is_displayed()
208
    active('input').send_keys('https://example.com///')
209
    active('add_but').click()
210
    WebDriverWait(driver, 10).until(lambda _: 'example.com' in list_div.text)
211
    assert execute_in_page('returnval(list.list_div.children.length);') == 1
212
    if 'disallow' in add_action:
213
        assert last_loader_msg() == \
214
            ["Blocking scripts on 'https://example.com/'..."]
215
    elif 'allow' in add_action:
216
        assert last_loader_msg() == \
217
            ["Allowing scripts on 'https://example.com/'..."]
218
    else:
219
        assert last_loader_msg() == \
220
            ["Adding repository 'https://example.com/'..."]
221

    
222
    assert not existing('editable_view').is_displayed()
223
    assert existing('text').is_displayed()
224
    assert existing('remove_but').is_displayed()
225

    
226
    # Test editing item. Submit via 'Enter' hit. Also test url pattern
227
    # normalization.
228
    existing('text').click()
229
    assert not active('noneditable_view').is_displayed()
230
    assert not active('add_but').is_displayed()
231
    assert active('save_but').is_displayed()
232
    assert active('cancel_but').is_displayed()
233
    assert active('input.value') == 'https://example.com/'
234
    active('input').send_keys(Keys.BACKSPACE * 30 + 'https://example.org//a//b'
235
                              + Keys.ENTER)
236
    WebDriverWait(driver, 10).until(lambda _: 'example.org' in list_div.text)
237
    assert execute_in_page('returnval(list.list_div.children.length);') == 1
238
    if 'disallow' in add_action:
239
        assert last_loader_msg() == ['Rewriting script blocking rule...']
240
    elif 'allow' in add_action:
241
        assert last_loader_msg() == ['Rewriting script allowing rule...']
242
    else:
243
        assert last_loader_msg() == ['Replacing repository...']
244

    
245
    # Test entry removal.
246
    existing('remove_but').click()
247
    WebDriverWait(driver, 10).until(lambda _: 'xample.org' not in list_div.text)
248
    assert execute_in_page('returnval(list.list_div.children.length);') == 0
249
    if 'allow' in add_action:
250
        assert last_loader_msg() == \
251
            ["Setting default scripts blocking policy on 'https://example.org/a/b'..."]
252
    else:
253
        assert last_loader_msg() == ["Removing repository 'https://example.org//a//b/'..."]
254

    
255
    # The rest of this test remains the same regardless of mode. No point
256
    # testing the same thing multiple times.
257
    if 'repo' not in add_action:
258
        return
259

    
260
    # Test that clicking hidden buttons of item not being edited does nothing.
261
    new_entry_but.click()
262
    active('input').send_keys('https://example.foo' + Keys.ENTER)
263
    WebDriverWait(driver, 10).until(lambda _: 'xample.foo/' in list_div.text)
264
    existing('add_but.click()')
265
    existing('save_but.click()')
266
    existing('cancel_but.click()')
267
    assert execute_in_page('returnval(dialog_ctx.shown);') == False
268
    assert execute_in_page('returnval(list.list_div.children.length);') == 1
269
    assert not existing('editable_view').is_displayed()
270

    
271
    # Test that clicking hidden buttons of item being edited does nothing.
272
    existing('text').click()
273
    active('remove_but.click()')
274
    active('add_but.click()')
275
    assert execute_in_page('returnval(dialog_ctx.shown);') == False
276
    assert execute_in_page('returnval(list.list_div.children.length);') == 1
277
    assert not active('noneditable_view').is_displayed()
278

    
279
    # Test that creating a new entry makes the other one noneditable again.
280
    new_entry_but.click()
281
    assert existing('text').is_displayed()
282

    
283
    # Test that clicking hidden buttons of new item entry does nothing.
284
    active('remove_but.click()')
285
    active('save_but.click()')
286
    assert execute_in_page('returnval(dialog_ctx.shown);') == False
287
    assert execute_in_page('returnval(list.list_div.children.length);') == 2
288
    assert not active('noneditable_view').is_displayed()
289

    
290
    # Test that starting edit of another entry removes the new entry.
291
    existing('text').click()
292
    assert existing('editable_view').is_displayed()
293
    assert execute_in_page('returnval(list.list_div.children.length);') == 1
294

    
295
    # Test that starting edit of another entry cancels edit of the first entry.
296
    new_entry_but.click()
297
    active('input').send_keys('https://example.net' + Keys.ENTER)
298
    WebDriverWait(driver, 10).until(lambda _: 'example.net/' in list_div.text)
299
    assert execute_in_page('returnval(list.list_div.children.length);') == 2
300
    existing('text', 0).click()
301
    assert existing('editable_view', 0).is_displayed()
302
    assert not existing('editable_view', 1).is_displayed()
303
    existing('text', 1).click()
304
    assert not existing('editable_view', 0).is_displayed()
305
    assert existing('editable_view', 1).is_displayed()
306

    
307
@pytest.mark.ext_data(dialog_html_test_ext_data)
308
@pytest.mark.usefixtures('webextension')
309
@pytest.mark.parametrize('mode', [mp for mp in mode_parameters
310
                                  if mp[1] != 'set_default_allowing'])
311
def test_text_entry_list_errors(driver, execute_in_page, mode):
312
    """
313
    A test case of error dialogs shown by repo URL list.
314
    """
315
    add_action, _, instantiate_code = mode
316

    
317
    execute_in_page(load_script('html/text_entry_list.js'))
318

    
319
    to_return = ['list.list_div', 'list.new_but', 'dialog_ctx.main_div']
320
    list_div, new_entry_but, dialog_div = instantiate_list(to_return)
321

    
322
    # Prepare one entry to use later.
323
    new_entry_but.click()
324
    active('input').send_keys('https://example.com' + Keys.ENTER)
325

    
326
    # Check invalid URL errors.
327
    for clickable in (existing('text'), new_entry_but):
328
        clickable.click()
329
        active('input').send_keys(Keys.BACKSPACE * 30 + 'ws://example'
330
                                  + Keys.ENTER)
331
        execute_in_page('dialog.close(dialog_ctx);')
332

    
333
        if 'allow' in add_action:
334
            assert "'ws://example' is not a valid URL pattern. See here for more details." \
335
                in dialog_div.text
336
            assert patterns_doc_url == \
337
                driver.find_element_by_link_text('here').get_attribute('href')
338
            continue
339
        else:
340
            assert 'Repository URLs shoud use https:// schema.' \
341
                in dialog_div.text
342

    
343
        active('input').send_keys(Keys.BACKSPACE * 30 + 'https://example'
344
                                  + Keys.ENTER)
345
        assert 'Provided URL is not valid.' in dialog_div.text
346
        execute_in_page('dialog.close(dialog_ctx);')
347

    
348
    # Mock errors to force error messages to appear.
349
    execute_in_page(
350
        '''
351
        for (const action of [
352
            "set_repo", "del_repo", "set_allowed", "set_default_allowing"
353
        ])
354
            haketilodb[action] = () => {throw "reckless, limitless scope";};
355
        ''')
356

    
357
    # Check database error dialogs.
358
    def check_reported_failure(txt):
359
        fail = lambda _: txt in dialog_div.text
360
        WebDriverWait(driver, 10).until(fail)
361
        execute_in_page('dialog.close(dialog_ctx);')
362

    
363
    existing('text').click()
364
    active('input').send_keys(Keys.BACKSPACE * 30 + 'https://example.org'
365
                              + Keys.ENTER)
366
    if 'disallow' in add_action:
367
        check_reported_failure('Failed to rewrite blocking rule :(')
368
    elif 'allow' in add_action:
369
        check_reported_failure('Failed to rewrite allowing rule :(')
370
    else:
371
        check_reported_failure('Failed to replace repository :(')
372

    
373
    active('cancel_but').click()
374
    existing('remove_but').click()
375
    if 'allow' in add_action:
376
        check_reported_failure("Failed to remove rule for 'https://example.com' :(")
377
    else:
378
        check_reported_failure("Failed to remove repository 'https://example.com/' :(")
379

    
380
    new_entry_but.click()
381
    active('input').send_keys('https://example.org' + Keys.ENTER)
382
    if 'disallow' in add_action:
383
        check_reported_failure("Failed to write blocking rule for 'https://example.org' :(")
384
    elif 'allow' in add_action:
385
        check_reported_failure("Failed to write allowing rule for 'https://example.org' :(")
386
    else:
387
        check_reported_failure("Failed to add repository 'https://example.org/' :(")
(23-23/25)