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/' :(")
|