# SPDX-License-Identifier: CC0-1.0 """ Haketilo unit tests - list of editable entries """ # This file is part of Haketilo # # Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org> # # This program is free software: you can redistribute it and/or modify # it under the terms of the CC0 1.0 Universal License as published by # the Creative Commons Corporation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # CC0 1.0 Universal License for more details. import pytest from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.keys import Keys import inspect from ..extension_crafting import ExtraHTML from ..script_loader import load_script from .utils import * list_code_template = '(await blocking_allowing_lists(%%s))[%d]' mode_parameters = [ #add_action del_action instantiate_code ('set_repo', 'del_repo', 'await repo_list(%s)'), ('set_disallowed', 'set_default_allowing', list_code_template % 0), ('set_disallowed', 'set_allowed', list_code_template % 0), ('set_allowed', 'set_default_allowing', list_code_template % 1), ('set_allowed', 'set_disallowed', list_code_template % 1) ] def instantiate_list(to_return): instantiate_code = inspect.stack()[1].frame.f_locals['instantiate_code'] return inspect.stack()[1].frame.f_locals['execute_in_page']( f''' let dialog_ctx = dialog.make(() => {{}}, () => {{}}), list; async function make_list() {{ list = {instantiate_code % 'dialog_ctx'}; document.body.append(list.main_div, dialog_ctx.main_div); return [{', '.join(to_return)}]; }} returnval(make_list()); ''') dialog_html_append = {'html/text_entry_list.html': '#INCLUDE html/dialog.html'} dialog_html_test_ext_data = { 'background_script': broker_js, 'extra_html': ExtraHTML('html/text_entry_list.html', dialog_html_append), 'navigate_to': 'html/text_entry_list.html' } @pytest.mark.ext_data(dialog_html_test_ext_data) @pytest.mark.usefixtures('webextension') @pytest.mark.parametrize('mode', mode_parameters) def test_text_entry_list_ordering(driver, execute_in_page, mode): """ A test case of ordering of repo URLs or URL patterns in the list. """ add_action, del_action, instantiate_code = mode execute_in_page(load_script('html/text_entry_list.js')) endings = ['hyd/', 'hydrilla/', 'Hydrilla/', 'HYDRILLA/', 'test/', 'test^it/', 'Test^it/', 'TEST^IT/'] indexes_added = set() for iteration, to_include in enumerate([ set([i for i in range(len(endings)) if is_prime(i)]), set([i for i in range(len(endings)) if not is_prime(i) and i & 1]), set([i for i in range(len(endings)) if i % 3 == 0]), set([i for i in range(len(endings)) if i % 3 and not i & 1 and not is_prime(i)]), set(range(len(endings))) ]): endings_to_include = [endings[i] for i in sorted(to_include)] urls = [f'https://example.com/{e}' for e in endings_to_include] def add_urls(): execute_in_page( '''{ async function add_urls(urls, add_action) { for (const url of urls) await haketilodb[add_action](url); } returnval(add_urls(...arguments)); }''', urls, add_action) def wait_for_completed(wait_id): """ We add an extra url to IndexedDB and wait for it to appear in the DOM list. Once this happes, we know other operations must have also finished. """ url = f'https://example.org/{iteration}/{wait_id}' execute_in_page( ''' returnval(haketilodb[arguments[1]](arguments[0])); ''', url, add_action) WebDriverWait(driver, 10).until(lambda _: url in list_div.text) def assert_order(indexes_present, empty_entry_expected=False): entries_texts = execute_in_page( ''' returnval([...list.list_div.children].map(n => n.textContent)); ''') if empty_entry_expected: assert 'example' not in entries_texts[0] entries_texts.pop(0) for i, et in zip(sorted(indexes_present), entries_texts): assert f'https://example.com/{endings[i]}' in et for et in entries_texts[len(indexes_present):]: assert 'example.org' in et add_urls() if iteration == 0: list_div, new_entry_but = \ instantiate_list(['list.list_div', 'list.new_but']) indexes_added.update(to_include) wait_for_completed(0) assert_order(indexes_added) execute_in_page( '''{ async function remove_urls(urls, del_action) { for (const url of urls) await haketilodb[del_action](url); } returnval(remove_urls(...arguments)); }''', urls, del_action) wait_for_completed(1) assert_order(indexes_added.difference(to_include)) # On the last iteration, add a new editable entry before re-additions. if len(to_include) == len(endings): new_entry_but.click() add_urls() wait_for_completed(2) assert_order(indexes_added, empty_entry_expected=True) else: add_urls() def active(id): return inspect.stack()[1].frame.f_locals['execute_in_page']\ (f'returnval(list.active_entry.{id});') def existing(id, entry_nr=0): return inspect.stack()[1].frame.f_locals['execute_in_page']( ''' returnval(list.entries_by_text.get(list.shown_texts[arguments[0]])\ [arguments[1]]); ''', entry_nr, id) @pytest.mark.ext_data(dialog_html_test_ext_data) @pytest.mark.usefixtures('webextension') @pytest.mark.parametrize('mode', [mp for mp in mode_parameters if mp[1] != 'set_default_allowing']) def test_text_entry_list_editing(driver, execute_in_page, mode): """ A test case of editing entries in repo URLs list. """ add_action, _, instantiate_code = mode execute_in_page(load_script('html/text_entry_list.js')) execute_in_page( ''' let original_loader = dialog.loader, last_loader_msg; dialog.loader = (ctx, ...msg) => { last_loader_msg = msg; return original_loader(ctx, ...msg); } ''') last_loader_msg = lambda: execute_in_page('returnval(last_loader_msg);') list_div, new_entry_but = \ instantiate_list(['list.list_div', 'list.new_but']) if 'allow' in add_action: assert last_loader_msg() == ['Loading script blocking settings...'] else: assert last_loader_msg() == ['Loading repositories...'] assert execute_in_page('returnval(dialog_ctx.shown);') == False # Test adding new item. Submit via button click. new_entry_but.click() assert not active('noneditable_view').is_displayed() assert not active('save_but').is_displayed() assert active('add_but').is_displayed() assert active('cancel_but').is_displayed() active('input').send_keys('https://example.com///') active('add_but').click() WebDriverWait(driver, 10).until(lambda _: 'example.com' in list_div.text) assert execute_in_page('returnval(list.list_div.children.length);') == 1 if 'disallow' in add_action: assert last_loader_msg() == \ ["Blocking scripts on 'https://example.com/'..."] elif 'allow' in add_action: assert last_loader_msg() == \ ["Allowing scripts on 'https://example.com/'..."] else: assert last_loader_msg() == \ ["Adding repository 'https://example.com/'..."] assert not existing('editable_view').is_displayed() assert existing('text').is_displayed() assert existing('remove_but').is_displayed() # Test editing item. Submit via 'Enter' hit. Also test url pattern # normalization. existing('text').click() assert not active('noneditable_view').is_displayed() assert not active('add_but').is_displayed() assert active('save_but').is_displayed() assert active('cancel_but').is_displayed() assert active('input.value') == 'https://example.com/' active('input').send_keys(Keys.BACKSPACE * 30 + 'https://example.org//a//b' + Keys.ENTER) WebDriverWait(driver, 10).until(lambda _: 'example.org' in list_div.text) assert execute_in_page('returnval(list.list_div.children.length);') == 1 if 'disallow' in add_action: assert last_loader_msg() == ['Rewriting script blocking rule...'] elif 'allow' in add_action: assert last_loader_msg() == ['Rewriting script allowing rule...'] else: assert last_loader_msg() == ['Replacing repository...'] # Test entry removal. existing('remove_but').click() WebDriverWait(driver, 10).until(lambda _: 'xample.org' not in list_div.text) assert execute_in_page('returnval(list.list_div.children.length);') == 0 if 'allow' in add_action: assert last_loader_msg() == \ ["Setting default scripts blocking policy on 'https://example.org/a/b'..."] else: assert last_loader_msg() == ["Removing repository 'https://example.org//a//b/'..."] # The rest of this test remains the same regardless of mode. No point # testing the same thing multiple times. if 'repo' not in add_action: return # Test that clicking hidden buttons of item not being edited does nothing. new_entry_but.click() active('input').send_keys('https://example.foo' + Keys.ENTER) WebDriverWait(driver, 10).until(lambda _: 'xample.foo/' in list_div.text) existing('add_but.click()') existing('save_but.click()') existing('cancel_but.click()') assert execute_in_page('returnval(dialog_ctx.shown);') == False assert execute_in_page('returnval(list.list_div.children.length);') == 1 assert not existing('editable_view').is_displayed() # Test that clicking hidden buttons of item being edited does nothing. existing('text').click() active('remove_but.click()') active('add_but.click()') assert execute_in_page('returnval(dialog_ctx.shown);') == False assert execute_in_page('returnval(list.list_div.children.length);') == 1 assert not active('noneditable_view').is_displayed() # Test that creating a new entry makes the other one noneditable again. new_entry_but.click() assert existing('text').is_displayed() # Test that clicking hidden buttons of new item entry does nothing. active('remove_but.click()') active('save_but.click()') assert execute_in_page('returnval(dialog_ctx.shown);') == False assert execute_in_page('returnval(list.list_div.children.length);') == 2 assert not active('noneditable_view').is_displayed() # Test that starting edit of another entry removes the new entry. existing('text').click() assert existing('editable_view').is_displayed() assert execute_in_page('returnval(list.list_div.children.length);') == 1 # Test that starting edit of another entry cancels edit of the first entry. new_entry_but.click() active('input').send_keys('https://example.net' + Keys.ENTER) WebDriverWait(driver, 10).until(lambda _: 'example.net/' in list_div.text) assert execute_in_page('returnval(list.list_div.children.length);') == 2 existing('text', 0).click() assert existing('editable_view', 0).is_displayed() assert not existing('editable_view', 1).is_displayed() existing('text', 1).click() assert not existing('editable_view', 0).is_displayed() assert existing('editable_view', 1).is_displayed() @pytest.mark.ext_data(dialog_html_test_ext_data) @pytest.mark.usefixtures('webextension') @pytest.mark.parametrize('mode', [mp for mp in mode_parameters if mp[1] != 'set_default_allowing']) def test_text_entry_list_errors(driver, execute_in_page, mode): """ A test case of error dialogs shown by repo URL list. """ add_action, _, instantiate_code = mode execute_in_page(load_script('html/text_entry_list.js')) to_return = ['list.list_div', 'list.new_but', 'dialog_ctx.main_div'] list_div, new_entry_but, dialog_div = instantiate_list(to_return) # Prepare one entry to use later. new_entry_but.click() active('input').send_keys('https://example.com' + Keys.ENTER) # Check invalid URL errors. for clickable in (existing('text'), new_entry_but): clickable.click() active('input').send_keys(Keys.BACKSPACE * 30 + 'ws://example' + Keys.ENTER) execute_in_page('dialog.close(dialog_ctx);') if 'allow' in add_action: assert "'ws://example' is not a valid URL pattern. See here for more details." \ in dialog_div.text assert patterns_doc_url == \ driver.find_element_by_link_text('here').get_attribute('href') continue else: assert 'Repository URLs shoud use https:// schema.' \ in dialog_div.text active('input').send_keys(Keys.BACKSPACE * 30 + 'https://example' + Keys.ENTER) assert 'Provided URL is not valid.' in dialog_div.text execute_in_page('dialog.close(dialog_ctx);') # Mock errors to force error messages to appear. execute_in_page( ''' for (const action of [ "set_repo", "del_repo", "set_allowed", "set_default_allowing" ]) haketilodb[action] = () => {throw "reckless, limitless scope";}; ''') # Check database error dialogs. def check_reported_failure(txt): fail = lambda _: txt in dialog_div.text WebDriverWait(driver, 10).until(fail) execute_in_page('dialog.close(dialog_ctx);') existing('text').click() active('input').send_keys(Keys.BACKSPACE * 30 + 'https://example.org' + Keys.ENTER) if 'disallow' in add_action: check_reported_failure('Failed to rewrite blocking rule :(') elif 'allow' in add_action: check_reported_failure('Failed to rewrite allowing rule :(') else: check_reported_failure('Failed to replace repository :(') active('cancel_but').click() existing('remove_but').click() if 'allow' in add_action: check_reported_failure("Failed to remove rule for 'https://example.com' :(") else: check_reported_failure("Failed to remove repository 'https://example.com/' :(") new_entry_but.click() active('input').send_keys('https://example.org' + Keys.ENTER) if 'disallow' in add_action: check_reported_failure("Failed to write blocking rule for 'https://example.org' :(") elif 'allow' in add_action: check_reported_failure("Failed to write allowing rule for 'https://example.org' :(") else: check_reported_failure("Failed to add repository 'https://example.org/' :(")