summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--html/base.css2
-rw-r--r--html/dialog.js64
-rw-r--r--html/grid.css8
-rw-r--r--html/item_preview.html34
-rw-r--r--html/payload_create.html59
-rw-r--r--html/payload_create.js31
-rw-r--r--test/unit/test_payload_create.py211
-rw-r--r--test/unit/utils.py5
8 files changed, 289 insertions, 125 deletions
diff --git a/html/base.css b/html/base.css
index 6085f5f..e5e4660 100644
--- a/html/base.css
+++ b/html/base.css
@@ -34,7 +34,7 @@ body {
overflow: auto;
}
-.bold, h1, h2, h3 {
+.bold, h1, h2, h3, label {
font-weight: bold;
}
diff --git a/html/dialog.js b/html/dialog.js
index 22e8aa9..c4bba5d 100644
--- a/html/dialog.js
+++ b/html/dialog.js
@@ -42,7 +42,6 @@
*/
#FROM html/DOM_helpers.js IMPORT clone_template
-#FROM common/lock.js IMPORT make_lock, lock, try_lock, unlock
function make(on_dialog_show, on_dialog_hide)
{
@@ -51,9 +50,7 @@ function make(on_dialog_show, on_dialog_hide)
on_dialog_show,
on_dialog_hide,
shown: false,
- queue: 0,
- lock: make_lock(),
- callback: null
+ queue: [],
});
for (const [id, val] of [["yes", true], ["no", false], ["ok", undefined]]) {
@@ -68,33 +65,30 @@ function make(on_dialog_show, on_dialog_hide)
function close_dialog(dialog_context, event)
{
- if (event && event.target.parentElement.classList.contains("hide"))
+ if ((event && event.target.parentElement.classList.contains("hide")) ||
+ dialog_context.queue.length === 0)
return;
- const result = event ? event.target.haketilo_dialog_result : undefined;
+ const [[shown_buts_id, msg, resolve]] = dialog_context.queue.splice(0, 1);
- if (dialog_context.queue > 0)
- dialog_context.callback(result);
+ if (dialog_context.queue.length > 0) {
+ process_queue_item(dialog_context);
+ } else {
+ dialog_context.shown = false;
+ try {
+ dialog_context.on_dialog_hide();
+ } catch(e) {
+ console.error(e);
+ }
+ }
+
+ resolve(event ? event.target.haketilo_dialog_result : undefined);
}
#EXPORT close_dialog AS close
-async function show_dialog(dialog_context, shown_buts_id, msg)
+function process_queue_item(dialog_context)
{
- dialog_context.queue++;
-
- if (!dialog_context.shown) {
- dialog_context.shown = true;
- dialog_context.on_dialog_show();
- }
-
- /*
- * We want the dialog to be ready for calling close() right after
- * show_dialog() gets called. For this, we want locking to happen
- * synchronously if possible. If impossible (lock taken), this means dialog
- * is already open, hence it's also ready for close()'ing.
- */
- if (!try_lock(dialog_context.lock))
- await lock(dialog_context.lock);
+ const [shown_buts_id, msg, resolve] = dialog_context.queue[0];
[...dialog_context.msg.childNodes].forEach(n => n.remove());
dialog_context.msg.append(...msg);
@@ -102,17 +96,25 @@ async function show_dialog(dialog_context, shown_buts_id, msg)
const action = buts_id === shown_buts_id ? "remove" : "add";
dialog_context[buts_id].classList[action]("hide");
}
+}
- const result = await new Promise(cb => dialog_context.callback = cb);
+async function show_dialog(dialog_context, shown_buts_id, msg)
+{
+ let resolve;
+ const result_prom = new Promise(cb => resolve = cb);
+ dialog_context.queue.push([shown_buts_id, msg, resolve]);
- if (--dialog_context.queue == 0) {
- dialog_context.shown = false;
- dialog_context.on_dialog_hide();
+ if (!dialog_context.shown) {
+ process_queue_item(dialog_context);
+ dialog_context.shown = true;
+ try {
+ dialog_context.on_dialog_show();
+ } catch(e) {
+ console.error(e);
+ }
}
- unlock(dialog_context.lock);
-
- return result;
+ return await result_prom;
}
const error = (ctx, ...msg) => show_dialog(ctx, "conf_buts", msg);
diff --git a/html/grid.css b/html/grid.css
index 591772e..aa8fc80 100644
--- a/html/grid.css
+++ b/html/grid.css
@@ -70,11 +70,3 @@ div.grid_col_both {
margin-left: 1em;
margin-right: 1em;
}
-
-.grid_form>span {
- font-weight: bold;
-}
-
-span.grid_col_2 {
- font-weight: initial;
-}
diff --git a/html/item_preview.html b/html/item_preview.html
index a00d299..6cd15a8 100644
--- a/html/item_preview.html
+++ b/html/item_preview.html
@@ -46,43 +46,43 @@
<div id="resource_preview" data-template="main_div"
class="grid_2 grid_form preview_main_div">
<h3 class="grid_col_both">resource preview</h3>
- <span class="grid_col_1">identifier:</span>
+ <label class="grid_col_1">identifier:</label>
<span data-template="identifier" class="grid_col_2">...</span>
- <span class="grid_col_1">long name:</span>
+ <label class="grid_col_1">long name:</label>
<span data-template="long_name" class="grid_col_2">...</span>
- <span class="grid_col_1">UUID:</span>
+ <label class="grid_col_1">UUID:</label>
<span data-template="uuid" class="grid_col_2">...</span>
- <span class="grid_col_1">version:</span>
+ <label class="grid_col_1">version:</label>
<span data-template="version" class="grid_col_2">...</span>
- <span class="grid_col_1">description:</span>
+ <label class="grid_col_1">description:</label>
<span data-template="description" class="grid_col_2">...</span>
- <span class="grid_col_1">dependencies:</span>
+ <label class="grid_col_1">dependencies:</label>
<span class="grid_col_2"><ul data-template="dependencies"></ul></span>
- <span class="grid_col_1">scripts:</span>
+ <label class="grid_col_1">scripts:</label>
<span class="grid_col_2"><ul data-template="scripts"></ul></span>
- <span class="grid_col_1">source name:</span>
+ <label class="grid_col_1">source name:</label>
<span data-template="source_name" class="grid_col_2">...</span>
- <span class="grid_col_1">copyright:</span>
+ <label class="grid_col_1">copyright:</label>
<span class="grid_col_2"><ul data-template="copyright"></ul></span>
</div>
<div id="mapping_preview" data-template="main_div"
class="grid_2 grid_form">
<h3 class="grid_col_both">mapping preview</h3>
- <span class="grid_col_1">identifier:</span>
+ <label class="grid_col_1">identifier:</label>
<span data-template="identifier" class="grid_col_2">...</span>
- <span class="grid_col_1">long name:</span>
+ <label class="grid_col_1">long name:</label>
<span data-template="long_name" class="grid_col_2">...</span>
- <span class="grid_col_1">UUID:</span>
+ <label class="grid_col_1">UUID:</label>
<span data-template="uuid" class="grid_col_2">...</span>
- <span class="grid_col_1">version:</span>
+ <label class="grid_col_1">version:</label>
<span data-template="version" class="grid_col_2">...</span>
- <span class="grid_col_1">description:</span>
+ <label class="grid_col_1">description:</label>
<span data-template="description" class="grid_col_2">...</span>
- <span class="grid_col_both">payloads:</span>
+ <label class="grid_col_both">payloads:</label>
<div data-template="payloads" class="grid_col_both grid_2"></div>
- <span class="grid_col_1">source name:</span>
+ <label class="grid_col_1">source name:</label>
<span data-template="source_name" class="grid_col_2">...</span>
- <span class="grid_col_1">copyright:</span>
+ <label class="grid_col_1">copyright:</label>
<span class="grid_col_2"><ul data-template="copyright"></ul></span>
</div>
</template>
diff --git a/html/payload_create.html b/html/payload_create.html
index 0f6927f..062e41c 100644
--- a/html/payload_create.html
+++ b/html/payload_create.html
@@ -43,29 +43,54 @@
.payload_create_main_view {
margin: 0.8em;
}
- .payload_create_buts {
+ .payload_create_form {
+ margin: 0 0.6em;
+ }
+ .payload_create_form>* {
+ margin: 0.3em 0;
+ }
+ .payload_create_form>textarea {
+ resize: vertical;
+ height: 5em;
+ }
+ .payload_create_form>textarea.payload_create_script {
+ height: 18em;
+ font-family: monospace;
+ }
+ .payload_create_buts, .payload_create_heading {
text-align: center;
+ margin: 0.5em 0;
}
</style>
<template>
<div id="payload_create" data-template="main_div"
class="payload_create_main_view">
- <div data-template="form_container" class="grid_1">
- <h3>create_payload</h3>
- <label>
- identifier* (may only contain digits 0-9, lowercase letters a-z and hyphens '-'; will have 'local-' prepended):
- </label>
- <input data-template="identifier">
- <label>long name (defaults to the same as identifier):</label>
- <input data-template="long_name">
- <label>description:</label>
- <textarea data-template="description"></textarea>
- <label>URL patterns* (put each on its own line):</label>
- <textarea data-template="patterns">https://example.com/***</textarea>
- <label>script to inject*:</label>
- <textarea data-template="script">console.log("Hello, World!");</textarea>
- <div class="payload_create_buts">
- <button data-template="create_but">Create</button>
+ <div data-template="form_container">
+ <div class="grid_1 payload_create_form">
+ <h3 class="payload_create_heading">Create payload</h3>
+ <span>
+ <label>identifier*</label>
+ (may only contain digits 0-9, lowercase letters a-z and hyphens '-';
+ will have 'local-' prepended):
+ </span>
+ <input data-template="identifier">
+ <span>
+ <label>long name</label> (defaults to the same as identifier):
+ </span>
+ <input data-template="long_name">
+ <span><label>description</label>:</span>
+ <textarea data-template="description"></textarea>
+ <span>
+ <label><a data-template="patterns_link">URL patterns</a>*</label>
+ (put each on its own line):
+ </span>
+ <textarea data-template="patterns">https://example.com/***</textarea>
+ <span><label>script to inject*</label>:</span>
+ <textarea data-template="script" class="payload_create_script"
+ >console.log("Hello, World!");</textarea>
+ <div class="payload_create_buts">
+ <button data-template="create_but">Create</button>
+ </div>
</div>
</div>
<div data-template="dialog_container" class="hide">
diff --git a/html/payload_create.js b/html/payload_create.js
index db63a82..a5f9854 100644
--- a/html/payload_create.js
+++ b/html/payload_create.js
@@ -48,6 +48,9 @@
#FROM common/sha256.js IMPORT sha256
#FROM common/patterns.js IMPORT deconstruct_url
+const patterns_doc_url =
+ "https://hydrillabugs.koszko.org/projects/haketilo/wiki/URL_patterns";
+
/* https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid */
/* This is a helper function used by uuidv4(). */
function uuid_replace_num(num)
@@ -85,8 +88,6 @@ function collect_form_data(form_ctx)
try {
deconstruct_url(pattern);
} catch(e) {
- const patterns_doc_url =
- "https://hydrillabugs.koszko.org/projects/haketilo/wiki/URL_patterns";
const patterns_doc_link = document.createElement("a");
patterns_doc_link.href = patterns_doc_url;
patterns_doc_link.innerText = "here";
@@ -97,7 +98,7 @@ function collect_form_data(form_ctx)
}
if (pattern in payloads)
- throw `Pattern '${pattern}' soecified multiple times!`;
+ throw `Pattern '${pattern}' specified multiple times!`;
payloads[pattern] = {identifier};
}
@@ -135,6 +136,15 @@ function collect_form_data(form_ctx)
return {identifier, resource, mapping, files: {[hash_key]: script}};
}
+function clear_form(form_ctx)
+{
+ form_ctx.identifier.value = "";
+ form_ctx.long_name.value = "";
+ form_ctx.description.value = "";
+ form_ctx.patterns.value = "https://example.com/***";
+ form_ctx.script.value = `console.log("Hello, World!");`;
+}
+
async function save_payload(saving)
{
const db = await haketilodb.get();
@@ -146,7 +156,7 @@ async function save_payload(saving)
if (!saving[`override_${type}`] &&
(await haketilodb.idb_get(tx_ctx.transaction, store_name,
saving.identifier))) {
- saving.ask_override = "resource";
+ saving.ask_override = type;
return;
}
}
@@ -160,8 +170,8 @@ async function save_payload(saving)
function override_question(saving)
{
return saving.ask_override === "resource" ?
- `Resource '${saving.identifier}' alredy exists. Override?` :
- `Mapping '${saving.identifier}' alredy exists. Override?`;
+ `Resource '${saving.identifier}' already exists. Override?` :
+ `Mapping '${saving.identifier}' already exists. Override?`;
}
async function create_clicked(form_ctx)
@@ -169,16 +179,15 @@ async function create_clicked(form_ctx)
if (form_ctx.dialog_ctx.shown)
return;
- dialog.loader(form_ctx.dialog_ctx, "Saving payload...");
-
try {
var saving = collect_form_data(form_ctx);
} catch(e) {
dialog.error(form_ctx.dialog_ctx, e);
- dialog.close(form_ctx.dialog_ctx);
return;
}
+ dialog.loader(form_ctx.dialog_ctx, "Saving payload...");
+
try {
do {
if (saving.ask_override) {
@@ -197,9 +206,10 @@ async function create_clicked(form_ctx)
} while (saving.ask_override);
dialog.info(form_ctx.dialog_ctx, "Successfully saved payload!");
+ clear_form(form_ctx);
} catch(e) {
console.error(e);
- dialog.error(form_ctx.dialog_ctx, "Failed to save payload :(")
+ dialog.error(form_ctx.dialog_ctx, "Failed to save payload :(");
}
dialog.close(form_ctx.dialog_ctx);
@@ -221,6 +231,7 @@ function payload_create_form()
() => on_show_hide(form_ctx, "form"));
form_ctx.dialog_container.prepend(form_ctx.dialog_ctx.main_div);
+ form_ctx.patterns_link.href = patterns_doc_url;
form_ctx.create_but.addEventListener("click",
() => create_clicked(form_ctx));
diff --git a/test/unit/test_payload_create.py b/test/unit/test_payload_create.py
index cd08d43..bda3293 100644
--- a/test/unit/test_payload_create.py
+++ b/test/unit/test_payload_create.py
@@ -23,7 +23,7 @@ from selenium.webdriver.support.ui import WebDriverWait
from ..extension_crafting import ExtraHTML
from ..script_loader import load_script
-from .utils import clear_indexeddb, get_db_contents, sample_files
+from .utils import *
broker_js = lambda: load_script('background/broadcast_broker.js') + ';start();'
@@ -55,6 +55,23 @@ def fill_form_with_sample_data(execute_in_page, sample_data_override={},
{form_ctx}[key].value = value;
''',
form_data)
+ return form_data
+
+cleared_form_inputs = {
+ 'identifier': '',
+ 'long_name': '',
+ 'description': '',
+ 'patterns': 'https://example.com/***',
+ 'script': 'console.log("Hello, World!");'
+}
+def assert_form_contents(execute_in_page, inputs=cleared_form_inputs):
+ inputs_keys = [*inputs.keys()]
+ values = execute_in_page(
+ 'returnval(arguments[0].map(i => form_ctx[i].value));',
+ inputs_keys
+ )
+ for key, value in zip(inputs_keys, values):
+ assert inputs[key] == value
@pytest.mark.ext_data({
'background_script': broker_js,
@@ -62,60 +79,174 @@ def fill_form_with_sample_data(execute_in_page, sample_data_override={},
'navigate_to': 'html/payload_create.html'
})
@pytest.mark.usefixtures('webextension')
-def test_payload_create(driver, execute_in_page):
+def test_payload_create_normal_usage(driver, execute_in_page):
"""
- A test case of creating a simple payload using a form.
+ A test case of normal usage of simple payload creation form.
"""
clear_indexeddb(execute_in_page)
execute_in_page(load_script('html/payload_create.js'))
- create_but, main_div = execute_in_page(
+ create_but, form_container, dialog_container = execute_in_page(
'''
const form_ctx = payload_create_form();
document.body.append(form_ctx.main_div);
- returnval([form_ctx.create_but, form_ctx.main_div]);
+ returnval([form_ctx.create_but, form_ctx.form_container,
+ form_ctx.dialog_container]);
''')
- fill_form_with_sample_data(execute_in_page)
+ assert doc_url == \
+ driver.find_element_by_link_text('URL patterns').get_attribute('href')
+
+ assert form_container.is_displayed()
+ assert not dialog_container.is_displayed()
+
+ assert_form_contents(execute_in_page)
+
+ form_data = fill_form_with_sample_data(execute_in_page)
create_but.click()
+ assert not form_container.is_displayed()
+ assert dialog_container.is_displayed()
+
def success_reported(driver):
- return 'Successfully saved payload' in main_div.text
+ return 'Successfully saved payload' in dialog_container.text
WebDriverWait(driver, 10).until(success_reported)
+ execute_in_page('form_ctx.dialog_ctx.ok_but.click();')
+
+ assert form_container.is_displayed()
+ assert not dialog_container.is_displayed()
+
+ def assert_db_contents():
+ db_contents = get_db_contents(execute_in_page)
+
+ assert uuidv4_re.match(db_contents['resources'][0]['uuid'])
+
+ localid = f'local-{form_data["identifier"]}'
+ long_name = form_data['long_name'] or form_data['identifier']
+ payloads = dict([(pat, {'identifier': localid})
+ for pat in form_data['patterns'].split('\n') if pat])
+
+ assert db_contents['resources'] == [{
+ 'source_name': localid,
+ 'source_copyright': [],
+ 'type': 'resource',
+ 'identifier': localid,
+ 'uuid': db_contents['resources'][0]['uuid'],
+ 'version': [1],
+ 'description': form_data['description'],
+ 'dependencies': [],
+ 'long_name': long_name,
+ 'scripts': [{
+ 'file': 'payload.js',
+ 'hash_key': make_hash_key(form_data['script'])
+ }]
+ }]
- db_contents = get_db_contents(execute_in_page)
-
- assert uuidv4_re.match(db_contents['resources'][0]['uuid'])
- assert db_contents['resources'] == [{
- 'source_name': 'local-someid',
- 'source_copyright': [],
- 'type': 'resource',
- 'identifier': 'local-someid',
- 'long_name': 'Some Name',
- 'uuid': db_contents['resources'][0]['uuid'],
- 'version': [1],
- 'description': 'blah blah blah',
- 'dependencies': [],
- 'scripts': [{
- 'file': 'payload.js',
- 'hash_key': sample_files['hello.js']['hash_key']
+ assert uuidv4_re.match(db_contents['mappings'][0]['uuid'])
+ assert db_contents['mappings'] == [{
+ 'source_name': localid,
+ 'source_copyright': [],
+ 'type': 'mapping',
+ 'identifier': localid,
+ 'uuid': db_contents['mappings'][0]['uuid'],
+ 'version': [1],
+ 'description': form_data['description'],
+ 'long_name': long_name,
+ 'payloads': payloads
}]
- }]
-
- assert uuidv4_re.match(db_contents['mappings'][0]['uuid'])
- assert db_contents['mappings'] == [{
- 'source_name': 'local-someid',
- 'source_copyright': [],
- 'type': 'mapping',
- 'identifier': 'local-someid',
- 'long_name': 'Some Name',
- 'uuid': db_contents['mappings'][0]['uuid'],
- 'version': [1],
- 'description': 'blah blah blah',
- 'payloads': {
- 'http://example.com/***': {'identifier': 'local-someid'},
- 'https://*.example.org/**': {'identifier': 'local-someid'}
- }
- }]
+
+ assert_db_contents()
+
+ form_data = fill_form_with_sample_data(execute_in_page, {
+ 'long_name': '',
+ 'description': 'bam bam bam',
+ 'patterns': 'https://new.example.com/***',
+ 'script': sample_files['bye.js']['contents']
+ })
+
+ create_but.click()
+
+ for type in ('Resource', 'Mapping'):
+ def override_asked(driver):
+ return f"{type} 'local-someid' already exists. Override?" \
+ in dialog_container.text
+ WebDriverWait(driver, 10).until(override_asked)
+ execute_in_page('form_ctx.dialog_ctx.yes_but.click();')
+
+ assert_db_contents()
+
+doc_url = 'https://hydrillabugs.koszko.org/projects/haketilo/wiki/URL_patterns'
+
+@pytest.mark.ext_data({
+ 'background_script': broker_js,
+ 'extra_html': ExtraHTML('html/payload_create.html', {}),
+ 'navigate_to': 'html/payload_create.html'
+})
+@pytest.mark.usefixtures('webextension')
+def test_payload_create_errors(driver, execute_in_page):
+ """
+ A test case of various error the simple payload form might show.
+ """
+ clear_indexeddb(execute_in_page)
+ execute_in_page(load_script('html/payload_create.js'))
+
+ create_but, dialog_container = execute_in_page(
+ '''
+ const form_ctx = payload_create_form();
+ document.body.append(form_ctx.main_div);
+ returnval([form_ctx.create_but, form_ctx.dialog_container]);
+ ''')
+
+ for data_override, expected_msg in [
+ ({'identifier': ''}, "The 'identifier' field is required!"),
+ ({'identifier': ':('}, 'Identifier may only contain '),
+ ({'script': ''}, "The 'script' field is required!"),
+ ({'patterns': ''}, "The 'URL patterns' field is required!"),
+ ({'patterns': ':d'}, "':d' is not a valid URL pattern. See here for more details."),
+ ({'patterns': '\n'.join(['http://example.com'] * 2)},
+ "Pattern 'http://example.com' specified multiple times!")
+ ]:
+ # Attempt creating the payload
+ form_data = fill_form_with_sample_data(execute_in_page, data_override)
+ create_but.click()
+ # Verify the error message
+ assert expected_msg in dialog_container.text
+
+ # Verify patterns documentation <a> link.
+ if expected_msg == {'patterns': ':d'}:
+ doc_link_elem = driver.find_element_by_link_text('here')
+ assert doc_link.get_attribute('href') == doc_url
+
+ # Verify the form was NOT cleared upon failed saving.
+ execute_in_page('form_ctx.dialog_ctx.ok_but.click();')
+ assert_form_contents(execute_in_page, form_data)
+
+ # Add a sample item and attempt overriding it.
+ fill_form_with_sample_data(execute_in_page)
+ create_but.click()
+ WebDriverWait(driver, 10).until(lambda _: 'Succes' in dialog_container.text)
+ execute_in_page('form_ctx.dialog_ctx.ok_but.click();')
+
+ # Verify that denying override leads to saving failure.
+ form_data = fill_form_with_sample_data(execute_in_page)
+ create_but.click()
+ WebDriverWait(driver, 10).until(lambda _: 'Overri' in dialog_container.text)
+ execute_in_page('form_ctx.dialog_ctx.no_but.click();')
+ assert 'Failed to save payload :(' in dialog_container.text
+ execute_in_page('form_ctx.dialog_ctx.ok_but.click();')
+ assert_form_contents(execute_in_page, form_data)
+
+ # Verify that IndexedDB errors get caught and reported as saving failures.
+ execute_in_page('haketilodb.get = async () => {throw "someerror";}')
+ form_data = fill_form_with_sample_data(execute_in_page, {'identifier': 'o'})
+ create_but.click()
+ WebDriverWait(driver, 10).until(lambda _: 'Failed' in dialog_container.text)
+ execute_in_page('form_ctx.dialog_ctx.ok_but.click();')
+ assert_form_contents(execute_in_page, form_data)
+
+ # Verify that the loading message gets shown during IndexedDB operations.
+ execute_in_page('haketilodb.get = () => new Promise(cb => null);')
+ create_but.click()
+ assert 'Saving payload...' in dialog_container.text
diff --git a/test/unit/utils.py b/test/unit/utils.py
index a61e215..255f89d 100644
--- a/test/unit/utils.py
+++ b/test/unit/utils.py
@@ -27,9 +27,12 @@ Various functions and objects that can be reused between unit tests
from hashlib import sha256
+def make_hash_key(file_contents):
+ return f'sha256-{sha256(file_contents.encode()).digest().hex()}'
+
def sample_file(contents):
return {
- 'hash_key': f'sha256-{sha256(contents.encode()).digest().hex()}',
+ 'hash_key': make_hash_key(contents),
'contents': contents
}