aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2022-01-08 14:47:39 +0100
committerWojtek Kosior <koszko@koszko.org>2022-01-08 14:48:12 +0100
commit448820a11634de6ec356c77b8c7c0cf4937b344c (patch)
tree06147c3d40475ba863ccea9904ba4cdfe1d66db0 /test
parent372d24ea3a52e376f953deeffeb7847d008b81c9 (diff)
downloadbrowser-extension-448820a11634de6ec356c77b8c7c0cf4937b344c.tar.gz
browser-extension-448820a11634de6ec356c77b8c7c0cf4937b344c.zip
work on UI components
This commit introduces some HTML and javascript (and tests for it) to use in constructing the new UI. This is partial work that is not yet finished.
Diffstat (limited to 'test')
-rw-r--r--test/extension_crafting.py4
-rw-r--r--test/unit/conftest.py14
-rw-r--r--test/unit/test_basic.py19
-rw-r--r--test/unit/test_default_policy_dialog.py50
-rw-r--r--test/unit/test_dialog.py130
-rw-r--r--test/unit/test_indexeddb.py36
-rw-r--r--test/unit/test_item_list.py180
-rw-r--r--test/unit/test_item_preview.py235
-rw-r--r--test/unit/utils.py59
9 files changed, 684 insertions, 43 deletions
diff --git a/test/extension_crafting.py b/test/extension_crafting.py
index 61f8530..efb2687 100644
--- a/test/extension_crafting.py
+++ b/test/extension_crafting.py
@@ -61,7 +61,7 @@ def manifest_template():
'<all_urls>',
'unlimitedStorage'
],
- 'content_security_policy': "default-src 'self'; script-src 'self' https://serve.scrip.ts;",
+ 'content_security_policy': "object-src 'none'; script-src 'self' https://serve.scrip.ts;",
'web_accessible_resources': ['testpage.html'],
'background': {
'persistent': True,
@@ -143,6 +143,8 @@ def make_extension(destination_dir,
content_script=default_content_script,
test_page=default_test_page,
extra_files={}, extra_html=[]):
+ if not hasattr(extra_html, '__iter__'):
+ extra_html = [extra_html]
manifest = manifest_template()
extension_id = '{%s}' % uuid4()
manifest['applications']['gecko']['id'] = extension_id
diff --git a/test/unit/conftest.py b/test/unit/conftest.py
index 48e66c1..9318f6e 100644
--- a/test/unit/conftest.py
+++ b/test/unit/conftest.py
@@ -66,15 +66,25 @@ def webextension(driver, request):
ext_data = request.node.get_closest_marker('ext_data')
if ext_data is None:
raise Exception('"webextension" fixture requires "ext_data" marker to be set')
+ ext_data = ext_data.args[0].copy()
+
+ navigate_to = ext_data.get('navigate_to')
+ if navigate_to is not None:
+ del ext_data['navigate_to']
- ext_path = make_extension(Path(driver.firefox_profile.path),
- **ext_data.args[0])
driver.get('https://gotmyowndoma.in/')
+ ext_path = make_extension(Path(driver.firefox_profile.path), **ext_data)
addon_id = driver.install_addon(str(ext_path), temporary=True)
WebDriverWait(driver, 10).until(
EC.url_matches('^moz-extension://.*')
)
+
+ if navigate_to is not None:
+ testpage_url = driver.execute_script('return window.location.href;')
+ driver.get(testpage_url.replace('testpage.html', navigate_to))
+
yield
+
close_all_but_one_window(driver)
driver.get('https://gotmyowndoma.in/')
driver.uninstall_addon(addon_id)
diff --git a/test/unit/test_basic.py b/test/unit/test_basic.py
index 612fe06..5f42f5d 100644
--- a/test/unit/test_basic.py
+++ b/test/unit/test_basic.py
@@ -58,15 +58,14 @@ def test_webextension(driver):
assert "Extension's options page for testing" in heading
@pytest.mark.ext_data({
- 'extra_html': [
- ExtraHTML(
- 'html/default_blocking_policy.html',
- {
- 'html/default_blocking_policy.js':
- 'document.body.innerHTML = `ski-ba-bop-ba ${typeof by_id}`;'
- }
- )
- ]
+ 'extra_html': ExtraHTML(
+ 'html/default_blocking_policy.html',
+ {
+ 'html/default_blocking_policy.js':
+ 'document.body.innerHTML = `ski-ba-bop-ba ${typeof by_id}`;'
+ }
+ ),
+ 'navigate_to': 'html/default_blocking_policy.html'
})
@pytest.mark.usefixtures('webextension')
def test_extra_html(driver):
@@ -74,7 +73,5 @@ def test_extra_html(driver):
A trivial test case of the facility for loading the Haketilo's HTML files
into test WebExtension for unit-testing.
"""
- driver.get(driver.execute_script('return window.location.href;')
- .replace('testpage.html', 'html/default_blocking_policy.html'))
assert driver.execute_script('return document.body.innerText') == \
'ski-ba-bop-ba function'
diff --git a/test/unit/test_default_policy_dialog.py b/test/unit/test_default_policy_dialog.py
new file mode 100644
index 0000000..992b487
--- /dev/null
+++ b/test/unit/test_default_policy_dialog.py
@@ -0,0 +1,50 @@
+# SPDX-License-Identifier: CC0-1.0
+
+"""
+Haketilo unit tests - default script blocking policy dialog
+"""
+
+# 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 ..extension_crafting import ExtraHTML
+from ..script_loader import load_script
+
+broker_js = lambda: load_script('background/broadcast_broker.js') + ';start();'
+
+@pytest.mark.ext_data({
+ 'background_script': broker_js,
+ 'extra_html': ExtraHTML(
+ 'html/default_blocking_policy.html',
+ {
+ 'html/default_blocking_policy.js':
+ 'init_default_policy_dialog();'
+ }
+ ),
+ 'navigate_to': 'html/default_blocking_policy.html'
+})
+@pytest.mark.usefixtures('webextension')
+def test_default_blocking_policy_dialog(driver, wait_elem_text):
+ """
+ A test case for the dialog that facilitates toggling the default policy of
+ script blocking.
+ """
+ wait_elem_text('current_policy_span', 'block')
+
+ driver.find_element_by_id('toggle_policy_but').click()
+ wait_elem_text('current_policy_span', 'allow')
+
+ driver.find_element_by_id('toggle_policy_but').click()
+ wait_elem_text('current_policy_span', 'block')
diff --git a/test/unit/test_dialog.py b/test/unit/test_dialog.py
new file mode 100644
index 0000000..384a889
--- /dev/null
+++ b/test/unit/test_dialog.py
@@ -0,0 +1,130 @@
+# SPDX-License-Identifier: CC0-1.0
+
+"""
+Haketilo unit tests - showing an error/info/question dalog
+"""
+
+# 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 ..extension_crafting import ExtraHTML
+from ..script_loader import load_script
+
+@pytest.mark.ext_data({
+ 'extra_html': ExtraHTML('html/dialog.html', {}),
+ 'navigate_to': 'html/dialog.html'
+})
+@pytest.mark.usefixtures('webextension')
+def test_dialog_show_close(driver, execute_in_page):
+ """
+ A test case of basing dialog showing/closing.
+ """
+ execute_in_page(load_script('html/dialog.js'))
+ execute_in_page(
+ '''
+ let cb_calls, call_prom;
+ const dialog_context = make(() => cb_calls.push("show"),
+ () => cb_calls.push("hide"));
+ document.body.append(dialog_context.main_div);
+ ''')
+
+ buts = driver.find_elements_by_tag_name('button')
+ buts = dict([(but.text, but) for but in buts])
+
+ for i, (dialog_function, button_text, expected_result) in enumerate([
+ ('info', 'Ok', None),
+ ('error', 'Ok', None),
+ ('ask', 'Yes', True),
+ ('ask', 'No', False)
+ ]):
+ cb_calls, is_shown = execute_in_page(
+ f'''
+ cb_calls = [];
+ call_prom = {dialog_function}(dialog_context,
+ `sample_text_${{arguments[0]}}`);
+ returnval([cb_calls, dialog_context.shown]);
+ ''',
+ i)
+ assert cb_calls == ['show']
+ assert is_shown == True
+
+ page_source = driver.page_source
+ assert f'sample_text_{i}' in page_source
+ assert f'sample_text_{i - 1}' not in page_source
+
+ assert any([not but.is_displayed() for but in buts.values()])
+
+ assert buts[button_text].is_displayed()
+ buts[button_text].click()
+
+ cb_calls, result, is_shown = execute_in_page(
+ '''{
+ console.error(dialog_context.msg.textContent);
+ const values_cb = r => [cb_calls, r, dialog_context.shown];
+ returnval(call_prom.then(values_cb));
+ }''')
+ assert cb_calls == ['show', 'hide']
+ assert result == expected_result
+ assert is_shown == False
+
+@pytest.mark.ext_data({
+ 'extra_html': ExtraHTML('html/dialog.html', {}),
+ 'navigate_to': 'html/dialog.html'
+})
+@pytest.mark.usefixtures('webextension')
+def test_dialog_queue(driver, execute_in_page):
+ """
+ A test case of queuing dialog display operations.
+ """
+ execute_in_page(load_script('html/dialog.js'))
+ execute_in_page(
+ '''
+ let cb_calls = [], call_proms = [];
+ const dialog_context = make(() => cb_calls.push("show"),
+ () => cb_calls.push("hide"));
+ document.body.append(dialog_context.main_div);
+ ''')
+
+ buts = driver.find_elements_by_tag_name('button')
+ buts = dict([(but.text, but) for but in buts])
+
+ for i in range(5):
+ cb_calls, is_shown, msg_elem = execute_in_page(
+ '''
+ call_proms.push(ask(dialog_context, "somequestion" + arguments[0]));
+ returnval([cb_calls, dialog_context.shown, dialog_context.msg]);
+ ''',
+ i)
+ assert cb_calls == ['show']
+ assert is_shown == True
+ assert msg_elem.text == 'somequestion0'
+
+ for i in range(5):
+ buts['Yes' if i & 1 else 'No'].click()
+ cb_calls, is_shown, msg_elem, result = execute_in_page(
+ '''{
+ const values_cb =
+ r => [cb_calls, dialog_context.shown, dialog_context.msg, r];
+ returnval(call_proms.splice(0, 1)[0].then(values_cb));
+ }''')
+ if i < 4:
+ assert cb_calls == ['show']
+ assert is_shown == True
+ assert msg_elem.text == f'somequestion{i + 1}'
+ else:
+ assert cb_calls == ['show', 'hide']
+ assert is_shown == False
+
+ assert result == bool(i & 1)
diff --git a/test/unit/test_indexeddb.py b/test/unit/test_indexeddb.py
index 447ee6e..9dfbe63 100644
--- a/test/unit/test_indexeddb.py
+++ b/test/unit/test_indexeddb.py
@@ -19,62 +19,41 @@ Haketilo unit tests - IndexedDB access
import pytest
import json
-from hashlib import sha256
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import WebDriverException
from ..script_loader import load_script
+from .utils import sample_files, sample_files_by_hash, sample_file_ref
indexeddb_js = lambda: load_script('common/indexeddb.js')
broker_js = lambda: load_script('background/broadcast_broker.js') + ';start();'
-def sample_file(contents):
- return {
- 'hash_key': f'sha256-{sha256(contents.encode()).digest().hex()}',
- 'contents': contents
- }
-
-sample_files = {
- 'report.spdx': sample_file('<!-- dummy report -->'),
- 'LICENSES/somelicense.txt': sample_file('Permission is granted...'),
- 'hello.js': sample_file('console.log("hello!");\n'),
- 'bye.js': sample_file('console.log("bye!");\n'),
- 'combined.js': sample_file('console.log("hello!\\nbye!");\n'),
- 'README.md': sample_file('# Python Frobnicator\n...')
-}
-
-sample_files_by_hash = dict([[file['hash_key'], file['contents']]
- for file in sample_files.values()])
-
# Sample resource definitions. They'd normally contain more fields but here we
# use simplified versions.
def make_sample_resource():
return {
'source_copyright': [
- file_ref('report.spdx'),
- file_ref('LICENSES/somelicense.txt')
+ sample_file_ref('report.spdx'),
+ sample_file_ref('LICENSES/somelicense.txt')
],
'type': 'resource',
'identifier': 'helloapple',
- 'scripts': [file_ref('hello.js'), file_ref('bye.js')]
+ 'scripts': [sample_file_ref('hello.js'), sample_file_ref('bye.js')]
}
def make_sample_mapping():
return {
'source_copyright': [
- file_ref('report.spdx'),
- file_ref('README.md')
+ sample_file_ref('report.spdx'),
+ sample_file_ref('README.md')
],
'type': 'mapping',
'identifier': 'helloapple'
}
-def file_ref(file_name):
- return {'file': file_name, 'hash_key': sample_files[file_name]['hash_key']}
-
def clear_indexeddb(execute_in_page):
execute_in_page(
'''{
@@ -168,7 +147,7 @@ def test_haketilodb_item_modifications(driver, execute_in_page):
# See if trying to add an item without providing all its files ends in an
# exception and aborts the transaction as it should.
- sample_item['scripts'].append(file_ref('combined.js'))
+ sample_item['scripts'].append(sample_file_ref('combined.js'))
incomplete_files = {**sample_files_by_hash}
incomplete_files.pop(sample_files['combined.js']['hash_key'])
exception = execute_in_page(
@@ -439,7 +418,6 @@ def test_haketilodb_track(driver, execute_in_page, wait_elem_text):
'''
function update_item(store_name, change)
{
- console.log('# update', ...arguments);
const elem_id = `${store_name}_${change.key}`;
let elem = document.getElementById(elem_id);
elem = elem || document.createElement("li");
diff --git a/test/unit/test_item_list.py b/test/unit/test_item_list.py
new file mode 100644
index 0000000..3aba006
--- /dev/null
+++ b/test/unit/test_item_list.py
@@ -0,0 +1,180 @@
+# SPDX-License-Identifier: CC0-1.0
+
+"""
+Haketilo unit tests - displaying list of resources/mappings
+"""
+
+# 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 ..extension_crafting import ExtraHTML
+from ..script_loader import load_script
+from .utils import sample_files, sample_files_by_hash, sample_file_ref, \
+ item_version_string
+
+broker_js = lambda: load_script('background/broadcast_broker.js') + ';start();'
+
+def make_sample_mapping(identifier, long_name):
+ return {
+ 'source_name': 'example-org-fixes-new',
+ 'source_copyright': [
+ sample_file_ref('report.spdx'),
+ sample_file_ref('LICENSES/CC0-1.0.txt')
+ ],
+ 'type': 'mapping',
+ 'identifier': identifier,
+ 'long_name': long_name,
+ 'uuid': '54d23bba-472e-42f5-9194-eaa24c0e3ee7',
+ 'version': [2022, 5, 10],
+ 'description': 'suckless something something',
+ 'payloads': {
+ 'https://example.org/a/*': {
+ 'identifier': 'some-KISS-resource'
+ },
+ 'https://example.org/t/*': {
+ 'identifier': 'another-KISS-resource'
+ }
+ }
+ }
+
+def make_sample_resource(identifier, long_name):
+ return {
+ 'source_name': 'hello',
+ 'source_copyright': [
+ sample_file_ref('report.spdx'),
+ sample_file_ref('LICENSES/CC0-1.0.txt')
+ ],
+ 'type': 'resource',
+ 'identifier': identifier,
+ 'long_name': long_name,
+ 'uuid': 'a6754dcb-58d8-4b7a-a245-24fd7ad4cd68',
+ 'version': [2021, 11, 10],
+ 'revision': 1,
+ 'description': 'greets an apple',
+ 'dependencies': ['hello-message'],
+ 'scripts': [
+ sample_file_ref('hello.js'),
+ sample_file_ref('bye.js')
+ ]
+ }
+
+@pytest.mark.ext_data({
+ 'background_script': broker_js,
+ 'extra_html': ExtraHTML('html/item_list.html', {}),
+ 'navigate_to': 'html/item_list.html'
+})
+@pytest.mark.usefixtures('webextension')
+@pytest.mark.parametrize('item_type', ['resource', 'mapping'])
+def test_item_list_ordering(driver, execute_in_page, item_type):
+ """
+ A test case of items list proper ordering.
+ """
+ execute_in_page(load_script('html/item_list.js'))
+
+ make_item = make_sample_resource if item_type == 'resource' \
+ else make_sample_mapping
+
+ # Choose sample long names so as to test automatic sorting of items.
+ long_names = ['sample', 'sample it', 'Sample it', 'SAMPLE IT',
+ 'test', 'test it', 'Test it', 'TEST IT']
+ # Let's operate on a reverse-sorted copy
+ long_names_reversed = [*long_names]
+ long_names_reversed.reverse()
+
+ items = [make_item(f'it_{hex(2 * i + copy)[-1]}', name)
+ for i, name in enumerate(long_names_reversed)
+ for copy in (1, 0)]
+ # When adding/updating items this item will be updated at the end and this
+ # last update will be used to verify that a set of opertions completed.
+ extra_item = make_item('extraitem', 'extra item')
+ extra_dict = {'extraitem': {item_version_string(extra_item): extra_item}}
+
+ # After this reversal items are sorted in the exact order they are expected
+ # to appear in the HTML list.
+ items.reverse()
+
+ sample_data = {
+ 'resources': {},
+ 'mappings': {},
+ 'files': sample_files_by_hash
+ }
+
+ def is_prime(n):
+ return n > 1 and all([n % i != 0 for i in range(2, n)])
+
+ indexes_added = set()
+ for iteration, to_include in enumerate([
+ set([i for i in range(len(items)) if is_prime(i)]),
+ set([i for i in range(len(items))
+ if not is_prime(i) and i & 1]),
+ set([i for i in range(len(items)) if i % 3 == 0]),
+ set([i for i in range(len(items))
+ if i % 3 and not i & 1 and not is_prime(i)]),
+ set(range(16))
+ ]):
+ # On the last iteration, re-add ALL items but with changed names.
+ if len(to_include) == 16:
+ for it in items:
+ it['long_name'] = f'somewhat renamed {it["long_name"]}'
+
+ items_to_inclue = [items[i] for i in sorted(to_include)]
+ sample_data[item_type + 's'] = \
+ dict([(it['identifier'], {item_version_string(it): it})
+ for it in items_to_inclue])
+ execute_in_page('returnval(haketilodb.save_items(arguments[0]));',
+ sample_data)
+
+ extra_item['long_name'] = f'{iteration} {extra_item["long_name"]}'
+ sample_data[item_type + 's'] = extra_dict
+ execute_in_page('returnval(haketilodb.save_items(arguments[0]));',
+ sample_data)
+
+ if iteration == 0:
+ execute_in_page(
+ f'''
+ let list_ctx, items = arguments[0];
+ async function create_list() {{
+ list_ctx = await {item_type}_list();
+ document.body.append(list_ctx.main_div);
+ }}
+ returnval(create_list());
+ ''')
+
+ def lis_ready(driver):
+ return extra_item['long_name'] == execute_in_page(
+ 'returnval(list_ctx.ul.firstElementChild.textContent);'
+ )
+
+ indexes_added.update(to_include)
+ WebDriverWait(driver, 10).until(lis_ready)
+
+ li_texts = execute_in_page(
+ '''
+ var lis = [...list_ctx.ul.children].slice(1);
+ returnval(lis.map(li => li.textContent));
+ ''')
+ assert li_texts == [items[i]['long_name'] for i in indexes_added]
+
+ preview_texts = execute_in_page(
+ '''{
+ const get_texts =
+ li => [li.click(), list_ctx.preview_container.textContent][1];
+ returnval(lis.map(get_texts));
+ }''')
+
+ for i, text in zip(sorted(indexes_added), preview_texts):
+ assert items[i]['identifier'] in text
+ assert items[i]['long_name'] in text
diff --git a/test/unit/test_item_preview.py b/test/unit/test_item_preview.py
new file mode 100644
index 0000000..887e4f4
--- /dev/null
+++ b/test/unit/test_item_preview.py
@@ -0,0 +1,235 @@
+# SPDX-License-Identifier: CC0-1.0
+
+"""
+Haketilo unit tests - displaying resources and mappings details
+"""
+
+# 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 ..extension_crafting import ExtraHTML
+from ..script_loader import load_script
+from .utils import *
+
+broker_js = lambda: load_script('background/broadcast_broker.js') + ';start();'
+
+def make_sample_mapping():
+ return {
+ 'source_name': 'example-org-fixes-new',
+ 'source_copyright': [
+ sample_file_ref('report.spdx'),
+ sample_file_ref('LICENSES/CC0-1.0.txt')
+ ],
+ 'type': 'mapping',
+ 'identifier': 'example-org-minimal',
+ 'long_name': 'Example.org Minimal',
+ 'uuid': '54d23bba-472e-42f5-9194-eaa24c0e3ee7',
+ 'version': [2022, 5, 10],
+ 'description': 'suckless something something',
+ 'payloads': {
+ 'https://example.org/a/*': {
+ 'identifier': 'some-KISS-resource'
+ },
+ 'https://example.org/t/*': {
+ 'identifier': 'another-KISS-resource'
+ }
+ }
+ }
+
+def make_sample_resource():
+ return {
+ 'source_name': 'hello',
+ 'source_copyright': [
+ sample_file_ref('report.spdx'),
+ sample_file_ref('LICENSES/CC0-1.0.txt')
+ ],
+ 'type': 'resource',
+ 'identifier': 'helloapple',
+ 'long_name': 'Hello Apple',
+ 'uuid': 'a6754dcb-58d8-4b7a-a245-24fd7ad4cd68',
+ 'version': [2021, 11, 10],
+ 'revision': 1,
+ 'description': 'greets an apple',
+ 'dependencies': ['hello-message'],
+ 'scripts': [
+ sample_file_ref('hello.js'),
+ sample_file_ref('bye.js')
+ ]
+ }
+
+@pytest.mark.ext_data({
+ 'extra_html': ExtraHTML('html/item_preview.html', {}),
+ 'navigate_to': 'html/item_preview.html'
+})
+@pytest.mark.usefixtures('webextension')
+def test_resource_preview(driver, execute_in_page):
+ """
+ A test case of the resource preview display function.
+ """
+ execute_in_page(load_script('html/item_preview.js'))
+
+ sample_resource = make_sample_resource()
+
+ preview_div = execute_in_page(
+ '''
+ let preview_object = resource_preview(arguments[0]);
+ document.body.append(preview_object.main_div);
+ returnval(preview_object.main_div);
+ ''',
+ sample_resource)
+ text = preview_div.text
+
+ assert '...' not in text
+
+ for string in [
+ *filter(lambda v: type(v) is str, sample_resource.values()),
+ *sample_resource['dependencies'],
+ *[c['file'] for k in ('source_copyright', 'scripts')
+ for c in sample_resource[k]],
+ item_version_string(sample_resource, True)
+ ]:
+ assert string in text
+
+ sample_resource['identifier'] = 'hellopear'
+ sample_resource['long_name'] = 'Hello Pear'
+ sample_resource['description'] = 'greets a pear'
+ sample_resource['dependencies'] = ['hello-msg'],
+ for key in ('scripts', 'source_copyright'):
+ for file_ref in sample_resource[key]:
+ file_ref['file'] = file_ref['file'].replace('.', '_')
+
+ preview_div = execute_in_page(
+ '''
+ returnval(resource_preview(arguments[0], preview_object).main_div);
+ ''',
+ sample_resource)
+ text = preview_div.text
+
+ for string in ['...', 'pple', 'hello-message', 'report.spdx',
+ 'LICENSES/CC0-1.0.txt', 'hello.js', 'bye.js']:
+ assert string not in text
+
+ for string in ['hellopear', 'Hello Pear', 'hello-msg', 'greets a pear',
+ 'report_spdx', 'LICENSES/CC0-1_0_txt', 'hello_js', 'bye_js']:
+ assert string in text
+
+@pytest.mark.ext_data({
+ 'extra_html': ExtraHTML('html/item_preview.html', {}),
+ 'navigate_to': 'html/item_preview.html'
+})
+@pytest.mark.usefixtures('webextension')
+def test_mapping_preview(driver, execute_in_page):
+ """
+ A test case of the mapping preview display function.
+ """
+ execute_in_page(load_script('html/item_preview.js'))
+
+ sample_mapping = make_sample_mapping()
+
+ preview_div = execute_in_page(
+ '''
+ let preview_object = mapping_preview(arguments[0]);
+ document.body.append(preview_object.main_div);
+ returnval(preview_object.main_div);
+ ''',
+ sample_mapping)
+ text = preview_div.text
+
+ assert '...' not in text
+
+ for string in [
+ *filter(lambda v: type(v) is str, sample_mapping.values()),
+ *[p['identifier'] for p in sample_mapping['payloads'].values()],
+ *[c['file'] for c in sample_mapping['source_copyright']],
+ item_version_string(sample_mapping)
+ ]:
+ assert string in text
+
+ sample_mapping['identifier'] = 'example-org-bloated'
+ sample_mapping['long_name'] = 'Example.org Bloated',
+ sample_mapping['payloads'] = dict(
+ [(pat.replace('.org', '.com'), res_id)
+ for pat, res_id in sample_mapping['payloads'].items()]
+ )
+ for file_ref in sample_mapping['source_copyright']:
+ file_ref['file'] = file_ref['file'].replace('.', '_')
+
+ preview_div = execute_in_page(
+ '''
+ returnval(mapping_preview(arguments[0], preview_object).main_div);
+ ''',
+ sample_mapping)
+ text = preview_div.text
+
+ for string in ['...', 'inimal', 'example.org', 'report.spdx',
+ 'LICENSES/CC0-1.0.txt']:
+ assert string not in text
+
+ for string in ['example-org-bloated', 'Example.org Bloated', 'example.com',
+ 'report_spdx', 'LICENSES/CC0-1_0_txt']:
+ assert string in text
+
+@pytest.mark.ext_data({
+ 'background_script': broker_js,
+ 'extra_html': ExtraHTML('html/item_preview.html', {}),
+ 'navigate_to': 'html/item_preview.html'
+})
+@pytest.mark.usefixtures('webextension')
+def test_file_preview_link(driver, execute_in_page):
+ """
+ A test case of <a> links created by preview functions that allow a
+ referenced file to be previewed.
+ """
+ execute_in_page(load_script('html/item_preview.js'))
+ # Mock dialog
+ execute_in_page('dialog.error = (...args) => window.error_args = args;')
+
+ sample_resource = make_sample_resource()
+ sample_data = {
+ 'resources': {
+ sample_resource['identifier']: {
+ item_version_string(sample_resource): sample_resource
+ }
+ },
+ 'mappings': {
+ },
+ 'files': sample_files_by_hash
+ }
+ execute_in_page('returnval(haketilodb.save_items(arguments[0]));',
+ sample_data)
+
+ # Cause the "link" to `bye.js` to be invalid.
+ sample_resource['scripts'][1]['hash_key'] = 'dummy nonexistent key'
+
+ execute_in_page(
+ '''
+ let resource_preview_object =
+ resource_preview(arguments[0], undefined, "dummy dialog ctx");
+ document.body.append(resource_preview_object.main_div);
+ ''',
+ sample_resource)
+
+ window0 = driver.window_handles[0]
+ driver.find_element_by_link_text('hello.js').click()
+ WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) > 1)
+ window1 = [wh for wh in driver.window_handles if wh != window0][0]
+ driver.switch_to.window(window1)
+ assert sample_files['hello.js']['contents'] in driver.page_source
+
+ driver.switch_to.window(window0)
+ driver.find_element_by_link_text('bye.js').click()
+ assert driver.execute_script('return window.error_args;') == \
+ ['dummy dialog ctx', "File missing from Haketilo's inernal database :("]
diff --git a/test/unit/utils.py b/test/unit/utils.py
new file mode 100644
index 0000000..e2d89b9
--- /dev/null
+++ b/test/unit/utils.py
@@ -0,0 +1,59 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+"""
+Various functions and objects that can be reused between unit tests
+"""
+
+# This file is part of Haketilo.
+#
+# Copyright (C) 2021,2022 Wojtek Kosior <koszko@koszko.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# I, Wojtek Kosior, thereby promise not to sue for violation of this file's
+# license. Although I request that you do not make use of this code in a
+# proprietary program, I am not going to enforce this in court.
+
+from hashlib import sha256
+
+def sample_file(contents):
+ return {
+ 'hash_key': f'sha256-{sha256(contents.encode()).digest().hex()}',
+ 'contents': contents
+ }
+
+sample_files = {
+ 'report.spdx': sample_file('<!-- dummy report -->'),
+ 'LICENSES/somelicense.txt': sample_file('Permission is granted...'),
+ 'LICENSES/CC0-1.0.txt': sample_file('Dummy Commons...'),
+ 'hello.js': sample_file('console.log("uńićódę hello!");\n'),
+ 'bye.js': sample_file('console.log("bye!");\n'),
+ 'combined.js': sample_file('console.log("hello!\\nbye!");\n'),
+ 'README.md': sample_file('# Python Frobnicator\n...')
+}
+
+sample_files_by_hash = dict([[file['hash_key'], file['contents']]
+ for file in sample_files.values()])
+
+def sample_file_ref(file_name):
+ return {'file': file_name, 'hash_key': sample_files[file_name]['hash_key']}
+
+def item_version_string(definition, include_revision=False):
+ """
+ Given a resource or mapping definition, read its "version" property (and
+ also "revision" if applicable) and produce a corresponding version string.
+ """
+ ver = '.'.join([str(num) for num in definition['version']])
+ revision = definition.get('revision') if include_revision else None
+ return f'{ver}-{revision}' if revision is not None else ver