Manage ASL magazines and their articles.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
asl-articles/asl_articles/tests/test_publications.py

486 lines
18 KiB

""" Test publication operations. """
import os
import urllib.request
import urllib.error
import json
import base64
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import StaleElementReferenceException
from asl_articles.search import SEARCH_ALL, SEARCH_ALL_PUBLICATIONS, SEARCH_ALL_ARTICLES
from asl_articles.tests.utils import init_tests, load_fixtures, \
do_search, get_search_results, get_search_result_names, check_search_result, \
wait_for, wait_for_elem, wait_for_not_elem, find_child, find_children, find_search_result, set_elem_text, \
set_toast_marker, check_toast, send_upload_data, check_ask_dialog, check_error_msg
from asl_articles.tests.react_select import ReactSelect
# ---------------------------------------------------------------------
def test_edit_publication( webdriver, flask_app, dbconn ):
"""Test editing publications."""
# initialize
init_tests( webdriver, flask_app, dbconn, fixtures="publications.json" )
# edit "ASL Journal #2"
results = do_search( SEARCH_ALL_PUBLICATIONS )
sr = find_search_result( "ASL Journal (2)", results )
edit_publication( sr, {
"name": " ASL Journal (updated) ",
"edition": " 2a ",
"description": " Updated ASLJ description. ",
"tags": [ "+abc", "+xyz" ],
"url": " http://aslj-updated.com ",
} )
# check that the search result was updated in the UI
expected = [ "ASL Journal (updated)", "2a", "Updated ASLJ description.", ["abc","xyz"], "http://aslj-updated.com/" ]
sr = find_search_result( "ASL Journal (updated) (2a)" )
check_search_result( sr, _check_sr, expected )
# NOTE: We used to try to remove all fields from "ASL Journal #2" (which should fail, since we can't
# have an empty name), but we can no longer remove an existing publication name (which is OK, since we
# don't want to allow that, only change it).
# check that the publication was updated in the database
results = do_search( SEARCH_ALL_PUBLICATIONS )
sr = find_search_result( "ASL Journal (updated) (2a)", results )
check_search_result( sr, _check_sr, expected )
# ---------------------------------------------------------------------
def test_create_publication( webdriver, flask_app, dbconn ):
"""Test creating new publications."""
# initialize
init_tests( webdriver, flask_app, dbconn, fixtures="publications.json" )
do_search( SEARCH_ALL_PUBLICATIONS )
# try creating a publication with no name (should fail)
create_publication( {}, toast_type=None )
check_error_msg( "Please specify the publication's name." )
# enter a name and other details
edit_publication( None, { # nb: the form is still on-screen
"name": "New publication",
"edition": "#1",
"description": "New publication description.",
"tags": [ "+111", "+222", "+333" ],
"url": "http://new-publication.com"
} )
# check that the new publication appears in the UI
expected = [ "New publication", "#1", "New publication description.",
["111","222","333"], "http://new-publication.com/"
]
check_search_result( "New publication (#1)", _check_sr, expected )
# check that the new publication has been saved in the database
results = do_search( SEARCH_ALL_PUBLICATIONS )
sr = find_search_result( "New publication (#1)", results )
check_search_result( sr, _check_sr, expected )
# ---------------------------------------------------------------------
def test_delete_publication( webdriver, flask_app, dbconn ):
"""Test deleting publications."""
# initialize
init_tests( webdriver, flask_app, dbconn, fixtures="publications.json" )
# start to delete a publication, but cancel the operation
article_name = "ASL Journal (2)"
results = do_search( SEARCH_ALL_PUBLICATIONS )
sr = find_search_result( article_name, results )
find_child( ".delete", sr ).click()
check_ask_dialog( ( "Delete this publication?", article_name ), "cancel" )
# check that search results are unchanged on-screen
results2 = get_search_results()
assert results2 == results
# check that the search results are unchanged in the database
results3 = do_search( SEARCH_ALL_PUBLICATIONS )
assert results3 == results
# delete the publication
sr = find_search_result( article_name, results3 )
find_child( ".delete", sr ).click()
set_toast_marker( "info" )
check_ask_dialog( ( "Delete this publication?", article_name ), "ok" )
wait_for( 2, lambda: check_toast( "info", "The publication was deleted." ) )
# check that search result was removed on-screen
wait_for( 2, lambda: article_name not in get_search_result_names() )
# check that the search result was deleted from the database
results = do_search( SEARCH_ALL_PUBLICATIONS )
assert article_name not in get_search_result_names( results )
# ---------------------------------------------------------------------
def test_images( webdriver, flask_app, dbconn ): #pylint: disable=too-many-statements
"""Test publication images."""
# initialize
init_tests( webdriver, flask_app, dbconn, max_image_upload_size=2*1024 )
def check_image( expected ):
# check the image in the publication's search result
def check_sr_image( expected ):
img = find_child( "img.image", pub_sr )
if expected:
expected_url = flask_app.url_for( "get_image", image_type="publication", image_id=pub_id )
image_url = img.get_attribute( "src" ).split( "?" )[0]
return image_url == expected_url
else:
return not img
wait_for( 2, lambda: check_sr_image( expected ) )
# check the image in the publisher's config
find_child( ".edit", pub_sr ).click()
dlg = wait_for_elem( 2, "#modal-form" )
if expected:
# make sure there is an image
img = find_child( ".row.image img.image", dlg )
image_url = img.get_attribute( "src" )
assert "/images/publication/{}".format( pub_id ) in image_url
# make sure the "remove image" icon is visible
btn = find_child( ".row.image .remove-image", dlg )
assert btn.is_displayed()
# make sure the publication's image is correct
resp = urllib.request.urlopen( image_url ).read()
assert resp == open( expected, "rb" ).read()
else:
# make sure there is no image
img = find_child( ".row.image img.image", dlg )
assert img.get_attribute( "src" ).endswith( "/images/placeholder.png" )
# make sure the "remove image" icon is hidden
btn = find_child( ".row.image .remove-image", dlg )
assert not btn.is_displayed()
# make sure the publication's image is not available
url = flask_app.url_for( "get_image", image_type="publication", image_id=pub_id )
try:
resp = urllib.request.urlopen( url )
assert False, "Should never get here!"
except urllib.error.HTTPError as ex:
assert ex.code == 404
find_child( ".cancel", dlg ).click()
# create an publication with no image
create_publication( {"name": "Test Publication" } )
results = get_search_results()
assert len(results) == 1
pub_sr = results[0]
pub_id = pub_sr.get_attribute( "testing--pub_id" )
check_image( None )
# add an image to the publication
fname = os.path.join( os.path.split(__file__)[0], "fixtures/images/1.gif" )
edit_publication( pub_sr, { "image": fname } )
check_image( fname )
# change the publication's image
fname = os.path.join( os.path.split(__file__)[0], "fixtures/images/2.gif" )
edit_publication( pub_sr, { "image": fname } )
check_image( fname )
# remove the publication's image
edit_publication( pub_sr, { "image": None } )
check_image( None )
# try to upload an image that's too large
find_child( ".edit", pub_sr ).click()
dlg = wait_for_elem( 2, "#modal-form" )
data = base64.b64encode( 5000 * b" " )
data = "{}|{}".format( "too-big.png", data.decode("ascii") )
send_upload_data( data,
lambda: find_child( ".row.image img.image", dlg ).click()
)
check_error_msg( "The file must be no more than 2 KB in size." )
# ---------------------------------------------------------------------
def test_parent_publisher( webdriver, flask_app, dbconn ):
"""Test setting a publication's parent publisher."""
# initialize
init_tests( webdriver, flask_app, dbconn, fixtures="parents.json" )
def check_result( sr, expected_parent ): #pylint: disable=too-many-return-statements
# check that the parent publisher was updated in the UI
elem = find_child( ".name .publisher", sr )
if expected_parent:
if elem.text != "({})".format( expected_parent[1] ):
return None
else:
if elem is not None:
return None
# check that the parent publisher was updated in the database
pub_id = sr.get_attribute( "testing--pub_id" )
url = flask_app.url_for( "get_publication", pub_id=pub_id )
pub = json.load( urllib.request.urlopen( url ) )
if expected_parent:
if pub["publ_id"] != expected_parent[0]:
return None
else:
if pub["publ_id"] is not None:
return None
# check that the parent publisher was updated in the UI
results = do_search( '"MMP News"' )
assert len(results) == 1
sr = results[0]
elem = find_child( ".name .publisher", sr )
if expected_parent:
if elem.text != "({})".format( expected_parent[1] ):
return None
else:
if elem is not None:
return None
return sr
# create a publication with no parent publisher
create_publication( { "name": "MMP News" } )
results = get_search_results()
assert len(results) == 1
sr = wait_for( 2, lambda: check_result( results[0], None ) )
# change the publication to have a publisher
edit_publication( sr, { "publisher": "Multiman Publishing" } )
sr = wait_for( 2, lambda: check_result( sr, (1, "Multiman Publishing") ) )
# change the publication back to having no publisher
edit_publication( sr, { "publisher": "(none)" } )
sr = wait_for( 2, lambda: check_result( results[0], None ) )
# ---------------------------------------------------------------------
def test_cascading_deletes( webdriver, flask_app, dbconn ):
"""Test cascading deletes."""
# initialize
session = init_tests( webdriver, flask_app, dbconn )
def do_test( pub_name, expected_warning, expected_articles ):
# initialize
load_fixtures( session, "cascading-deletes-2.json" )
results = do_search( SEARCH_ALL )
# delete the specified publication
sr = find_search_result( pub_name, results )
find_child( ".delete", sr ).click()
check_ask_dialog( ( "Delete this publication?", pub_name, expected_warning ), "ok" )
def check_results():
results = wait_for( 2, lambda: get_results( len(expected_articles) ) )
assert set( results ) == set( expected_articles )
def get_results( expected_len ):
# NOTE: The UI will remove anything that has been deleted, so we need to
# give it a bit of time to finish doing this.
try:
results = get_search_result_names()
except StaleElementReferenceException:
return None
results = [ r for r in results if r.startswith( "article" ) ]
if len(results) == expected_len:
return results
return None
# check that deleted associated articles were removed from the UI
check_results()
# check that associated articles were removed from the database
results = do_search( SEARCH_ALL_ARTICLES )
check_results()
# do the tests
do_test( "Cascading Deletes 1",
"No articles will be deleted", ["article 2","article 3a","article 3b"]
)
do_test( "Cascading Deletes 2",
"1 associated article will also be deleted", ["article 3a","article 3b"]
)
do_test( "Cascading Deletes 3",
"2 associated articles will also be deleted", ["article 2"]
)
# ---------------------------------------------------------------------
def test_unicode( webdriver, flask_app, dbconn ):
"""Test Unicode content."""
# initialize
init_tests( webdriver, flask_app, dbconn )
# create a publication with Unicode content
create_publication( {
"name": "japan = \u65e5\u672c",
"edition": "\u263a",
"tags": [ "+\u0e51", "+\u0e52", "+\u0e53" ],
"url": "http://\ud55c\uad6d.com",
"description": "greece = \u0395\u03bb\u03bb\u03ac\u03b4\u03b1"
} )
# check that the new publication is showing the Unicode content correctly
results = do_search( SEARCH_ALL_PUBLICATIONS )
assert len( results ) == 1
check_search_result( results[0], _check_sr, [
"japan = \u65e5\u672c", "\u263a",
"greece = \u0395\u03bb\u03bb\u03ac\u03b4\u03b1",
[ "\u0e51", "\u0e52", "\u0e53" ],
"http://xn--3e0b707e.com/"
] )
# ---------------------------------------------------------------------
def test_clean_html( webdriver, flask_app, dbconn ):
"""Test cleaning HTML content."""
# initialize
init_tests( webdriver, flask_app, dbconn )
# create a publication with HTML content
create_publication( {
"name": "name: <span style='boo!'> <b>bold</b> <xxx>xxx</xxx> <i>italic</i>",
"edition": "<i>2</i>",
"description": "bad stuff here: <script>HCF</script>"
}, toast_type="warning" )
# check that the HTML was cleaned
sr = check_search_result( None, _check_sr, [
"name: bold xxx italic", "2", "bad stuff here:", [], None
] )
assert find_child( ".name span", sr ).get_attribute( "innerHTML" ) \
== "name: <span> <b>bold</b> xxx <i>italic</i></span> (<i>2</i>)"
assert check_toast( "warning", "Some values had HTML removed.", contains=True )
# update the publication with new HTML content
edit_publication( sr, {
"name": "<div style='...'>updated</div>"
}, toast_type="warning" )
results = get_search_results()
assert len(results) == 1
wait_for( 2, lambda: find_child( ".name", results[0] ).text == "updated (2)" )
assert check_toast( "warning", "Some values had HTML removed.", contains=True )
# ---------------------------------------------------------------------
def create_publication( vals, toast_type="info" ):
"""Create a new publication."""
# initialize
if toast_type:
set_toast_marker( toast_type )
# create the new publication
find_child( "#menu .new-publication" ).click()
dlg = wait_for_elem( 2, "#modal-form" )
for key,val in vals.items():
if key == "name":
elem = find_child( ".row.name .react-select input", dlg )
set_elem_text( elem, val )
elem.send_keys( Keys.RETURN )
elif key == "tags":
select = ReactSelect( find_child( ".row.tags .react-select", dlg ) )
select.update_multiselect_values( *val )
else:
sel = ".row.{} {}".format( key , "textarea" if key == "description" else "input" )
set_elem_text( find_child( sel, dlg ), val )
find_child( "button.ok", dlg ).click()
if toast_type:
# check that the new publication was created successfully
wait_for( 2,
lambda: check_toast( toast_type, "created OK", contains=True )
)
wait_for_not_elem( 2, "#modal-form" )
def edit_publication( sr, vals, toast_type="info", expected_error=None ):
"""Edit a publication's details."""
# initialize
if sr:
find_child( ".edit", sr ).click()
else:
pass # nb: we assume that the dialog is already on-screen
dlg = wait_for_elem( 2, "#modal-form" )
# update the specified publication's details
for key,val in vals.items():
if key == "image":
if val:
data = base64.b64encode( open( val, "rb" ).read() )
data = "{}|{}".format( os.path.split(val)[1], data.decode("ascii") )
send_upload_data( data,
lambda: find_child( ".row.image img", dlg ).click()
)
else:
find_child( ".row.image .remove-image", dlg ).click()
elif key == "name":
elem = find_child( ".row.name .react-select input", dlg )
set_elem_text( elem, val )
elem.send_keys( Keys.RETURN )
elif key == "publisher":
select = ReactSelect( find_child( ".row.publisher .react-select", dlg ) )
select.select_by_name( val )
elif key == "tags":
select = ReactSelect( find_child( ".row.tags .react-select", dlg ) )
select.update_multiselect_values( *val )
else:
sel = ".row.{} {}".format( key , "textarea" if key == "description" else "input" )
set_elem_text( find_child( sel, dlg ), val )
set_toast_marker( toast_type )
find_child( "button.ok", dlg ).click()
# check what happened
if expected_error:
# we were expecting an error, confirm the error message
check_error_msg( expected_error )
else:
# we were expecting the update to work, confirm this
expected = "updated OK" if sr else "created OK"
wait_for( 2,
lambda: check_toast( toast_type, expected, contains=True )
)
wait_for_not_elem( 2, "#modal-form" )
# ---------------------------------------------------------------------
def _check_sr( sr, expected ):
"""Check a search result."""
# check the name and edition
expected_name = expected[0]
if expected[1]:
expected_name += " ({})".format( expected[1] )
if find_child( ".name", sr ).text != expected_name:
return False
# check the description
if find_child( ".description", sr ).text != expected[2]:
return False
# check the tags
tags = [ t.text for t in find_children( ".tag", sr ) ]
if tags != expected[3]:
return False
# check the publication's link
elem = find_child( ".name a", sr )
if elem:
if elem.get_attribute( "href" ) != expected[4]:
return False
else:
if expected[4] is not None:
return False
return True